diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000000..e5b6d8d6a6 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/commit.js b/.changeset/commit.js new file mode 100644 index 0000000000..d9ddde3fc9 --- /dev/null +++ b/.changeset/commit.js @@ -0,0 +1,24 @@ +const { execSync } = require('node:child_process') + +const getSignedOffBy = () => { + const gitUserName = execSync('git config user.name').toString('utf-8').trim() + const gitEmail = execSync('git config user.email').toString('utf-8').trim() + + return `Signed-off-by: ${gitUserName} <${gitEmail}>` +} + +const getAddMessage = async (changeset) => { + return `docs(changeset): ${changeset.summary}\n\n${getSignedOffBy()}\n` +} + +const getVersionMessage = async (releasePlan) => { + const publishableReleases = releasePlan.releases.filter((release) => release.type !== 'none') + const releasedVersion = publishableReleases[0].newVersion + + return `chore(release): version ${releasedVersion}\n\n${getSignedOffBy()}\n` +} + +module.exports = { + getAddMessage, + getVersionMessage, +} diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000000..b6b6972d35 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": "./commit", + "privatePackages": false, + "fixed": [["@credo-ts/*"]], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "snapshot": { + "useCalculatedVersion": true + } +} diff --git a/.devcontainer/devcontainer.env b/.devcontainer/devcontainer.env new file mode 100644 index 0000000000..1b31a5ab62 --- /dev/null +++ b/.devcontainer/devcontainer.env @@ -0,0 +1,7 @@ +# +# Any environment variables that the container needs +# go in here. +# +# Example(s) +# GENESIS_TXN_PATH=/work/network/genesis/local-genesis.txn +# diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..4b236bff0f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,12 @@ +{ + "image": "node:18", + "runArgs": ["--env-file", ".devcontainer/devcontainer.env"], + "workspaceMount": "source=${localWorkspaceFolder},target=/work,type=bind", + "workspaceFolder": "/work", + "customizations": { + "vscode": { + "extensions": ["esbenp.prettier-vscode"] + } + }, + "postCreateCommand": "pnpm install" +} diff --git a/.dockerignore b/.dockerignore index dd87e2d73f..2399fcb20b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,4 @@ +# Skip unncecessary folders node_modules build +.github diff --git a/.env b/.env deleted file mode 100644 index b0e0aff87d..0000000000 --- a/.env +++ /dev/null @@ -1,7 +0,0 @@ -AGENT_URL= -AGENT_PORT= -AGENT_LABEL= -WALLET_NAME= -WALLET_KEY= -PUBLIC_DID= -PUBLIC_DID_SEED= diff --git a/.eslintrc.js b/.eslintrc.js index 7e6530bd76..e1ab5de77a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,28 +1,142 @@ module.exports = { parser: '@typescript-eslint/parser', extends: [ - 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin + 'eslint:recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'plugin:@typescript-eslint/recommended', + 'plugin:workspaces/recommended', 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. ], - plugins: ['@typescript-eslint'], + plugins: ['workspaces'], + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.eslint.json'], + }, + settings: { + 'import/extensions': ['.js', '.ts'], + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'], + }, + 'import/resolver': { + typescript: { + project: 'packages/*/tsconfig.json', + alwaysTryTypes: true, + }, + }, + }, rules: { - // Type is enforced by callers. Not entirely, but it's good enough. + '@typescript-eslint/no-unsafe-declaration-merging': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', - // Aries protocol defines attributes with snake case. - '@typescript-eslint/camelcase': 'off', '@typescript-eslint/no-use-before-define': ['error', { functions: false, classes: false, variables: true }], '@typescript-eslint/explicit-member-accessibility': 'error', 'no-console': 'error', - // Because of early development, we only warn on ts-ignore. In future we want to move to error '@typescript-eslint/ban-ts-comment': 'warn', + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/no-floating-promises': 'error', + 'import/no-cycle': 'error', + 'import/newline-after-import': ['error', { count: 1 }], + 'import/order': [ + 'error', + { + groups: ['type', ['builtin', 'external'], 'parent', 'sibling', 'index'], + alphabetize: { + order: 'asc', + }, + 'newlines-between': 'always', + }, + ], + '@typescript-eslint/no-non-null-assertion': 'error', + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: false, + }, + ], + 'no-restricted-imports': [ + 'error', + { + patterns: ['packages/*'], + }, + ], + // Do not allow const enums + // https://github.com/typescript-eslint/typescript-eslint/issues/561#issuecomment-593059472 + // https://ncjamieson.com/dont-export-const-enums/ + 'no-restricted-syntax': [ + 'error', + { + selector: 'TSEnumDeclaration[const=true]', + message: "Don't declare const enums", + }, + ], }, overrides: [ { - files: ['*.test.*'], + files: ['packages/core/**'], + rules: { + 'no-restricted-globals': [ + 'error', + { + name: 'Buffer', + message: 'Global buffer is not supported on all platforms. Import buffer from `src/utils/buffer`', + }, + { + name: 'AbortController', + message: + "Global AbortController is not supported on all platforms. Use `import { AbortController } from 'abort-controller'`", + }, + ], + }, + }, + { + files: ['jest.config.ts', '.eslintrc.js', './scripts/**'], + env: { + node: true, + }, + rules: { + '@typescript-eslint/no-var-requires': 'off', + 'no-undef': 'off', + }, + }, + { + files: ['demo/**', 'demo-openid/**'], rules: { 'no-console': 'off', }, }, + { + files: [ + '*.test.ts', + '**/__tests__/**', + '**/tests/**', + 'jest.*.ts', + 'samples/**', + 'demo/**', + 'demo-openid/**', + 'scripts/**', + '**/tests/**', + ], + env: { + jest: true, + node: false, + }, + rules: { + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, + }, + ], + }, + }, + { + files: ['*.test.ts', '**/__tests__/**', '**/tests/**', '**/tests/**'], + rules: { + 'workspaces/no-relative-imports': 'off', + 'workspaces/require-dependency': 'off', + 'workspaces/no-absolute-imports': 'off', + }, + }, ], -}; +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..2501ef7f66 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,39 @@ +######################################################################################################################################## +# GitHub Dependabot Config info # +# For details on how this file works refer to: # +# - https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file # +######################################################################################################################################## + +version: 2 +updates: + # Maintain dependencies for NPM + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'monthly' + allow: + # Focus on main dependencies, not devDependencies + - dependency-type: 'production' + + # Maintain dependencies for GitHub Actions + # - Check for updates once a month + # - Group all updates into a single PR + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'monthly' + groups: + all-actions: + patterns: ['*'] + + # Maintain dependencies for Docker + - package-ecosystem: 'docker' + directory: '/' + schedule: + interval: 'monthly' + + # Maintain dependencies for Cargo + - package-ecosystem: 'cargo' + directory: '/' + schedule: + interval: 'monthly' diff --git a/.github/settings.yml b/.github/settings.yml new file mode 100644 index 0000000000..aff08f32b0 --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,18 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +repository: + name: credo-ts + description: Typescript framework for building decentralized identity and verifiable credential solutions + homepage: https://credo.js.org + default_branch: main + has_downloads: false + has_issues: true + has_projects: false + has_wiki: true + archived: false + private: false + allow_squash_merge: true + allow_merge_commit: false + allow_rebase_merge: true diff --git a/.github/workflows/cleanup-cache.yml b/.github/workflows/cleanup-cache.yml new file mode 100644 index 0000000000..4b4ecd5bd5 --- /dev/null +++ b/.github/workflows/cleanup-cache.yml @@ -0,0 +1,16 @@ +# Repositories have 10 GB of cache storage per repository +# Documentation: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy +name: 'Cleanup - Cache' +on: + schedule: + - cron: '0 0 * * 0/3' + workflow_dispatch: + +jobs: + delete-caches: + name: 'Delete Actions caches' + runs-on: ubuntu-latest + + steps: + - name: 'Wipe Github Actions cache' + uses: easimon/wipe-cache@v2 diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ec6f271498..23ef4c4a1f 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -2,70 +2,202 @@ name: Continuous Integration on: pull_request: - branches: [main] + branches: + - main + - '**-pre' + types: [opened, synchronize, reopened, labeled] push: - branches: [main] + branches: + - main + - '**-pre' + workflow_dispatch: env: - TEST_AGENT_PUBLIC_DID_SEED: 000000000000000000000000Trustee9 - GENESIS_TXN_PATH: network/genesis/local-genesis.txn + NODE_OPTIONS: --max_old_space_size=6144 + +# Make sure we're not running multiple release steps at the same time as this can give issues with determining the next npm version to release. +# Ideally we only add this to the 'release' job so it doesn't limit PR runs, but github can't guarantee the job order in that case: +# "When concurrency is specified at the job level, order is not guaranteed for jobs or runs that queue within 5 minutes of each other." +concurrency: + # Cancel previous runs that are not completed yet + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - integration-test: - runs-on: ubuntu-18.04 - name: Integration Tests + # PRs created by github actions won't trigger CI. Before we can merge a PR we need to run the tests and + # validation scripts. To still be able to run the CI we can manually trigger it by adding the 'ci-test' + # label to the pull request + ci-trigger: + runs-on: ubuntu-20.04 + outputs: + triggered: ${{ steps.check.outputs.triggered }} steps: - - name: Checkout aries-framework-javascript - uses: actions/checkout@v2 + - name: Determine if CI should run + id: check + run: | + if [[ "${{ github.event.action }}" == "labeled" && "${{ github.event.label.name }}" == "ci-test" ]]; then + export SHOULD_RUN='true' + elif [[ "${{ github.event.action }}" == "labeled" && "${{ github.event.label.name }}" != "ci-test" ]]; then + export SHOULD_RUN='false' + else + export SHOULD_RUN='true' + fi - - name: Get docker cache - uses: satackey/action-docker-layer-caching@v0.0.11 + echo "SHOULD_RUN: ${SHOULD_RUN}" + echo triggered="${SHOULD_RUN}" >> "$GITHUB_OUTPUT" - - name: Start indy pool - run: | - docker build -f network/indy-pool.dockerfile -t indy-pool . - docker run -d --name indy-pool -p 9701-9708:9701-9708 indy-pool - docker exec indy-pool indy-cli-setup + validate: + runs-on: ubuntu-20.04 + name: Validate + steps: + - name: Checkout credo-ts + uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v2 + with: + version: 9.1.0 - - name: Register DID on ledger - run: docker exec indy-pool add-did-from-seed "${TEST_AGENT_PUBLIC_DID_SEED}" + - name: Setup NodeJS + uses: actions/setup-node@v4 + with: + node-version: 18 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Linting + run: pnpm lint - - name: Build framework docker image - run: docker build -t aries-framework-javascript . + - name: Prettier + run: pnpm check-format - - name: Run lint and format validation - run: docker run aries-framework-javascript yarn validate + - name: Check Types + run: pnpm check-types - - name: Start mediator agents - run: docker-compose -f docker/docker-compose-mediators.yml up -d + - name: Compile + run: pnpm build + + unit-tests: + runs-on: ubuntu-20.04 + name: Unit Tests + + strategy: + fail-fast: false + matrix: + node-version: [18, 20] + # Each shard runs a set of the tests + # Make sure to UPDATE THE TEST command with the total length of + # the shards if you change this!! + shard: [1, 2] + + steps: + - name: Checkout credo + uses: actions/checkout@v4 + + - name: Setup NodeJS + id: setup-node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - uses: pnpm/action-setup@v2 + with: + version: 9.1.0 + + # See https://github.com/actions/setup-node/issues/641#issuecomment-1358859686 + - name: pnpm cache path + id: pnpm-cache-path + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: pnpm cache + uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-cache-path.outputs.STORE_PATH }} + key: ${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile - name: Run tests - run: >- - docker run - --network host - --name framework-jest-tests - --env TEST_AGENT_PUBLIC_DID_SEED=${TEST_AGENT_PUBLIC_DID_SEED} - --env GENESIS_TXN_PATH=${GENESIS_TXN_PATH} - aries-framework-javascript - yarn test --coverage - - - name: Export logs - if: always() + run: pnpm test:unit --coverage --forceExit --shard=${{ matrix.shard }}/2 + + # Upload coverage for shard + - run: mv coverage/coverage-final.json coverage/${{ matrix.shard }}.json + - uses: actions/upload-artifact@v4 + with: + name: coverage-artifacts + path: coverage/${{ matrix.shard }}.json + overwrite: true + + e2e-tests: + runs-on: ubuntu-20.04 + name: E2E Tests + + strategy: + fail-fast: false + matrix: + node-version: [18, 20] + + steps: + - name: Checkout credo + uses: actions/checkout@v4 + + # setup dependencies + - name: Setup services + run: docker compose up -d + + - name: Setup NodeJS + id: setup-node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - uses: pnpm/action-setup@v2 + with: + version: 9.1.0 + + # See https://github.com/actions/setup-node/issues/641#issuecomment-1358859686 + - name: pnpm cache path + id: pnpm-cache-path run: | - mkdir logs - docker cp alice-mediator:/www/logs.txt ./logs/alice-mediator.txt - docker cp bob-mediator:/www/logs.txt ./logs/bob-mediator.txt - docker cp framework-jest-tests:/www/logs.txt ./logs/jest.txt - - - name: Upload docker logs - uses: actions/upload-artifact@v1 - if: always() + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: pnpm cache + uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-cache-path.outputs.STORE_PATH }} + key: ${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests + run: pnpm test:e2e --coverage --forceExit + + # Upload coverage for e2e + - run: mv coverage/coverage-final.json coverage/e2e.json + - uses: actions/upload-artifact@v4 + with: + name: coverage-artifacts + path: coverage/e2e.json + overwrite: true + + # Upload all the coverage reports + report-coverage: + runs-on: ubuntu-20.04 + needs: [e2e-tests, unit-tests] + steps: + - uses: actions/download-artifact@v4 + with: + name: coverage-artifacts + path: coverage + + - uses: codecov/codecov-action@v4 with: - name: docker-logs - path: logs - - - name: Export test coverage - if: always() - run: docker cp framework-jest-tests:/www/coverage ./ - - uses: codecov/codecov-action@v1 - if: always() + directory: coverage diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml new file mode 100644 index 0000000000..06d8acabcd --- /dev/null +++ b/.github/workflows/lint-pr.yml @@ -0,0 +1,21 @@ +name: 'Lint PR' + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + # Please look up the latest version from + # https://github.com/amannn/action-semantic-pull-request/releases + - uses: amannn/action-semantic-pull-request@v5.5.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + validateSingleCommit: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..af90c043cc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,109 @@ +name: Release + +on: + push: + branches: + - main + - '**-pre' + +permissions: + pull-requests: write + contents: write + +jobs: + release: + name: Release + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v2 + with: + version: 9.1.0 + + - name: Setup Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: 'pnpm' + + - name: Install Dependencies + run: pnpm install --frozen-lockfile + + - name: Create Release Pull Request or Publish to npm + id: changesets + uses: changesets/action@v1 + with: + # This expects you to have a script called release which does a build for your packages and calls changeset publish + publish: pnpm release + title: 'chore(release): new version' + createGithubReleases: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_PUBLISH }} + + - name: Get current package version + id: get_version + run: echo "CURRENT_PACKAGE_VERSION=$(node -p "require('./packages/core/package.json').version")" >> $GITHUB_ENV + + - name: Create Github Release + if: steps.changesets.outputs.published == 'true' + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ env.CURRENT_PACKAGE_VERSION }} + + release-unstable: + name: Release Unstable + runs-on: ubuntu-latest + if: "!startsWith(github.event.head_commit.message, 'chore(release): version')" + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v2 + with: + version: 9.1.0 + + - name: Setup Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: 'pnpm' + + - name: Install Dependencies + run: pnpm install --frozen-lockfile + + - name: Creating .npmrc + run: | + cat << EOF > ".npmrc" + //registry.npmjs.org/:_authToken=$NPM_TOKEN + EOF + env: + NPM_TOKEN: ${{ secrets.NPM_PUBLISH }} + + - name: Create unstable release + run: | + # this ensures there's always a patch release created + cat << 'EOF' > .changeset/snapshot-template-changeset.md + --- + '@credo-ts/core': patch + --- + + snapshot release + EOF + + pnpm changeset version --snapshot alpha + pnpm build + pnpm changeset publish --tag alpha + + CURRENT_PACKAGE_VERSION=$(node -p "require('./packages/core/package.json').version") + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag v$CURRENT_PACKAGE_VERSION + git push origin v$CURRENT_PACKAGE_VERSION --no-verify + + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_PUBLISH }} diff --git a/.github/workflows/repolinter.yml b/.github/workflows/repolinter.yml new file mode 100644 index 0000000000..b6414ecd49 --- /dev/null +++ b/.github/workflows/repolinter.yml @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +# Hyperledger Repolinter Action + +name: Repolinter + +on: + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + container: ghcr.io/todogroup/repolinter:v0.10.1 + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Lint Repo + run: bundle exec /app/bin/repolinter.js --rulesetUrl https://raw.githubusercontent.com/hyperledger-labs/hyperledger-community-management-tools/master/repo_structure/repolint.json diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000000..9b306aacd4 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,51 @@ +name: OpenSSF Scorecard supply-chain security +on: + schedule: + - cron: '00 08 * * 5' + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + + steps: + - name: 'Checkout code' + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + + - name: 'Run analysis' + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + with: + results_file: results.sarif + results_format: sarif + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: 'Upload artifact' + uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: 'Upload to code-scanning' + uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index 81c887a990..cf4a81ebb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ node_modules build .vscode -yarn-error.log .idea -aries-framework-javascript-*.tgz -src/lib/__tests__/genesis-von.txn +credo-*.tgz +# Keeping this one in for now to prevent accidental +# old build still in the local repository getting pushed +aries-framework-*.tgz coverage .DS_Store logs.txt diff --git a/.husky/.gitignore b/.husky/.gitignore deleted file mode 100644 index 31354ec138..0000000000 --- a/.husky/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_ diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100755 index b969aaaf8d..0000000000 --- a/.husky/pre-push +++ /dev/null @@ -1 +0,0 @@ -yarn validate \ No newline at end of file diff --git a/.node-dev.json b/.node-dev.json deleted file mode 100644 index 5c2556cc9b..0000000000 --- a/.node-dev.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "notify": false -} diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index dae199aecb..0000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v12 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..ad06de8f8e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +node_modules +build +.idea +coverage +pnpm-lock.yaml \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index c94fbd2805..cbe842acd7 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,5 @@ { "printWidth": 120, - "semi": true, - "singleQuote": true, - "trailingComma": "es5", - "arrowParens": "avoid" + "semi": false, + "singleQuote": true } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..a0405b59d6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,787 @@ +# Changelog + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +### Bug Fixes + +- allow did document for didcomm without authentication or keyAgreement ([#1848](https://github.com/openwallet-foundation/credo-ts/issues/1848)) ([5d986f0](https://github.com/openwallet-foundation/credo-ts/commit/5d986f0da67de78b4df2ad7ab92eeb2bdf9f2c83)) +- **anoncreds:** migration script credential id ([#1849](https://github.com/openwallet-foundation/credo-ts/issues/1849)) ([e58ec5b](https://github.com/openwallet-foundation/credo-ts/commit/e58ec5bd97043d57fcc3c5a4aee926943e6c5326)) +- cheqd create from did document ([#1850](https://github.com/openwallet-foundation/credo-ts/issues/1850)) ([dcd028e](https://github.com/openwallet-foundation/credo-ts/commit/dcd028ea04863bf9bc93e6bd2f73c6d2a70f274b)) +- store recipient keys by default ([#1847](https://github.com/openwallet-foundation/credo-ts/issues/1847)) ([e9238cf](https://github.com/openwallet-foundation/credo-ts/commit/e9238cfde4d76c5b927f6f76b3529d4c80808a3a)) + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +### Bug Fixes + +- access token can only be used for offer ([#1828](https://github.com/openwallet-foundation/credo-ts/issues/1828)) ([f54b90b](https://github.com/openwallet-foundation/credo-ts/commit/f54b90b0530b43a04df6299a39414a142d73276e)) +- **anoncreds:** credential exchange record migration ([#1844](https://github.com/openwallet-foundation/credo-ts/issues/1844)) ([93b3986](https://github.com/openwallet-foundation/credo-ts/commit/93b3986348a86365c3a2faf8023a51390528df93)) +- **anoncreds:** unqualified revocation registry processing ([#1833](https://github.com/openwallet-foundation/credo-ts/issues/1833)) ([edc5735](https://github.com/openwallet-foundation/credo-ts/commit/edc5735ccb663acabe8b8480f36cc3a72a1cf63d)) +- close tenant session after migration ([#1835](https://github.com/openwallet-foundation/credo-ts/issues/1835)) ([eb2c513](https://github.com/openwallet-foundation/credo-ts/commit/eb2c51384c077038e6cd38c1ab737d0d47c1b81e)) +- node-ffi-napi compatibility ([#1821](https://github.com/openwallet-foundation/credo-ts/issues/1821)) ([81d351b](https://github.com/openwallet-foundation/credo-ts/commit/81d351bc9d4d508ebfac9e7f2b2f10276ab1404a)) +- oid4vp can be used separate from idtoken ([#1827](https://github.com/openwallet-foundation/credo-ts/issues/1827)) ([ca383c2](https://github.com/openwallet-foundation/credo-ts/commit/ca383c284e2073992a1fd280fca99bee1c2e19f8)) +- **openid4vc:** update verified state for more states ([#1831](https://github.com/openwallet-foundation/credo-ts/issues/1831)) ([958bf64](https://github.com/openwallet-foundation/credo-ts/commit/958bf647c086a2ca240e9ad140defc39b7f20f43)) +- remove mediation keys after hangup ([#1843](https://github.com/openwallet-foundation/credo-ts/issues/1843)) ([9c3b950](https://github.com/openwallet-foundation/credo-ts/commit/9c3b9507ec5e33d155cebf9fab97703267b549bd)) +- udpate cheqd deps ([#1830](https://github.com/openwallet-foundation/credo-ts/issues/1830)) ([6b4b71b](https://github.com/openwallet-foundation/credo-ts/commit/6b4b71bf365262e8c2c9718547b60c44f2afc920)) +- update cheqd to 2.4.2 ([#1817](https://github.com/openwallet-foundation/credo-ts/issues/1817)) ([8154df4](https://github.com/openwallet-foundation/credo-ts/commit/8154df45f45bd9da0c60abe3792ff0f081e81818)) + +### Features + +- add disclosures so you know which fields are disclosed ([#1834](https://github.com/openwallet-foundation/credo-ts/issues/1834)) ([6ec43eb](https://github.com/openwallet-foundation/credo-ts/commit/6ec43eb1f539bd8d864d5bbd2ab35459809255ec)) +- apply new version of SD JWT package ([#1787](https://github.com/openwallet-foundation/credo-ts/issues/1787)) ([b41e158](https://github.com/openwallet-foundation/credo-ts/commit/b41e158098773d2f59b5b5cfb82cc6be06a57acd)) +- did rotate event ([#1840](https://github.com/openwallet-foundation/credo-ts/issues/1840)) ([d16bebb](https://github.com/openwallet-foundation/credo-ts/commit/d16bebb7d63bfbad90cedea3c6b4fb3ec20a4be1)) +- openid4vc issued state per credential ([#1829](https://github.com/openwallet-foundation/credo-ts/issues/1829)) ([229c621](https://github.com/openwallet-foundation/credo-ts/commit/229c62177c04060c7ca4c19dfd35bab328035067)) +- queued messages reception time ([#1824](https://github.com/openwallet-foundation/credo-ts/issues/1824)) ([0b4b8dd](https://github.com/openwallet-foundation/credo-ts/commit/0b4b8dd42117eb8e92fcc4be695ff149b49a06c7)) +- sort requested credentials ([#1839](https://github.com/openwallet-foundation/credo-ts/issues/1839)) ([b46c7fa](https://github.com/openwallet-foundation/credo-ts/commit/b46c7fa459d7e1a81744353bf595c754fad1b3a1)) +- support invitationDid when creating an invitation ([#1811](https://github.com/openwallet-foundation/credo-ts/issues/1811)) ([e5c6698](https://github.com/openwallet-foundation/credo-ts/commit/e5c66988e75fd9a5f047fd96774c0bf494061cbc)) +- **tenants:** return value from withTenatnAgent ([#1832](https://github.com/openwallet-foundation/credo-ts/issues/1832)) ([8371d87](https://github.com/openwallet-foundation/credo-ts/commit/8371d8728685295a1f648ca677cc6de2cb873c09)) + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +### Bug Fixes + +- anoncreds w3c migration metadata ([#1803](https://github.com/openwallet-foundation/credo-ts/issues/1803)) ([069c9c4](https://github.com/openwallet-foundation/credo-ts/commit/069c9c4fe362ee6c8af233df154d2d9b2c0f2d44)) +- **cheqd:** do not crash agent if cheqd down ([#1808](https://github.com/openwallet-foundation/credo-ts/issues/1808)) ([842efd4](https://github.com/openwallet-foundation/credo-ts/commit/842efd4512748a0787ce331add394426b3b07943)) +- import of websocket ([#1804](https://github.com/openwallet-foundation/credo-ts/issues/1804)) ([48b31ae](https://github.com/openwallet-foundation/credo-ts/commit/48b31ae9229cd188defb0ed3b4e64b0346013f3d)) +- **openid4vc:** several fixes and improvements ([#1795](https://github.com/openwallet-foundation/credo-ts/issues/1795)) ([b83c517](https://github.com/openwallet-foundation/credo-ts/commit/b83c5173070594448d92f801331b3a31c7ac8049)) +- remove strict w3c subjectId uri validation ([#1805](https://github.com/openwallet-foundation/credo-ts/issues/1805)) ([65f7611](https://github.com/openwallet-foundation/credo-ts/commit/65f7611b7668d3242b4526831f442c68d6cfbea8)) +- unsubscribe from emitter after pickup completion ([#1806](https://github.com/openwallet-foundation/credo-ts/issues/1806)) ([9fb6ae0](https://github.com/openwallet-foundation/credo-ts/commit/9fb6ae0005f11197eefdb864aa8a7cf3b79357f0)) + +### Features + +- **anoncreds:** expose methods and metadata ([#1797](https://github.com/openwallet-foundation/credo-ts/issues/1797)) ([5992c57](https://github.com/openwallet-foundation/credo-ts/commit/5992c57a34d3b48dfa86cb659c77af498b6e8708)) +- credentials api decline offer report ([#1800](https://github.com/openwallet-foundation/credo-ts/issues/1800)) ([15c62a8](https://github.com/openwallet-foundation/credo-ts/commit/15c62a8e20df7189ae8068e3ff42bf7e20a38ad5)) + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +### Bug Fixes + +- abandon proof protocol if presentation fails ([#1610](https://github.com/openwallet-foundation/credo-ts/issues/1610)) ([b2ba7c7](https://github.com/openwallet-foundation/credo-ts/commit/b2ba7c7197139e780cbb95eed77dc0a2ad3b3210)) +- **anoncreds:** allow for zero idx to be used for revocation ([#1742](https://github.com/openwallet-foundation/credo-ts/issues/1742)) ([a1b9901](https://github.com/openwallet-foundation/credo-ts/commit/a1b9901b8bb232560118c902d86464e28d8a73fa)) +- **anoncreds:** only store the revocation registry definition when the state is finished ([#1735](https://github.com/openwallet-foundation/credo-ts/issues/1735)) ([f7785c5](https://github.com/openwallet-foundation/credo-ts/commit/f7785c52b814dfa01c6d16dbecfcc937d533b710)) +- **anoncreds:** pass along options for registry and status list ([#1734](https://github.com/openwallet-foundation/credo-ts/issues/1734)) ([e4b99a8](https://github.com/openwallet-foundation/credo-ts/commit/e4b99a86c76a1a4a41aebb94da0b57f774dd6aaf)) +- **core:** allow string for did document controller ([#1644](https://github.com/openwallet-foundation/credo-ts/issues/1644)) ([ed874ce](https://github.com/openwallet-foundation/credo-ts/commit/ed874ce38ed1a1a0f01b12958e5b14823661b06a)) +- **core:** query credential and proof records by correct DIDComm role ([#1780](https://github.com/openwallet-foundation/credo-ts/issues/1780)) ([add7e09](https://github.com/openwallet-foundation/credo-ts/commit/add7e091e845fdaddaf604335f19557f47a31079)) +- did:peer:2 creation and parsing ([#1752](https://github.com/openwallet-foundation/credo-ts/issues/1752)) ([7c60918](https://github.com/openwallet-foundation/credo-ts/commit/7c609183b2da16f2a698646ac39b03c2ab44318e)) +- **indy-vdr:** for creating latest delta ([#1737](https://github.com/openwallet-foundation/credo-ts/issues/1737)) ([68f0d70](https://github.com/openwallet-foundation/credo-ts/commit/68f0d70b9fd2b7acc8b6120b23b65144c93af391)) +- jsonld document loader node 18 ([#1454](https://github.com/openwallet-foundation/credo-ts/issues/1454)) ([3656d49](https://github.com/openwallet-foundation/credo-ts/commit/3656d4902fb832e5e75142b1846074d4f39c11a2)) +- **present-proof:** isolated tests ([#1696](https://github.com/openwallet-foundation/credo-ts/issues/1696)) ([1d33377](https://github.com/openwallet-foundation/credo-ts/commit/1d333770dcc9e261446b43b5f4cd5626fa7ac4a7)) +- presentation submission format ([#1792](https://github.com/openwallet-foundation/credo-ts/issues/1792)) ([1a46e9f](https://github.com/openwallet-foundation/credo-ts/commit/1a46e9f02599ed8b2bf36f5b9d3951d143852f03)) +- properly print key class ([#1684](https://github.com/openwallet-foundation/credo-ts/issues/1684)) ([99b801d](https://github.com/openwallet-foundation/credo-ts/commit/99b801dfb6edcd3b7baaa8108ad361be4e05ff67)) +- query the record by credential and proof role ([#1784](https://github.com/openwallet-foundation/credo-ts/issues/1784)) ([d2b5cd9](https://github.com/openwallet-foundation/credo-ts/commit/d2b5cd9cbbfa95cbdcde9a4fed3305bab6161faf)) +- remove check for DifPresentationExchangeService dependency ([#1702](https://github.com/openwallet-foundation/credo-ts/issues/1702)) ([93d9d8b](https://github.com/openwallet-foundation/credo-ts/commit/93d9d8bb3a93e47197a2c01998807523d783b0bf)) +- **rn:** more flexible react native version ([#1760](https://github.com/openwallet-foundation/credo-ts/issues/1760)) ([af82918](https://github.com/openwallet-foundation/credo-ts/commit/af82918f5401bad113dfc32fc903d981e4389c4e)) +- save AnonCredsCredentialRecord createdAt ([#1603](https://github.com/openwallet-foundation/credo-ts/issues/1603)) ([a1942f8](https://github.com/openwallet-foundation/credo-ts/commit/a1942f8a8dffb11558dcbb900cbeb052e7d0227e)) +- some log messages ([#1636](https://github.com/openwallet-foundation/credo-ts/issues/1636)) ([d40bfd1](https://github.com/openwallet-foundation/credo-ts/commit/d40bfd1b96001870a3a1553cb9d6faaefe71e364)) +- stopped recvRequest from receiving outbound messages ([#1786](https://github.com/openwallet-foundation/credo-ts/issues/1786)) ([2005566](https://github.com/openwallet-foundation/credo-ts/commit/20055668765e1070cbf4db13a598e3e0d7881599)) +- support all minor versions handshake ([#1711](https://github.com/openwallet-foundation/credo-ts/issues/1711)) ([40063e0](https://github.com/openwallet-foundation/credo-ts/commit/40063e06ff6afc139516459e81e85b36195985ca)) +- unused imports ([#1733](https://github.com/openwallet-foundation/credo-ts/issues/1733)) ([e0b971e](https://github.com/openwallet-foundation/credo-ts/commit/e0b971e86b506bb78dafa21f76ae3b193abe9a9d)) +- w3c anoncreds ([#1791](https://github.com/openwallet-foundation/credo-ts/issues/1791)) ([913596c](https://github.com/openwallet-foundation/credo-ts/commit/913596c4e843855f77a490428c55daac220bc8c6)) +- websocket outbound transport ([#1788](https://github.com/openwallet-foundation/credo-ts/issues/1788)) ([ed06d00](https://github.com/openwallet-foundation/credo-ts/commit/ed06d002c2c3d1f35b6790b8624cda0e506cf7d4)) + +- feat(indy-vdr)!: include config in getAllPoolTransactions (#1770) ([29c589d](https://github.com/openwallet-foundation/credo-ts/commit/29c589dd2f5b6da0a6bed129b5f733851785ccba)), closes [#1770](https://github.com/openwallet-foundation/credo-ts/issues/1770) + +### Features + +- add credo logo ([#1717](https://github.com/openwallet-foundation/credo-ts/issues/1717)) ([c7886cb](https://github.com/openwallet-foundation/credo-ts/commit/c7886cb8377ceb8ee4efe8d264211e561a75072d)) +- add goal codes to v2 protocols ([#1739](https://github.com/openwallet-foundation/credo-ts/issues/1739)) ([c5c5b85](https://github.com/openwallet-foundation/credo-ts/commit/c5c5b850f27e66f7a2e39acd5fc14267babee208)) +- add Multikey as supported vm type ([#1720](https://github.com/openwallet-foundation/credo-ts/issues/1720)) ([5562cb1](https://github.com/openwallet-foundation/credo-ts/commit/5562cb1751643eee16b4bf3304a5178a394a7f15)) +- add secp256k1 diddoc and verification method ([#1736](https://github.com/openwallet-foundation/credo-ts/issues/1736)) ([f245386](https://github.com/openwallet-foundation/credo-ts/commit/f245386eef2e0daad7a5c948df29625f60a020ea)) +- add some default contexts ([#1741](https://github.com/openwallet-foundation/credo-ts/issues/1741)) ([0bec03c](https://github.com/openwallet-foundation/credo-ts/commit/0bec03c3b97590a1484e8b803401569998655b87)) +- add support for key type k256 ([#1722](https://github.com/openwallet-foundation/credo-ts/issues/1722)) ([22d5bff](https://github.com/openwallet-foundation/credo-ts/commit/22d5bffc939f6644f324f6ddba4c8269212e9dc4)) +- anoncreds w3c migration ([#1744](https://github.com/openwallet-foundation/credo-ts/issues/1744)) ([d7c2bbb](https://github.com/openwallet-foundation/credo-ts/commit/d7c2bbb4fde57cdacbbf1ed40c6bd1423f7ab015)) +- **anoncreds:** issue revocable credentials ([#1427](https://github.com/openwallet-foundation/credo-ts/issues/1427)) ([c59ad59](https://github.com/openwallet-foundation/credo-ts/commit/c59ad59fbe63b6d3760d19030e0f95fb2ea8488a)) +- bump indy-vdr version ([#1637](https://github.com/openwallet-foundation/credo-ts/issues/1637)) ([a641a96](https://github.com/openwallet-foundation/credo-ts/commit/a641a9699b7816825a88f2c883c9e65aaa4c0f87)) +- did rotate ([#1699](https://github.com/openwallet-foundation/credo-ts/issues/1699)) ([adc7d4e](https://github.com/openwallet-foundation/credo-ts/commit/adc7d4ecfea9be5f707ab7b50d19dbe7690c6d25)) +- did:peer:2 and did:peer:4 support in DID Exchange ([#1550](https://github.com/openwallet-foundation/credo-ts/issues/1550)) ([edf493d](https://github.com/openwallet-foundation/credo-ts/commit/edf493dd7e707543af5bbdbf6daba2b02c74158d)) +- **indy-vdr:** ability to refresh the pool manually ([#1623](https://github.com/openwallet-foundation/credo-ts/issues/1623)) ([0865ea5](https://github.com/openwallet-foundation/credo-ts/commit/0865ea52fb99103fba0cc71cb118f0eb3fb909e4)) +- **indy-vdr:** register revocation registry definitions and status list ([#1693](https://github.com/openwallet-foundation/credo-ts/issues/1693)) ([ee34fe7](https://github.com/openwallet-foundation/credo-ts/commit/ee34fe71780a0787db96e28575eeedce3b4704bd)) +- **mesage-pickup:** option for awaiting completion ([#1755](https://github.com/openwallet-foundation/credo-ts/issues/1755)) ([faa390f](https://github.com/openwallet-foundation/credo-ts/commit/faa390f2e2bb438596b5d9e3a69e1442f551ff1e)) +- New developer quality of life updates ([#1766](https://github.com/openwallet-foundation/credo-ts/issues/1766)) ([3c58ae0](https://github.com/openwallet-foundation/credo-ts/commit/3c58ae04a6cfec5841d510dda576c974cd491853)) +- **openid4vc:** persistance and events ([#1793](https://github.com/openwallet-foundation/credo-ts/issues/1793)) ([f4c386a](https://github.com/openwallet-foundation/credo-ts/commit/f4c386a6ccf8adb829cad30b81d524e6ffddb029)) +- optional backup on storage migration ([#1745](https://github.com/openwallet-foundation/credo-ts/issues/1745)) ([81ff63c](https://github.com/openwallet-foundation/credo-ts/commit/81ff63ccf7c71eccf342899d298a780d66045534)) +- **present-proof:** add support for aries RFC 510 ([#1676](https://github.com/openwallet-foundation/credo-ts/issues/1676)) ([40c9bb6](https://github.com/openwallet-foundation/credo-ts/commit/40c9bb6e9efe6cceb62c79d34366edf77ba84b0d)) +- **presentation-exchange:** added PresentationExchangeService ([#1672](https://github.com/openwallet-foundation/credo-ts/issues/1672)) ([50db5c7](https://github.com/openwallet-foundation/credo-ts/commit/50db5c7d207130b80e38ce5d94afb9e3b96f2fb1)) +- **sd-jwt-vc:** Module for Issuer, Holder and verifier ([#1607](https://github.com/openwallet-foundation/credo-ts/issues/1607)) ([ec3182d](https://github.com/openwallet-foundation/credo-ts/commit/ec3182d9934319b761649edb4c80ede2dd46dbd4)) +- sped up lookup for revocation registries ([#1605](https://github.com/openwallet-foundation/credo-ts/issues/1605)) ([32ef8c5](https://github.com/openwallet-foundation/credo-ts/commit/32ef8c5a002c2cfe209c72e01f95b43337922fc6)) +- support DRPC protocol ([#1753](https://github.com/openwallet-foundation/credo-ts/issues/1753)) ([4f58925](https://github.com/openwallet-foundation/credo-ts/commit/4f58925dc3adb6bae1ab2a24e00b461e9c4881b9)) +- support short legacy connectionless invitations ([#1705](https://github.com/openwallet-foundation/credo-ts/issues/1705)) ([34a6c9f](https://github.com/openwallet-foundation/credo-ts/commit/34a6c9f185d7b177956e5e2c5d79408e52915136)) +- **tenants:** expose get all tenants on public API ([#1731](https://github.com/openwallet-foundation/credo-ts/issues/1731)) ([f11f8fd](https://github.com/openwallet-foundation/credo-ts/commit/f11f8fdf7748b015a6f321fb16da2b075e1267ca)) +- **tenants:** support for tenant storage migration ([#1747](https://github.com/openwallet-foundation/credo-ts/issues/1747)) ([12c617e](https://github.com/openwallet-foundation/credo-ts/commit/12c617efb45d20fda8965b9b4da24c92e975c9a2)) +- update dockerfile to node 18 and sample mediator to askar ([#1622](https://github.com/openwallet-foundation/credo-ts/issues/1622)) ([1785479](https://github.com/openwallet-foundation/credo-ts/commit/178547906b092bc9f102a37cd99a139ffb4b907d)) + +### BREAKING CHANGES + +- `IndyVdrApi.getAllPoolTransactions()` now returns an array of objects containing transactions and config of each pool + +``` +{ + config: IndyVdrPoolConfig; + transactions: Transactions; +} +``` + +## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) + +### Bug Fixes + +- **askar:** throw error if imported wallet exists ([#1593](https://github.com/hyperledger/aries-framework-javascript/issues/1593)) ([c2bb2a5](https://github.com/hyperledger/aries-framework-javascript/commit/c2bb2a52f10add35de883c9a27716db01b9028df)) +- **cheqd:** changed the name formatting to a encoded hex value ([#1574](https://github.com/hyperledger/aries-framework-javascript/issues/1574)) ([d299f55](https://github.com/hyperledger/aries-framework-javascript/commit/d299f55113cb4c59273ae9fbbb8773b6f0009192)) +- **core:** remove node-fetch dependency ([#1578](https://github.com/hyperledger/aries-framework-javascript/issues/1578)) ([9ee2ce7](https://github.com/hyperledger/aries-framework-javascript/commit/9ee2ce7f0913510fc5b36aef1b7eeffb259b4aed)) +- do not send package via outdated session ([#1559](https://github.com/hyperledger/aries-framework-javascript/issues/1559)) ([de6a735](https://github.com/hyperledger/aries-framework-javascript/commit/de6a735a900b6d7444b17d79e63acaca19cb812a)) +- duplicate service ids in connections protocol ([#1589](https://github.com/hyperledger/aries-framework-javascript/issues/1589)) ([dd75be8](https://github.com/hyperledger/aries-framework-javascript/commit/dd75be88c4e257b6ca76868ceaeb3a8b7d67c185)) +- implicit invitation to specific service ([#1592](https://github.com/hyperledger/aries-framework-javascript/issues/1592)) ([4071dc9](https://github.com/hyperledger/aries-framework-javascript/commit/4071dc97b8ca779e6def3711a538ae821e1e513c)) +- log and throw on WebSocket sending errors ([#1573](https://github.com/hyperledger/aries-framework-javascript/issues/1573)) ([11050af](https://github.com/hyperledger/aries-framework-javascript/commit/11050afc7965adfa9b00107ba34abfbe3afaf874)) +- **oob:** support oob with connection and messages ([#1558](https://github.com/hyperledger/aries-framework-javascript/issues/1558)) ([9732ce4](https://github.com/hyperledger/aries-framework-javascript/commit/9732ce436a0ddee8760b02ac5182e216a75176c2)) +- service validation in OOB invitation objects ([#1575](https://github.com/hyperledger/aries-framework-javascript/issues/1575)) ([91a9434](https://github.com/hyperledger/aries-framework-javascript/commit/91a9434efd53ccbaf80f5613cd908913ad3b806b)) +- update tsyringe for ts 5 support ([#1588](https://github.com/hyperledger/aries-framework-javascript/issues/1588)) ([296955b](https://github.com/hyperledger/aries-framework-javascript/commit/296955b3a648416ac6b502da05a10001920af222)) + +### Features + +- allow connection invitation encoded in oob url param ([#1583](https://github.com/hyperledger/aries-framework-javascript/issues/1583)) ([9d789fa](https://github.com/hyperledger/aries-framework-javascript/commit/9d789fa4e9d159312872f45089d73609eb3d6835)) + +## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) + +### Bug Fixes + +- **anoncreds:** wrong key name for predicates in proof object ([#1517](https://github.com/hyperledger/aries-framework-javascript/issues/1517)) ([d895c78](https://github.com/hyperledger/aries-framework-javascript/commit/d895c78e0e02954a95ad1fd7e2251ee9a02445dc)) +- **askar:** in memory wallet creation ([#1498](https://github.com/hyperledger/aries-framework-javascript/issues/1498)) ([4a158e6](https://github.com/hyperledger/aries-framework-javascript/commit/4a158e64b97595be0733d4277c28c462bd47c908)) +- check if URL already encoded ([#1485](https://github.com/hyperledger/aries-framework-javascript/issues/1485)) ([38a0578](https://github.com/hyperledger/aries-framework-javascript/commit/38a0578011896cfcf217713d34f285cd381ad72c)) +- **cheqd:** make cosmos payer seed optional ([#1547](https://github.com/hyperledger/aries-framework-javascript/issues/1547)) ([9377378](https://github.com/hyperledger/aries-framework-javascript/commit/9377378b0124bf2f593342dba95a13ea5d8944c8)) +- create message subscription first ([#1549](https://github.com/hyperledger/aries-framework-javascript/issues/1549)) ([93276de](https://github.com/hyperledger/aries-framework-javascript/commit/93276debeff1e56c9803e7700875c4254a48236b)) +- encode tails url ([#1479](https://github.com/hyperledger/aries-framework-javascript/issues/1479)) ([fd190b9](https://github.com/hyperledger/aries-framework-javascript/commit/fd190b96106ca4916539d96ff6c4ecef7833f148)) +- force did:key resolver/registrar presence ([#1535](https://github.com/hyperledger/aries-framework-javascript/issues/1535)) ([aaa13dc](https://github.com/hyperledger/aries-framework-javascript/commit/aaa13dc77d6d5133cd02e768e4173462fa65064a)) +- **indy-vdr:** role property not included in nym request ([#1488](https://github.com/hyperledger/aries-framework-javascript/issues/1488)) ([002be4f](https://github.com/hyperledger/aries-framework-javascript/commit/002be4f578729aed1c8ae337f3d2eeecce9e3725)) +- listen to incoming messages on agent initialize not constructor ([#1542](https://github.com/hyperledger/aries-framework-javascript/issues/1542)) ([8f2d593](https://github.com/hyperledger/aries-framework-javascript/commit/8f2d593bcda0bb2d7bea25ad06b9e37784961997)) +- priority sorting for didcomm services ([#1555](https://github.com/hyperledger/aries-framework-javascript/issues/1555)) ([80c37b3](https://github.com/hyperledger/aries-framework-javascript/commit/80c37b30eb9ac3b438288e14c252f79f619dd12f)) +- race condition singleton records ([#1495](https://github.com/hyperledger/aries-framework-javascript/issues/1495)) ([6c2dda5](https://github.com/hyperledger/aries-framework-javascript/commit/6c2dda544bf5f5d3a972a778c389340da6df97c4)) +- **samples:** mediator wallet and http transport ([#1508](https://github.com/hyperledger/aries-framework-javascript/issues/1508)) ([04a8058](https://github.com/hyperledger/aries-framework-javascript/commit/04a80589b19725fb493e51e52a7345915b2c7341)) +- **transport:** Use connection in WebSocket ID ([#1551](https://github.com/hyperledger/aries-framework-javascript/issues/1551)) ([8d2057f](https://github.com/hyperledger/aries-framework-javascript/commit/8d2057f3fe6f3ba236ba5a811b57a7256eae92bf)) + +### Features + +- **anoncreds:** auto create link secret ([#1521](https://github.com/hyperledger/aries-framework-javascript/issues/1521)) ([c6f03e4](https://github.com/hyperledger/aries-framework-javascript/commit/c6f03e49d79a33b1c4b459cef11add93dee051d0)) +- oob without handhsake improvements and routing ([#1511](https://github.com/hyperledger/aries-framework-javascript/issues/1511)) ([9e69cf4](https://github.com/hyperledger/aries-framework-javascript/commit/9e69cf441a75bf7a3c5556cf59e730ee3fce8c28)) +- support askar profiles for multi-tenancy ([#1538](https://github.com/hyperledger/aries-framework-javascript/issues/1538)) ([e448a2a](https://github.com/hyperledger/aries-framework-javascript/commit/e448a2a58dddff2cdf80c4549ea2d842a54b43d1)) +- **w3c:** add convenience methods to vc and vp ([#1477](https://github.com/hyperledger/aries-framework-javascript/issues/1477)) ([83cbfe3](https://github.com/hyperledger/aries-framework-javascript/commit/83cbfe38e788366b616dc244fe34cc49a5a4d331)) + +# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) + +### Bug Fixes + +- add reflect-metadata ([#1409](https://github.com/hyperledger/aries-framework-javascript/issues/1409)) ([692defa](https://github.com/hyperledger/aries-framework-javascript/commit/692defa45ffcb4f36b0fa36970c4dc27aa75317c)) +- **anoncreds-rs:** revocation status list as JSON ([#1422](https://github.com/hyperledger/aries-framework-javascript/issues/1422)) ([ec5c233](https://github.com/hyperledger/aries-framework-javascript/commit/ec5c2335394e2df6bd8717907f03e5d2a430e9f9)) +- **anoncreds-rs:** save revocation registry index ([#1351](https://github.com/hyperledger/aries-framework-javascript/issues/1351)) ([1bda3f0](https://github.com/hyperledger/aries-framework-javascript/commit/1bda3f0733a472b536059cee8d34e25fb04c9f2d)) +- **anoncreds:** Buffer not imported from core ([#1367](https://github.com/hyperledger/aries-framework-javascript/issues/1367)) ([c133538](https://github.com/hyperledger/aries-framework-javascript/commit/c133538356471a6a0887322a3f6245aa5193e7e4)) +- **anoncreds:** include prover_did for legacy indy ([#1342](https://github.com/hyperledger/aries-framework-javascript/issues/1342)) ([d38ecb1](https://github.com/hyperledger/aries-framework-javascript/commit/d38ecb14cb58f1eb78e01c91699bb990d805dc08)) +- **anoncreds:** make revocation status list inline with the spec ([#1421](https://github.com/hyperledger/aries-framework-javascript/issues/1421)) ([644e860](https://github.com/hyperledger/aries-framework-javascript/commit/644e860a05f40166e26c497a2e8619c9a38df11d)) +- **askar:** anoncrypt messages unpacking ([#1332](https://github.com/hyperledger/aries-framework-javascript/issues/1332)) ([1c6aeae](https://github.com/hyperledger/aries-framework-javascript/commit/1c6aeae31ac57e83f4059f3dba35ccb1ca36926e)) +- **askar:** custom error handling ([#1372](https://github.com/hyperledger/aries-framework-javascript/issues/1372)) ([c72ba14](https://github.com/hyperledger/aries-framework-javascript/commit/c72ba149bad3a4596f5818b28516f6286b9088bf)) +- **askar:** default key derivation method ([#1420](https://github.com/hyperledger/aries-framework-javascript/issues/1420)) ([7b59629](https://github.com/hyperledger/aries-framework-javascript/commit/7b5962917488cfd0c5adc170d3c3fc64aa82ef2c)) +- **askar:** generate nonce suitable for anoncreds ([#1295](https://github.com/hyperledger/aries-framework-javascript/issues/1295)) ([ecce0a7](https://github.com/hyperledger/aries-framework-javascript/commit/ecce0a71578f45f55743198a1f3699bd257dc74b)) +- connection id in sessions for new connections ([#1383](https://github.com/hyperledger/aries-framework-javascript/issues/1383)) ([0351eec](https://github.com/hyperledger/aries-framework-javascript/commit/0351eec52a9f5e581508819df3005be7b995e59e)) +- **connections:** store imageUrl when using DIDExchange ([#1433](https://github.com/hyperledger/aries-framework-javascript/issues/1433)) ([66afda2](https://github.com/hyperledger/aries-framework-javascript/commit/66afda2fe7311977047928e0b1c857ed2c5602c7)) +- **core:** repository event when calling deleteById ([#1356](https://github.com/hyperledger/aries-framework-javascript/issues/1356)) ([953069a](https://github.com/hyperledger/aries-framework-javascript/commit/953069a785f2a6b8d1e11123aab3a09aab1e65ff)) +- create new socket if socket state is 'closing' ([#1337](https://github.com/hyperledger/aries-framework-javascript/issues/1337)) ([da8f2ad](https://github.com/hyperledger/aries-framework-javascript/commit/da8f2ad36c386497b16075790a364faae50fcd47)) +- did cache key not being set correctly ([#1394](https://github.com/hyperledger/aries-framework-javascript/issues/1394)) ([1125e81](https://github.com/hyperledger/aries-framework-javascript/commit/1125e81962ffa752bf40fa8f7f4226e186f22013)) +- Emit RoutingCreated event for mediator routing record ([#1445](https://github.com/hyperledger/aries-framework-javascript/issues/1445)) ([4145957](https://github.com/hyperledger/aries-framework-javascript/commit/414595727d611ff774c4f404a4eeea509cf03a71)) +- expose indy pool configs and action menu messages ([#1333](https://github.com/hyperledger/aries-framework-javascript/issues/1333)) ([518e5e4](https://github.com/hyperledger/aries-framework-javascript/commit/518e5e4dfb59f9c0457bfd233409e9f4b3c429ee)) +- imports from core ([#1303](https://github.com/hyperledger/aries-framework-javascript/issues/1303)) ([3e02227](https://github.com/hyperledger/aries-framework-javascript/commit/3e02227a7b23677e9886eb1c03d1a3ec154947a9)) +- incorrect type for anoncreds registration ([#1396](https://github.com/hyperledger/aries-framework-javascript/issues/1396)) ([9f0f8f2](https://github.com/hyperledger/aries-framework-javascript/commit/9f0f8f21e7436c0a422d8c3a42a4cb601bcf7c77)) +- **indy-sdk:** import from core ([#1346](https://github.com/hyperledger/aries-framework-javascript/issues/1346)) ([254f661](https://github.com/hyperledger/aries-framework-javascript/commit/254f661c2e925b62dd07c3565099f9e226bd2b41)) +- **indy-vdr:** do not force indy-vdr version ([#1434](https://github.com/hyperledger/aries-framework-javascript/issues/1434)) ([8a933c0](https://github.com/hyperledger/aries-framework-javascript/commit/8a933c057e0c88870779bf8eb98b4684de4745de)) +- **indy-vdr:** export relevant packages from root ([#1291](https://github.com/hyperledger/aries-framework-javascript/issues/1291)) ([b570e0f](https://github.com/hyperledger/aries-framework-javascript/commit/b570e0f923fc46adef3ce20ee76a683a867b85f4)) +- isNewSocket logic ([#1355](https://github.com/hyperledger/aries-framework-javascript/issues/1355)) ([18abb18](https://github.com/hyperledger/aries-framework-javascript/commit/18abb18316f155d0375af477dedef9cdfdada70e)) +- issuance with unqualified identifiers ([#1431](https://github.com/hyperledger/aries-framework-javascript/issues/1431)) ([de90caf](https://github.com/hyperledger/aries-framework-javascript/commit/de90cafb8d12b7a940f881184cd745c4b5043cbc)) +- jsonld credential format identifier version ([#1412](https://github.com/hyperledger/aries-framework-javascript/issues/1412)) ([c46a6b8](https://github.com/hyperledger/aries-framework-javascript/commit/c46a6b81b8a1e28e05013c27ffe2eeaee4724130)) +- loosen base64 validation ([#1312](https://github.com/hyperledger/aries-framework-javascript/issues/1312)) ([af384e8](https://github.com/hyperledger/aries-framework-javascript/commit/af384e8a92f877c647999f9356b72a8017308230)) +- migration of link secret ([#1444](https://github.com/hyperledger/aries-framework-javascript/issues/1444)) ([9a43afe](https://github.com/hyperledger/aries-framework-javascript/commit/9a43afec7ea72a6fa8c6133f0fad05d8a3d2a595)) +- reference to indyLedgers in IndyXXXNotConfiguredError ([#1397](https://github.com/hyperledger/aries-framework-javascript/issues/1397)) ([d6e2ea2](https://github.com/hyperledger/aries-framework-javascript/commit/d6e2ea2194a4860265fe299ef8ee4cb4799ab1a6)) +- registered connection problem report message handler ([#1462](https://github.com/hyperledger/aries-framework-javascript/issues/1462)) ([d2d8ee0](https://github.com/hyperledger/aries-framework-javascript/commit/d2d8ee09c4eb6c050660b2bf9973195fd531df18)) +- remove `deleteOnFinish` and added documentation ([#1418](https://github.com/hyperledger/aries-framework-javascript/issues/1418)) ([c8b16a6](https://github.com/hyperledger/aries-framework-javascript/commit/c8b16a6fec8bb693e67e65709ded05d19fd1919f)) +- remove named capture groups ([#1378](https://github.com/hyperledger/aries-framework-javascript/issues/1378)) ([a4204ef](https://github.com/hyperledger/aries-framework-javascript/commit/a4204ef2db769de53d12f0d881d2c4422545c390)) +- remove scope check from response ([#1450](https://github.com/hyperledger/aries-framework-javascript/issues/1450)) ([7dd4061](https://github.com/hyperledger/aries-framework-javascript/commit/7dd406170c75801529daf4bebebde81e84a4cb79)) +- return HTTP 415 if unsupported content type ([#1313](https://github.com/hyperledger/aries-framework-javascript/issues/1313)) ([122cdde](https://github.com/hyperledger/aries-framework-javascript/commit/122cdde6982174a8e9cf70ef26a1393cb3912066)) +- **samples:** dummy module response message type ([#1321](https://github.com/hyperledger/aries-framework-javascript/issues/1321)) ([64a5da9](https://github.com/hyperledger/aries-framework-javascript/commit/64a5da937059d25e693e2491af329548b2975ef6)) +- seed and private key validation and return type in registrars ([#1324](https://github.com/hyperledger/aries-framework-javascript/issues/1324)) ([c0e5339](https://github.com/hyperledger/aries-framework-javascript/commit/c0e5339edfa32df92f23fb9c920796b4b59adf52)) +- set updateAt on records when updating a record ([#1272](https://github.com/hyperledger/aries-framework-javascript/issues/1272)) ([2669d7d](https://github.com/hyperledger/aries-framework-javascript/commit/2669d7dd3d7c0ddfd1108dfd65e6115dd3418500)) +- small issues with migration and WAL files ([#1443](https://github.com/hyperledger/aries-framework-javascript/issues/1443)) ([83cf387](https://github.com/hyperledger/aries-framework-javascript/commit/83cf387fa52bb51d8adb2d5fedc5111994d4dde1)) +- small updates to cheqd module and demo ([#1439](https://github.com/hyperledger/aries-framework-javascript/issues/1439)) ([61daf0c](https://github.com/hyperledger/aries-framework-javascript/commit/61daf0cb27de80a5e728e2e9dad13d729baf476c)) +- **tenant:** Correctly configure storage for multi tenant agents ([#1359](https://github.com/hyperledger/aries-framework-javascript/issues/1359)) ([7795975](https://github.com/hyperledger/aries-framework-javascript/commit/779597563a4236fdab851df9e102dca18ce2d4e4)), closes [hyperledger#1353](https://github.com/hyperledger/issues/1353) +- thread id improvements ([#1311](https://github.com/hyperledger/aries-framework-javascript/issues/1311)) ([229ed1b](https://github.com/hyperledger/aries-framework-javascript/commit/229ed1b9540ca0c9380b5cca6c763fefd6628960)) +- various anoncreds revocation fixes ([#1416](https://github.com/hyperledger/aries-framework-javascript/issues/1416)) ([d9cfc7d](https://github.com/hyperledger/aries-framework-javascript/commit/d9cfc7df6679d2008d66070a6c8a818440d066ab)) + +- refactor!: remove Dispatcher.registerMessageHandler (#1354) ([78ecf1e](https://github.com/hyperledger/aries-framework-javascript/commit/78ecf1ed959c9daba1c119d03f4596f1db16c57c)), closes [#1354](https://github.com/hyperledger/aries-framework-javascript/issues/1354) +- refactor!: set default outbound content type to didcomm v1 (#1314) ([4ab3b54](https://github.com/hyperledger/aries-framework-javascript/commit/4ab3b54e9db630a6ba022af6becdd7276692afc5)), closes [#1314](https://github.com/hyperledger/aries-framework-javascript/issues/1314) +- feat!: add data, cache and temp dirs to FileSystem (#1306) ([ff5596d](https://github.com/hyperledger/aries-framework-javascript/commit/ff5596d0631e93746494c017797d0191b6bdb0b1)), closes [#1306](https://github.com/hyperledger/aries-framework-javascript/issues/1306) + +### Features + +- 0.4.0 migration script ([#1392](https://github.com/hyperledger/aries-framework-javascript/issues/1392)) ([bc5455f](https://github.com/hyperledger/aries-framework-javascript/commit/bc5455f7b42612a2b85e504bc6ddd36283a42bfa)) +- add anoncreds-rs package ([#1275](https://github.com/hyperledger/aries-framework-javascript/issues/1275)) ([efe0271](https://github.com/hyperledger/aries-framework-javascript/commit/efe0271198f21f1307df0f934c380f7a5c720b06)) +- Add cheqd demo and localnet for tests ([#1435](https://github.com/hyperledger/aries-framework-javascript/issues/1435)) ([1ffb011](https://github.com/hyperledger/aries-framework-javascript/commit/1ffb0111fc3db170e5623d350cb912b22027387a)) +- Add cheqd-sdk module ([#1334](https://github.com/hyperledger/aries-framework-javascript/issues/1334)) ([b38525f](https://github.com/hyperledger/aries-framework-javascript/commit/b38525f3433e50418ea149949108b4218ac9ba2a)) +- add devcontainer support ([#1282](https://github.com/hyperledger/aries-framework-javascript/issues/1282)) ([4ac5332](https://github.com/hyperledger/aries-framework-javascript/commit/4ac533231ff8126c73ccc071adbf5a415fd3d6e9)) +- add fetch indy schema method ([#1290](https://github.com/hyperledger/aries-framework-javascript/issues/1290)) ([1d782f5](https://github.com/hyperledger/aries-framework-javascript/commit/1d782f54bbb4abfeb6b6db6cd4f7164501b6c3d9)) +- add initial askar package ([#1211](https://github.com/hyperledger/aries-framework-javascript/issues/1211)) ([f18d189](https://github.com/hyperledger/aries-framework-javascript/commit/f18d1890546f7d66571fe80f2f3fc1fead1cd4c3)) +- add message pickup module ([#1413](https://github.com/hyperledger/aries-framework-javascript/issues/1413)) ([a8439db](https://github.com/hyperledger/aries-framework-javascript/commit/a8439db90fd11e014b457db476e8327b6ced6358)) +- added endpoint setter to agent InitConfig ([#1278](https://github.com/hyperledger/aries-framework-javascript/issues/1278)) ([1d487b1](https://github.com/hyperledger/aries-framework-javascript/commit/1d487b1a7e11b3f18b5229ba580bd035a7f564a0)) +- allow sending problem report when declining a proof request ([#1408](https://github.com/hyperledger/aries-framework-javascript/issues/1408)) ([b35fec4](https://github.com/hyperledger/aries-framework-javascript/commit/b35fec433f8fab513be2b8b6d073f23c6371b2ee)) +- **anoncreds-rs:** use new API methods for json conversion ([#1373](https://github.com/hyperledger/aries-framework-javascript/issues/1373)) ([dd6c020](https://github.com/hyperledger/aries-framework-javascript/commit/dd6c02005135fb0260f589658643d68089233bab)) +- **anoncreds:** add anoncreds API ([#1232](https://github.com/hyperledger/aries-framework-javascript/issues/1232)) ([3a4c5ec](https://github.com/hyperledger/aries-framework-javascript/commit/3a4c5ecd940e49d4d192eef1d41f2aaedb34d85a)) +- **anoncreds:** add AnonCreds format services ([#1385](https://github.com/hyperledger/aries-framework-javascript/issues/1385)) ([5f71dc2](https://github.com/hyperledger/aries-framework-javascript/commit/5f71dc2b403f6cb0fc9bb13f35051d377c2d1250)) +- **anoncreds:** add getCredential(s) methods ([#1386](https://github.com/hyperledger/aries-framework-javascript/issues/1386)) ([2efc009](https://github.com/hyperledger/aries-framework-javascript/commit/2efc0097138585391940fbb2eb504e50df57ec87)) +- **anoncreds:** add legacy indy credential format ([#1220](https://github.com/hyperledger/aries-framework-javascript/issues/1220)) ([13f3740](https://github.com/hyperledger/aries-framework-javascript/commit/13f374079262168f90ec7de7c3393beb9651295c)) +- **anoncreds:** legacy indy proof format service ([#1283](https://github.com/hyperledger/aries-framework-javascript/issues/1283)) ([c72fd74](https://github.com/hyperledger/aries-framework-javascript/commit/c72fd7416f2c1bc0497a84036e16adfa80585e49)) +- **anoncreds:** store method name in records ([#1387](https://github.com/hyperledger/aries-framework-javascript/issues/1387)) ([47636b4](https://github.com/hyperledger/aries-framework-javascript/commit/47636b4a08ffbfa9a3f2a5a3c5aebda44f7d16c8)) +- **anoncreds:** support credential attribute value and marker ([#1369](https://github.com/hyperledger/aries-framework-javascript/issues/1369)) ([5559996](https://github.com/hyperledger/aries-framework-javascript/commit/555999686a831e6988564fd5c9c937fc1023f567)) +- **anoncreds:** use legacy prover did ([#1374](https://github.com/hyperledger/aries-framework-javascript/issues/1374)) ([c17013c](https://github.com/hyperledger/aries-framework-javascript/commit/c17013c808a278d624210ce9e4333860cd78fc19)) +- **askar:** import/export wallet support for SQLite ([#1377](https://github.com/hyperledger/aries-framework-javascript/issues/1377)) ([19cefa5](https://github.com/hyperledger/aries-framework-javascript/commit/19cefa54596a4e4848bdbe89306a884a5ce2e991)) +- basic message pthid/thid support ([#1381](https://github.com/hyperledger/aries-framework-javascript/issues/1381)) ([f27fb99](https://github.com/hyperledger/aries-framework-javascript/commit/f27fb9921e11e5bcd654611d97d9fa1c446bc2d5)) +- **cache:** add caching interface ([#1229](https://github.com/hyperledger/aries-framework-javascript/issues/1229)) ([25b2bcf](https://github.com/hyperledger/aries-framework-javascript/commit/25b2bcf81648100b572784e4489a288cc9da0557)) +- **core:** add W3cCredentialsApi ([c888736](https://github.com/hyperledger/aries-framework-javascript/commit/c888736cb6b51014e23f5520fbc4074cf0e49e15)) +- default return route ([#1327](https://github.com/hyperledger/aries-framework-javascript/issues/1327)) ([dbfebb4](https://github.com/hyperledger/aries-framework-javascript/commit/dbfebb4720da731dbe11efdccdd061d1da3d1323)) +- indy sdk aries askar migration script ([#1289](https://github.com/hyperledger/aries-framework-javascript/issues/1289)) ([4a6b99c](https://github.com/hyperledger/aries-framework-javascript/commit/4a6b99c617de06edbaf1cb07c8adfa8de9b3ec15)) +- **indy-vdr:** add indy-vdr package and indy vdr pool ([#1160](https://github.com/hyperledger/aries-framework-javascript/issues/1160)) ([e8d6ac3](https://github.com/hyperledger/aries-framework-javascript/commit/e8d6ac31a8e18847d99d7998bd7658439e48875b)) +- **indy-vdr:** add IndyVdrAnonCredsRegistry ([#1270](https://github.com/hyperledger/aries-framework-javascript/issues/1270)) ([d056316](https://github.com/hyperledger/aries-framework-javascript/commit/d056316712b5ee5c42a159816b5dda0b05ad84a8)) +- **indy-vdr:** did:sov resolver ([#1247](https://github.com/hyperledger/aries-framework-javascript/issues/1247)) ([b5eb08e](https://github.com/hyperledger/aries-framework-javascript/commit/b5eb08e99d7ea61adefb8c6c0c5c99c6c1ba1597)) +- **indy-vdr:** module registration ([#1285](https://github.com/hyperledger/aries-framework-javascript/issues/1285)) ([51030d4](https://github.com/hyperledger/aries-framework-javascript/commit/51030d43a7e3cca3da29c5add38e35f731576927)) +- **indy-vdr:** resolver and registrar for did:indy ([#1253](https://github.com/hyperledger/aries-framework-javascript/issues/1253)) ([efab8dd](https://github.com/hyperledger/aries-framework-javascript/commit/efab8ddfc34e47a3f0ffe35b55fa5018a7e96544)) +- **indy-vdr:** schema + credential definition endorsement ([#1451](https://github.com/hyperledger/aries-framework-javascript/issues/1451)) ([25b981b](https://github.com/hyperledger/aries-framework-javascript/commit/25b981b6e23d02409e90dabdccdccc8904d4e357)) +- **indy-vdr:** use [@hyperledger](https://github.com/hyperledger) packages ([#1252](https://github.com/hyperledger/aries-framework-javascript/issues/1252)) ([acdb20a](https://github.com/hyperledger/aries-framework-javascript/commit/acdb20a79d038fb4163d281ee8de0ccb649fdc32)) +- IndyVdrAnonCredsRegistry revocation methods ([#1328](https://github.com/hyperledger/aries-framework-javascript/issues/1328)) ([fb7ee50](https://github.com/hyperledger/aries-framework-javascript/commit/fb7ee5048c33d5335cd9f07cad3dffc60dee7376)) +- **oob:** implicit invitations ([#1348](https://github.com/hyperledger/aries-framework-javascript/issues/1348)) ([fd13bb8](https://github.com/hyperledger/aries-framework-javascript/commit/fd13bb87a9ce9efb73bd780bd076b1da867688c5)) +- **openid4vc-client:** openid authorization flow ([#1384](https://github.com/hyperledger/aries-framework-javascript/issues/1384)) ([996c08f](https://github.com/hyperledger/aries-framework-javascript/commit/996c08f8e32e58605408f5ed5b6d8116cea3b00c)) +- **openid4vc-client:** pre-authorized ([#1243](https://github.com/hyperledger/aries-framework-javascript/issues/1243)) ([3d86e78](https://github.com/hyperledger/aries-framework-javascript/commit/3d86e78a4df87869aa5df4e28b79cd91787b61fb)) +- **openid4vc:** jwt format and more crypto ([#1472](https://github.com/hyperledger/aries-framework-javascript/issues/1472)) ([bd4932d](https://github.com/hyperledger/aries-framework-javascript/commit/bd4932d34f7314a6d49097b6460c7570e1ebc7a8)) +- optional routing for legacy connectionless invitation ([#1271](https://github.com/hyperledger/aries-framework-javascript/issues/1271)) ([7f65ba9](https://github.com/hyperledger/aries-framework-javascript/commit/7f65ba999ad1f49065d24966a1d7f3b82264ea55)) +- outbound message send via session ([#1335](https://github.com/hyperledger/aries-framework-javascript/issues/1335)) ([582c711](https://github.com/hyperledger/aries-framework-javascript/commit/582c711728db12b7d38a0be2e9fa78dbf31b34c6)) +- **proofs:** sort credentials based on revocation ([#1225](https://github.com/hyperledger/aries-framework-javascript/issues/1225)) ([0f6d231](https://github.com/hyperledger/aries-framework-javascript/commit/0f6d2312471efab20f560782c171434f907b6b9d)) +- support for did:jwk and p-256, p-384, p-512 ([#1446](https://github.com/hyperledger/aries-framework-javascript/issues/1446)) ([700d3f8](https://github.com/hyperledger/aries-framework-javascript/commit/700d3f89728ce9d35e22519e505d8203a4c9031e)) +- support more key types in jws service ([#1453](https://github.com/hyperledger/aries-framework-javascript/issues/1453)) ([8a3f03e](https://github.com/hyperledger/aries-framework-javascript/commit/8a3f03eb0dffcf46635556defdcebe1d329cf428)) + +### BREAKING CHANGES + +- `Dispatcher.registerMessageHandler` has been removed in favour of `MessageHandlerRegistry.registerMessageHandler`. If you want to register message handlers in an extension module, you can use directly `agentContext.dependencyManager.registerMessageHandlers`. + +Signed-off-by: Ariel Gentile + +- Agent default outbound content type has been changed to DIDComm V1. If you want to use former behaviour, you can do it so by manually setting `didcommMimeType` in `Agent`'s init config: + +``` + const agent = new Agent({ config: { + ... + didCommMimeType: DidCommMimeType.V0 + }, ... }) +``` + +- Agent-produced files will now be divided in different system paths depending on their nature: data, temp and cache. Previously, they were located at a single location, defaulting to a temporary directory. + +If you specified a custom path in `FileSystem` object constructor, you now must provide an object containing `baseDataPath`, `baseTempPath` and `baseCachePath`. They can point to the same path, although it's recommended to specify different path to avoid future file clashes. + +## [0.3.3](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.2...v0.3.3) (2023-01-18) + +### Bug Fixes + +- fix typing issues with typescript 4.9 ([#1214](https://github.com/hyperledger/aries-framework-javascript/issues/1214)) ([087980f](https://github.com/hyperledger/aries-framework-javascript/commit/087980f1adf3ee0bc434ca9782243a62c6124444)) +- **openid4vc-client:** set package to private ([#1210](https://github.com/hyperledger/aries-framework-javascript/issues/1210)) ([c697716](https://github.com/hyperledger/aries-framework-javascript/commit/c697716bf1837b9fef307f60ff97f01d3d926728)) + +### Features + +- add anoncreds package ([#1118](https://github.com/hyperledger/aries-framework-javascript/issues/1118)) ([adba83d](https://github.com/hyperledger/aries-framework-javascript/commit/adba83d8df176288083969f2c3f975bbfc1acd9c)) +- add minimal oidc-client package ([#1197](https://github.com/hyperledger/aries-framework-javascript/issues/1197)) ([b6f89f9](https://github.com/hyperledger/aries-framework-javascript/commit/b6f89f943dc4417626f868ac9f43a3d890ab62c6)) +- adding trust ping events and trust ping command ([#1182](https://github.com/hyperledger/aries-framework-javascript/issues/1182)) ([fd006f2](https://github.com/hyperledger/aries-framework-javascript/commit/fd006f262a91f901e7f8a9c6e6882ea178230005)) +- **anoncreds:** add anoncreds registry service ([#1204](https://github.com/hyperledger/aries-framework-javascript/issues/1204)) ([86647e7](https://github.com/hyperledger/aries-framework-javascript/commit/86647e7f55c9a362f6ab500538c4de2112e42206)) +- **indy-sdk:** add indy-sdk package ([#1200](https://github.com/hyperledger/aries-framework-javascript/issues/1200)) ([9933b35](https://github.com/hyperledger/aries-framework-javascript/commit/9933b35a6aa4524caef8a885e71b742cd0d7186b)) + +## [0.3.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.1...v0.3.2) (2023-01-04) + +### Bug Fixes + +- **credentials:** typing if no modules provided ([#1188](https://github.com/hyperledger/aries-framework-javascript/issues/1188)) ([541356e](https://github.com/hyperledger/aries-framework-javascript/commit/541356e866bcd3ce06c69093d8cb6100dca4d09f)) + +## [0.3.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.0...v0.3.1) (2022-12-27) + +### Bug Fixes + +- missing migration script and exports ([#1184](https://github.com/hyperledger/aries-framework-javascript/issues/1184)) ([460510d](https://github.com/hyperledger/aries-framework-javascript/commit/460510db43a7c63fd8dc1c3614be03fd8772f63c)) + +# [0.3.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.5...v0.3.0) (2022-12-22) + +### Bug Fixes + +- **connections:** do not log AgentContext object ([#1085](https://github.com/hyperledger/aries-framework-javascript/issues/1085)) ([ef20f1e](https://github.com/hyperledger/aries-framework-javascript/commit/ef20f1ef420e5345825cc9e79f52ecfb191489fc)) +- **connections:** use new did for each connection from reusable invitation ([#1174](https://github.com/hyperledger/aries-framework-javascript/issues/1174)) ([c0569b8](https://github.com/hyperledger/aries-framework-javascript/commit/c0569b88c27ee7785cf150ee14a5f9ebcc99898b)) +- credential values encoding ([#1157](https://github.com/hyperledger/aries-framework-javascript/issues/1157)) ([0e89e6c](https://github.com/hyperledger/aries-framework-javascript/commit/0e89e6c9f4a3cdbf98c5d85de2e015becdc3e1fc)) +- **demo:** direct import to remove warnings ([#1094](https://github.com/hyperledger/aries-framework-javascript/issues/1094)) ([6747756](https://github.com/hyperledger/aries-framework-javascript/commit/674775692bd60b2a0d8a726fa0ed3603b4fc724e)) +- expose AttachmentData and DiscoverFeaturesEvents ([#1146](https://github.com/hyperledger/aries-framework-javascript/issues/1146)) ([e48f481](https://github.com/hyperledger/aries-framework-javascript/commit/e48f481024810a0eba17e32b995a8db0730bbcb1)) +- expose OutOfBandEvents ([#1151](https://github.com/hyperledger/aries-framework-javascript/issues/1151)) ([3c040b6](https://github.com/hyperledger/aries-framework-javascript/commit/3c040b68e0c8a7f5625df427a2ace28f0223bfbc)) +- invalid injection symbols in W3cCredService ([#786](https://github.com/hyperledger/aries-framework-javascript/issues/786)) ([38cb106](https://github.com/hyperledger/aries-framework-javascript/commit/38cb1065e6fbf46c676c7ad52e160b721cb1b4e6)) +- peer dependency for rn bbs signatures ([#785](https://github.com/hyperledger/aries-framework-javascript/issues/785)) ([c751e28](https://github.com/hyperledger/aries-framework-javascript/commit/c751e286aa11a1d2b9424ae23de5647efc5d536f)) +- **problem-report:** proper string interpolation ([#1120](https://github.com/hyperledger/aries-framework-javascript/issues/1120)) ([c4e9679](https://github.com/hyperledger/aries-framework-javascript/commit/c4e96799d8390225ba5aaecced19c79ec1f12fa8)) +- **proofs:** await shouldAutoRespond to correctly handle the check ([#1116](https://github.com/hyperledger/aries-framework-javascript/issues/1116)) ([f294129](https://github.com/hyperledger/aries-framework-javascript/commit/f294129821cd6fcb9b82d875f19cab5a63310b23)) +- **react-native:** move bbs dep to bbs package ([#1076](https://github.com/hyperledger/aries-framework-javascript/issues/1076)) ([c6762bb](https://github.com/hyperledger/aries-framework-javascript/commit/c6762bbe9d64ac5220915af3425d493e505dcc2c)) +- remove sensitive information from agent config toJSON() method ([#1112](https://github.com/hyperledger/aries-framework-javascript/issues/1112)) ([427a80f](https://github.com/hyperledger/aries-framework-javascript/commit/427a80f7759e029222119cf815a866fe9899a170)) +- **routing:** add connection type on mediation grant ([#1147](https://github.com/hyperledger/aries-framework-javascript/issues/1147)) ([979c695](https://github.com/hyperledger/aries-framework-javascript/commit/979c69506996fb1853e200b53d052d474f497bf1)) +- **routing:** async message pickup on init ([#1093](https://github.com/hyperledger/aries-framework-javascript/issues/1093)) ([15cfd91](https://github.com/hyperledger/aries-framework-javascript/commit/15cfd91d1c6ba8e3f8355db4c4941fcbd85382ac)) +- unable to resolve nodejs document loader in react native environment ([#1003](https://github.com/hyperledger/aries-framework-javascript/issues/1003)) ([5cdcfa2](https://github.com/hyperledger/aries-framework-javascript/commit/5cdcfa203e6d457f74250028678dbc3393d8eb5c)) +- use custom document loader in jsonld.frame ([#1119](https://github.com/hyperledger/aries-framework-javascript/issues/1119)) ([36d4656](https://github.com/hyperledger/aries-framework-javascript/commit/36d465669c6714b00167b17fe2924f3c53b5fa68)) +- **vc:** change pubKey input from Buffer to Uint8Array ([#935](https://github.com/hyperledger/aries-framework-javascript/issues/935)) ([80c3740](https://github.com/hyperledger/aries-framework-javascript/commit/80c3740f625328125fe8121035f2d83ce1dee6a5)) + +- refactor!: rename Handler to MessageHandler (#1161) ([5e48696](https://github.com/hyperledger/aries-framework-javascript/commit/5e48696ec16d88321f225628e6cffab243718b4c)), closes [#1161](https://github.com/hyperledger/aries-framework-javascript/issues/1161) +- feat!: use did:key in protocols by default (#1149) ([9f10da8](https://github.com/hyperledger/aries-framework-javascript/commit/9f10da85d8739f7be6c5e6624ba5f53a1d6a3116)), closes [#1149](https://github.com/hyperledger/aries-framework-javascript/issues/1149) +- feat(action-menu)!: move to separate package (#1049) ([e0df0d8](https://github.com/hyperledger/aries-framework-javascript/commit/e0df0d884b1a7816c7c638406606e45f6e169ff4)), closes [#1049](https://github.com/hyperledger/aries-framework-javascript/issues/1049) +- feat(question-answer)!: separate logic to a new module (#1040) ([97d3073](https://github.com/hyperledger/aries-framework-javascript/commit/97d3073aa9300900740c3e8aee8233d38849293d)), closes [#1040](https://github.com/hyperledger/aries-framework-javascript/issues/1040) +- feat!: agent module registration api (#955) ([82a17a3](https://github.com/hyperledger/aries-framework-javascript/commit/82a17a3a1eff61008b2e91695f6527501fe44237)), closes [#955](https://github.com/hyperledger/aries-framework-javascript/issues/955) +- feat!: Discover Features V2 (#991) ([273e353](https://github.com/hyperledger/aries-framework-javascript/commit/273e353f4b36ab5d2420356eb3a53dcfb1c59ec6)), closes [#991](https://github.com/hyperledger/aries-framework-javascript/issues/991) +- refactor!: module to api and module config (#943) ([7cbccb1](https://github.com/hyperledger/aries-framework-javascript/commit/7cbccb1ce9dae2cb1e4887220898f2f74cca8dbe)), closes [#943](https://github.com/hyperledger/aries-framework-javascript/issues/943) +- refactor!: add agent context (#920) ([b47cfcb](https://github.com/hyperledger/aries-framework-javascript/commit/b47cfcba1450cd1d6839bf8192d977bfe33f1bb0)), closes [#920](https://github.com/hyperledger/aries-framework-javascript/issues/920) + +### Features + +- add agent context provider ([#921](https://github.com/hyperledger/aries-framework-javascript/issues/921)) ([a1b1e5a](https://github.com/hyperledger/aries-framework-javascript/commit/a1b1e5a22fd4ab9ef593b5cd7b3c710afcab3142)) +- add base agent class ([#922](https://github.com/hyperledger/aries-framework-javascript/issues/922)) ([113a575](https://github.com/hyperledger/aries-framework-javascript/commit/113a5756ed1b630b3c05929d79f6afcceae4fa6a)) +- add dynamic suite and signing provider ([#949](https://github.com/hyperledger/aries-framework-javascript/issues/949)) ([ab8b8ef](https://github.com/hyperledger/aries-framework-javascript/commit/ab8b8ef1357c7a8dc338eaea16b20d93a0c92d4f)) +- add indynamespace for ledger id for anoncreds ([#965](https://github.com/hyperledger/aries-framework-javascript/issues/965)) ([df3777e](https://github.com/hyperledger/aries-framework-javascript/commit/df3777ee394211a401940bf27b3e5a9e1688f6b2)) +- add present proof v2 ([#979](https://github.com/hyperledger/aries-framework-javascript/issues/979)) ([f38ac05](https://github.com/hyperledger/aries-framework-javascript/commit/f38ac05875e38b6cc130bcb9f603e82657aabe9c)) +- bbs createKey, sign and verify ([#684](https://github.com/hyperledger/aries-framework-javascript/issues/684)) ([5f91738](https://github.com/hyperledger/aries-framework-javascript/commit/5f91738337fac1efbbb4597e7724791e542f0762)) +- **bbs:** extract bbs logic into separate module ([#1035](https://github.com/hyperledger/aries-framework-javascript/issues/1035)) ([991151b](https://github.com/hyperledger/aries-framework-javascript/commit/991151bfff829fa11cd98a1951be9b54a77385a8)) +- **dids:** add did registrar ([#953](https://github.com/hyperledger/aries-framework-javascript/issues/953)) ([93f3c93](https://github.com/hyperledger/aries-framework-javascript/commit/93f3c93310f9dae032daa04a920b7df18e2f8a65)) +- fetch verification method types by proof type ([#913](https://github.com/hyperledger/aries-framework-javascript/issues/913)) ([ed69dac](https://github.com/hyperledger/aries-framework-javascript/commit/ed69dac7784feea7abe430ad685911faa477fa11)) +- issue credentials v2 (W3C/JSON-LD) ([#1092](https://github.com/hyperledger/aries-framework-javascript/issues/1092)) ([574e6a6](https://github.com/hyperledger/aries-framework-javascript/commit/574e6a62ebbd77902c50da821afdfd1b1558abe7)) +- jsonld-credential support ([#718](https://github.com/hyperledger/aries-framework-javascript/issues/718)) ([ea34c47](https://github.com/hyperledger/aries-framework-javascript/commit/ea34c4752712efecf3367c5a5fc4b06e66c1e9d7)) +- **ledger:** smart schema and credential definition registration ([#900](https://github.com/hyperledger/aries-framework-javascript/issues/900)) ([1e708e9](https://github.com/hyperledger/aries-framework-javascript/commit/1e708e9aeeb63977a7305999a5027d9743a56f91)) +- **oob:** receive Invitation with timeout ([#1156](https://github.com/hyperledger/aries-framework-javascript/issues/1156)) ([9352fa5](https://github.com/hyperledger/aries-framework-javascript/commit/9352fa5eea1e01d29acd0757298398aac45fcab2)) +- **proofs:** add getRequestedCredentialsForProofRequest ([#1028](https://github.com/hyperledger/aries-framework-javascript/issues/1028)) ([26bb9c9](https://github.com/hyperledger/aries-framework-javascript/commit/26bb9c9989a97bf22859a7eccbeabc632521a6c2)) +- **proofs:** delete associated didcomm messages ([#1021](https://github.com/hyperledger/aries-framework-javascript/issues/1021)) ([dba46c3](https://github.com/hyperledger/aries-framework-javascript/commit/dba46c3bc3a1d6b5669f296f0c45cd03dc2294b1)) +- **proofs:** proof negotiation ([#1131](https://github.com/hyperledger/aries-framework-javascript/issues/1131)) ([c752461](https://github.com/hyperledger/aries-framework-javascript/commit/c75246147ffc6be3c815c66b0a7ad66e48996568)) +- **proofs:** proofs module migration script for 0.3.0 ([#1020](https://github.com/hyperledger/aries-framework-javascript/issues/1020)) ([5e9e0fc](https://github.com/hyperledger/aries-framework-javascript/commit/5e9e0fcc7f13b8a27e35761464c8fd970c17d28c)) +- remove keys on mediator when deleting connections ([#1143](https://github.com/hyperledger/aries-framework-javascript/issues/1143)) ([1af57fd](https://github.com/hyperledger/aries-framework-javascript/commit/1af57fde5016300e243eafbbdea5ea26bd8ef313)) +- **routing:** add reconnection parameters to RecipientModuleConfig ([#1070](https://github.com/hyperledger/aries-framework-javascript/issues/1070)) ([d4fd1ae](https://github.com/hyperledger/aries-framework-javascript/commit/d4fd1ae16dc1fd99b043835b97b33f4baece6790)) +- specify httpinboundtransport path ([#1115](https://github.com/hyperledger/aries-framework-javascript/issues/1115)) ([03cdf39](https://github.com/hyperledger/aries-framework-javascript/commit/03cdf397b61253d2eb20694049baf74843b7ed92)) +- **tenants:** initial tenants module ([#932](https://github.com/hyperledger/aries-framework-javascript/issues/932)) ([7cbd08c](https://github.com/hyperledger/aries-framework-javascript/commit/7cbd08c9bb4b14ab2db92b0546d6fcb520f5fec9)) +- **tenants:** tenant lifecycle ([#942](https://github.com/hyperledger/aries-framework-javascript/issues/942)) ([adfa65b](https://github.com/hyperledger/aries-framework-javascript/commit/adfa65b13152a980ba24b03082446e91d8ec5b37)) +- **vc:** delete w3c credential record ([#886](https://github.com/hyperledger/aries-framework-javascript/issues/886)) ([be37011](https://github.com/hyperledger/aries-framework-javascript/commit/be37011c139c5cc69fc591060319d8c373e9508b)) +- **w3c:** add custom document loader option ([#1159](https://github.com/hyperledger/aries-framework-javascript/issues/1159)) ([ff6abdf](https://github.com/hyperledger/aries-framework-javascript/commit/ff6abdfc4e8ca64dd5a3b9859474bfc09e1a6c21)) + +### BREAKING CHANGES + +- Handler has been renamed to MessageHandler to be more descriptive, along with related types and methods. This means: + +Handler is now MessageHandler +HandlerInboundMessage is now MessageHandlerInboundMessage +Dispatcher.registerHandler is now Dispatcher.registerMessageHandlers + +- `useDidKeyInProtocols` configuration parameter is now enabled by default. If your agent only interacts with modern agents (e.g. AFJ 0.2.5 and newer) this will not represent any issue. Otherwise it is safer to explicitly set it to `false`. However, keep in mind that we expect this setting to be deprecated in the future, so we encourage you to update all your agents to use did:key. +- action-menu module has been removed from the core and moved to a separate package. To integrate it in an Agent instance, it can be injected in constructor like this: + +```ts +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + actionMenu: new ActionMenuModule(), + /* other custom modules */ + }, +}) +``` + +Then, module API can be accessed in `agent.modules.actionMenu`. + +- question-answer module has been removed from the core and moved to a separate package. To integrate it in an Agent instance, it can be injected in constructor like this: + +```ts +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + questionAnswer: new QuestionAnswerModule(), + /* other custom modules */ + }, +}) +``` + +Then, module API can be accessed in `agent.modules.questionAnswer`. + +- custom modules have been moved to the .modules namespace. In addition the agent constructor has been updated to a single options object that contains the `config` and `dependencies` properties. Instead of constructing the agent like this: + +```ts +const agent = new Agent( + { + /* config */ + }, + agentDependencies +) +``` + +You should now construct it like this: + +```ts +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, +}) +``` + +This allows for the new custom modules to be defined in the agent constructor. + +- - `queryFeatures` method parameters have been unified to a single `QueryFeaturesOptions` object that requires specification of Discover Features protocol to be used. + +* `isProtocolSupported` has been replaced by the more general synchronous mode of `queryFeatures`, which works when `awaitDisclosures` in options is set. Instead of returning a boolean, it returns an object with matching features +* Custom modules implementing protocols must register them in Feature Registry in order to let them be discovered by other agents (this can be done in module `register(dependencyManager, featureRegistry)` method) + +- All module api classes have been renamed from `XXXModule` to `XXXApi`. A module now represents a module plugin, and is separate from the API of a module. If you previously imported e.g. the `CredentialsModule` class, you should now import the `CredentialsApi` class +- To make AFJ multi-tenancy ready, all services and repositories have been made stateless. A new `AgentContext` is introduced that holds the current context, which is passed to each method call. The public API hasn't been affected, but due to the large impact of this change it is marked as breaking. + +## [0.2.5](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.4...v0.2.5) (2022-10-13) + +### Bug Fixes + +- **oob:** allow encoding in content type header ([#1037](https://github.com/hyperledger/aries-framework-javascript/issues/1037)) ([e1d6592](https://github.com/hyperledger/aries-framework-javascript/commit/e1d6592b818bc4348078ca6593eea4641caafae5)) +- **oob:** set connection alias when creating invitation ([#1047](https://github.com/hyperledger/aries-framework-javascript/issues/1047)) ([7be979a](https://github.com/hyperledger/aries-framework-javascript/commit/7be979a74b86c606db403c8df04cfc8be2aae249)) + +### Features + +- connection type ([#994](https://github.com/hyperledger/aries-framework-javascript/issues/994)) ([0d14a71](https://github.com/hyperledger/aries-framework-javascript/commit/0d14a7157e2118592829109dbc5c793faee1e201)) +- expose findAllByQuery method in modules and services ([#1044](https://github.com/hyperledger/aries-framework-javascript/issues/1044)) ([9dd95e8](https://github.com/hyperledger/aries-framework-javascript/commit/9dd95e81770d3140558196d2b5b508723f918f04)) +- improve sending error handling ([#1045](https://github.com/hyperledger/aries-framework-javascript/issues/1045)) ([a230841](https://github.com/hyperledger/aries-framework-javascript/commit/a230841aa99102bcc8b60aa2a23040f13a929a6c)) +- possibility to set masterSecretId inside of WalletConfig ([#1043](https://github.com/hyperledger/aries-framework-javascript/issues/1043)) ([8a89ad2](https://github.com/hyperledger/aries-framework-javascript/commit/8a89ad2624922e5e5455f8881d1ccc656d6b33ec)) +- use did:key flag ([#1029](https://github.com/hyperledger/aries-framework-javascript/issues/1029)) ([8efade5](https://github.com/hyperledger/aries-framework-javascript/commit/8efade5b2a885f0767ac8b10cba8582fe9ff486a)) + +## [0.2.4](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.3...v0.2.4) (2022-09-10) + +### Bug Fixes + +- avoid crash when an unexpected message arrives ([#1019](https://github.com/hyperledger/aries-framework-javascript/issues/1019)) ([2cfadd9](https://github.com/hyperledger/aries-framework-javascript/commit/2cfadd9167438a9446d26b933aa64521d8be75e7)) +- **ledger:** check taa version instad of aml version ([#1013](https://github.com/hyperledger/aries-framework-javascript/issues/1013)) ([4ca56f6](https://github.com/hyperledger/aries-framework-javascript/commit/4ca56f6b677f45aa96c91b5c5ee8df210722609e)) +- **ledger:** remove poolConnected on pool close ([#1011](https://github.com/hyperledger/aries-framework-javascript/issues/1011)) ([f0ca8b6](https://github.com/hyperledger/aries-framework-javascript/commit/f0ca8b6346385fc8c4811fbd531aa25a386fcf30)) +- **question-answer:** question answer protocol state/role check ([#1001](https://github.com/hyperledger/aries-framework-javascript/issues/1001)) ([4b90e87](https://github.com/hyperledger/aries-framework-javascript/commit/4b90e876cc8377e7518e05445beb1a6b524840c4)) + +### Features + +- Action Menu protocol (Aries RFC 0509) implementation ([#974](https://github.com/hyperledger/aries-framework-javascript/issues/974)) ([60a8091](https://github.com/hyperledger/aries-framework-javascript/commit/60a8091d6431c98f764b2b94bff13ee97187b915)) +- **routing:** add settings to control back off strategy on mediator reconnection ([#1017](https://github.com/hyperledger/aries-framework-javascript/issues/1017)) ([543437c](https://github.com/hyperledger/aries-framework-javascript/commit/543437cd94d3023139b259ee04d6ad51cf653794)) + +## [0.2.3](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.2...v0.2.3) (2022-08-30) + +### Bug Fixes + +- export the KeyDerivationMethod ([#958](https://github.com/hyperledger/aries-framework-javascript/issues/958)) ([04ab1cc](https://github.com/hyperledger/aries-framework-javascript/commit/04ab1cca853284d144fd64d35e26e9dfe77d4a1b)) +- expose oob domain ([#990](https://github.com/hyperledger/aries-framework-javascript/issues/990)) ([dad975d](https://github.com/hyperledger/aries-framework-javascript/commit/dad975d9d9b658c6b37749ece2a91381e2a314c9)) +- **generic-records:** support custom id property ([#964](https://github.com/hyperledger/aries-framework-javascript/issues/964)) ([0f690a0](https://github.com/hyperledger/aries-framework-javascript/commit/0f690a0564a25204cacfae7cd958f660f777567e)) + +### Features + +- always initialize mediator ([#985](https://github.com/hyperledger/aries-framework-javascript/issues/985)) ([b699977](https://github.com/hyperledger/aries-framework-javascript/commit/b69997744ac9e30ffba22daac7789216d2683e36)) +- delete by record id ([#983](https://github.com/hyperledger/aries-framework-javascript/issues/983)) ([d8a30d9](https://github.com/hyperledger/aries-framework-javascript/commit/d8a30d94d336cf3417c2cd00a8110185dde6a106)) +- **ledger:** handle REQNACK response for write request ([#967](https://github.com/hyperledger/aries-framework-javascript/issues/967)) ([6468a93](https://github.com/hyperledger/aries-framework-javascript/commit/6468a9311c8458615871e1e85ba3f3b560453715)) +- OOB public did ([#930](https://github.com/hyperledger/aries-framework-javascript/issues/930)) ([c99f3c9](https://github.com/hyperledger/aries-framework-javascript/commit/c99f3c9152a79ca6a0a24fdc93e7f3bebbb9d084)) +- **proofs:** present proof as nested protocol ([#972](https://github.com/hyperledger/aries-framework-javascript/issues/972)) ([52247d9](https://github.com/hyperledger/aries-framework-javascript/commit/52247d997c5910924d3099c736dd2e20ec86a214)) +- **routing:** manual mediator pickup lifecycle management ([#989](https://github.com/hyperledger/aries-framework-javascript/issues/989)) ([69d4906](https://github.com/hyperledger/aries-framework-javascript/commit/69d4906a0ceb8a311ca6bdad5ed6d2048335109a)) +- **routing:** pickup v2 mediator role basic implementation ([#975](https://github.com/hyperledger/aries-framework-javascript/issues/975)) ([a989556](https://github.com/hyperledger/aries-framework-javascript/commit/a98955666853471d504f8a5c8c4623e18ba8c8ed)) +- **routing:** support promise in message repo ([#959](https://github.com/hyperledger/aries-framework-javascript/issues/959)) ([79c5d8d](https://github.com/hyperledger/aries-framework-javascript/commit/79c5d8d76512b641167bce46e82f34cf22bc285e)) + +## [0.2.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.1...v0.2.2) (2022-07-15) + +### Bug Fixes + +- no return routing and wait for ping ([#946](https://github.com/hyperledger/aries-framework-javascript/issues/946)) ([f48f3c1](https://github.com/hyperledger/aries-framework-javascript/commit/f48f3c18bcc550b5304f43d8564dbeb1192490e0)) + +### Features + +- **oob:** support fetching shortened invitation urls ([#840](https://github.com/hyperledger/aries-framework-javascript/issues/840)) ([60ee0e5](https://github.com/hyperledger/aries-framework-javascript/commit/60ee0e59bbcdf7fab0e5880a714f0ca61d5da508)) +- **routing:** support did:key in RFC0211 ([#950](https://github.com/hyperledger/aries-framework-javascript/issues/950)) ([dc45c01](https://github.com/hyperledger/aries-framework-javascript/commit/dc45c01a27fa68f8caacf3e51382c37f26b1d4fa)) + +## [0.2.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.0...v0.2.1) (2022-07-08) + +### Bug Fixes + +- clone record before emitting event ([#938](https://github.com/hyperledger/aries-framework-javascript/issues/938)) ([f907fe9](https://github.com/hyperledger/aries-framework-javascript/commit/f907fe99558dd77dc2f77696be2a1b846466ab95)) +- missing module exports ([#927](https://github.com/hyperledger/aries-framework-javascript/issues/927)) ([95f90a5](https://github.com/hyperledger/aries-framework-javascript/commit/95f90a5dbe16a90ecb697d164324db20115976ae)) +- **oob:** support legacy prefix in attachments ([#931](https://github.com/hyperledger/aries-framework-javascript/issues/931)) ([82863f3](https://github.com/hyperledger/aries-framework-javascript/commit/82863f326d95025c4c01349a4c14b37e6ff6a1db)) + +### Features + +- **credentials:** added credential sendProblemReport method ([#906](https://github.com/hyperledger/aries-framework-javascript/issues/906)) ([90dc7bb](https://github.com/hyperledger/aries-framework-javascript/commit/90dc7bbdb18a77e62026f4d837723ed9a208c19b)) +- initial plugin api ([#907](https://github.com/hyperledger/aries-framework-javascript/issues/907)) ([6d88aa4](https://github.com/hyperledger/aries-framework-javascript/commit/6d88aa4537ab2a9494ffea8cdfb4723cf915f291)) +- **oob:** allow to append attachments to invitations ([#926](https://github.com/hyperledger/aries-framework-javascript/issues/926)) ([4800700](https://github.com/hyperledger/aries-framework-javascript/commit/4800700e9f138f02e67c93e8882f45d723dd22cb)) +- **routing:** add routing service ([#909](https://github.com/hyperledger/aries-framework-javascript/issues/909)) ([6e51e90](https://github.com/hyperledger/aries-framework-javascript/commit/6e51e9023cca524252f40a18bf37ec81ec582a1a)) + +# [0.2.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.1.0...v0.2.0) (2022-06-24) + +### Bug Fixes + +- add BBS context to DidDoc ([#789](https://github.com/hyperledger/aries-framework-javascript/issues/789)) ([c8ca091](https://github.com/hyperledger/aries-framework-javascript/commit/c8ca091f22c58c8d5273be36908df0a188020ddb)) +- add oob state and role check ([#777](https://github.com/hyperledger/aries-framework-javascript/issues/777)) ([1c74618](https://github.com/hyperledger/aries-framework-javascript/commit/1c7461836578a62ec545de3a0c8fcdc7de2f4d8f)) +- agent isinitialized on shutdown ([#665](https://github.com/hyperledger/aries-framework-javascript/issues/665)) ([d1049e0](https://github.com/hyperledger/aries-framework-javascript/commit/d1049e0fe99665e7fff8c4f1fe89f7ce19ccce84)) +- allow agent without inbound endpoint to connect when using multi-use invitation ([#712](https://github.com/hyperledger/aries-framework-javascript/issues/712)) ([01c5bb3](https://github.com/hyperledger/aries-framework-javascript/commit/01c5bb3b67786fa7efa361d02bfddde7d113eacf)), closes [#483](https://github.com/hyperledger/aries-framework-javascript/issues/483) +- **basic-message:** assert connection is ready ([#657](https://github.com/hyperledger/aries-framework-javascript/issues/657)) ([9f9156c](https://github.com/hyperledger/aries-framework-javascript/commit/9f9156cb96a4e8d7013d4968359bd0858830f833)) +- check for "REQNACK" response from indy ledger ([#626](https://github.com/hyperledger/aries-framework-javascript/issues/626)) ([ce66f07](https://github.com/hyperledger/aries-framework-javascript/commit/ce66f0744976e8f2abfa05055bfa384f3d084321)) +- check proof request group names do not overlap ([#638](https://github.com/hyperledger/aries-framework-javascript/issues/638)) ([0731ccd](https://github.com/hyperledger/aries-framework-javascript/commit/0731ccd7683ab1e0e8057fbf3b909bdd3227da88)) +- clone record before emitting event ([#833](https://github.com/hyperledger/aries-framework-javascript/issues/833)) ([8192861](https://github.com/hyperledger/aries-framework-javascript/commit/819286190985934438cb236e8d3f4ea7145f0cec)) +- close session early if no return route ([#715](https://github.com/hyperledger/aries-framework-javascript/issues/715)) ([2e65408](https://github.com/hyperledger/aries-framework-javascript/commit/2e6540806f2d67bef16004f6e8398c5bf7a05bcf)) +- **connections:** allow ; to convert legacy did ([#882](https://github.com/hyperledger/aries-framework-javascript/issues/882)) ([448a29d](https://github.com/hyperledger/aries-framework-javascript/commit/448a29db44e5ec0b8f01d36ba139ac760654a635)) +- **connections:** didexchange to connection state ([#823](https://github.com/hyperledger/aries-framework-javascript/issues/823)) ([dda1bd3](https://github.com/hyperledger/aries-framework-javascript/commit/dda1bd33882f7915a0ef1720eff0b1804f2c946c)) +- **connections:** fix log of object in string ([#904](https://github.com/hyperledger/aries-framework-javascript/issues/904)) ([95d893e](https://github.com/hyperledger/aries-framework-javascript/commit/95d893e6f37014f14bb991c5f12a9da0f4d627ab)) +- **connections:** set image url in create request ([#896](https://github.com/hyperledger/aries-framework-javascript/issues/896)) ([8396965](https://github.com/hyperledger/aries-framework-javascript/commit/8396965bfb2922bd5606383c12788d9c60968918)) +- **core:** allow JSON as input for indy attributes ([#813](https://github.com/hyperledger/aries-framework-javascript/issues/813)) ([478fda3](https://github.com/hyperledger/aries-framework-javascript/commit/478fda3bb28171ce395bb67f25d2f2e3668c52b0)) +- **core:** error if unpacked message does not match JWE structure ([#639](https://github.com/hyperledger/aries-framework-javascript/issues/639)) ([c43cfaa](https://github.com/hyperledger/aries-framework-javascript/commit/c43cfaa340c6ea8f42f015f6f280cbaece8c58bb)) +- **core:** expose CredentialPreviewAttribute ([#796](https://github.com/hyperledger/aries-framework-javascript/issues/796)) ([65d7f15](https://github.com/hyperledger/aries-framework-javascript/commit/65d7f15cff3384c2f34e9b0c64fab574e6299484)) +- **core:** set tags in MediationRecord constructor ([#686](https://github.com/hyperledger/aries-framework-javascript/issues/686)) ([1b01bce](https://github.com/hyperledger/aries-framework-javascript/commit/1b01bceed3435fc7f92b051110fcc315bcac08f3)) +- credential preview attributes mismatch schema attributes ([#625](https://github.com/hyperledger/aries-framework-javascript/issues/625)) ([c0095b8](https://github.com/hyperledger/aries-framework-javascript/commit/c0095b8ee855514c7b3c01010041e623458eb8de)) +- **credentials:** add missing issue credential v1 proposal attributes ([#798](https://github.com/hyperledger/aries-framework-javascript/issues/798)) ([966cc3d](https://github.com/hyperledger/aries-framework-javascript/commit/966cc3d178be7296f073eb815c36792e2137b64b)) +- **credentials:** default for credentials in exchange record ([#816](https://github.com/hyperledger/aries-framework-javascript/issues/816)) ([df1a00b](https://github.com/hyperledger/aries-framework-javascript/commit/df1a00b0968fa42dbaf606c9ec2325b778a0317d)) +- **credentials:** do not store offer attributes ([#892](https://github.com/hyperledger/aries-framework-javascript/issues/892)) ([39c4c0d](https://github.com/hyperledger/aries-framework-javascript/commit/39c4c0ddee5e8b9563b6f174a8ad808d4b9cf307)) +- **credentials:** indy cred attachment format ([#862](https://github.com/hyperledger/aries-framework-javascript/issues/862)) ([16935e2](https://github.com/hyperledger/aries-framework-javascript/commit/16935e2976252aac6bd67c5000779da1c5c1a828)) +- **credentials:** miscellaneous typing issues ([#880](https://github.com/hyperledger/aries-framework-javascript/issues/880)) ([ad35b08](https://github.com/hyperledger/aries-framework-javascript/commit/ad35b0826b5ee592b64d898fe629391bd34444aa)) +- **credentials:** parse and validate preview [@type](https://github.com/type) ([#861](https://github.com/hyperledger/aries-framework-javascript/issues/861)) ([1cc8f46](https://github.com/hyperledger/aries-framework-javascript/commit/1cc8f4661c666fb49625cf935877ff5e5d88b524)) +- **credentials:** proposal preview attribute ([#855](https://github.com/hyperledger/aries-framework-javascript/issues/855)) ([3022bd2](https://github.com/hyperledger/aries-framework-javascript/commit/3022bd2c37dac381f2045f5afab329bcc3806d26)) +- **credentials:** store revocation identifiers ([#864](https://github.com/hyperledger/aries-framework-javascript/issues/864)) ([7374799](https://github.com/hyperledger/aries-framework-javascript/commit/73747996dab4f7d63f616ebfc9758d0fcdffd3eb)) +- **credentials:** use interface in module api ([#856](https://github.com/hyperledger/aries-framework-javascript/issues/856)) ([58e6603](https://github.com/hyperledger/aries-framework-javascript/commit/58e6603ab925aa1f4f41673452b83ef75b538bdc)) +- delete credentials ([#766](https://github.com/hyperledger/aries-framework-javascript/issues/766)) ([cbdff28](https://github.com/hyperledger/aries-framework-javascript/commit/cbdff28d566e3eaabcb806d9158c62476379b5dd)) +- delete credentials ([#770](https://github.com/hyperledger/aries-framework-javascript/issues/770)) ([f1e0412](https://github.com/hyperledger/aries-framework-javascript/commit/f1e0412200fcc77ba928c0af2b099326f7a47ebf)) +- did sov service type resolving ([#689](https://github.com/hyperledger/aries-framework-javascript/issues/689)) ([dbcd8c4](https://github.com/hyperledger/aries-framework-javascript/commit/dbcd8c4ae88afd12098b55acccb70237a8d54cd7)) +- disallow floating promises ([#704](https://github.com/hyperledger/aries-framework-javascript/issues/704)) ([549647d](https://github.com/hyperledger/aries-framework-javascript/commit/549647db6b7492e593022dff1d4162efd2d95a39)) +- disallow usage of global buffer ([#601](https://github.com/hyperledger/aries-framework-javascript/issues/601)) ([87ecd8c](https://github.com/hyperledger/aries-framework-javascript/commit/87ecd8c622c6b602a23af9fa2ecc50820bce32f8)) +- do not import from src dir ([#748](https://github.com/hyperledger/aries-framework-javascript/issues/748)) ([1dfa32e](https://github.com/hyperledger/aries-framework-javascript/commit/1dfa32edc6029793588040de9b8b933a0615e926)) +- do not import test logger in src ([#746](https://github.com/hyperledger/aries-framework-javascript/issues/746)) ([5c80004](https://github.com/hyperledger/aries-framework-javascript/commit/5c80004228211a338c1358c99921a45c344a33bb)) +- do not use basic message id as record id ([#677](https://github.com/hyperledger/aries-framework-javascript/issues/677)) ([3713398](https://github.com/hyperledger/aries-framework-javascript/commit/3713398b87f732841db8131055d2437b0af9a435)) +- extract indy did from peer did in indy credential request ([#790](https://github.com/hyperledger/aries-framework-javascript/issues/790)) ([09e5557](https://github.com/hyperledger/aries-framework-javascript/commit/09e55574440e63418df0697067b9ffad11936027)) +- incorrect encoding of services for did:peer ([#610](https://github.com/hyperledger/aries-framework-javascript/issues/610)) ([28b1715](https://github.com/hyperledger/aries-framework-javascript/commit/28b1715e388f5ed15cb937712b663627c3619465)) +- **indy:** async ledger connection issues on iOS ([#803](https://github.com/hyperledger/aries-framework-javascript/issues/803)) ([8055652](https://github.com/hyperledger/aries-framework-javascript/commit/8055652e63309cf7b20676119b71d846b295d468)) +- issue where attributes and predicates match ([#640](https://github.com/hyperledger/aries-framework-javascript/issues/640)) ([15a5e6b](https://github.com/hyperledger/aries-framework-javascript/commit/15a5e6be73d1d752dbaef40fc26416e545f763a4)) +- leading zeros in credential value encoding ([#632](https://github.com/hyperledger/aries-framework-javascript/issues/632)) ([0d478a7](https://github.com/hyperledger/aries-framework-javascript/commit/0d478a7f198fec2ed5fceada77c9819ebab96a81)) +- mediation record checks for pickup v2 ([#736](https://github.com/hyperledger/aries-framework-javascript/issues/736)) ([2ad600c](https://github.com/hyperledger/aries-framework-javascript/commit/2ad600c066598526c421244cbe82bafc6cfbb85a)) +- miscellaneous issue credential v2 fixes ([#769](https://github.com/hyperledger/aries-framework-javascript/issues/769)) ([537b51e](https://github.com/hyperledger/aries-framework-javascript/commit/537b51efbf5ca1d50cd03e3ca4314da8b431c076)) +- **node:** allow to import node package without postgres ([#757](https://github.com/hyperledger/aries-framework-javascript/issues/757)) ([59e1058](https://github.com/hyperledger/aries-framework-javascript/commit/59e10589acee987fb46f9cbaa3583ba8dcd70b87)) +- **node:** only send 500 if no headers sent yet ([#857](https://github.com/hyperledger/aries-framework-javascript/issues/857)) ([4be8f82](https://github.com/hyperledger/aries-framework-javascript/commit/4be8f82c214f99538eaa0fd0aac5a8f7a6e1dd6b)) +- **oob:** allow legacy did sov prefix ([#889](https://github.com/hyperledger/aries-framework-javascript/issues/889)) ([c7766d0](https://github.com/hyperledger/aries-framework-javascript/commit/c7766d0454cb764b771bb1ef263e81210368588a)) +- **oob:** check service is string instance ([#814](https://github.com/hyperledger/aries-framework-javascript/issues/814)) ([bd1e677](https://github.com/hyperledger/aries-framework-javascript/commit/bd1e677f41a6d37f75746616681fc6d6ad7ca90e)) +- **oob:** export messages to public ([#828](https://github.com/hyperledger/aries-framework-javascript/issues/828)) ([10cf74d](https://github.com/hyperledger/aries-framework-javascript/commit/10cf74d473ce00dca4bc624d60f379e8a78f9b63)) +- **oob:** expose oob record ([#839](https://github.com/hyperledger/aries-framework-javascript/issues/839)) ([c297dfd](https://github.com/hyperledger/aries-framework-javascript/commit/c297dfd9cbdafcb2cdb1f7bcbd466c42f1b8e319)) +- **oob:** expose parseInvitation publicly ([#834](https://github.com/hyperledger/aries-framework-javascript/issues/834)) ([5767500](https://github.com/hyperledger/aries-framework-javascript/commit/5767500b3a797f794fc9ed8147e501e9566d2675)) +- **oob:** legacy invitation with multiple endpoint ([#825](https://github.com/hyperledger/aries-framework-javascript/issues/825)) ([8dd7f80](https://github.com/hyperledger/aries-framework-javascript/commit/8dd7f8049ea9c566b5c66b0c46c36f69e001ed3a)) +- optional fields in did document ([#726](https://github.com/hyperledger/aries-framework-javascript/issues/726)) ([2da845d](https://github.com/hyperledger/aries-framework-javascript/commit/2da845dd4c88c5e93fa9f02107d69f479946024f)) +- process ws return route messages serially ([#826](https://github.com/hyperledger/aries-framework-javascript/issues/826)) ([2831a8e](https://github.com/hyperledger/aries-framework-javascript/commit/2831a8ee1bcda649e33eb68b002890f6670f660e)) +- **proofs:** allow duplicates in proof attributes ([#848](https://github.com/hyperledger/aries-framework-javascript/issues/848)) ([ca6c1ce](https://github.com/hyperledger/aries-framework-javascript/commit/ca6c1ce82bb84a638f98977191b04a249633be76)) +- propose payload attachment in in snake_case JSON format ([#775](https://github.com/hyperledger/aries-framework-javascript/issues/775)) ([6c2dfdb](https://github.com/hyperledger/aries-framework-javascript/commit/6c2dfdb625f7a8f2504f8bc8cf878e01ee1c50cc)) +- relax validation of thread id in revocation notification ([#768](https://github.com/hyperledger/aries-framework-javascript/issues/768)) ([020e6ef](https://github.com/hyperledger/aries-framework-javascript/commit/020e6efa6e878401dede536dd99b3c9814d9541b)) +- remove deprecated multibase and multihash ([#674](https://github.com/hyperledger/aries-framework-javascript/issues/674)) ([3411f1d](https://github.com/hyperledger/aries-framework-javascript/commit/3411f1d20f09cab47b77bf9eb6b66cf135d19d4c)) +- remove unqualified did from out of band record ([#782](https://github.com/hyperledger/aries-framework-javascript/issues/782)) ([0c1423d](https://github.com/hyperledger/aries-framework-javascript/commit/0c1423d7203d92aea5440aac0488dae5dad6b05e)) +- remove usage of const enum ([#888](https://github.com/hyperledger/aries-framework-javascript/issues/888)) ([a7754bd](https://github.com/hyperledger/aries-framework-javascript/commit/a7754bd7bfeaac1ca30df8437554e041d4cf103e)) +- **routing:** also use pickup strategy from config ([#808](https://github.com/hyperledger/aries-framework-javascript/issues/808)) ([fd08ae3](https://github.com/hyperledger/aries-framework-javascript/commit/fd08ae3afaa334c4644aaacee2b6547f171d9d7d)) +- **routing:** mediation recipient role for recipient ([#661](https://github.com/hyperledger/aries-framework-javascript/issues/661)) ([88ad790](https://github.com/hyperledger/aries-framework-javascript/commit/88ad790d8291aaf9113f0de5c7b13563a4967ee7)) +- **routing:** remove sentTime from request message ([#670](https://github.com/hyperledger/aries-framework-javascript/issues/670)) ([1e9715b](https://github.com/hyperledger/aries-framework-javascript/commit/1e9715b894538f57e6ff3aa2d2e4225f8b2f7dc1)) +- **routing:** sending of trustping in pickup v2 ([#787](https://github.com/hyperledger/aries-framework-javascript/issues/787)) ([45b024d](https://github.com/hyperledger/aries-framework-javascript/commit/45b024d62d370e2c646b20993647740f314356e2)) +- send message to service ([#838](https://github.com/hyperledger/aries-framework-javascript/issues/838)) ([270c347](https://github.com/hyperledger/aries-framework-javascript/commit/270c3478f76ba5c3702377d78027afb71549de5c)) +- support pre-aip2 please ack decorator ([#835](https://github.com/hyperledger/aries-framework-javascript/issues/835)) ([a4bc215](https://github.com/hyperledger/aries-framework-javascript/commit/a4bc2158351129aef5281639bbb44127ebcf5ad8)) +- update inbound message validation ([#678](https://github.com/hyperledger/aries-framework-javascript/issues/678)) ([e383343](https://github.com/hyperledger/aries-framework-javascript/commit/e3833430104e3a0415194bd6f27d71c3b5b5ef9b)) +- verify jws contains at least 1 signature ([#600](https://github.com/hyperledger/aries-framework-javascript/issues/600)) ([9c96518](https://github.com/hyperledger/aries-framework-javascript/commit/9c965185de7908bdde1776369453cce384f9e82c)) + +### Code Refactoring + +- delete credentials by default when deleting exchange ([#767](https://github.com/hyperledger/aries-framework-javascript/issues/767)) ([656ed73](https://github.com/hyperledger/aries-framework-javascript/commit/656ed73b95d8a8483a38ff0b5462a4671cb82898)) +- do not add ~service in createOOBOffer method ([#772](https://github.com/hyperledger/aries-framework-javascript/issues/772)) ([a541949](https://github.com/hyperledger/aries-framework-javascript/commit/a541949c7dbf907e29eb798e60901b92fbec6443)) + +- chore!: update indy-sdk-react-native version to 0.2.0 (#754) ([4146778](https://github.com/hyperledger/aries-framework-javascript/commit/414677828be7f6c08fa02905d60d6555dc4dd438)), closes [#754](https://github.com/hyperledger/aries-framework-javascript/issues/754) + +### Features + +- 0.2.0 migration script for connections ([#773](https://github.com/hyperledger/aries-framework-javascript/issues/773)) ([0831b9b](https://github.com/hyperledger/aries-framework-javascript/commit/0831b9b451d8ac74a018fc525cdbac8ec9f6cd1c)) +- ability to add generic records ([#702](https://github.com/hyperledger/aries-framework-javascript/issues/702)) ([e617496](https://github.com/hyperledger/aries-framework-javascript/commit/e61749609a072f0f8d869e6c278d0a4a79938ee4)), closes [#688](https://github.com/hyperledger/aries-framework-javascript/issues/688) +- add didcomm message record ([#593](https://github.com/hyperledger/aries-framework-javascript/issues/593)) ([e547fb1](https://github.com/hyperledger/aries-framework-javascript/commit/e547fb1c0b01f821b5425bf9bb632e885f92b398)) +- add find and save/update methods to DidCommMessageRepository ([#620](https://github.com/hyperledger/aries-framework-javascript/issues/620)) ([beff6b0](https://github.com/hyperledger/aries-framework-javascript/commit/beff6b0ae0ad100ead1a4820ebf6c12fb3ad148d)) +- add generic did resolver ([#554](https://github.com/hyperledger/aries-framework-javascript/issues/554)) ([8e03f35](https://github.com/hyperledger/aries-framework-javascript/commit/8e03f35f8e1cd02dac4df02d1f80f2c5a921dfef)) +- add issue credential v2 ([#745](https://github.com/hyperledger/aries-framework-javascript/issues/745)) ([245223a](https://github.com/hyperledger/aries-framework-javascript/commit/245223acbc6f50de418b310025665e5c1316f1af)) +- add out-of-band and did exchange ([#717](https://github.com/hyperledger/aries-framework-javascript/issues/717)) ([16c6d60](https://github.com/hyperledger/aries-framework-javascript/commit/16c6d6080db93b5f4a86e81bdbd7a3e987728d82)) +- add question answer protocol ([#557](https://github.com/hyperledger/aries-framework-javascript/issues/557)) ([b5a2536](https://github.com/hyperledger/aries-framework-javascript/commit/b5a25364ff523214fc8e56a7133bfa5c1db9b935)) +- add role and method to did record tags ([#692](https://github.com/hyperledger/aries-framework-javascript/issues/692)) ([3b6504b](https://github.com/hyperledger/aries-framework-javascript/commit/3b6504ba6053c62f0841cb64a0e9a5be0e78bf80)) +- add support for did:peer ([#608](https://github.com/hyperledger/aries-framework-javascript/issues/608)) ([c5c4172](https://github.com/hyperledger/aries-framework-javascript/commit/c5c41722e9b626d7cea929faff562c2a69a079fb)) +- add support for signed attachments ([#595](https://github.com/hyperledger/aries-framework-javascript/issues/595)) ([eb49374](https://github.com/hyperledger/aries-framework-javascript/commit/eb49374c7ac7a61c10c8cb9079acffe689d0b402)) +- add update assistant for storage migrations ([#690](https://github.com/hyperledger/aries-framework-javascript/issues/690)) ([c9bff93](https://github.com/hyperledger/aries-framework-javascript/commit/c9bff93cfac43c4ae2cbcad1f96c1a74cde39602)) +- add validation to JSON transformer ([#830](https://github.com/hyperledger/aries-framework-javascript/issues/830)) ([5b9efe3](https://github.com/hyperledger/aries-framework-javascript/commit/5b9efe3b6fdaaec6dda387c542979e0e8fd51d5c)) +- add wallet key derivation method option ([#650](https://github.com/hyperledger/aries-framework-javascript/issues/650)) ([8386506](https://github.com/hyperledger/aries-framework-javascript/commit/83865067402466ffb51ba5008f52ea3e4169c31d)) +- add wallet module with import export ([#652](https://github.com/hyperledger/aries-framework-javascript/issues/652)) ([6cf5a7b](https://github.com/hyperledger/aries-framework-javascript/commit/6cf5a7b9de84dee1be61c315a734328ec209e87d)) +- **core:** add support for postgres wallet type ([#699](https://github.com/hyperledger/aries-framework-javascript/issues/699)) ([83ff0f3](https://github.com/hyperledger/aries-framework-javascript/commit/83ff0f36401cbf6e95c0a1ceb9fa921a82dc6830)) +- **core:** added timeOut to the module level ([#603](https://github.com/hyperledger/aries-framework-javascript/issues/603)) ([09950c7](https://github.com/hyperledger/aries-framework-javascript/commit/09950c706c0827a75eb93ffb05cc926f8472f66d)) +- **core:** allow to set auto accept connetion exchange when accepting invitation ([#589](https://github.com/hyperledger/aries-framework-javascript/issues/589)) ([2d95dce](https://github.com/hyperledger/aries-framework-javascript/commit/2d95dce70fb36dbbae459e17cfb0dea4dbbbe237)) +- **core:** generic repository events ([#842](https://github.com/hyperledger/aries-framework-javascript/issues/842)) ([74dd289](https://github.com/hyperledger/aries-framework-javascript/commit/74dd289669080b1406562ac575dd7c3c3d442e72)) +- **credentials:** add get format data method ([#877](https://github.com/hyperledger/aries-framework-javascript/issues/877)) ([521d489](https://github.com/hyperledger/aries-framework-javascript/commit/521d489cccaf9c4c3f3650ccf980a8dec0b8f729)) +- **credentials:** delete associated didCommMessages ([#870](https://github.com/hyperledger/aries-framework-javascript/issues/870)) ([1f8b6ab](https://github.com/hyperledger/aries-framework-javascript/commit/1f8b6aba9c34bd45ea61cdfdc5f7ab1e825368fc)) +- **credentials:** find didcomm message methods ([#887](https://github.com/hyperledger/aries-framework-javascript/issues/887)) ([dc12427](https://github.com/hyperledger/aries-framework-javascript/commit/dc12427bb308e53bb1c5749c61769b5f08c684c2)) +- delete credential from wallet ([#691](https://github.com/hyperledger/aries-framework-javascript/issues/691)) ([abec3a2](https://github.com/hyperledger/aries-framework-javascript/commit/abec3a2c95815d1c54b22a6370222f024eefb060)) +- extension module creation ([#688](https://github.com/hyperledger/aries-framework-javascript/issues/688)) ([2b6441a](https://github.com/hyperledger/aries-framework-javascript/commit/2b6441a2de5e9940bdf225b1ad9028cdfbf15cd5)) +- filter retrieved credential by revocation state ([#641](https://github.com/hyperledger/aries-framework-javascript/issues/641)) ([5912c0c](https://github.com/hyperledger/aries-framework-javascript/commit/5912c0ce2dbc8f773cec5324ffb19c40b15009b0)) +- indy revocation (prover & verifier) ([#592](https://github.com/hyperledger/aries-framework-javascript/issues/592)) ([fb19ff5](https://github.com/hyperledger/aries-framework-javascript/commit/fb19ff555b7c10c9409450dcd7d385b1eddf41ac)) +- **indy:** add choice for taa mechanism ([#849](https://github.com/hyperledger/aries-framework-javascript/issues/849)) ([ba03fa0](https://github.com/hyperledger/aries-framework-javascript/commit/ba03fa0c23f270274a592dfd6556a35adf387b51)) +- ledger connections happen on agent init in background ([#580](https://github.com/hyperledger/aries-framework-javascript/issues/580)) ([61695ce](https://github.com/hyperledger/aries-framework-javascript/commit/61695ce7737ffef363b60e341ae5b0e67e0e2c90)) +- pickup v2 protocol ([#711](https://github.com/hyperledger/aries-framework-javascript/issues/711)) ([b281673](https://github.com/hyperledger/aries-framework-javascript/commit/b281673b3503bb85ebda7afdd68b6d792d8f5bf5)) +- regex for schemaVersion, issuerDid, credDefId, schemaId, schemaIssuerDid ([#679](https://github.com/hyperledger/aries-framework-javascript/issues/679)) ([36b9d46](https://github.com/hyperledger/aries-framework-javascript/commit/36b9d466d400a0f87f6272bc428965601023581a)) +- **routing:** allow to discover mediator pickup strategy ([#669](https://github.com/hyperledger/aries-framework-javascript/issues/669)) ([5966da1](https://github.com/hyperledger/aries-framework-javascript/commit/5966da130873607a41919bbe1239e5e44afb47e4)) +- support advanced wallet query ([#831](https://github.com/hyperledger/aries-framework-javascript/issues/831)) ([28e0ffa](https://github.com/hyperledger/aries-framework-javascript/commit/28e0ffa151d41a39197f01bcc5f9c9834a0b2537)) +- support handling messages with different minor version ([#714](https://github.com/hyperledger/aries-framework-javascript/issues/714)) ([ad12360](https://github.com/hyperledger/aries-framework-javascript/commit/ad123602682214f02250e82a80ac7cf5255b8d12)) +- support new did document in didcomm message exchange ([#609](https://github.com/hyperledger/aries-framework-javascript/issues/609)) ([a1a3b7d](https://github.com/hyperledger/aries-framework-javascript/commit/a1a3b7d95a6e6656dc5630357ac4e692b33b49bc)) +- support revocation notification messages ([#579](https://github.com/hyperledger/aries-framework-javascript/issues/579)) ([9f04375](https://github.com/hyperledger/aries-framework-javascript/commit/9f04375edc5eaffa0aa3583efcf05c83d74987bb)) +- support wallet key rotation ([#672](https://github.com/hyperledger/aries-framework-javascript/issues/672)) ([5cd1598](https://github.com/hyperledger/aries-framework-javascript/commit/5cd1598b496a832c82f35a363fabe8f408abd439)) +- update recursive backoff & trust ping record updates ([#631](https://github.com/hyperledger/aries-framework-javascript/issues/631)) ([f64a9da](https://github.com/hyperledger/aries-framework-javascript/commit/f64a9da2ef9fda9693b23ddbd25bd885b88cdb1e)) + +### BREAKING CHANGES + +- **indy:** the transaction author agreement acceptance mechanism was previously automatically the first acceptance mechanism from the acceptance mechanism list. With this addition, the framework never automatically selects the acceptance mechanism anymore and it needs to be specified in the transactionAuthorAgreement in the indyLedgers agent config array. +- the credentials associated with a credential exchange record are now deleted by default when deleting a credential exchange record. If you only want to delete the credential exchange record and not the associated credentials, you can pass the deleteAssociatedCredentials to the deleteById method: + +```ts +await agent.credentials.deleteById('credentialExchangeId', { + deleteAssociatedCredentials: false, +}) +``` + +- with the addition of the out of band module `credentials.createOutOfBandOffer` is renamed to `credentials.createOffer` and no longer adds the `~service` decorator to the message. You need to call `oob.createLegacyConnectionlessInvitation` afterwards to use it for AIP-1 style connectionless exchanges. See [Migrating from AFJ 0.1.0 to 0.2.x](https://github.com/hyperledger/aries-framework-javascript/blob/main/docs/migration/0.1-to-0.2.md) for detailed migration instructions. +- the connections module has been extended with an out of band module and support for the DID Exchange protocol. Some methods have been moved to the out of band module, see [Migrating from AFJ 0.1.0 to 0.2.x](https://github.com/hyperledger/aries-framework-javascript/blob/main/docs/migration/0.1-to-0.2.md) for detailed migration instructions. +- indy-sdk-react-native has been updated to 0.2.0. The new version now depends on libindy version 1.16 and requires you to update the binaries in your react-native application. See the [indy-sdk-react-native](https://github.com/hyperledger/indy-sdk-react-native) repository for instructions on how to get the latest binaries for both iOS and Android. +- The mediator pickup strategy enum value `MediatorPickupStrategy.Explicit` has been renamed to `MediatorPickupStrategy.PickUpV1` to better align with the naming of the new `MediatorPickupStrategy.PickUpV2` +- attachment method `getDataAsJson` is now located one level up. So instead of `attachment.data.getDataAsJson()` you should now call `attachment.getDataAsJson()` + +# 0.1.0 (2021-12-23) + +### Bug Fixes + +- add details to connection signing error ([#484](https://github.com/hyperledger/aries-framework-javascript/issues/484)) ([e24eafd](https://github.com/hyperledger/aries-framework-javascript/commit/e24eafd83f53a9833b95bc3a4587cf825ee5d975)) +- add error message to log ([#342](https://github.com/hyperledger/aries-framework-javascript/issues/342)) ([a79e4f4](https://github.com/hyperledger/aries-framework-javascript/commit/a79e4f4556a9a9b59203cf529343c97cd418658b)) +- add option check to attribute constructor ([#450](https://github.com/hyperledger/aries-framework-javascript/issues/450)) ([8aad3e9](https://github.com/hyperledger/aries-framework-javascript/commit/8aad3e9f16c249e9f9291388ec8efc9bf27213c8)) +- Add samples folder as test root ([#215](https://github.com/hyperledger/aries-framework-javascript/issues/215)) ([b6a3c1c](https://github.com/hyperledger/aries-framework-javascript/commit/b6a3c1c47f00768e8b7ec1be8cca4c00a05fcf70)) +- added ariesframeworkerror to httpoutboundtransport ([#438](https://github.com/hyperledger/aries-framework-javascript/issues/438)) ([ee1a229](https://github.com/hyperledger/aries-framework-javascript/commit/ee1a229f8fc21739bca05c516a7b561f53726b91)) +- alter mediation recipient websocket transport priority ([#434](https://github.com/hyperledger/aries-framework-javascript/issues/434)) ([52c7897](https://github.com/hyperledger/aries-framework-javascript/commit/52c789724c731340daa8528b7d7b4b7fdcb40032)) +- check instance types of record properties ([#163](https://github.com/hyperledger/aries-framework-javascript/issues/163)) ([cc61c80](https://github.com/hyperledger/aries-framework-javascript/commit/cc61c8023bb5adbff599a6e0d563897ddb5e00dc)) +- connection record type was BaseRecord ([#278](https://github.com/hyperledger/aries-framework-javascript/issues/278)) ([515395d](https://github.com/hyperledger/aries-framework-javascript/commit/515395d847c492dd3b55cc44c94715de94a12bb8)) +- convert from buffer now also accepts uint8Array ([#283](https://github.com/hyperledger/aries-framework-javascript/issues/283)) ([dae123b](https://github.com/hyperledger/aries-framework-javascript/commit/dae123bc18f62f01c0962d099c88eed723dba972)) +- **core:** convert legacy prefix for inner msgs ([#479](https://github.com/hyperledger/aries-framework-javascript/issues/479)) ([a2b655a](https://github.com/hyperledger/aries-framework-javascript/commit/a2b655ac79bf0c7460671c8d31e92828e6f5ccf0)) +- **core:** do not throw error on timeout in http ([#512](https://github.com/hyperledger/aries-framework-javascript/issues/512)) ([4e73a7b](https://github.com/hyperledger/aries-framework-javascript/commit/4e73a7b0d9224bc102b396d821a8ea502a9a509d)) +- **core:** do not use did-communication service ([#402](https://github.com/hyperledger/aries-framework-javascript/issues/402)) ([cdf2edd](https://github.com/hyperledger/aries-framework-javascript/commit/cdf2eddc61e12f7ecd5a29e260eef82394d2e467)) +- **core:** export AgentMessage ([#480](https://github.com/hyperledger/aries-framework-javascript/issues/480)) ([af39ad5](https://github.com/hyperledger/aries-framework-javascript/commit/af39ad535320133ee38fc592309f42670a8517a1)) +- **core:** expose record metadata types ([#556](https://github.com/hyperledger/aries-framework-javascript/issues/556)) ([68995d7](https://github.com/hyperledger/aries-framework-javascript/commit/68995d7e2b049ff6496723d8a895e07b72fe72fb)) +- **core:** fix empty error log in console logger ([#524](https://github.com/hyperledger/aries-framework-javascript/issues/524)) ([7d9c541](https://github.com/hyperledger/aries-framework-javascript/commit/7d9c541de22fb2644455cf1949184abf3d8e528c)) +- **core:** improve wallet not initialized error ([#513](https://github.com/hyperledger/aries-framework-javascript/issues/513)) ([b948d4c](https://github.com/hyperledger/aries-framework-javascript/commit/b948d4c83b4eb0ab0594ae2117c0bb05b0955b21)) +- **core:** improved present-proof tests ([#482](https://github.com/hyperledger/aries-framework-javascript/issues/482)) ([41d9282](https://github.com/hyperledger/aries-framework-javascript/commit/41d9282ca561ca823b28f179d409c70a22d95e9b)) +- **core:** log errors if message is undeliverable ([#528](https://github.com/hyperledger/aries-framework-javascript/issues/528)) ([20b586d](https://github.com/hyperledger/aries-framework-javascript/commit/20b586db6eb9f92cce16d87d0dcfa4919f27ffa8)) +- **core:** remove isPositive validation decorators ([#477](https://github.com/hyperledger/aries-framework-javascript/issues/477)) ([e316e04](https://github.com/hyperledger/aries-framework-javascript/commit/e316e047b3e5aeefb929a5c47ad65d8edd4caba5)) +- **core:** remove unused url import ([#466](https://github.com/hyperledger/aries-framework-javascript/issues/466)) ([0f1323f](https://github.com/hyperledger/aries-framework-javascript/commit/0f1323f5bccc2dc3b67426525b161d7e578bb961)) +- **core:** requested predicates transform type ([#393](https://github.com/hyperledger/aries-framework-javascript/issues/393)) ([69684bc](https://github.com/hyperledger/aries-framework-javascript/commit/69684bc48a4002483662a211ec1ddd289dbaf59b)) +- **core:** send messages now takes a connection id ([#491](https://github.com/hyperledger/aries-framework-javascript/issues/491)) ([ed9db11](https://github.com/hyperledger/aries-framework-javascript/commit/ed9db11592b4948a1d313dbeb074e15d59503d82)) +- **core:** using query-string to parse URLs ([#457](https://github.com/hyperledger/aries-framework-javascript/issues/457)) ([78e5057](https://github.com/hyperledger/aries-framework-javascript/commit/78e505750557f296cc72ef19c0edd8db8e1eaa7d)) +- Correctly persist createdAt attribute ([#119](https://github.com/hyperledger/aries-framework-javascript/issues/119)) ([797a112](https://github.com/hyperledger/aries-framework-javascript/commit/797a112270dd67b75d9fe39dcf6753c64b049a39)), closes [#118](https://github.com/hyperledger/aries-framework-javascript/issues/118) +- date parsing ([#426](https://github.com/hyperledger/aries-framework-javascript/issues/426)) ([2d31b87](https://github.com/hyperledger/aries-framework-javascript/commit/2d31b87e99d04136f57cb457e2c67397ad65cc62)) +- export indy pool config ([#504](https://github.com/hyperledger/aries-framework-javascript/issues/504)) ([b1e2b8c](https://github.com/hyperledger/aries-framework-javascript/commit/b1e2b8c54e909927e5afa8b8212e0c8e156b97f7)) +- export module classes from framework root ([#315](https://github.com/hyperledger/aries-framework-javascript/issues/315)) ([a41cc75](https://github.com/hyperledger/aries-framework-javascript/commit/a41cc755f29887bc8ea7690791284ea9e375f5ce)) +- export ProofsModule to public API ([#325](https://github.com/hyperledger/aries-framework-javascript/issues/325)) ([f2e3a06](https://github.com/hyperledger/aries-framework-javascript/commit/f2e3a06d84bd40b5dcfa59f7b07bd77876fda861)) +- handle receive message promise rejection ([#318](https://github.com/hyperledger/aries-framework-javascript/issues/318)) ([ca6fb13](https://github.com/hyperledger/aries-framework-javascript/commit/ca6fb13eb9bf6c6218e3b042670fd1d41ff3dfd2)) +- include error when message cannot be handled ([#533](https://github.com/hyperledger/aries-framework-javascript/issues/533)) ([febfb05](https://github.com/hyperledger/aries-framework-javascript/commit/febfb05330c097aa918087ec3853a247d6a31b7c)) +- incorrect recip key with multi routing keys ([#446](https://github.com/hyperledger/aries-framework-javascript/issues/446)) ([db76823](https://github.com/hyperledger/aries-framework-javascript/commit/db76823400cfecc531575584ef7210af0c3b3e5c)) +- legacy did:sov prefix on invitation ([#216](https://github.com/hyperledger/aries-framework-javascript/issues/216)) ([dce3081](https://github.com/hyperledger/aries-framework-javascript/commit/dce308120045bb155d24b3b675621856937c0d2b)) +- make presentation proposal optional ([#197](https://github.com/hyperledger/aries-framework-javascript/issues/197)) ([1c5bfbd](https://github.com/hyperledger/aries-framework-javascript/commit/1c5bfbdf262323a5741b68c047161fd8af882839)) +- make records serializable ([#448](https://github.com/hyperledger/aries-framework-javascript/issues/448)) ([7e2946e](https://github.com/hyperledger/aries-framework-javascript/commit/7e2946eaa9e35083f3aa70c26c732a972f6eb12f)) +- mediator transports ([#419](https://github.com/hyperledger/aries-framework-javascript/issues/419)) ([87bc589](https://github.com/hyperledger/aries-framework-javascript/commit/87bc589695505de21294a1373afcf874fe8d22f6)) +- mediator updates ([#432](https://github.com/hyperledger/aries-framework-javascript/issues/432)) ([163cda1](https://github.com/hyperledger/aries-framework-javascript/commit/163cda19ba8437894a48c9bc948528ea0486ccdf)) +- monorepo release issues ([#386](https://github.com/hyperledger/aries-framework-javascript/issues/386)) ([89a628f](https://github.com/hyperledger/aries-framework-javascript/commit/89a628f7c3ea9e5730d2ba5720819ac6283ee404)) +- **node:** node v12 support for is-indy-installed ([#542](https://github.com/hyperledger/aries-framework-javascript/issues/542)) ([17e9157](https://github.com/hyperledger/aries-framework-javascript/commit/17e9157479d6bba90c2a94bce64697d7f65fac96)) +- proof configurable on proofRecord ([#397](https://github.com/hyperledger/aries-framework-javascript/issues/397)) ([8e83c03](https://github.com/hyperledger/aries-framework-javascript/commit/8e83c037e1d59c670cfd4a8a575d4459999a64f8)) +- **redux-store:** add reducers to initializeStore ([#413](https://github.com/hyperledger/aries-framework-javascript/issues/413)) ([d9aeabf](https://github.com/hyperledger/aries-framework-javascript/commit/d9aeabff3b8eec08aa86c005959ae4fafd7e948b)) +- **redux-store:** credential and proof selector by id ([#407](https://github.com/hyperledger/aries-framework-javascript/issues/407)) ([fd8933d](https://github.com/hyperledger/aries-framework-javascript/commit/fd8933dbda953177044c6ac737102c9608b4a2c6)) +- Remove apostrophe from connection request message type ([#364](https://github.com/hyperledger/aries-framework-javascript/issues/364)) ([ee81d01](https://github.com/hyperledger/aries-framework-javascript/commit/ee81d0115f2365fd33156105ba69a80e265d5846)) +- remove dependency on global types ([#327](https://github.com/hyperledger/aries-framework-javascript/issues/327)) ([fb28935](https://github.com/hyperledger/aries-framework-javascript/commit/fb28935a0658ef29ee6dc3bcf7cd064f15ac471b)) +- removed check for senderkey for connectionless exchange ([#555](https://github.com/hyperledger/aries-framework-javascript/issues/555)) ([ba3f17e](https://github.com/hyperledger/aries-framework-javascript/commit/ba3f17e073b28ee5f16031f0346de0b71119e6f3)) +- return valid schema in create schema method ([#193](https://github.com/hyperledger/aries-framework-javascript/issues/193)) ([4ca020b](https://github.com/hyperledger/aries-framework-javascript/commit/4ca020bd1ec0f3284064d4a52f5e81fee88e81c9)) +- revert target back to es2017 ([#319](https://github.com/hyperledger/aries-framework-javascript/issues/319)) ([9859db1](https://github.com/hyperledger/aries-framework-javascript/commit/9859db1d04b8e13e54a00e645e9837134d176154)) +- revert to ES2017 to fix function generator issues in react native ([#226](https://github.com/hyperledger/aries-framework-javascript/issues/226)) ([6078324](https://github.com/hyperledger/aries-framework-javascript/commit/60783247c7cf753c731b9a152b994dcf23285805)) +- support mediation for connectionless exchange ([#577](https://github.com/hyperledger/aries-framework-javascript/issues/577)) ([3dadfc7](https://github.com/hyperledger/aries-framework-javascript/commit/3dadfc7a202b3642e93e39cd79c9fd98a3dc4de2)) +- test failing because of moved import ([#282](https://github.com/hyperledger/aries-framework-javascript/issues/282)) ([e5efce0](https://github.com/hyperledger/aries-framework-javascript/commit/e5efce0b92d6eb10ab8fe0d1caa3a6b1d17b7f99)) +- their did doc not ours ([#436](https://github.com/hyperledger/aries-framework-javascript/issues/436)) ([0226609](https://github.com/hyperledger/aries-framework-javascript/commit/0226609a279303f5e8d09a2c01e54ff97cf61839)) +- use both thread id and connection id ([#299](https://github.com/hyperledger/aries-framework-javascript/issues/299)) ([3366a55](https://github.com/hyperledger/aries-framework-javascript/commit/3366a552959b63662809b612ae1162612dc6a50a)) +- Use custom make-error with cause that works in RN ([#285](https://github.com/hyperledger/aries-framework-javascript/issues/285)) ([799b6c8](https://github.com/hyperledger/aries-framework-javascript/commit/799b6c8e44933b03acce25636a8bf8dfbbd234d5)) +- websocket and fetch fix for browser ([#291](https://github.com/hyperledger/aries-framework-javascript/issues/291)) ([84e570d](https://github.com/hyperledger/aries-framework-javascript/commit/84e570dc1ffff9ff60792b43ce6bc19241ae2886)) + +### Code Refactoring + +- make a connection with mediator asynchronously ([#231](https://github.com/hyperledger/aries-framework-javascript/issues/231)) ([bafa839](https://github.com/hyperledger/aries-framework-javascript/commit/bafa8399b32b0f814c90a2406a00a74036df96c8)) +- fix(core)!: Improved typing on metadata api (#585) ([4ab8d73](https://github.com/hyperledger/aries-framework-javascript/commit/4ab8d73e5fc866a91085f95f973022846ed431fb)), closes [#585](https://github.com/hyperledger/aries-framework-javascript/issues/585) +- fix(core)!: update class transformer library (#547) ([dee03e3](https://github.com/hyperledger/aries-framework-javascript/commit/dee03e38d2732ba0bd38eeacca6ad58b191e87f8)), closes [#547](https://github.com/hyperledger/aries-framework-javascript/issues/547) +- fix(core)!: prefixed internal metadata with \_internal/ (#535) ([aa1b320](https://github.com/hyperledger/aries-framework-javascript/commit/aa1b3206027fdb71e6aaa4c6491f8ba84dca7b9a)), closes [#535](https://github.com/hyperledger/aries-framework-javascript/issues/535) +- feat(core)!: metadata on records (#505) ([c92393a](https://github.com/hyperledger/aries-framework-javascript/commit/c92393a8b5d8abd38d274c605cd5c3f97f96cee9)), closes [#505](https://github.com/hyperledger/aries-framework-javascript/issues/505) +- fix(core)!: do not request ping res for connection (#527) ([3db5519](https://github.com/hyperledger/aries-framework-javascript/commit/3db5519f0d9f49b71b647ca86be3b336399459cb)), closes [#527](https://github.com/hyperledger/aries-framework-javascript/issues/527) +- refactor(core)!: simplify get creds for proof api (#523) ([ba9698d](https://github.com/hyperledger/aries-framework-javascript/commit/ba9698de2606e5c78f018dc5e5253aeb1f5fc616)), closes [#523](https://github.com/hyperledger/aries-framework-javascript/issues/523) +- fix(core)!: improve proof request validation (#525) ([1b4d8d6](https://github.com/hyperledger/aries-framework-javascript/commit/1b4d8d6b6c06821a2a981fffb6c47f728cac803e)), closes [#525](https://github.com/hyperledger/aries-framework-javascript/issues/525) +- feat(core)!: added basic message sent event (#507) ([d2c04c3](https://github.com/hyperledger/aries-framework-javascript/commit/d2c04c36c00d772943530bd599dbe56f3e1fb17d)), closes [#507](https://github.com/hyperledger/aries-framework-javascript/issues/507) + +### Features + +- Add assertions for credential state transitions ([#130](https://github.com/hyperledger/aries-framework-javascript/issues/130)) ([00d2b1f](https://github.com/hyperledger/aries-framework-javascript/commit/00d2b1f2ea42ff70bfc70c54da9f2341a27aa479)), closes [#123](https://github.com/hyperledger/aries-framework-javascript/issues/123) +- add credential info to access attributes ([#254](https://github.com/hyperledger/aries-framework-javascript/issues/254)) ([2fef3aa](https://github.com/hyperledger/aries-framework-javascript/commit/2fef3aafd954df93911579f82d0945d04b086750)) +- add delete methods to services and modules ([#447](https://github.com/hyperledger/aries-framework-javascript/issues/447)) ([e7ed602](https://github.com/hyperledger/aries-framework-javascript/commit/e7ed6027d2aa9be7f64d5968c4338e63e56657fb)) +- add dependency injection ([#257](https://github.com/hyperledger/aries-framework-javascript/issues/257)) ([1965bfe](https://github.com/hyperledger/aries-framework-javascript/commit/1965bfe660d7fd335a5988056bdea7335c88021b)) +- add from record method to cred preview ([#428](https://github.com/hyperledger/aries-framework-javascript/issues/428)) ([895f7d0](https://github.com/hyperledger/aries-framework-javascript/commit/895f7d084287f99221c9492a25fed58191868edd)) +- add inbound message queue ([#339](https://github.com/hyperledger/aries-framework-javascript/issues/339)) ([93893b7](https://github.com/hyperledger/aries-framework-javascript/commit/93893b7ab6afd1b4d4f3be4c6b807bab970dd63a)) +- add internal http outbound transporter ([#255](https://github.com/hyperledger/aries-framework-javascript/issues/255)) ([4dd950e](https://github.com/hyperledger/aries-framework-javascript/commit/4dd950eab6390fa08bf4c59c9efe69b5f4541640)) +- add internal polling inbound transporter ([#323](https://github.com/hyperledger/aries-framework-javascript/issues/323)) ([6dd273b](https://github.com/hyperledger/aries-framework-javascript/commit/6dd273b266fdfb336592bcd2a4834d4b508c0425)) +- add internal ws outbound transporter ([#267](https://github.com/hyperledger/aries-framework-javascript/issues/267)) ([2933207](https://github.com/hyperledger/aries-framework-javascript/commit/29332072f49e645bfe0fa394bb4c6f66b0bc0600)) +- add isInitialized agent property ([#293](https://github.com/hyperledger/aries-framework-javascript/issues/293)) ([deb5554](https://github.com/hyperledger/aries-framework-javascript/commit/deb5554d912587a1298eb86e42b64df6700907f9)) +- add multiple inbound transports ([#433](https://github.com/hyperledger/aries-framework-javascript/issues/433)) ([56cb9f2](https://github.com/hyperledger/aries-framework-javascript/commit/56cb9f2202deb83b3c133905f21651bfefcb63f7)) +- add problem report protocol ([#560](https://github.com/hyperledger/aries-framework-javascript/issues/560)) ([baee5db](https://github.com/hyperledger/aries-framework-javascript/commit/baee5db29f3d545c16a651c80392ddcbbca6bf0e)) +- add support for Multibase, Multihash and Hashlinks ([#263](https://github.com/hyperledger/aries-framework-javascript/issues/263)) ([36ceaea](https://github.com/hyperledger/aries-framework-javascript/commit/36ceaea4c500da90babd8d54bb88b2d9e7846e4e)) +- add support for RFC 0211 mediator coordination ([2465d4d](https://github.com/hyperledger/aries-framework-javascript/commit/2465d4d88771b0d415492585ee60d3dc78163786)) +- Add support for WebSocket transports ([#256](https://github.com/hyperledger/aries-framework-javascript/issues/256)) ([07b479f](https://github.com/hyperledger/aries-framework-javascript/commit/07b479fbff87bfc914a2b933f1216969a29cf790)) +- add toJson method to BaseRecord ([#455](https://github.com/hyperledger/aries-framework-javascript/issues/455)) ([f3790c9](https://github.com/hyperledger/aries-framework-javascript/commit/f3790c97c4d9a0aaec9abdce417ecd5429c6026f)) +- Added attachment extension ([#266](https://github.com/hyperledger/aries-framework-javascript/issues/266)) ([e8ab5fa](https://github.com/hyperledger/aries-framework-javascript/commit/e8ab5fa5c13c9633febfbdf3d5fdf2b352947322)) +- added decline credential offer method ([#416](https://github.com/hyperledger/aries-framework-javascript/issues/416)) ([d9ac141](https://github.com/hyperledger/aries-framework-javascript/commit/d9ac141122f1d4902f91f9537e6526796fef1e01)) +- added declined proof state and decline method for presentations ([e5aedd0](https://github.com/hyperledger/aries-framework-javascript/commit/e5aedd02737d3764871c6b5d4ae61a3a33ed8398)) +- added their label to the connection record ([#370](https://github.com/hyperledger/aries-framework-javascript/issues/370)) ([353e1d8](https://github.com/hyperledger/aries-framework-javascript/commit/353e1d8733cb2ea217dcf7c815a70eb89527cffc)) +- adds support for linked attachments ([#320](https://github.com/hyperledger/aries-framework-javascript/issues/320)) ([ea91559](https://github.com/hyperledger/aries-framework-javascript/commit/ea915590217b1bf4a560cd2931b9891374b03188)) +- allow for lazy wallet initialization ([#331](https://github.com/hyperledger/aries-framework-javascript/issues/331)) ([46918a1](https://github.com/hyperledger/aries-framework-javascript/commit/46918a1d971bc93a1b6e2ad5ef5f7b3a8e8f2bdc)) +- allow to use legacy did sov prefix ([#442](https://github.com/hyperledger/aries-framework-javascript/issues/442)) ([c41526f](https://github.com/hyperledger/aries-framework-javascript/commit/c41526fb57a7e2e89e923b95ede43f890a6cbcbb)) +- auto accept proofs ([#367](https://github.com/hyperledger/aries-framework-javascript/issues/367)) ([735d578](https://github.com/hyperledger/aries-framework-javascript/commit/735d578f72fc5f3bfcbcf40d27394bd013e7cf4f)) +- automatic transformation of record classes ([#253](https://github.com/hyperledger/aries-framework-javascript/issues/253)) ([e07b90e](https://github.com/hyperledger/aries-framework-javascript/commit/e07b90e264c4bb29ff0d7246ceec7c664782c546)) +- break out indy wallet, better indy handling ([#396](https://github.com/hyperledger/aries-framework-javascript/issues/396)) ([9f1a4a7](https://github.com/hyperledger/aries-framework-javascript/commit/9f1a4a754a61573ce3fee78d52615363c7e25d58)) +- **core:** add discover features protocol ([#390](https://github.com/hyperledger/aries-framework-javascript/issues/390)) ([3347424](https://github.com/hyperledger/aries-framework-javascript/commit/3347424326cd15e8bf2544a8af53b2fa57b1dbb8)) +- **core:** add support for multi use inviations ([#460](https://github.com/hyperledger/aries-framework-javascript/issues/460)) ([540ad7b](https://github.com/hyperledger/aries-framework-javascript/commit/540ad7be2133ee6609c2336b22b726270db98d6c)) +- **core:** connection-less issuance and verification ([#359](https://github.com/hyperledger/aries-framework-javascript/issues/359)) ([fb46ade](https://github.com/hyperledger/aries-framework-javascript/commit/fb46ade4bc2dd4f3b63d4194bb170d2f329562b7)) +- **core:** d_m invitation parameter and invitation image ([#456](https://github.com/hyperledger/aries-framework-javascript/issues/456)) ([f92c322](https://github.com/hyperledger/aries-framework-javascript/commit/f92c322b97be4a4867a82c3a35159d6068951f0b)) +- **core:** ledger module registerPublicDid implementation ([#398](https://github.com/hyperledger/aries-framework-javascript/issues/398)) ([5f2d512](https://github.com/hyperledger/aries-framework-javascript/commit/5f2d5126baed2ff58268c38755c2dbe75a654947)) +- **core:** store mediator id in connection record ([#503](https://github.com/hyperledger/aries-framework-javascript/issues/503)) ([da51f2e](https://github.com/hyperledger/aries-framework-javascript/commit/da51f2e8337f5774d23e9aeae0459bd7355a3760)) +- **core:** support image url in invitations ([#463](https://github.com/hyperledger/aries-framework-javascript/issues/463)) ([9fda24e](https://github.com/hyperledger/aries-framework-javascript/commit/9fda24ecf55fdfeba74211447e9fadfdcbf57385)) +- **core:** support multiple indy ledgers ([#474](https://github.com/hyperledger/aries-framework-javascript/issues/474)) ([47149bc](https://github.com/hyperledger/aries-framework-javascript/commit/47149bc5742456f4f0b75e0944ce276972e645b8)) +- **core:** update agent label and imageUrl plus per connection label and imageUrl ([#516](https://github.com/hyperledger/aries-framework-javascript/issues/516)) ([5e9a641](https://github.com/hyperledger/aries-framework-javascript/commit/5e9a64130c02c8a5fdf11f0e25d0c23929a33a4f)) +- **core:** validate outbound messages ([#526](https://github.com/hyperledger/aries-framework-javascript/issues/526)) ([9c3910f](https://github.com/hyperledger/aries-framework-javascript/commit/9c3910f1e67200b71bb4888c6fee62942afaff20)) +- expose wallet API ([#566](https://github.com/hyperledger/aries-framework-javascript/issues/566)) ([4027fc9](https://github.com/hyperledger/aries-framework-javascript/commit/4027fc975d7e4118892f43cb8c6a0eea412eaad4)) +- generic attachment handler ([#578](https://github.com/hyperledger/aries-framework-javascript/issues/578)) ([4d7d3c1](https://github.com/hyperledger/aries-framework-javascript/commit/4d7d3c1502d5eafa2b884a4a84934e072fe70ea6)) +- method to retrieve credentials for proof request ([#329](https://github.com/hyperledger/aries-framework-javascript/issues/329)) ([012afa6](https://github.com/hyperledger/aries-framework-javascript/commit/012afa6e455ebef1df024b5ba67b63ec66d1d8d5)) +- negotiation and auto accept credentials ([#336](https://github.com/hyperledger/aries-framework-javascript/issues/336)) ([55e8697](https://github.com/hyperledger/aries-framework-javascript/commit/55e86973e52e55235308696f4a7e0477b0dc01c6)) +- **node:** add http and ws inbound transport ([#392](https://github.com/hyperledger/aries-framework-javascript/issues/392)) ([34a6ff2](https://github.com/hyperledger/aries-framework-javascript/commit/34a6ff2699197b9d525422a0a405e241582a476c)) +- **node:** add is-indy-installed command ([#510](https://github.com/hyperledger/aries-framework-javascript/issues/510)) ([e50b821](https://github.com/hyperledger/aries-framework-javascript/commit/e50b821343970d299a4cacdcba3a051893524ed6)) +- only connect to ledger when needed ([#273](https://github.com/hyperledger/aries-framework-javascript/issues/273)) ([a9c261e](https://github.com/hyperledger/aries-framework-javascript/commit/a9c261eb22c86ad5d804e7a1bc792bb74cce5015)) +- Pack and send a message based on DidDoc services ([#304](https://github.com/hyperledger/aries-framework-javascript/issues/304)) ([6a26337](https://github.com/hyperledger/aries-framework-javascript/commit/6a26337fe1f52d661bd33208354a85e15512aec4)) +- **redux-store:** add mediation store ([#424](https://github.com/hyperledger/aries-framework-javascript/issues/424)) ([03e4341](https://github.com/hyperledger/aries-framework-javascript/commit/03e43418fb45cfa4d52e36fc04b98cd59a8eb21e)) +- **redux-store:** move from mobile agent repo ([#388](https://github.com/hyperledger/aries-framework-javascript/issues/388)) ([d84acc7](https://github.com/hyperledger/aries-framework-javascript/commit/d84acc75e24de4cd1cae99256df293276cc69c18)) +- **redux:** delete credentialRecord and proofRecord ([#421](https://github.com/hyperledger/aries-framework-javascript/issues/421)) ([9fa6c6d](https://github.com/hyperledger/aries-framework-javascript/commit/9fa6c6daf77ac56b9bc83ae3bfdae72cd919bc6c)) +- support newer did-communication service type ([#233](https://github.com/hyperledger/aries-framework-javascript/issues/233)) ([cf29d8f](https://github.com/hyperledger/aries-framework-javascript/commit/cf29d8fa3b4b6e098b9c7db87e73e84143a71c48)) +- support node v12+ ([#294](https://github.com/hyperledger/aries-framework-javascript/issues/294)) ([6ec201b](https://github.com/hyperledger/aries-framework-javascript/commit/6ec201bacb618bb08612dac832681e56a099bdde)) +- use computed tags for records ([#313](https://github.com/hyperledger/aries-framework-javascript/issues/313)) ([4e9a48b](https://github.com/hyperledger/aries-framework-javascript/commit/4e9a48b077dddd000e1c9826c653ec31d4b7897f)) +- Use session to send outbound message ([#362](https://github.com/hyperledger/aries-framework-javascript/issues/362)) ([7366ca7](https://github.com/hyperledger/aries-framework-javascript/commit/7366ca7b6ba2925a28020d5d063272505d53b0d5)) + +### BREAKING CHANGES + +- removed the getAll() function. +- The agent’s `shutdown` method does not delete the wallet anymore. If you want to delete the wallet, you can do it via exposed wallet API. +- class-transformer released a breaking change in a patch version, causing AFJ to break. I updated to the newer version and pinned the version exactly as this is the second time this has happened now. +- internal metadata is now prefixed with \_internal to avoid clashing and accidental overwriting of internal data. +- fix(core): added \_internal/ prefix on metadata +- credentialRecord.credentialMetadata has been replaced by credentialRecord.metadata. +- a trust ping response will not be requested anymore after completing a connection. This is not required, and also non-standard behaviour. It was also causing some tests to be flaky as response messages were stil being sent after one of the agents had already shut down. +- The `ProofsModule.getRequestedCredentialsForProofRequest` expected some low level message objects as input. This is not in line with the public API of the rest of the framework and has been simplified to only require a proof record id and optionally a boolean whether the retrieved credentials should be filtered based on the proof proposal (if available). +- Proof request requestedAttributes and requestedPredicates are now a map instead of record. This is needed to have proper validation using class-validator. +- `BasicMessageReceivedEvent` has been replaced by the more general `BasicMessageStateChanged` event which triggers when a basic message is received or sent. +- Tags on a record can now be accessed using the `getTags()` method. Records should be updated with this method and return the properties from the record to include in the tags. +- extracts outbound transporter from Agent's constructor. diff --git a/CODEOWNERS b/CODEOWNERS index 3d2299408b..f1b505c931 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,4 +1,4 @@ # SPDX-License-Identifier: Apache-2.0 -# Aries Framework Javascript maintainers -* @hyperledger/aries-framework-javascript-committers +# Agent Framework Javascript maintainers +* @openwallet-foundation/agent-framework-javascript-maintainers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0d8341111..0c11943d32 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,10 +2,22 @@ You are encouraged to contribute to the repository by **forking and submitting a pull request**. -If you would like to propose a significant change, please open an issue first to discuss the work with the community to avoid re-work. +(If you are new to GitHub, you might start with a [basic tutorial](https://help.github.com/articles/set-up-git) and check out a more detailed guide to [pull requests](https://help.github.com/articles/using-pull-requests/).) -(If you are new to GitHub, you might want to start with a [basic tutorial](https://help.github.com/articles/set-up-git) and check out a more detailed guide to [pull requests](https://help.github.com/articles/using-pull-requests/).) +Pull requests will be evaluated by the repository guardians on a schedule and if deemed beneficial will be committed to the main branch. Pull requests should have a descriptive name and include an summary of all changes made in the pull request description. -Pull requests will be evaluated by the repository guardians on a schedule and if deemed beneficial will be committed to the `main` branch. Pull requests should have a descriptive name and include an summary of all changes made in the pull request description. +If you would like to propose a significant change, please open an issue first to discuss the proposed changes with the community and to avoid re-work. -All contributors retain the original copyright to their stuff, but by contributing to this project, you grant a world-wide, royalty-free, perpetual, irrevocable, non-exclusive, transferable license to all users **under the terms of the license under which this project is distributed.** +Contributions are made pursuant to the Developer's Certificate of Origin, available at [https://developercertificate.org](https://developercertificate.org), and licensed under the Apache License, version 2.0 (Apache-2.0). + +## Contributing checklist: + +- It is difficult to manage a release with too many changes. + - We should **release more often**, not months apart. + - We should focus on feature releases (minor and patch releases) to speed iteration. + - See our [Credo Docs on semantic versioning](https://credo.js.org/guides/updating#versioning). Notably, while our versions are pre 1.0.0, minor versions are breaking change versions. +- Mixing breaking changes with other PRs slows development. + - Non-breaking change PRs are merged earlier into **main** + - Breaking change PRs will go to a branch named **-pre (ie. 0.3.0-pre)** and merged later in the release cycle. + - Consider separating your PR into a (usually larger) non-breaking PR and a (usually smaller) breaking change PR. +- Relevant changes for the changelog must be documented using changesets. See [Changesets](.changeset/README.md) for more info. To add a changelog, run `pnpm changeset` and commit the files afterwards. diff --git a/DEVREADME.md b/DEVREADME.md new file mode 100644 index 0000000000..c344933ede --- /dev/null +++ b/DEVREADME.md @@ -0,0 +1,72 @@ +# Framework Developers + +This file is intended for developers working on the internals of the framework. If you're just looking how to get started with the framework, see the [docs](./docs) + +# Environment Setup + +## VSCode devContainer + +This project comes with a [.devcontainer](./devcontainer) to make it as easy as possible to setup your dev environment and begin contributing to this project. + +All the [environment variables](https://code.visualstudio.com/remote/advancedcontainers/environment-variables) noted below can be added to [devcontainer.env](./devcontainer.env) and exposed to the development docker container. + +When running in a container your project root directory will be `/work`. Use this to correctly path any environment variables, for example: + +```console +GENESIS_TXN_PATH=/work/network/genesis/local-genesis.txn +``` + +## Running tests + +Test are executed using jest. E2E tests (ending in `.e2e.test.ts`) require the **indy ledger**, **cheqd ledger** or **postgres database** to be running. + +When running tests that require a connection to the indy ledger pool, you can set the `TEST_AGENT_PUBLIC_DID_SEED`, `ENDORSER_AGENT_PUBLIC_DID_SEED` and `GENESIS_TXN_PATH` environment variables. + +### Quick Setup + +To quickly set up all services needed to run tests (Postgres, Hyperledger Indy Ledger, and Cheqd Ledger), run the following command: + +```sh +docker compose up -d +``` + +If you're running on an ARM based machine (such as Apple Silicon), you can use the `docker-compose.arm.yml` file instead: + +```sh +docker compose -f docker-compose.arm.yml up -d +``` + +### Run all tests + +You can run all unit tests (which **do not** require the docker services to be running) using the following command. + +```sh +pnpm test:unit +``` + +To run the e2e tests: + +```sh +pnpm test:e2e +``` + +You can also run **all** tests: + +```sh +pnpm test +``` + +### Setting environment variables + +If you're using the setup as described in this document, you don't need to provide any environment variables as the default will be sufficient. + +- `GENESIS_TXN_PATH`: The path to the genesis transaction that allows us to connect to the indy pool. + - `GENESIS_TXN_PATH=network/genesis/local-genesis.txn` - default. Works with the [ledger setup](#setup-indy-ledger) from the previous step. + - `GENESIS_TXN_PATH=network/genesis/builder-net-genesis.txn` - Sovrin BuilderNet genesis. + - `GENESIS_TXN_PATH=/path/to/any/ledger/you/like` +- `TEST_AGENT_PUBLIC_DID_SEED`: The seed to use for the public DID. This will be used to do public write operations to the ledger. You should use a seed for a DID that is already registered on the ledger. + - If using the local or default genesis, use the same seed you used for the `add-did-from-seed` command from the [ledger setup](#setup-indy-ledger) in the previous step. (default is `000000000000000000000000Trustee9`) + - If using the BuilderNet genesis, make sure your seed is registered on the BuilderNet using [selfserve.sovrin.org](https://selfserve.sovrin.org/) and you have read and accepted the associated [Transaction Author Agreement](https://github.com/sovrin-foundation/sovrin/blob/master/TAA/TAA.md). We are not responsible for any unwanted consequences of using the BuilderNet. +- `ENDORSER_AGENT_PUBLIC_DID_SEED`: The seed to use for the public Endorser DID. This will be used to endorse transactions. You should use a seed for a DID that is already registered on the ledger. + - If using the local or default genesis, use the same seed you used for the `add-did-from-seed` command from the [ledger setup](#setup-indy-ledger) in the previous step. (default is `00000000000000000000000Endorser9`) + - If using the BuilderNet genesis, make sure your seed is registered on the BuilderNet using [selfserve.sovrin.org](https://selfserve.sovrin.org/) and you have read and accepted the associated [Transaction Author Agreement](https://github.com/sovrin-foundation/sovrin/blob/master/TAA/TAA.md). We are not responsible for any unwanted consequences of using the BuilderNet. diff --git a/Dockerfile b/Dockerfile index bc854bef72..a0c91b34e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,47 +1,15 @@ -FROM ubuntu:18.04 as base +FROM node:22 -ENV DEBIAN_FRONTEND noninteractive - -RUN apt-get update -y && apt-get install -y \ - software-properties-common \ - apt-transport-https \ - curl \ - # Only needed to build indy-sdk - build-essential - -# libindy -RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 -RUN add-apt-repository "deb https://repo.sovrin.org/sdk/deb bionic stable" - -# nodejs -RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - -# yarn -RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list - -# install depdencies -RUN apt-get update -y && apt-get install -y --allow-unauthenticated \ - libindy \ - nodejs - -# Install yarn seperately due to `no-install-recommends` to skip nodejs install -RUN apt-get install -y --no-install-recommends yarn - -FROM base as final - -# AFJ specifc setup +# Set working directory WORKDIR /www -ENV RUN_MODE="docker" -COPY package.json package.json -COPY yarn.lock yarn.lock +# Copy repository files +COPY . . -# Run install after copying only depdendency file -# to make use of docker layer caching -RUN yarn install +RUN corepack enable -# Copy other depdencies -COPY . . +# Run pnpm install and build +RUN pnpm install --frozen-lockfile \ + && pnpm build -RUN yarn compile \ No newline at end of file +entrypoint ["pnpm", "run-mediator"] \ No newline at end of file diff --git a/LICENSE b/LICENSE index 261eeb9e9f..c6d085027b 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,8 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020-present Hyperledger Contributors. + Copyright 2021 Queen’s Printer for Ontario. Mostafa Youssef (https://github.com/MosCD3), Amit Padmani (https://github.com/nbAmit), Prasad Katkar (https://github.com/NB-PrasadKatkar), Mike Richardson (https://github.com/NB-MikeRichardson) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000000..a5830b63f1 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,11 @@ +## Maintainers + +### Active Maintainers + +| name | Github | Discord | +| ------------------ | ---------------------------------------------------------- | ---------------- | +| Berend Sliedrecht | [@berendsliedrecht](https://github.com/berendsliedrecht) | blu3beri#2230 | +| Jakub Kočí | [@jakubkoci](https://github.com/jakubkoci) | jakubkoci#1481 | +| Karim Stekelenburg | [@karimStekelenburg](https://github.com/karimStekelenburg) | ssi_karim#3505 | +| Timo Glastra | [@TimoGlastra](https://github.com/TimoGlastra) | TimoGlastra#2988 | +| Ariel Gentile | [@genaris](https://github.com/genaris) | GenAris#4962 | diff --git a/README.md b/README.md index 0f9209fe89..59b7bbc3c7 100644 --- a/README.md +++ b/README.md @@ -1,251 +1,203 @@

- Hyperledger Aries logo -

Aries Framework JavaScript

-

Built using TypeScript

-

- Pipeline Status - Language grade: JavaScript - Codecov Coverage - License - aries-framework-javascript npm version +
+ Credo Logo

+

Credo

+

+ Pipeline Status + Codecov Coverage + License + typescript

+
-Aries Framework JavaScript is a framework for building SSI Agents and DIDComm services that aims to be compliant and interoperable with the standards defined in the [Aries RFCs](https://github.com/hyperledger/aries-rfcs). - -## Table of Contents - -- [Goals](#goals) -- [Usage](#usage) - - [Prerequisites](#prerequisites) - - [Installing](#installing) - - [Using the framework](#using-the-framework) - - [Usage in React Native](#usage-in-react-native) - - [Logs](#logs) -- [Architecture](#architecture) -- [Development](#development) - - [Setup Ledger](#setup-ledger) - - [Running tests](#running-tests) - - [Setting environment variables](#setting-environment-variables) - - [Starting mediator agents](#starting-mediator-agents) - - [With Docker](#with-docker) - - [Only run e2e tests with in memory messaging](#only-run-e2e-tests-with-in-memory-messaging) - - [Only run e2e tests with HTTP based routing agencies](#only-run-e2e-tests-with-http-based-routing-agencies) - - [Run all tests](#run-all-tests) -- [Usage with Docker](#usage-with-docker) -- [Contributing](#contributing) -- [License](#license) - -## Goals - -The framework is still in early development. At the moment the goal of this implementation is to run two independent Edge Agents (clients), running on mobile or desktop, which are able to communicate via a Mediator agent. It should at least adhere to the following requirements: - -- Edge Agent is independent on underlying communication layer. It can communicate via either HTTP request-response, WebSockets or Push Notifications. -- Edge Agent can go offline and still receive its messages when it goes back to online. -- There should be an option to connect more clients (Edge Agents) to one Routing Agent. -- Prevent correlation. - -See the [Roadmap](https://github.com/hyperledger/aries-framework-javascript/issues/39) or the [Framework Development](https://github.com/hyperledger/aries-framework-javascript/projects/1) project board for current progress. - -## Usage - -### Prerequisites - -Aries Framework JavaScript depends on the indy-sdk which has some manual installation requirements. Before installing dependencies make sure to [install](https://github.com/hyperledger/indy-sdk/#installing-the-sdk) `libindy` and have the right tools installed for the [NodeJS wrapper](https://github.com/hyperledger/indy-sdk/tree/master/wrappers/nodejs#installing). The NodeJS wrapper link also contains some common troubleshooting steps. The [Dockerfile](./Dockerfile) contains everything needed to get started with the framework. See [Usage with Docker](#usage-with-docker) for more information. - -> If you're having trouble running this project, please the the [troubleshooting](./TROUBLESHOOTING.md) section. It contains the most common errors that arise when first installing libindy. - -### Installing - -> NOTE: The package is not tested in multiple versions of Node at the moment. If you're having trouble installing dependencies or running the framework know that at least **Node v12 DOES WORK** and **Node v14 DOES NOT WORk**. - -Currently the framework is working towards the first release of the package, until then the framework won't be available on NPM. However you can use the framework by packaging and adding it as a file to your project. - -```sh -# Clone the repo -git clone https://github.com/hyperledger/aries-framework-javascript.git -cd aries-framework-javascript - -# Install dependencies -yarn install - -# Pack the framework -yarn pack -``` - -In a project, where you want to use this library as dependency, run: - -``` -yarn add file:PATH_TO_REPOSITORY_FOLDER/aries-framework-javascript/aries-framework-javascript-v1.0.0.tgz -``` - -### Using the framework - -While the framework is still in early development the best way to know what API the framework exposes is by looking at the [tests](src/lib/__tests__), the [source code](src/lib) or the [samples](./src/samples). As the framework reaches a more mature state, documentation on the usage of the framework will be added. - -### Usage in React Native - -The framework is designed to be usable in multiple environments. The indy-sdk is the only dependency that needs special handling and is therefore an parameter when initializing the agent. Alongside Aries Framework JavaScript you need to install the indy-sdk for the environment you're using. - -```sh -# for NodeJS -yarn install indy-sdk - -# for React Native -yarn install rn-indy-sdk -``` - -The when initializing the agent you can pass the specific Indy API as an input parameter: - -```typescript -// for NodeJS -import indy from 'indy-sdk'; - -// for React Native -import indy from 'rn-indy-sdk'; - -const config = { - // ... other config properties ... - indy, -}; - -agent = new Agent(config, inboundTransport, outboundTransport); -``` - -For an example react native app that makes use of the framework see [Aries Mobile Agent React Native](https://github.com/animo/aries-mobile-agent-react-native.git) - -### Logs - -To enable logging inside the framework a logger must be passed to the agent config. A simple `ConsoleLogger` can be imported from the framework, for more advanced use cases the `ILogger` interface can implemented. See [`TestLogger`](./src/lib/__tests__/logger.ts) for a more advanced example. - -```ts -import { ILogger, ConsoleLogger, LogLevel } from 'aries-framework-javascript'; - -const agentConfig = { - // ... other config properties ... - logger: new ConsoleLogger(LogLevel.debug), -}; -``` - -## Architecture - -Agent class has method `receiveMessage` which **unpacks** incoming **inbound message** and then pass it to the `dispatch` method. This method just tries to find particular `handler` according to message `@type` attribute. Handler then process the message, calls services if needed and also creates **outbound message** to be send by sender, if it's required by protocol. - -If handler returns an outbound message then method `sendMessage` **packs** the message with defined recipient and routing keys. This method also creates **forwardMessage** when routing keys are available. The way an outbound message is send depends on the implementation of MessageSender interface. Outbound message just need to contain all information which is needed for given communication (e. g. HTTP endpoint for HTTP protocol). - -## Development - -### Setup Ledger - -```sh -# Build indy pool -docker build -f network/indy-pool.dockerfile -t indy-pool . - -# Start indy pool -docker run -d --rm --name indy-pool -p 9701-9708:9701-9708 indy-pool - -# Setup CLI. This creates a wallet, connects to the ledger and sets the Transaction Author Agreement -docker exec indy-pool indy-cli-setup - -# DID and Verkey from seed -docker exec indy-pool add-did-from-seed 000000000000000000000000Trustee9 - -# If you want to register using the DID/Verkey you can use -# docker exec indy-pool add-did "NkGXDEPgpFGjQKMYmz6SyF" "CrSA1WbYYWLJoHm16Xw1VEeWxFvXtWjtsfEzMsjB5vDT" -``` - -### Running tests - -Test are executed using jest. Some test require either the **mediator agents** or the **ledger** to be running. When running tests that require a connection to the ledger pool, you need to set the `TEST_AGENT_PUBLIC_DID_SEED` and `GENESIS_TXN_PATH` environment variables. - -#### Setting environment variables - -- `GENESIS_TXN_PATH`: The path to the genesis transaction that allows us to connect to the indy pool. - - `GENESIS_TXN_PATH=network/genesis/local-genesis.txn` - default. Works with the [ledger setup](#setup-ledger) from the previous step. - - `GENESIS_TXN_PATH=network/genesis/builder-net-genesis.txn` - Sovrin BuilderNet genesis. - - `GENESIS_TXN_PATH=/path/to/any/ledger/you/like` -- `TEST_AGENT_PUBLIC_DID_SEED`: The seed to use for the public DID. This will be used to do public write operations to the ledger. You should use a seed for a DID that is already registered on the ledger. - - If using the local or default genesis, use the same seed you used for the `add-did-from-seed` command form the [ledger setup](#setup-ledger) in the previous step. - - If using the BuilderNet genesis, make sure your seed is registered on the BuilderNet using [selfserve.sovrin.org](https://selfserve.sovrin.org/) and you have read and accepted the associated [Transaction Author Agreement](https://github.com/sovrin-foundation/sovrin/blob/master/TAA/TAA.md). We are not responsible for any unwanted consequences of using the BuilderNet. - -#### Starting mediator agents - -To start the mediator agents you need to run two commands. See the [Usage with Docker](#usage-with-docker) section on how to run the mediators inside docker. - -Open terminal and start Alice's mediator: - -``` -./scripts/run-mediator.sh mediator01 -``` - -Open new terminal and start Bob's mediator: - -``` -./scripts/run-mediator.sh mediator02 -``` - -##### With Docker - -To run the mediators inside docker you can use the `docker-compose-mediators.yml` file: - -```sh -# Run alice-mediator and bob-mediator -docker-compose -f docker/docker-compose-mediators.yml up -d -``` - -If you want the ports to be exposed to the outside world using ngrok you can use the `docker-compose-mediators-ngrok.yml` extension. Make sure the ngrok docker compose file is used after the normal docker compose file. - -```sh -# Run alice-mediator and bob-mediator exposed via ngrok -docker-compose -f docker/docker-compose-mediators.yml -f docker/docker-compose-mediators-ngrok.yml up -d -``` - -#### Only run e2e tests with in memory messaging - -You don't have to start mediator agents or the ledger for these tests. Communication is done via RxJS subscriptions. - -``` -yarn test -t "agents" -``` - -#### Only run e2e tests with HTTP based routing agencies - -Make sure the **mediator agents** from the [Starting mediator agents](#starting-mediator-agents) step are running and then run: - -``` -yarn test -t "with mediator" -``` - -#### Run all tests - -Make sure the **mediator agents** from [Starting mediator agents](#starting-mediator-agents) are running and you pass the correct environment variables from [Setting environment variables](#setting-environment-variables) for connecting to the indy **ledger** pool. - -``` -GENESIS_TXN_PATH=network/genesis/local-genesis.txn TEST_AGENT_PUBLIC_DID_SEED=000000000000000000000000Trustee9 yarn test -``` - -## Usage with Docker - -If you don't want to install the libindy dependencies yourself, or want a clean environment when running the framework or tests you can use docker. - -Make sure you followed the [local ledger setup](#setup-ledger) to setup a local indy pool inside docker. +

+ Quickstart  |  + Features  |  + Contributing  |  + License +

-```sh -# Builds the framework docker image with all dependencies installed -docker build -t aries-framework-javascript . +Credo is a framework written in TypeScript for building **decentralized identity solutions** that aims to be compliant and **interoperable with identity standards across the world**. Credo is agnostic to any specific exchange protocol, credential format, signature suite or did method, but currently mainly focuses on alignment with [OpenID4VC](https://openid.net/sg/openid4vc/), [DIDComm](https://identity.foundation/didcomm-messaging/spec/) and [Hyperledger Aries](https://hyperledger.github.io/aries-rfcs/latest/). + +## Quickstart + +Documentation on how to get started with Credo can be found at https://credo.js.org/ + +## Features + +See [Supported Features](https://credo.js.org/guides/features) on the Credo website for a full list of supported features. + +- 🏃 **Platform agnostic** - out of the box support for Node.JS and React Native +- 🔒 **DIDComm and AIP** - Support for [DIDComm v1](https://hyperledger.github.io/aries-rfcs/latest/concepts/0005-didcomm/), and both v1 and v2 of the [Aries Interop Profile](https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0302-aries-interop-profile/README.md). +- 🛂 **Extendable [DID](https://www.w3.org/TR/did-core/) resolver and registrar** - out of the box support for `did:web`, `did:key`, `did:jwk`, `did:peer`, `did:sov`, `did:indy` and `did:cheqd`. +- 🔑 **[OpenID4VC](https://openid.net/sg/openid4vc/)** - support for [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html), [OpenID for Verifiable Presentations](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) and [Self-Issued OpenID Provider v2](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html). +- 🪪 **Multiple credential formats** - [W3C Verifiable Credential Data Model v1.1](https://www.w3.org/TR/vc-data-model/), [SD-JWT VCs](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-03.html), and [AnonCreds](https://hyperledger.github.io/anoncreds-spec/). +- 🏢 **Multi-tenant** - Optional multi-tenant module for managing multiple tenants under a single agent. + +### Packages + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PackageVersion
@credo-ts/core + + @credo-ts/core version + +
@credo-ts/node + + @credo-ts/node version + +
@credo-ts/react-native + + @credo-ts/react-native version + +
@credo-ts/indy-vdr + + @credo-ts/indy-vdr version + +
@credo-ts/cheqd + + @credo-ts/cheqd version + +
@credo-ts/askar + + @credo-ts/askar version + +
@credo-ts/anoncreds + + @credo-ts/anoncreds version + +
@credo-ts/openid4vc + + @credo-ts/openid4vc version + +
@credo-ts/action-menu + + @credo-ts/action-menu version + +
@credo-ts/question-answer + + @credo-ts/question-answer version + +
@credo-ts/tenants + + @credo-ts/tenants version + +
@credo-ts/drpc + + @credo-ts/drpc version + +
@aries-framework/indy-sdk (deprecated, unmaintained after 0.4.x) + + @aries-framework/indy-sdk version + +
@aries-framework/anoncreds-rs (deprecated and combined with @credo-ts/anoncreds) + + @aries-framework/anoncreds-rs version + +
@credo-ts/openid4vc-client (deprecated in favour of @credo-ts/openid4vc) + + @credo-ts/openid4vc-client version + +
+ +## Demo + +To get to know the Credo issuance and verification flow, we built a demo to walk through it yourself together with agents Alice and Faber. + +- OpenID4VC and SD-JWT VC demo in the [`/demo-openid`](/demo-openid) directory. +- DIDComm and AnonCreds demo in the [`/demo`](/demo) directory. -# Run tests without network -docker run -it --rm aries-framework-javascript yarn test -t "agents" +## Contributing -# Run test with mediator agents and ledger pool -docker-compose -f docker/docker-compose-mediators.yml up -d # Run alice-mediator and bob-mediator -docker run --rm --network host --env TEST_AGENT_PUBLIC_DID_SEED=000000000000000000000000Trustee9 --env GENESIS_TXN_PATH=network/genesis/local-genesis.txn aries-framework-javascript yarn test -``` +If you would like to contribute to the framework, please read the [Framework Developers README](/DEVREADME.md) and the [CONTRIBUTING](/CONTRIBUTING.md) guidelines. These documents will provide more information to get you started! -## Contributing +There are regular community working groups to discuss ongoing efforts within the framework, showcase items you've built with Credo, or ask questions. See [Meeting Information](https://github.com/openwallet-foundation/credo-ts/wiki/Meeting-Information) for up to date information on the meeting schedule. Everyone is welcome to join! -Found a bug? Ready to submit a PR? Want to submit a proposal for your grand idea? See our [CONTRIBUTING](CONTRIBUTING.md) file for more information to get you started! +We welcome you to join our mailing list and Discord channel. See the [Wiki](https://github.com/openwallet-foundation/credo-ts/wiki/Communication) for up to date information. ## License -Hyperledger Aries Framework JavaScript is licensed under the [Apache License Version 2.0 (Apache-2.0)](LICENSE). +OpenWallet Foundation Credo is licensed under the [Apache License Version 2.0 (Apache-2.0)](/LICENSE). diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md deleted file mode 100644 index c8f698a43b..0000000000 --- a/TROUBLESHOOTING.md +++ /dev/null @@ -1,85 +0,0 @@ -# Troubleshooting - -This document contains the most common errors that arise when first installing libindy and Aries Framework JavaScript. If you encounter a problem that is not listed here and manage to fix it, please open a PR describing the steps taken to resolve the issue. - -- [macOS](#macos) - - [Unable to find `libindy.dylib`](#unable-to-find-libindydylib) - - [Unable to find `libssl.1.0.0.dylib`](#unable-to-find-libssl100dylib) - - [Library not loaded: `libsodium.18.dylib`](#library-not-loaded-libsodium18dylib) - -## macOS - -### Unable to find `libindy.dylib` - -Installing Libindy on macOS can be tricky. If the the troubleshooting section of the NodeJS Wrapper documentation doesn't provide an answer and you're getting the following error: - -``` -dlopen(//aries-framework-javascript/node_modules/indy-sdk/build/Release/indynodejs.node, 1): Library not loaded: /Users/jenkins/workspace/indy-sdk_indy-sdk-cd_master/libindy/target/release/deps/libindy.dylib - Referenced from: //aries-framework-javascript/node_modules/indy-sdk/build/Release/indynodejs.node - Reason: image not found -``` - -See this StackOverflow answer: https://stackoverflow.com/questions/19776571/error-dlopen-library-not-loaded-reason-image-not-found - -The NodeJS Wrapper tries to find the library at the hardcoded CI built path `/Users/jenkins/workspace/indy-sdk_indy-sdk-cd_master/libindy/target/release/deps/libindy.dylib`. However the library will probably be located at `/usr/local/lib/libindy.dylib` (depending on how you installed libindy). - -To check where the NodeJS wrapper points to the static CI build path you can run: - -```bash -$ otool -L node_modules/indy-sdk/build/Release/indynodejs.node -node_modules/indy-sdk/build/Release/indynodejs.node: - /Users/jenkins/workspace/indy-sdk_indy-sdk-cd_master/libindy/target/release/deps/libindy.dylib (compatibility version 0.0.0, current version 0.0.0) - /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 902.1.0) - /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1) -``` - -You can manually change the path using the `install_name_tool`. Be sure change the path if you're not using the default. - -```bash -install_name_tool -change /Users/jenkins/workspace/indy-sdk_indy-sdk-cd_master/libindy/target/release/deps/libindy.dylib /usr/local/lib/libindy.dylib node_modules/indy-sdk/build/Release/indynodejs.node -``` - -### Unable to find `libssl.1.0.0.dylib` - -Libindy makes use of OpenSSL 1.0, however macOS by default has OpenSSL version 1.1. The standard brew repo also doesn't contain version 1.0 anymore. So if you're getting something that looks like the following error: - -``` -dlopen(//aries-framework-javascript/node_modules/indy-sdk/build/Release/indynodejs.node, 1): Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib - Referenced from: //libindy_1.15.0/lib/libindy.dylib - Reason: image not found -``` - -You can manually install OpenSSL 1.0 with the following Brew command: - -```sh -brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/64555220bfbf4a25598523c2e4d3a232560eaad7/Formula/openssl.rb -f -``` - -In newer versions of HomeBrew installing packages is disabled, which will give an error that looks something like this: - -``` -Error: Calling Installation of openssl from a GitHub commit URL is disabled! Use 'brew extract openssl' to stable tap on GitHub instead. -``` - -They advise to use `brew extract` which also gives errors. The easiest way is to download the file and then extract it: - -```sh -curl https://raw.githubusercontent.com/Homebrew/homebrew-core/64555220bfbf4a25598523c2e4d3a232560eaad7/Formula/openssl.rb -o openssl.rb -brew install openssl.rb -``` - -### Library not loaded: `libsodium.18.dylib` - -When you install `libsodium` it automatically installs version 23. However libindy needs version 18. So if you're getting something that looks like the following error: - -``` -dyld: Library not loaded: /usr/local/opt/libsodium/lib/libsodium.18.dylib -``` - -You can manually link the path for version 18 to the path of version 23 with the following command: - -```sh -ln -s /usr/local/opt/libsodium/lib/libsodium.23.dylib /usr/local/opt/libsodium/lib/libsodium.18.dylib -``` - -Inspired by [this answer](https://github.com/Homebrew/homebrew-php/issues/4589) to the same error using php71-libsodium. diff --git a/demo-openid/README.md b/demo-openid/README.md new file mode 100644 index 0000000000..f8ad0777c3 --- /dev/null +++ b/demo-openid/README.md @@ -0,0 +1,101 @@ +

DEMO

+ +This is the Credo OpenID4VC demo. Walk through the Credo flow yourself together with agents Alice and Faber. + +Alice, a former student of Faber College, connects with the College, is issued a credential about her degree and then is asked by the College for a proof. + +## Features + +- ✅ Issuing a credential. +- ✅ Resolving a credential offer. +- ✅ Accepting a credential offer. +- ✅ Requesting a credential presentation. +- ✅ Resolving a presentation request. +- ✅ Accepting a resolved presentation request. + +## Getting Started + +### Platform Specific Setup + +In order to run the Credo demo, you need to make sure you have Node.JS and PNPM installed. See the [Credo Prerequisites](https://credo.js.org/guides/getting-started/prerequisites) for more information. + +### Run the demo + +These are the steps for running the Credo OpenID4VC demo: + +Clone the Credo git repository: + +```sh +git clone https://github.com/openwallet-foundation/credo-ts.git +``` + +Open three different terminals next to each other and in both, go to the demo folder: + +```sh +cd credo-ts/demo-openid +``` + +Install the project in one of the terminals: + +```sh +pnpm install +``` + +In the first terminal run the Issuer: + +```sh +pnpm issuer +``` + +In the second terminal run the Holder: + +```sh +pnpm holder +``` + +In the last terminal run the Verifier: + +```sh +pnpm verifier +``` + +### Usage + +To create a credential offer: + +- Go to the Issuer terminal. +- Select `Create a credential offer`. +- Select `UniversityDegreeCredential`. +- Now copy the content INSIDE the quotes (without the quotes). + +To resolve and accept the credential: + +- Go to the Holder terminal. +- Select `Resolve a credential offer`. +- Paste the content copied from the credential offer and hit enter. +- Select `Accept the credential offer`. +- You have now stored your credential. + +To create a presentation request: + +- Go to the Verifier terminal. +- Select `Request the presentation of a credential`. +- Select `UniversityDegreeCredential`. +- Copy the presentation request string content, without the quotes. + +To resolve and accept the presentation request: + +- Go to the Holder terminal. +- Select `Resolve a proof request`. +- Paste the copied string (without the quotes). +- Hit enter: You should see a Green message saying what will be presented. +- Select `Accept presentation request`. +- The presentation should be sent (WIP). + +Exit: + +- Select 'exit' to shutdown the program. + +Restart: + +- Select 'restart', to shutdown the current program and start a new one diff --git a/demo-openid/package.json b/demo-openid/package.json new file mode 100644 index 0000000000..53e30f93a4 --- /dev/null +++ b/demo-openid/package.json @@ -0,0 +1,35 @@ +{ + "name": "afj-demo-openid", + "version": "1.0.0", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "demo-openid" + }, + "license": "Apache-2.0", + "scripts": { + "issuer": "ts-node src/IssuerInquirer.ts", + "holder": "ts-node src/HolderInquirer.ts", + "verifier": "ts-node src/VerifierInquirer.ts" + }, + "dependencies": { + "@hyperledger/anoncreds-nodejs": "^0.2.2", + "@hyperledger/aries-askar-nodejs": "^0.2.1", + "@hyperledger/indy-vdr-nodejs": "^0.2.2", + "express": "^4.18.1", + "inquirer": "^8.2.5" + }, + "devDependencies": { + "@credo-ts/openid4vc": "workspace:*", + "@credo-ts/askar": "workspace:*", + "@credo-ts/core": "workspace:*", + "@credo-ts/node": "workspace:*", + "@types/express": "^4.17.13", + "@types/figlet": "^1.5.4", + "@types/inquirer": "^8.2.6", + "clear": "^0.1.0", + "figlet": "^1.5.2", + "ts-node": "^10.4.0" + } +} diff --git a/demo-openid/src/BaseAgent.ts b/demo-openid/src/BaseAgent.ts new file mode 100644 index 0000000000..f0c79a4e86 --- /dev/null +++ b/demo-openid/src/BaseAgent.ts @@ -0,0 +1,61 @@ +import type { InitConfig, KeyDidCreateOptions, ModulesMap, VerificationMethod } from '@credo-ts/core' +import type { Express } from 'express' + +import { Agent, DidKey, HttpOutboundTransport, KeyType, TypedArrayEncoder } from '@credo-ts/core' +import { HttpInboundTransport, agentDependencies } from '@credo-ts/node' +import express from 'express' + +import { greenText } from './OutputClass' + +export class BaseAgent { + public app: Express + public port: number + public name: string + public config: InitConfig + public agent: Agent + public did!: string + public didKey!: DidKey + public kid!: string + public verificationMethod!: VerificationMethod + + public constructor({ port, name, modules }: { port: number; name: string; modules: AgentModules }) { + this.name = name + this.port = port + this.app = express() + + const config = { + label: name, + walletConfig: { id: name, key: name }, + } satisfies InitConfig + + this.config = config + + this.agent = new Agent({ config, dependencies: agentDependencies, modules }) + + const httpInboundTransport = new HttpInboundTransport({ app: this.app, port: this.port }) + const httpOutboundTransport = new HttpOutboundTransport() + + this.agent.registerInboundTransport(httpInboundTransport) + this.agent.registerOutboundTransport(httpOutboundTransport) + } + + public async initializeAgent(secretPrivateKey: string) { + await this.agent.initialize() + + const didCreateResult = await this.agent.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString(secretPrivateKey) }, + }) + + this.did = didCreateResult.didState.did as string + this.didKey = DidKey.fromDid(this.did) + this.kid = `${this.did}#${this.didKey.key.fingerprint}` + + const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(this.kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + this.verificationMethod = verificationMethod + + console.log(greenText(`\nAgent ${this.name} created!\n`)) + } +} diff --git a/demo-openid/src/BaseInquirer.ts b/demo-openid/src/BaseInquirer.ts new file mode 100644 index 0000000000..358d72b632 --- /dev/null +++ b/demo-openid/src/BaseInquirer.ts @@ -0,0 +1,55 @@ +import { prompt } from 'inquirer' + +import { Title } from './OutputClass' + +export enum ConfirmOptions { + Yes = 'yes', + No = 'no', +} + +export class BaseInquirer { + public optionsInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] } + public inputInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] } + + public constructor() { + this.optionsInquirer = { + type: 'list', + prefix: '', + name: 'options', + message: '', + choices: [], + } + + this.inputInquirer = { + type: 'input', + prefix: '', + name: 'input', + message: '', + choices: [], + } + } + + public inquireOptions(promptOptions: string[]) { + this.optionsInquirer.message = Title.OptionsTitle + this.optionsInquirer.choices = promptOptions + return this.optionsInquirer + } + + public inquireInput(title: string) { + this.inputInquirer.message = title + return this.inputInquirer + } + + public inquireConfirmation(title: string) { + this.optionsInquirer.message = title + this.optionsInquirer.choices = [ConfirmOptions.Yes, ConfirmOptions.No] + return this.optionsInquirer + } + + public async inquireMessage() { + this.inputInquirer.message = Title.MessageTitle + const message = await prompt([this.inputInquirer]) + + return message.input[0] === 'q' ? null : message.input + } +} diff --git a/demo-openid/src/Holder.ts b/demo-openid/src/Holder.ts new file mode 100644 index 0000000000..762ce5ee31 --- /dev/null +++ b/demo-openid/src/Holder.ts @@ -0,0 +1,101 @@ +import type { OpenId4VciResolvedCredentialOffer, OpenId4VcSiopResolvedAuthorizationRequest } from '@credo-ts/openid4vc' + +import { AskarModule } from '@credo-ts/askar' +import { + W3cJwtVerifiableCredential, + W3cJsonLdVerifiableCredential, + DifPresentationExchangeService, +} from '@credo-ts/core' +import { OpenId4VcHolderModule } from '@credo-ts/openid4vc' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' + +import { BaseAgent } from './BaseAgent' +import { Output } from './OutputClass' + +function getOpenIdHolderModules() { + return { + askar: new AskarModule({ ariesAskar }), + openId4VcHolder: new OpenId4VcHolderModule(), + } as const +} + +export class Holder extends BaseAgent> { + public constructor(port: number, name: string) { + super({ port, name, modules: getOpenIdHolderModules() }) + } + + public static async build(): Promise { + const holder = new Holder(3000, 'OpenId4VcHolder ' + Math.random().toString()) + await holder.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598e') + + return holder + } + + public async resolveCredentialOffer(credentialOffer: string) { + return await this.agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + } + + public async requestAndStoreCredentials( + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, + credentialsToRequest: string[] + ) { + const credentials = await this.agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + credentialsToRequest, + // TODO: add jwk support for holder binding + credentialBindingResolver: async () => ({ + method: 'did', + didUrl: this.verificationMethod.id, + }), + } + ) + + const storedCredentials = await Promise.all( + credentials.map((credential) => { + if (credential instanceof W3cJwtVerifiableCredential || credential instanceof W3cJsonLdVerifiableCredential) { + return this.agent.w3cCredentials.storeCredential({ credential }) + } else { + return this.agent.sdJwtVc.store(credential.compact) + } + }) + ) + + return storedCredentials + } + + public async resolveProofRequest(proofRequest: string) { + const resolvedProofRequest = await this.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest(proofRequest) + + return resolvedProofRequest + } + + public async acceptPresentationRequest(resolvedPresentationRequest: OpenId4VcSiopResolvedAuthorizationRequest) { + const presentationExchangeService = this.agent.dependencyManager.resolve(DifPresentationExchangeService) + + if (!resolvedPresentationRequest.presentationExchange) { + throw new Error('Missing presentation exchange on resolved authorization request') + } + + const submissionResult = await this.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedPresentationRequest.authorizationRequest, + presentationExchange: { + credentials: presentationExchangeService.selectCredentialsForRequest( + resolvedPresentationRequest.presentationExchange.credentialsForRequest + ), + }, + }) + + return submissionResult.serverResponse + } + + public async exit() { + console.log(Output.Exit) + await this.agent.shutdown() + process.exit(0) + } + + public async restart() { + await this.agent.shutdown() + } +} diff --git a/demo-openid/src/HolderInquirer.ts b/demo-openid/src/HolderInquirer.ts new file mode 100644 index 0000000000..f346e951e1 --- /dev/null +++ b/demo-openid/src/HolderInquirer.ts @@ -0,0 +1,198 @@ +import type { SdJwtVcRecord, W3cCredentialRecord } from '@credo-ts/core' +import type { OpenId4VcSiopResolvedAuthorizationRequest, OpenId4VciResolvedCredentialOffer } from '@credo-ts/openid4vc' + +import { DifPresentationExchangeService } from '@credo-ts/core' +import console, { clear } from 'console' +import { textSync } from 'figlet' +import { prompt } from 'inquirer' + +import { BaseInquirer, ConfirmOptions } from './BaseInquirer' +import { Holder } from './Holder' +import { Title, greenText, redText } from './OutputClass' + +export const runHolder = async () => { + clear() + console.log(textSync('Holder', { horizontalLayout: 'full' })) + const holder = await HolderInquirer.build() + await holder.processAnswer() +} + +enum PromptOptions { + ResolveCredentialOffer = 'Resolve a credential offer.', + RequestCredential = 'Accept the credential offer.', + ResolveProofRequest = 'Resolve a proof request.', + AcceptPresentationRequest = 'Accept the presentation request.', + Exit = 'Exit', + Restart = 'Restart', +} + +export class HolderInquirer extends BaseInquirer { + public holder: Holder + public resolvedCredentialOffer?: OpenId4VciResolvedCredentialOffer + public resolvedPresentationRequest?: OpenId4VcSiopResolvedAuthorizationRequest + + public constructor(holder: Holder) { + super() + this.holder = holder + } + + public static async build(): Promise { + const holder = await Holder.build() + return new HolderInquirer(holder) + } + + private async getPromptChoice() { + const promptOptions = [PromptOptions.ResolveCredentialOffer, PromptOptions.ResolveProofRequest] + + if (this.resolvedCredentialOffer) promptOptions.push(PromptOptions.RequestCredential) + if (this.resolvedPresentationRequest) promptOptions.push(PromptOptions.AcceptPresentationRequest) + + return prompt([this.inquireOptions(promptOptions.map((o) => o.valueOf()))]) + } + + public async processAnswer() { + const choice = await this.getPromptChoice() + + switch (choice.options) { + case PromptOptions.ResolveCredentialOffer: + await this.resolveCredentialOffer() + break + case PromptOptions.RequestCredential: + await this.requestCredential() + break + case PromptOptions.ResolveProofRequest: + await this.resolveProofRequest() + break + case PromptOptions.AcceptPresentationRequest: + await this.acceptPresentationRequest() + break + case PromptOptions.Exit: + await this.exit() + break + case PromptOptions.Restart: + await this.restart() + return + } + await this.processAnswer() + } + + public async exitUseCase(title: string) { + const confirm = await prompt([this.inquireConfirmation(title)]) + if (confirm.options === ConfirmOptions.No) { + return false + } else if (confirm.options === ConfirmOptions.Yes) { + return true + } + } + + public async resolveCredentialOffer() { + const credentialOffer = await prompt([this.inquireInput('Enter credential offer: ')]) + const resolvedCredentialOffer = await this.holder.resolveCredentialOffer(credentialOffer.input) + this.resolvedCredentialOffer = resolvedCredentialOffer + + console.log(greenText(`Received credential offer for the following credentials.`)) + console.log(greenText(resolvedCredentialOffer.offeredCredentials.map((credential) => credential.id).join('\n'))) + } + + public async requestCredential() { + if (!this.resolvedCredentialOffer) { + throw new Error('No credential offer resolved yet.') + } + + const credentialsThatCanBeRequested = this.resolvedCredentialOffer.offeredCredentials.map( + (credential) => credential.id + ) + + const choice = await prompt([this.inquireOptions(credentialsThatCanBeRequested)]) + + const credentialToRequest = this.resolvedCredentialOffer.offeredCredentials.find( + (credential) => credential.id === choice.options + ) + if (!credentialToRequest) throw new Error('Credential to request not found.') + + console.log(greenText(`Requesting the following credential '${credentialToRequest.id}'`)) + + const credentials = await this.holder.requestAndStoreCredentials( + this.resolvedCredentialOffer, + this.resolvedCredentialOffer.offeredCredentials.map((o) => o.id) + ) + + console.log(greenText(`Received and stored the following credentials.`)) + console.log('') + credentials.forEach(this.printCredential) + } + + public async resolveProofRequest() { + const proofRequestUri = await prompt([this.inquireInput('Enter proof request: ')]) + this.resolvedPresentationRequest = await this.holder.resolveProofRequest(proofRequestUri.input) + + const presentationDefinition = this.resolvedPresentationRequest?.presentationExchange?.definition + console.log(greenText(`Presentation Purpose: '${presentationDefinition?.purpose}'`)) + + if (this.resolvedPresentationRequest?.presentationExchange?.credentialsForRequest.areRequirementsSatisfied) { + const selectedCredentials = Object.values( + this.holder.agent.dependencyManager + .resolve(DifPresentationExchangeService) + .selectCredentialsForRequest(this.resolvedPresentationRequest.presentationExchange.credentialsForRequest) + ).flatMap((e) => e) + console.log( + greenText( + `All requirements for creating the presentation are satisfied. The following credentials will be shared`, + true + ) + ) + selectedCredentials.forEach(this.printCredential) + } else { + console.log(redText(`No credentials available that satisfy the proof request.`)) + } + } + + public async acceptPresentationRequest() { + if (!this.resolvedPresentationRequest) throw new Error('No presentation request resolved yet.') + + console.log(greenText(`Accepting the presentation request.`)) + + const serverResponse = await this.holder.acceptPresentationRequest(this.resolvedPresentationRequest) + + if (serverResponse.status >= 200 && serverResponse.status < 300) { + console.log(`received success status code '${serverResponse.status}'`) + } else { + console.log(`received error status code '${serverResponse.status}'. ${JSON.stringify(serverResponse.body)}`) + } + } + + public async exit() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.holder.exit() + } + } + + public async restart() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.processAnswer() + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.holder.restart() + await runHolder() + } + } + + private printCredential = (credential: W3cCredentialRecord | SdJwtVcRecord) => { + if (credential.type === 'W3cCredentialRecord') { + console.log(greenText(`W3cCredentialRecord with claim format ${credential.credential.claimFormat}`, true)) + console.log(JSON.stringify(credential.credential.jsonCredential, null, 2)) + console.log('') + } else { + console.log(greenText(`SdJwtVcRecord`, true)) + const prettyClaims = this.holder.agent.sdJwtVc.fromCompact(credential.compactSdJwtVc).prettyClaims + console.log(JSON.stringify(prettyClaims, null, 2)) + console.log('') + } + } +} + +void runHolder() diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts new file mode 100644 index 0000000000..414f10cef4 --- /dev/null +++ b/demo-openid/src/Issuer.ts @@ -0,0 +1,184 @@ +import type { DidKey } from '@credo-ts/core' +import type { + OpenId4VcCredentialHolderBinding, + OpenId4VcCredentialHolderDidBinding, + OpenId4VciCredentialRequestToCredentialMapper, + OpenId4VciCredentialSupportedWithId, + OpenId4VcIssuerRecord, +} from '@credo-ts/openid4vc' + +import { AskarModule } from '@credo-ts/askar' +import { + ClaimFormat, + parseDid, + CredoError, + W3cCredential, + W3cCredentialSubject, + W3cIssuer, + w3cDate, +} from '@credo-ts/core' +import { OpenId4VcIssuerModule, OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import { Router } from 'express' + +import { BaseAgent } from './BaseAgent' +import { Output } from './OutputClass' + +export const universityDegreeCredential = { + id: 'UniversityDegreeCredential', + format: OpenId4VciCredentialFormatProfile.JwtVcJson, + types: ['VerifiableCredential', 'UniversityDegreeCredential'], +} satisfies OpenId4VciCredentialSupportedWithId + +export const openBadgeCredential = { + id: 'OpenBadgeCredential', + format: OpenId4VciCredentialFormatProfile.JwtVcJson, + types: ['VerifiableCredential', 'OpenBadgeCredential'], +} satisfies OpenId4VciCredentialSupportedWithId + +export const universityDegreeCredentialSdJwt = { + id: 'UniversityDegreeCredential-sdjwt', + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + vct: 'UniversityDegreeCredential', +} satisfies OpenId4VciCredentialSupportedWithId + +export const credentialsSupported = [ + universityDegreeCredential, + openBadgeCredential, + universityDegreeCredentialSdJwt, +] satisfies OpenId4VciCredentialSupportedWithId[] + +function getCredentialRequestToCredentialMapper({ + issuerDidKey, +}: { + issuerDidKey: DidKey +}): OpenId4VciCredentialRequestToCredentialMapper { + return async ({ holderBinding, credentialsSupported }) => { + const credentialSupported = credentialsSupported[0] + + if (credentialSupported.id === universityDegreeCredential.id) { + assertDidBasedHolderBinding(holderBinding) + + return { + credentialSupportedId: universityDegreeCredential.id, + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: universityDegreeCredential.types, + issuer: new W3cIssuer({ + id: issuerDidKey.did, + }), + credentialSubject: new W3cCredentialSubject({ + id: parseDid(holderBinding.didUrl).did, + }), + issuanceDate: w3cDate(Date.now()), + }), + verificationMethod: `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}`, + } + } + + if (credentialSupported.id === openBadgeCredential.id) { + assertDidBasedHolderBinding(holderBinding) + + return { + format: ClaimFormat.JwtVc, + credentialSupportedId: openBadgeCredential.id, + credential: new W3cCredential({ + type: openBadgeCredential.types, + issuer: new W3cIssuer({ + id: issuerDidKey.did, + }), + credentialSubject: new W3cCredentialSubject({ + id: parseDid(holderBinding.didUrl).did, + }), + issuanceDate: w3cDate(Date.now()), + }), + verificationMethod: `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}`, + } + } + + if (credentialSupported.id === universityDegreeCredentialSdJwt.id) { + return { + credentialSupportedId: universityDegreeCredentialSdJwt.id, + format: ClaimFormat.SdJwtVc, + payload: { vct: universityDegreeCredentialSdJwt.vct, university: 'innsbruck', degree: 'bachelor' }, + holder: holderBinding, + issuer: { + method: 'did', + didUrl: `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}`, + }, + disclosureFrame: { _sd: ['university', 'degree'] }, + } + } + + throw new Error('Invalid request') + } +} + +export class Issuer extends BaseAgent<{ + askar: AskarModule + openId4VcIssuer: OpenId4VcIssuerModule +}> { + public issuerRecord!: OpenId4VcIssuerRecord + + public constructor(port: number, name: string) { + const openId4VciRouter = Router() + + super({ + port, + name, + modules: { + askar: new AskarModule({ ariesAskar }), + openId4VcIssuer: new OpenId4VcIssuerModule({ + baseUrl: 'http://localhost:2000/oid4vci', + router: openId4VciRouter, + endpoints: { + credential: { + credentialRequestToCredentialMapper: (...args) => + getCredentialRequestToCredentialMapper({ issuerDidKey: this.didKey })(...args), + }, + }, + }), + }, + }) + + this.app.use('/oid4vci', openId4VciRouter) + } + + public static async build(): Promise { + const issuer = new Issuer(2000, 'OpenId4VcIssuer ' + Math.random().toString()) + await issuer.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598f') + issuer.issuerRecord = await issuer.agent.modules.openId4VcIssuer.createIssuer({ + credentialsSupported, + }) + + return issuer + } + + public async createCredentialOffer(offeredCredentials: string[]) { + const { credentialOffer } = await this.agent.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: this.issuerRecord.issuerId, + offeredCredentials, + preAuthorizedCodeFlowConfig: { userPinRequired: false }, + }) + + return credentialOffer + } + + public async exit() { + console.log(Output.Exit) + await this.agent.shutdown() + process.exit(0) + } + + public async restart() { + await this.agent.shutdown() + } +} + +function assertDidBasedHolderBinding( + holderBinding: OpenId4VcCredentialHolderBinding +): asserts holderBinding is OpenId4VcCredentialHolderDidBinding { + if (holderBinding.method !== 'did') { + throw new CredoError('Only did based holder bindings supported for this credential type') + } +} diff --git a/demo-openid/src/IssuerInquirer.ts b/demo-openid/src/IssuerInquirer.ts new file mode 100644 index 0000000000..dca38ed86a --- /dev/null +++ b/demo-openid/src/IssuerInquirer.ts @@ -0,0 +1,88 @@ +import { clear } from 'console' +import { textSync } from 'figlet' +import { prompt } from 'inquirer' + +import { BaseInquirer, ConfirmOptions } from './BaseInquirer' +import { Issuer, credentialsSupported } from './Issuer' +import { Title, purpleText } from './OutputClass' + +export const runIssuer = async () => { + clear() + console.log(textSync('Issuer', { horizontalLayout: 'full' })) + const issuer = await IssuerInquirer.build() + await issuer.processAnswer() +} + +enum PromptOptions { + CreateCredentialOffer = 'Create a credential offer', + Exit = 'Exit', + Restart = 'Restart', +} + +export class IssuerInquirer extends BaseInquirer { + public issuer: Issuer + public promptOptionsString: string[] + + public constructor(issuer: Issuer) { + super() + this.issuer = issuer + this.promptOptionsString = Object.values(PromptOptions) + } + + public static async build(): Promise { + const issuer = await Issuer.build() + return new IssuerInquirer(issuer) + } + + private async getPromptChoice() { + return prompt([this.inquireOptions(this.promptOptionsString)]) + } + + public async processAnswer() { + const choice = await this.getPromptChoice() + + switch (choice.options) { + case PromptOptions.CreateCredentialOffer: + await this.createCredentialOffer() + break + case PromptOptions.Exit: + await this.exit() + break + case PromptOptions.Restart: + await this.restart() + return + } + await this.processAnswer() + } + + public async createCredentialOffer() { + const choice = await prompt([this.inquireOptions(credentialsSupported.map((credential) => credential.id))]) + const offeredCredential = credentialsSupported.find((credential) => credential.id === choice.options) + if (!offeredCredential) throw new Error(`No credential of type ${choice.options} found, that can be offered.`) + const offerRequest = await this.issuer.createCredentialOffer([offeredCredential.id]) + + console.log(purpleText(`credential offer: '${offerRequest}'`)) + } + + public async exit() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.issuer.exit() + } + } + + public async restart() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.processAnswer() + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.issuer.restart() + await runIssuer() + } + } +} + +void runIssuer() diff --git a/demo-openid/src/OutputClass.ts b/demo-openid/src/OutputClass.ts new file mode 100644 index 0000000000..b9e69c72f0 --- /dev/null +++ b/demo-openid/src/OutputClass.ts @@ -0,0 +1,40 @@ +export enum Color { + Green = `\x1b[32m`, + Red = `\x1b[31m`, + Purple = `\x1b[35m`, + Reset = `\x1b[0m`, +} + +export enum Output { + NoConnectionRecordFromOutOfBand = `\nNo connectionRecord has been created from invitation\n`, + ConnectionEstablished = `\nConnection established!`, + MissingConnectionRecord = `\nNo connectionRecord ID has been set yet\n`, + ConnectionLink = `\nRun 'Receive connection invitation' in Alice and paste this invitation link:\n\n`, + Exit = 'Shutting down agent...\nExiting...', +} + +export enum Title { + OptionsTitle = '\nOptions:', + InvitationTitle = '\n\nPaste the invitation url here:', + MessageTitle = '\n\nWrite your message here:\n(Press enter to send or press q to exit)\n', + ConfirmTitle = '\n\nAre you sure?', + CredentialOfferTitle = '\n\nCredential offer received, do you want to accept it?', + ProofRequestTitle = '\n\nProof request received, do you want to accept it?', +} + +export const greenText = (text: string, reset?: boolean) => { + if (reset) return Color.Green + text + Color.Reset + + return Color.Green + text +} + +export const purpleText = (text: string, reset?: boolean) => { + if (reset) return Color.Purple + text + Color.Reset + return Color.Purple + text +} + +export const redText = (text: string, reset?: boolean) => { + if (reset) return Color.Red + text + Color.Reset + + return Color.Red + text +} diff --git a/demo-openid/src/Verifier.ts b/demo-openid/src/Verifier.ts new file mode 100644 index 0000000000..8382b8fb25 --- /dev/null +++ b/demo-openid/src/Verifier.ts @@ -0,0 +1,115 @@ +import type { DifPresentationExchangeDefinitionV2 } from '@credo-ts/core' +import type { OpenId4VcVerifierRecord } from '@credo-ts/openid4vc' + +import { AskarModule } from '@credo-ts/askar' +import { OpenId4VcVerifierModule } from '@credo-ts/openid4vc' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import { Router } from 'express' + +import { BaseAgent } from './BaseAgent' +import { Output } from './OutputClass' + +const universityDegreePresentationDefinition = { + id: 'UniversityDegreeCredential', + purpose: 'Present your UniversityDegreeCredential to verify your education level.', + input_descriptors: [ + { + id: 'UniversityDegreeCredentialDescriptor', + constraints: { + fields: [ + { + // Works for JSON-LD, SD-JWT and JWT + path: ['$.vc.type.*', '$.vct', '$.type'], + filter: { + type: 'string', + pattern: 'UniversityDegree', + }, + }, + ], + }, + }, + ], +} + +const openBadgeCredentialPresentationDefinition = { + id: 'OpenBadgeCredential', + purpose: 'Provide proof of employment to confirm your employment status.', + input_descriptors: [ + { + id: 'OpenBadgeCredentialDescriptor', + constraints: { + fields: [ + { + // Works for JSON-LD, SD-JWT and JWT + path: ['$.vc.type.*', '$.vct', '$.type'], + filter: { + type: 'string', + pattern: 'OpenBadgeCredential', + }, + }, + ], + }, + }, + ], +} + +export const presentationDefinitions = [ + universityDegreePresentationDefinition, + openBadgeCredentialPresentationDefinition, +] + +export class Verifier extends BaseAgent<{ askar: AskarModule; openId4VcVerifier: OpenId4VcVerifierModule }> { + public verifierRecord!: OpenId4VcVerifierRecord + + public constructor(port: number, name: string) { + const openId4VcSiopRouter = Router() + + super({ + port, + name, + modules: { + askar: new AskarModule({ ariesAskar }), + openId4VcVerifier: new OpenId4VcVerifierModule({ + baseUrl: 'http://localhost:4000/siop', + router: openId4VcSiopRouter, + }), + }, + }) + + this.app.use('/siop', openId4VcSiopRouter) + } + + public static async build(): Promise { + const verifier = new Verifier(4000, 'OpenId4VcVerifier ' + Math.random().toString()) + await verifier.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598g') + verifier.verifierRecord = await verifier.agent.modules.openId4VcVerifier.createVerifier() + + return verifier + } + + // TODO: add method to show the received presentation submission + public async createProofRequest(presentationDefinition: DifPresentationExchangeDefinitionV2) { + const { authorizationRequest } = await this.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'did', + didUrl: this.verificationMethod.id, + }, + verifierId: this.verifierRecord.verifierId, + presentationExchange: { + definition: presentationDefinition, + }, + }) + + return authorizationRequest + } + + public async exit() { + console.log(Output.Exit) + await this.agent.shutdown() + process.exit(0) + } + + public async restart() { + await this.agent.shutdown() + } +} diff --git a/demo-openid/src/VerifierInquirer.ts b/demo-openid/src/VerifierInquirer.ts new file mode 100644 index 0000000000..8877242fb6 --- /dev/null +++ b/demo-openid/src/VerifierInquirer.ts @@ -0,0 +1,89 @@ +import { clear } from 'console' +import { textSync } from 'figlet' +import { prompt } from 'inquirer' + +import { BaseInquirer, ConfirmOptions } from './BaseInquirer' +import { Title, purpleText } from './OutputClass' +import { Verifier, presentationDefinitions } from './Verifier' + +export const runVerifier = async () => { + clear() + console.log(textSync('Verifier', { horizontalLayout: 'full' })) + const verifier = await VerifierInquirer.build() + await verifier.processAnswer() +} + +enum PromptOptions { + CreateProofOffer = 'Request the presentation of a credential.', + Exit = 'Exit', + Restart = 'Restart', +} + +export class VerifierInquirer extends BaseInquirer { + public verifier: Verifier + public promptOptionsString: string[] + + public constructor(verifier: Verifier) { + super() + this.verifier = verifier + this.promptOptionsString = Object.values(PromptOptions) + } + + public static async build(): Promise { + const verifier = await Verifier.build() + return new VerifierInquirer(verifier) + } + + private async getPromptChoice() { + return prompt([this.inquireOptions(this.promptOptionsString)]) + } + + public async processAnswer() { + const choice = await this.getPromptChoice() + + switch (choice.options) { + case PromptOptions.CreateProofOffer: + await this.createProofRequest() + break + case PromptOptions.Exit: + await this.exit() + break + case PromptOptions.Restart: + await this.restart() + return + } + await this.processAnswer() + } + + public async createProofRequest() { + const choice = await prompt([this.inquireOptions(presentationDefinitions.map((p) => p.id))]) + const presentationDefinition = presentationDefinitions.find((p) => p.id === choice.options) + if (!presentationDefinition) throw new Error('No presentation definition found') + + const proofRequest = await this.verifier.createProofRequest(presentationDefinition) + + console.log(purpleText(`Proof request for the presentation of an ${choice.options}.\n'${proofRequest}'`)) + } + + public async exit() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.verifier.exit() + } + } + + public async restart() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.processAnswer() + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.verifier.restart() + await runVerifier() + } + } +} + +void runVerifier() diff --git a/demo-openid/tsconfig.json b/demo-openid/tsconfig.json new file mode 100644 index 0000000000..b7d9de6c8e --- /dev/null +++ b/demo-openid/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "skipLibCheck": true + } +} diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000000..5bd3fd71c3 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,87 @@ +

DEMO

+ +This is the Credo demo. Walk through the Credo flow yourself together with agents Alice and Faber. + +Alice, a former student of Faber College, connects with the College, is issued a credential about her degree and then is asked by the College for a proof. + +## Features + +- ✅ Creating a connection +- ✅ Offering a credential +- ✅ Requesting a proof +- ✅ Sending basic messages + +## Getting Started + +### Platform Specific Setup + +In order to run the Credo demo, you need to make sure you have Node.JS and PNPM installed. See the [Credo Prerequisites](https://credo.js.org/guides/getting-started/prerequisites) for more information. + +### Run the demo + +These are the steps for running the Credo demo: + +Clone the Credo git repository: + +```sh +git clone https://github.com/openwallet-foundation/credo-ts.git +``` + +Open two different terminals next to each other and in both, go to the demo folder: + +```sh +cd credo-ts/demo +``` + +Install the project in one of the terminals: + +```sh +pnpm install +``` + +In the left terminal run Alice: + +```sh +pnpm alice +``` + +In the right terminal run Faber: + +```sh +pnpm faber +``` + +### Usage + +To set up a connection: + +- Select 'receive connection invitation' in Alice and 'create connection invitation' in Faber +- Faber will print a invitation link which you then copy and paste to Alice +- You have now set up a connection! + +To offer a credential: + +- Select 'offer credential' in Faber +- Faber will start with registering a schema and the credential definition accordingly +- You have now send a credential offer to Alice! +- Go to Alice to accept the incoming credential offer by selecting 'yes'. + +To request a proof: + +- Select 'request proof' in Faber +- Faber will create a new proof attribute and will then send a proof request to Alice! +- Go to Alice to accept the incoming proof request + +To send a basic message: + +- Select 'send message' in either one of the Agents +- Type your message and press enter +- Message sent! + +Exit: + +- Select 'exit' to shutdown the agent. + +Restart: + +- Select 'restart', to shutdown the current agent and start a new one diff --git a/demo/package.json b/demo/package.json new file mode 100644 index 0000000000..5d8a5a15bc --- /dev/null +++ b/demo/package.json @@ -0,0 +1,34 @@ +{ + "name": "credo-demo", + "version": "1.0.0", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "demo/" + }, + "license": "Apache-2.0", + "scripts": { + "alice": "ts-node src/AliceInquirer.ts", + "faber": "ts-node src/FaberInquirer.ts" + }, + "dependencies": { + "@hyperledger/indy-vdr-nodejs": "^0.2.2", + "@hyperledger/anoncreds-nodejs": "^0.2.2", + "@hyperledger/aries-askar-nodejs": "^0.2.1", + "inquirer": "^8.2.5" + }, + "devDependencies": { + "@credo-ts/anoncreds": "workspace:*", + "@credo-ts/askar": "workspace:*", + "@credo-ts/core": "workspace:*", + "@credo-ts/indy-vdr": "workspace:*", + "@credo-ts/cheqd": "workspace:*", + "@credo-ts/node": "workspace:*", + "@types/figlet": "^1.5.4", + "@types/inquirer": "^8.2.6", + "clear": "^0.1.0", + "figlet": "^1.5.2", + "ts-node": "^10.4.0" + } +} diff --git a/demo/src/Alice.ts b/demo/src/Alice.ts new file mode 100644 index 0000000000..ca50a0f50a --- /dev/null +++ b/demo/src/Alice.ts @@ -0,0 +1,80 @@ +import type { ConnectionRecord, CredentialExchangeRecord, ProofExchangeRecord } from '@credo-ts/core' + +import { BaseAgent } from './BaseAgent' +import { greenText, Output, redText } from './OutputClass' + +export class Alice extends BaseAgent { + public connected: boolean + public connectionRecordFaberId?: string + + public constructor(port: number, name: string) { + super({ port, name }) + this.connected = false + } + + public static async build(): Promise { + const alice = new Alice(9000, 'alice') + await alice.initializeAgent() + return alice + } + + private async getConnectionRecord() { + if (!this.connectionRecordFaberId) { + throw Error(redText(Output.MissingConnectionRecord)) + } + return await this.agent.connections.getById(this.connectionRecordFaberId) + } + + private async receiveConnectionRequest(invitationUrl: string) { + const { connectionRecord } = await this.agent.oob.receiveInvitationFromUrl(invitationUrl) + if (!connectionRecord) { + throw new Error(redText(Output.NoConnectionRecordFromOutOfBand)) + } + return connectionRecord + } + + private async waitForConnection(connectionRecord: ConnectionRecord) { + connectionRecord = await this.agent.connections.returnWhenIsConnected(connectionRecord.id) + this.connected = true + console.log(greenText(Output.ConnectionEstablished)) + return connectionRecord.id + } + + public async acceptConnection(invitation_url: string) { + const connectionRecord = await this.receiveConnectionRequest(invitation_url) + this.connectionRecordFaberId = await this.waitForConnection(connectionRecord) + } + + public async acceptCredentialOffer(credentialRecord: CredentialExchangeRecord) { + await this.agent.credentials.acceptOffer({ + credentialRecordId: credentialRecord.id, + }) + } + + public async acceptProofRequest(proofRecord: ProofExchangeRecord) { + const requestedCredentials = await this.agent.proofs.selectCredentialsForRequest({ + proofRecordId: proofRecord.id, + }) + + await this.agent.proofs.acceptRequest({ + proofRecordId: proofRecord.id, + proofFormats: requestedCredentials.proofFormats, + }) + console.log(greenText('\nProof request accepted!\n')) + } + + public async sendMessage(message: string) { + const connectionRecord = await this.getConnectionRecord() + await this.agent.basicMessages.sendMessage(connectionRecord.id, message) + } + + public async exit() { + console.log(Output.Exit) + await this.agent.shutdown() + process.exit(0) + } + + public async restart() { + await this.agent.shutdown() + } +} diff --git a/demo/src/AliceInquirer.ts b/demo/src/AliceInquirer.ts new file mode 100644 index 0000000000..9e1674bdfb --- /dev/null +++ b/demo/src/AliceInquirer.ts @@ -0,0 +1,128 @@ +import type { CredentialExchangeRecord, ProofExchangeRecord } from '@credo-ts/core' + +import { clear } from 'console' +import { textSync } from 'figlet' +import { prompt } from 'inquirer' + +import { Alice } from './Alice' +import { BaseInquirer, ConfirmOptions } from './BaseInquirer' +import { Listener } from './Listener' +import { Title } from './OutputClass' + +export const runAlice = async () => { + clear() + console.log(textSync('Alice', { horizontalLayout: 'full' })) + const alice = await AliceInquirer.build() + await alice.processAnswer() +} + +enum PromptOptions { + ReceiveConnectionUrl = 'Receive connection invitation', + SendMessage = 'Send message', + Exit = 'Exit', + Restart = 'Restart', +} + +export class AliceInquirer extends BaseInquirer { + public alice: Alice + public promptOptionsString: string[] + public listener: Listener + + public constructor(alice: Alice) { + super() + this.alice = alice + this.listener = new Listener() + this.promptOptionsString = Object.values(PromptOptions) + this.listener.messageListener(this.alice.agent, this.alice.name) + } + + public static async build(): Promise { + const alice = await Alice.build() + return new AliceInquirer(alice) + } + + private async getPromptChoice() { + if (this.alice.connectionRecordFaberId) return prompt([this.inquireOptions(this.promptOptionsString)]) + + const reducedOption = [PromptOptions.ReceiveConnectionUrl, PromptOptions.Exit, PromptOptions.Restart] + return prompt([this.inquireOptions(reducedOption)]) + } + + public async processAnswer() { + const choice = await this.getPromptChoice() + if (this.listener.on) return + + switch (choice.options) { + case PromptOptions.ReceiveConnectionUrl: + await this.connection() + break + case PromptOptions.SendMessage: + await this.message() + break + case PromptOptions.Exit: + await this.exit() + break + case PromptOptions.Restart: + await this.restart() + return + } + await this.processAnswer() + } + + public async acceptCredentialOffer(credentialRecord: CredentialExchangeRecord) { + const confirm = await prompt([this.inquireConfirmation(Title.CredentialOfferTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.alice.agent.credentials.declineOffer(credentialRecord.id) + } else if (confirm.options === ConfirmOptions.Yes) { + await this.alice.acceptCredentialOffer(credentialRecord) + } + } + + public async acceptProofRequest(proofRecord: ProofExchangeRecord) { + const confirm = await prompt([this.inquireConfirmation(Title.ProofRequestTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.alice.agent.proofs.declineRequest({ proofRecordId: proofRecord.id }) + } else if (confirm.options === ConfirmOptions.Yes) { + await this.alice.acceptProofRequest(proofRecord) + } + } + + public async connection() { + const title = Title.InvitationTitle + const getUrl = await prompt([this.inquireInput(title)]) + await this.alice.acceptConnection(getUrl.input) + if (!this.alice.connected) return + + this.listener.credentialOfferListener(this.alice, this) + this.listener.proofRequestListener(this.alice, this) + } + + public async message() { + const message = await this.inquireMessage() + if (!message) return + + await this.alice.sendMessage(message) + } + + public async exit() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.alice.exit() + } + } + + public async restart() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.processAnswer() + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.alice.restart() + await runAlice() + } + } +} + +void runAlice() diff --git a/demo/src/BaseAgent.ts b/demo/src/BaseAgent.ts new file mode 100644 index 0000000000..c0acab7e88 --- /dev/null +++ b/demo/src/BaseAgent.ts @@ -0,0 +1,149 @@ +import type { InitConfig } from '@credo-ts/core' +import type { IndyVdrPoolConfig } from '@credo-ts/indy-vdr' + +import { + AnonCredsCredentialFormatService, + AnonCredsModule, + AnonCredsProofFormatService, + LegacyIndyCredentialFormatService, + LegacyIndyProofFormatService, + V1CredentialProtocol, + V1ProofProtocol, +} from '@credo-ts/anoncreds' +import { AskarModule } from '@credo-ts/askar' +import { + CheqdAnonCredsRegistry, + CheqdDidRegistrar, + CheqdDidResolver, + CheqdModule, + CheqdModuleConfig, +} from '@credo-ts/cheqd' +import { + ConnectionsModule, + DidsModule, + V2ProofProtocol, + V2CredentialProtocol, + ProofsModule, + AutoAcceptProof, + AutoAcceptCredential, + CredentialsModule, + Agent, + HttpOutboundTransport, +} from '@credo-ts/core' +import { IndyVdrIndyDidResolver, IndyVdrAnonCredsRegistry, IndyVdrModule } from '@credo-ts/indy-vdr' +import { agentDependencies, HttpInboundTransport } from '@credo-ts/node' +import { anoncreds } from '@hyperledger/anoncreds-nodejs' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import { indyVdr } from '@hyperledger/indy-vdr-nodejs' + +import { greenText } from './OutputClass' + +const bcovrin = `{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node1","blskey":"4N8aUNHSgjQVgkpm8nhNEfDf6txHznoYREg9kirmJrkivgL4oSEimFF6nsQ6M41QvhM2Z33nves5vfSn9n1UwNFJBYtWVnHYMATn76vLuL3zU88KyeAYcHfsih3He6UHcXDxcaecHVz6jhCYz1P2UZn2bDVruL5wXpehgBfBaLKm3Ba","blskey_pop":"RahHYiCvoNCtPTrVtP7nMC5eTYrsUA8WjXbdhNc8debh1agE9bGiJxWBXYNFbnJXoXhWFMvyqhqhRoq737YQemH5ik9oL7R4NTTCz2LEZhkgLJzB3QRQqJyBNyv7acbdHrAT8nQ9UkLbaVL9NBpnWXBTw4LEMePaSHEw66RzPNdAX1","client_ip":"138.197.138.255","client_port":9702,"node_ip":"138.197.138.255","node_port":9701,"services":["VALIDATOR"]},"dest":"Gw6pDLhcBcoQesN72qfotTgFa7cbuqZpkX3Xo6pLhPhv"},"metadata":{"from":"Th7MpTaRZVRYnPiabds81Y"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"fea82e10e894419fe2bea7d96296a6d46f50f93f9eeda954ec461b2ed2950b62"},"ver":"1"} +{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node2","blskey":"37rAPpXVoxzKhz7d9gkUe52XuXryuLXoM6P6LbWDB7LSbG62Lsb33sfG7zqS8TK1MXwuCHj1FKNzVpsnafmqLG1vXN88rt38mNFs9TENzm4QHdBzsvCuoBnPH7rpYYDo9DZNJePaDvRvqJKByCabubJz3XXKbEeshzpz4Ma5QYpJqjk","blskey_pop":"Qr658mWZ2YC8JXGXwMDQTzuZCWF7NK9EwxphGmcBvCh6ybUuLxbG65nsX4JvD4SPNtkJ2w9ug1yLTj6fgmuDg41TgECXjLCij3RMsV8CwewBVgVN67wsA45DFWvqvLtu4rjNnE9JbdFTc1Z4WCPA3Xan44K1HoHAq9EVeaRYs8zoF5","client_ip":"138.197.138.255","client_port":9704,"node_ip":"138.197.138.255","node_port":9703,"services":["VALIDATOR"]},"dest":"8ECVSk179mjsjKRLWiQtssMLgp6EPhWXtaYyStWPSGAb"},"metadata":{"from":"EbP4aYNeTHL6q385GuVpRV"},"type":"0"},"txnMetadata":{"seqNo":2,"txnId":"1ac8aece2a18ced660fef8694b61aac3af08ba875ce3026a160acbc3a3af35fc"},"ver":"1"} +{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node3","blskey":"3WFpdbg7C5cnLYZwFZevJqhubkFALBfCBBok15GdrKMUhUjGsk3jV6QKj6MZgEubF7oqCafxNdkm7eswgA4sdKTRc82tLGzZBd6vNqU8dupzup6uYUf32KTHTPQbuUM8Yk4QFXjEf2Usu2TJcNkdgpyeUSX42u5LqdDDpNSWUK5deC5","blskey_pop":"QwDeb2CkNSx6r8QC8vGQK3GRv7Yndn84TGNijX8YXHPiagXajyfTjoR87rXUu4G4QLk2cF8NNyqWiYMus1623dELWwx57rLCFqGh7N4ZRbGDRP4fnVcaKg1BcUxQ866Ven4gw8y4N56S5HzxXNBZtLYmhGHvDtk6PFkFwCvxYrNYjh","client_ip":"138.197.138.255","client_port":9706,"node_ip":"138.197.138.255","node_port":9705,"services":["VALIDATOR"]},"dest":"DKVxG2fXXTU8yT5N7hGEbXB3dfdAnYv1JczDUHpmDxya"},"metadata":{"from":"4cU41vWW82ArfxJxHkzXPG"},"type":"0"},"txnMetadata":{"seqNo":3,"txnId":"7e9f355dffa78ed24668f0e0e369fd8c224076571c51e2ea8be5f26479edebe4"},"ver":"1"} +{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node4","blskey":"2zN3bHM1m4rLz54MJHYSwvqzPchYp8jkHswveCLAEJVcX6Mm1wHQD1SkPYMzUDTZvWvhuE6VNAkK3KxVeEmsanSmvjVkReDeBEMxeDaayjcZjFGPydyey1qxBHmTvAnBKoPydvuTAqx5f7YNNRAdeLmUi99gERUU7TD8KfAa6MpQ9bw","blskey_pop":"RPLagxaR5xdimFzwmzYnz4ZhWtYQEj8iR5ZU53T2gitPCyCHQneUn2Huc4oeLd2B2HzkGnjAff4hWTJT6C7qHYB1Mv2wU5iHHGFWkhnTX9WsEAbunJCV2qcaXScKj4tTfvdDKfLiVuU2av6hbsMztirRze7LvYBkRHV3tGwyCptsrP","client_ip":"138.197.138.255","client_port":9708,"node_ip":"138.197.138.255","node_port":9707,"services":["VALIDATOR"]},"dest":"4PS3EDQ3dW1tci1Bp6543CfuuebjFrg36kLAUcskGfaA"},"metadata":{"from":"TWwCRQRZ2ZHMJFn9TzLp7W"},"type":"0"},"txnMetadata":{"seqNo":4,"txnId":"aa5e817d7cc626170eca175822029339a444eb0ee8f0bd20d3b0b76e566fb008"},"ver":"1"}` + +export const indyNetworkConfig = { + genesisTransactions: bcovrin, + indyNamespace: 'bcovrin:test', + isProduction: false, + connectOnStartup: true, +} satisfies IndyVdrPoolConfig + +type DemoAgent = Agent> + +export class BaseAgent { + public port: number + public name: string + public config: InitConfig + public agent: DemoAgent + + public constructor({ port, name }: { port: number; name: string }) { + this.name = name + this.port = port + + const config = { + label: name, + walletConfig: { + id: name, + key: name, + }, + endpoints: [`http://localhost:${this.port}`], + } satisfies InitConfig + + this.config = config + + this.agent = new Agent({ + config, + dependencies: agentDependencies, + modules: getAskarAnonCredsIndyModules(), + }) + this.agent.registerInboundTransport(new HttpInboundTransport({ port })) + this.agent.registerOutboundTransport(new HttpOutboundTransport()) + } + + public async initializeAgent() { + await this.agent.initialize() + + console.log(greenText(`\nAgent ${this.name} created!\n`)) + } +} + +function getAskarAnonCredsIndyModules() { + const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService() + const legacyIndyProofFormatService = new LegacyIndyProofFormatService() + + return { + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + credentials: new CredentialsModule({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + credentialProtocols: [ + new V1CredentialProtocol({ + indyCredentialFormat: legacyIndyCredentialFormatService, + }), + new V2CredentialProtocol({ + credentialFormats: [legacyIndyCredentialFormatService, new AnonCredsCredentialFormatService()], + }), + ], + }), + proofs: new ProofsModule({ + autoAcceptProofs: AutoAcceptProof.ContentApproved, + proofProtocols: [ + new V1ProofProtocol({ + indyProofFormat: legacyIndyProofFormatService, + }), + new V2ProofProtocol({ + proofFormats: [legacyIndyProofFormatService, new AnonCredsProofFormatService()], + }), + ], + }), + anoncreds: new AnonCredsModule({ + registries: [new IndyVdrAnonCredsRegistry(), new CheqdAnonCredsRegistry()], + anoncreds, + }), + indyVdr: new IndyVdrModule({ + indyVdr, + networks: [indyNetworkConfig], + }), + cheqd: new CheqdModule( + new CheqdModuleConfig({ + networks: [ + { + network: 'testnet', + cosmosPayerSeed: + 'robust across amount corn curve panther opera wish toe ring bleak empower wreck party abstract glad average muffin picnic jar squeeze annual long aunt', + }, + ], + }) + ), + dids: new DidsModule({ + resolvers: [new IndyVdrIndyDidResolver(), new CheqdDidResolver()], + registrars: [new CheqdDidRegistrar()], + }), + askar: new AskarModule({ + ariesAskar, + }), + } as const +} diff --git a/demo/src/BaseInquirer.ts b/demo/src/BaseInquirer.ts new file mode 100644 index 0000000000..358d72b632 --- /dev/null +++ b/demo/src/BaseInquirer.ts @@ -0,0 +1,55 @@ +import { prompt } from 'inquirer' + +import { Title } from './OutputClass' + +export enum ConfirmOptions { + Yes = 'yes', + No = 'no', +} + +export class BaseInquirer { + public optionsInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] } + public inputInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] } + + public constructor() { + this.optionsInquirer = { + type: 'list', + prefix: '', + name: 'options', + message: '', + choices: [], + } + + this.inputInquirer = { + type: 'input', + prefix: '', + name: 'input', + message: '', + choices: [], + } + } + + public inquireOptions(promptOptions: string[]) { + this.optionsInquirer.message = Title.OptionsTitle + this.optionsInquirer.choices = promptOptions + return this.optionsInquirer + } + + public inquireInput(title: string) { + this.inputInquirer.message = title + return this.inputInquirer + } + + public inquireConfirmation(title: string) { + this.optionsInquirer.message = title + this.optionsInquirer.choices = [ConfirmOptions.Yes, ConfirmOptions.No] + return this.optionsInquirer + } + + public async inquireMessage() { + this.inputInquirer.message = Title.MessageTitle + const message = await prompt([this.inputInquirer]) + + return message.input[0] === 'q' ? null : message.input + } +} diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts new file mode 100644 index 0000000000..5f3542b803 --- /dev/null +++ b/demo/src/Faber.ts @@ -0,0 +1,287 @@ +import type { RegisterCredentialDefinitionReturnStateFinished } from '@credo-ts/anoncreds' +import type { ConnectionRecord, ConnectionStateChangedEvent } from '@credo-ts/core' +import type { IndyVdrRegisterSchemaOptions, IndyVdrRegisterCredentialDefinitionOptions } from '@credo-ts/indy-vdr' +import type BottomBar from 'inquirer/lib/ui/bottom-bar' + +import { ConnectionEventTypes, KeyType, TypedArrayEncoder, utils } from '@credo-ts/core' +import { ui } from 'inquirer' + +import { BaseAgent, indyNetworkConfig } from './BaseAgent' +import { Color, Output, greenText, purpleText, redText } from './OutputClass' + +export enum RegistryOptions { + indy = 'did:indy', + cheqd = 'did:cheqd', +} + +export class Faber extends BaseAgent { + public outOfBandId?: string + public credentialDefinition?: RegisterCredentialDefinitionReturnStateFinished + public anonCredsIssuerId?: string + public ui: BottomBar + + public constructor(port: number, name: string) { + super({ port, name }) + this.ui = new ui.BottomBar() + } + + public static async build(): Promise { + const faber = new Faber(9001, 'faber') + await faber.initializeAgent() + return faber + } + + public async importDid(registry: string) { + // NOTE: we assume the did is already registered on the ledger, we just store the private key in the wallet + // and store the existing did in the wallet + // indy did is based on private key (seed) + const unqualifiedIndyDid = '2jEvRuKmfBJTRa7QowDpNN' + const cheqdDid = 'did:cheqd:testnet:d37eba59-513d-42d3-8f9f-d1df0548b675' + const indyDid = `did:indy:${indyNetworkConfig.indyNamespace}:${unqualifiedIndyDid}` + + const did = registry === RegistryOptions.indy ? indyDid : cheqdDid + await this.agent.dids.import({ + did, + overwrite: true, + privateKeys: [ + { + keyType: KeyType.Ed25519, + privateKey: TypedArrayEncoder.fromString('afjdemoverysercure00000000000000'), + }, + ], + }) + this.anonCredsIssuerId = did + } + + private async getConnectionRecord() { + if (!this.outOfBandId) { + throw Error(redText(Output.MissingConnectionRecord)) + } + + const [connection] = await this.agent.connections.findAllByOutOfBandId(this.outOfBandId) + + if (!connection) { + throw Error(redText(Output.MissingConnectionRecord)) + } + + return connection + } + + private async printConnectionInvite() { + const outOfBand = await this.agent.oob.createInvitation() + this.outOfBandId = outOfBand.id + + console.log( + Output.ConnectionLink, + outOfBand.outOfBandInvitation.toUrl({ domain: `http://localhost:${this.port}` }), + '\n' + ) + } + + private async waitForConnection() { + if (!this.outOfBandId) { + throw new Error(redText(Output.MissingConnectionRecord)) + } + + console.log('Waiting for Alice to finish connection...') + + const getConnectionRecord = (outOfBandId: string) => + new Promise((resolve, reject) => { + // Timeout of 20 seconds + const timeoutId = setTimeout(() => reject(new Error(redText(Output.MissingConnectionRecord))), 20000) + + // Start listener + this.agent.events.on(ConnectionEventTypes.ConnectionStateChanged, (e) => { + if (e.payload.connectionRecord.outOfBandId !== outOfBandId) return + + clearTimeout(timeoutId) + resolve(e.payload.connectionRecord) + }) + + // Also retrieve the connection record by invitation if the event has already fired + void this.agent.connections.findAllByOutOfBandId(outOfBandId).then(([connectionRecord]) => { + if (connectionRecord) { + clearTimeout(timeoutId) + resolve(connectionRecord) + } + }) + }) + + const connectionRecord = await getConnectionRecord(this.outOfBandId) + + try { + await this.agent.connections.returnWhenIsConnected(connectionRecord.id) + } catch (e) { + console.log(redText(`\nTimeout of 20 seconds reached.. Returning to home screen.\n`)) + return + } + console.log(greenText(Output.ConnectionEstablished)) + } + + public async setupConnection() { + await this.printConnectionInvite() + await this.waitForConnection() + } + + private printSchema(name: string, version: string, attributes: string[]) { + console.log(`\n\nThe credential definition will look like this:\n`) + console.log(purpleText(`Name: ${Color.Reset}${name}`)) + console.log(purpleText(`Version: ${Color.Reset}${version}`)) + console.log(purpleText(`Attributes: ${Color.Reset}${attributes[0]}, ${attributes[1]}, ${attributes[2]}\n`)) + } + + private async registerSchema() { + if (!this.anonCredsIssuerId) { + throw new Error(redText('Missing anoncreds issuerId')) + } + const schemaTemplate = { + name: 'Faber College' + utils.uuid(), + version: '1.0.0', + attrNames: ['name', 'degree', 'date'], + issuerId: this.anonCredsIssuerId, + } + this.printSchema(schemaTemplate.name, schemaTemplate.version, schemaTemplate.attrNames) + this.ui.updateBottomBar(greenText('\nRegistering schema...\n', false)) + + const { schemaState } = await this.agent.modules.anoncreds.registerSchema({ + schema: schemaTemplate, + options: { + endorserMode: 'internal', + endorserDid: this.anonCredsIssuerId, + }, + }) + + if (schemaState.state !== 'finished') { + throw new Error( + `Error registering schema: ${schemaState.state === 'failed' ? schemaState.reason : 'Not Finished'}` + ) + } + this.ui.updateBottomBar('\nSchema registered!\n') + return schemaState + } + + private async registerCredentialDefinition(schemaId: string) { + if (!this.anonCredsIssuerId) { + throw new Error(redText('Missing anoncreds issuerId')) + } + + this.ui.updateBottomBar('\nRegistering credential definition...\n') + const { credentialDefinitionState } = + await this.agent.modules.anoncreds.registerCredentialDefinition({ + credentialDefinition: { + schemaId, + issuerId: this.anonCredsIssuerId, + tag: 'latest', + }, + options: { + supportRevocation: false, + endorserMode: 'internal', + endorserDid: this.anonCredsIssuerId, + }, + }) + + if (credentialDefinitionState.state !== 'finished') { + throw new Error( + `Error registering credential definition: ${ + credentialDefinitionState.state === 'failed' ? credentialDefinitionState.reason : 'Not Finished' + }}` + ) + } + + this.credentialDefinition = credentialDefinitionState + this.ui.updateBottomBar('\nCredential definition registered!!\n') + return this.credentialDefinition + } + + public async issueCredential() { + const schema = await this.registerSchema() + const credentialDefinition = await this.registerCredentialDefinition(schema.schemaId) + const connectionRecord = await this.getConnectionRecord() + + this.ui.updateBottomBar('\nSending credential offer...\n') + + await this.agent.credentials.offerCredential({ + connectionId: connectionRecord.id, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + attributes: [ + { + name: 'name', + value: 'Alice Smith', + }, + { + name: 'degree', + value: 'Computer Science', + }, + { + name: 'date', + value: '01/01/2022', + }, + ], + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + }, + }, + }) + this.ui.updateBottomBar( + `\nCredential offer sent!\n\nGo to the Alice agent to accept the credential offer\n\n${Color.Reset}` + ) + } + + private async printProofFlow(print: string) { + this.ui.updateBottomBar(print) + await new Promise((f) => setTimeout(f, 2000)) + } + + private async newProofAttribute() { + await this.printProofFlow(greenText(`Creating new proof attribute for 'name' ...\n`)) + const proofAttribute = { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: this.credentialDefinition?.credentialDefinitionId, + }, + ], + }, + } + + return proofAttribute + } + + public async sendProofRequest() { + const connectionRecord = await this.getConnectionRecord() + const proofAttribute = await this.newProofAttribute() + await this.printProofFlow(greenText('\nRequesting proof...\n', false)) + + await this.agent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: connectionRecord.id, + proofFormats: { + anoncreds: { + name: 'proof-request', + version: '1.0', + requested_attributes: proofAttribute, + }, + }, + }) + this.ui.updateBottomBar( + `\nProof request sent!\n\nGo to the Alice agent to accept the proof request\n\n${Color.Reset}` + ) + } + + public async sendMessage(message: string) { + const connectionRecord = await this.getConnectionRecord() + await this.agent.basicMessages.sendMessage(connectionRecord.id, message) + } + + public async exit() { + console.log(Output.Exit) + await this.agent.shutdown() + process.exit(0) + } + + public async restart() { + await this.agent.shutdown() + } +} diff --git a/demo/src/FaberInquirer.ts b/demo/src/FaberInquirer.ts new file mode 100644 index 0000000000..7eb4ac7785 --- /dev/null +++ b/demo/src/FaberInquirer.ts @@ -0,0 +1,133 @@ +import { clear } from 'console' +import { textSync } from 'figlet' +import { prompt } from 'inquirer' + +import { BaseInquirer, ConfirmOptions } from './BaseInquirer' +import { Faber, RegistryOptions } from './Faber' +import { Listener } from './Listener' +import { Title } from './OutputClass' + +export const runFaber = async () => { + clear() + console.log(textSync('Faber', { horizontalLayout: 'full' })) + const faber = await FaberInquirer.build() + await faber.processAnswer() +} + +enum PromptOptions { + CreateConnection = 'Create connection invitation', + OfferCredential = 'Offer credential', + RequestProof = 'Request proof', + SendMessage = 'Send message', + Exit = 'Exit', + Restart = 'Restart', +} + +export class FaberInquirer extends BaseInquirer { + public faber: Faber + public promptOptionsString: string[] + public listener: Listener + + public constructor(faber: Faber) { + super() + this.faber = faber + this.listener = new Listener() + this.promptOptionsString = Object.values(PromptOptions) + this.listener.messageListener(this.faber.agent, this.faber.name) + } + + public static async build(): Promise { + const faber = await Faber.build() + return new FaberInquirer(faber) + } + + private async getPromptChoice() { + if (this.faber.outOfBandId) return prompt([this.inquireOptions(this.promptOptionsString)]) + + const reducedOption = [PromptOptions.CreateConnection, PromptOptions.Exit, PromptOptions.Restart] + return prompt([this.inquireOptions(reducedOption)]) + } + + public async processAnswer() { + const choice = await this.getPromptChoice() + if (this.listener.on) return + + switch (choice.options) { + case PromptOptions.CreateConnection: + await this.connection() + break + case PromptOptions.OfferCredential: + await this.credential() + return + case PromptOptions.RequestProof: + await this.proof() + return + case PromptOptions.SendMessage: + await this.message() + break + case PromptOptions.Exit: + await this.exit() + break + case PromptOptions.Restart: + await this.restart() + return + } + await this.processAnswer() + } + + public async connection() { + await this.faber.setupConnection() + } + + public async exitUseCase(title: string) { + const confirm = await prompt([this.inquireConfirmation(title)]) + if (confirm.options === ConfirmOptions.No) { + return false + } else if (confirm.options === ConfirmOptions.Yes) { + return true + } + } + + public async credential() { + const registry = await prompt([this.inquireOptions([RegistryOptions.indy, RegistryOptions.cheqd])]) + await this.faber.importDid(registry.options) + await this.faber.issueCredential() + const title = 'Is the credential offer accepted?' + await this.listener.newAcceptedPrompt(title, this) + } + + public async proof() { + await this.faber.sendProofRequest() + const title = 'Is the proof request accepted?' + await this.listener.newAcceptedPrompt(title, this) + } + + public async message() { + const message = await this.inquireMessage() + if (!message) return + + await this.faber.sendMessage(message) + } + + public async exit() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.faber.exit() + } + } + + public async restart() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.processAnswer() + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.faber.restart() + await runFaber() + } + } +} + +void runFaber() diff --git a/demo/src/Listener.ts b/demo/src/Listener.ts new file mode 100644 index 0000000000..8410537043 --- /dev/null +++ b/demo/src/Listener.ts @@ -0,0 +1,110 @@ +import type { Alice } from './Alice' +import type { AliceInquirer } from './AliceInquirer' +import type { Faber } from './Faber' +import type { FaberInquirer } from './FaberInquirer' +import type { + Agent, + BasicMessageStateChangedEvent, + CredentialExchangeRecord, + CredentialStateChangedEvent, + ProofExchangeRecord, + ProofStateChangedEvent, +} from '@credo-ts/core' +import type BottomBar from 'inquirer/lib/ui/bottom-bar' + +import { + BasicMessageEventTypes, + BasicMessageRole, + CredentialEventTypes, + CredentialState, + ProofEventTypes, + ProofState, +} from '@credo-ts/core' +import { ui } from 'inquirer' + +import { Color, purpleText } from './OutputClass' + +export class Listener { + public on: boolean + private ui: BottomBar + + public constructor() { + this.on = false + this.ui = new ui.BottomBar() + } + + private turnListenerOn() { + this.on = true + } + + private turnListenerOff() { + this.on = false + } + + private printCredentialAttributes(credentialRecord: CredentialExchangeRecord) { + if (credentialRecord.credentialAttributes) { + const attribute = credentialRecord.credentialAttributes + console.log('\n\nCredential preview:') + attribute.forEach((element) => { + console.log(purpleText(`${element.name} ${Color.Reset}${element.value}`)) + }) + } + } + + private async newCredentialPrompt(credentialRecord: CredentialExchangeRecord, aliceInquirer: AliceInquirer) { + this.printCredentialAttributes(credentialRecord) + this.turnListenerOn() + await aliceInquirer.acceptCredentialOffer(credentialRecord) + this.turnListenerOff() + await aliceInquirer.processAnswer() + } + + public credentialOfferListener(alice: Alice, aliceInquirer: AliceInquirer) { + alice.agent.events.on( + CredentialEventTypes.CredentialStateChanged, + async ({ payload }: CredentialStateChangedEvent) => { + if (payload.credentialRecord.state === CredentialState.OfferReceived) { + await this.newCredentialPrompt(payload.credentialRecord, aliceInquirer) + } + } + ) + } + + public messageListener(agent: Agent, name: string) { + agent.events.on(BasicMessageEventTypes.BasicMessageStateChanged, async (event: BasicMessageStateChangedEvent) => { + if (event.payload.basicMessageRecord.role === BasicMessageRole.Receiver) { + this.ui.updateBottomBar(purpleText(`\n${name} received a message: ${event.payload.message.content}\n`)) + } + }) + } + + private async newProofRequestPrompt(proofRecord: ProofExchangeRecord, aliceInquirer: AliceInquirer) { + this.turnListenerOn() + await aliceInquirer.acceptProofRequest(proofRecord) + this.turnListenerOff() + await aliceInquirer.processAnswer() + } + + public proofRequestListener(alice: Alice, aliceInquirer: AliceInquirer) { + alice.agent.events.on(ProofEventTypes.ProofStateChanged, async ({ payload }: ProofStateChangedEvent) => { + if (payload.proofRecord.state === ProofState.RequestReceived) { + await this.newProofRequestPrompt(payload.proofRecord, aliceInquirer) + } + }) + } + + public proofAcceptedListener(faber: Faber, faberInquirer: FaberInquirer) { + faber.agent.events.on(ProofEventTypes.ProofStateChanged, async ({ payload }: ProofStateChangedEvent) => { + if (payload.proofRecord.state === ProofState.Done) { + await faberInquirer.processAnswer() + } + }) + } + + public async newAcceptedPrompt(title: string, faberInquirer: FaberInquirer) { + this.turnListenerOn() + await faberInquirer.exitUseCase(title) + this.turnListenerOff() + await faberInquirer.processAnswer() + } +} diff --git a/demo/src/OutputClass.ts b/demo/src/OutputClass.ts new file mode 100644 index 0000000000..b9e69c72f0 --- /dev/null +++ b/demo/src/OutputClass.ts @@ -0,0 +1,40 @@ +export enum Color { + Green = `\x1b[32m`, + Red = `\x1b[31m`, + Purple = `\x1b[35m`, + Reset = `\x1b[0m`, +} + +export enum Output { + NoConnectionRecordFromOutOfBand = `\nNo connectionRecord has been created from invitation\n`, + ConnectionEstablished = `\nConnection established!`, + MissingConnectionRecord = `\nNo connectionRecord ID has been set yet\n`, + ConnectionLink = `\nRun 'Receive connection invitation' in Alice and paste this invitation link:\n\n`, + Exit = 'Shutting down agent...\nExiting...', +} + +export enum Title { + OptionsTitle = '\nOptions:', + InvitationTitle = '\n\nPaste the invitation url here:', + MessageTitle = '\n\nWrite your message here:\n(Press enter to send or press q to exit)\n', + ConfirmTitle = '\n\nAre you sure?', + CredentialOfferTitle = '\n\nCredential offer received, do you want to accept it?', + ProofRequestTitle = '\n\nProof request received, do you want to accept it?', +} + +export const greenText = (text: string, reset?: boolean) => { + if (reset) return Color.Green + text + Color.Reset + + return Color.Green + text +} + +export const purpleText = (text: string, reset?: boolean) => { + if (reset) return Color.Purple + text + Color.Reset + return Color.Purple + text +} + +export const redText = (text: string, reset?: boolean) => { + if (reset) return Color.Red + text + Color.Reset + + return Color.Red + text +} diff --git a/demo/tsconfig.json b/demo/tsconfig.json new file mode 100644 index 0000000000..df890c6054 --- /dev/null +++ b/demo/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["node"] + } +} diff --git a/docker-compose.arm.yml b/docker-compose.arm.yml new file mode 100644 index 0000000000..bfbb05a2e5 --- /dev/null +++ b/docker-compose.arm.yml @@ -0,0 +1,48 @@ +version: '3' +services: + postgres: + image: postgres:15-alpine + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - '5432:5432' + + indy-pool: + build: + context: . + dockerfile: network/indy-pool-arm.dockerfile + platform: linux/arm64/v8 + ports: + - '9701-9708:9701-9708' + # Start supervisord in bg, run commands, bring supervisor to fg + command: > + /bin/bash -c " + /usr/bin/supervisord & + indy-cli-setup && + add-did-from-seed 00000000000000000000000Endorser9 ENDORSER && + add-did-from-seed 000000000000000000000000Trustee9 TRUSTEE && + /usr/bin/supervisord -n + " + + cheqd-ledger: + image: ghcr.io/cheqd/cheqd-testnet:latest + platform: linux/amd64 + ports: + - '26657:26657' + command: > + /bin/bash -c ' + run-testnet & + export RUN_TESTNET_PID=$! && + sleep 10 && + (echo "sketch mountain erode window enact net enrich smoke claim kangaroo another visual write meat latin bacon pulp similar forum guilt father state erase bright"; echo "12345678"; echo "12345678";) | cheqd-noded keys add base --recover && + (echo "silk theme damp share lens select artefact orbit artwork weather mixture alarm remain oppose own wolf reduce melody cheap venture lady spy wise loud"; echo "12345678";) | cheqd-noded keys add extra1 --recover && + (echo "lobster pizza cost soft else rather rich find rose pride catch bar cube switch help joy stable dirt stumble voyage bind cabbage cram exist"; echo "12345678";) | cheqd-noded keys add extra2 --recover && + (echo "state online hedgehog turtle daring lab panda bottom agent pottery mixture venue letter decade bridge win snake mandate trust village emerge awkward fire mimic"; echo "12345678";) | cheqd-noded keys add extra3 --recover && + (echo "12345678";) | cheqd-noded tx bank send cheqd1rnr5jrt4exl0samwj0yegv99jeskl0hsxmcz96 cheqd1yeahnxhfa583wwpm9xt452xzet4xsgsqacgjkr 10000000000000000ncheq --from base --gas auto --fees 100000000ncheq --chain-id cheqd -y && + sleep 2 && + (echo "12345678";) | cheqd-noded tx bank send cheqd1rnr5jrt4exl0samwj0yegv99jeskl0hsxmcz96 cheqd14y3xeqd2xmhl9sxn8cf974k6nntqrveufqpqrs 10000000000000000ncheq --from base --gas auto --fees 100000000ncheq --chain-id cheqd -y && + sleep 2 && + (echo "12345678";) | cheqd-noded tx bank send cheqd1rnr5jrt4exl0samwj0yegv99jeskl0hsxmcz96 cheqd10qh2vl0jrax6yh2mzes03cm6vt27vd47geu375 10000000000000000ncheq --from base --gas auto --fees 100000000ncheq --chain-id cheqd -y && + wait $RUN_TESTNET_PID + ' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..6a8358da12 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +version: '3' +services: + postgres: + image: postgres:15-alpine + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - '5432:5432' + + indy-pool: + build: + context: . + dockerfile: network/indy-pool.dockerfile + ports: + - '9701-9708:9701-9708' + # Start supervisord in bg, run commands, bring supervisor to fg + command: > + /bin/bash -c " + /usr/bin/supervisord & + indy-cli-setup && + add-did-from-seed 00000000000000000000000Endorser9 ENDORSER && + add-did-from-seed 000000000000000000000000Trustee9 TRUSTEE && + /usr/bin/supervisord -n + " + + cheqd-ledger: + image: ghcr.io/cheqd/cheqd-testnet:latest + platform: linux/amd64 + ports: + - '26657:26657' + command: > + /bin/bash -c ' + run-testnet & + export RUN_TESTNET_PID=$! && + sleep 10 && + (echo "sketch mountain erode window enact net enrich smoke claim kangaroo another visual write meat latin bacon pulp similar forum guilt father state erase bright"; echo "12345678"; echo "12345678";) | cheqd-noded keys add base --recover && + (echo "silk theme damp share lens select artefact orbit artwork weather mixture alarm remain oppose own wolf reduce melody cheap venture lady spy wise loud"; echo "12345678";) | cheqd-noded keys add extra1 --recover && + (echo "lobster pizza cost soft else rather rich find rose pride catch bar cube switch help joy stable dirt stumble voyage bind cabbage cram exist"; echo "12345678";) | cheqd-noded keys add extra2 --recover && + (echo "state online hedgehog turtle daring lab panda bottom agent pottery mixture venue letter decade bridge win snake mandate trust village emerge awkward fire mimic"; echo "12345678";) | cheqd-noded keys add extra3 --recover && + (echo "12345678";) | cheqd-noded tx bank send cheqd1rnr5jrt4exl0samwj0yegv99jeskl0hsxmcz96 cheqd1yeahnxhfa583wwpm9xt452xzet4xsgsqacgjkr 10000000000000000ncheq --from base --gas auto --fees 100000000ncheq --chain-id cheqd -y && + sleep 2 && + (echo "12345678";) | cheqd-noded tx bank send cheqd1rnr5jrt4exl0samwj0yegv99jeskl0hsxmcz96 cheqd14y3xeqd2xmhl9sxn8cf974k6nntqrveufqpqrs 10000000000000000ncheq --from base --gas auto --fees 100000000ncheq --chain-id cheqd -y && + sleep 2 && + (echo "12345678";) | cheqd-noded tx bank send cheqd1rnr5jrt4exl0samwj0yegv99jeskl0hsxmcz96 cheqd10qh2vl0jrax6yh2mzes03cm6vt27vd47geu375 10000000000000000ncheq --from base --gas auto --fees 100000000ncheq --chain-id cheqd -y && + wait $RUN_TESTNET_PID + ' diff --git a/docker/docker-compose-mediators-ngrok.yml b/docker/docker-compose-mediators-ngrok.yml deleted file mode 100644 index 95f799250d..0000000000 --- a/docker/docker-compose-mediators-ngrok.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: '3' - -# This file extends docker-compose-mediators.yml - -services: - alice-mediator: - environment: - NGROK_NAME: alice-ngrok - entrypoint: ./scripts/ngrok-wait.sh - depends_on: [alice-ngrok] - - alice-ngrok: - image: wernight/ngrok - command: ngrok http -bind-tls=true --log stdout alice-mediator:3001 - networks: - - hyperledger - - bob-mediator: - environment: - NGROK_NAME: bob-ngrok - entrypoint: ./scripts/ngrok-wait.sh - depends_on: [bob-ngrok] - - bob-ngrok: - image: wernight/ngrok - command: ngrok http -bind-tls=true --log stdout bob-mediator:3002 - networks: - - hyperledger - -networks: - hyperledger: diff --git a/docker/docker-compose-mediators.yml b/docker/docker-compose-mediators.yml deleted file mode 100644 index f04e307e5e..0000000000 --- a/docker/docker-compose-mediators.yml +++ /dev/null @@ -1,25 +0,0 @@ -version: '3' - -services: - alice-mediator: - build: .. - image: aries-framework-javascript - container_name: alice-mediator - command: ./scripts/run-mediator.sh alice - networks: - - hyperledger - ports: - - 3001:3001 - - bob-mediator: - build: .. - image: aries-framework-javascript - container_name: bob-mediator - command: ./scripts/run-mediator.sh bob - networks: - - hyperledger - ports: - - 3002:3002 - -networks: - hyperledger: diff --git a/images/credo-logo.png b/images/credo-logo.png new file mode 100644 index 0000000000..1b83bdaa0d Binary files /dev/null and b/images/credo-logo.png differ diff --git a/jest.config.base.ts b/jest.config.base.ts new file mode 100644 index 0000000000..5dfdf49c1f --- /dev/null +++ b/jest.config.base.ts @@ -0,0 +1,22 @@ +import type { Config } from '@jest/types' + +const config: Config.InitialOptions = { + preset: 'ts-jest', + testEnvironment: 'node', + // NOTE: overridden in e2e test. Make sure to + // update that match as well when changing this one + testMatch: ['**/?(*.)test.ts'], + moduleNameMapper: { + '@credo-ts/(.+)': ['/../../packages/$1/src', '/../packages/$1/src', '/packages/$1/src'], + }, + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + isolatedModules: true, + }, + ], + }, +} + +export default config diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index b7187c2879..0000000000 --- a/jest.config.js +++ /dev/null @@ -1,17 +0,0 @@ -// TODO: maybe we use runner groups to make it -// easier to run specific groups of tests -// @see https://www.npmjs.com/package/jest-runner-groups -module.exports = { - preset: 'ts-jest', - roots: ['/src'], - testEnvironment: 'node', - setupFilesAfterEnv: ['/src/lib/__tests__/setup.ts'], - testPathIgnorePatterns: ['/node_modules/'], - testMatch: ['**/?(*.)+(spec|test).+(ts|tsx|js)'], - transform: { - '^.+\\.(ts|tsx)?$': 'ts-jest', - }, - collectCoverageFrom: ['src/lib/**/*.{js,jsx,tsx,ts}'], - coveragePathIgnorePatterns: ['/node_modules/', '/__tests__/'], - testTimeout: 60000, -}; diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000000..286a152f79 --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,17 @@ +import type { Config } from '@jest/types' + +import base from './jest.config.base' + +const config: Config.InitialOptions = { + ...base, + roots: [''], + coveragePathIgnorePatterns: ['/build/', '/node_modules/', '/__tests__/', 'tests'], + coverageDirectory: '/coverage/', + projects: [ + '/packages/*/jest.config.ts', + '/tests/jest.config.ts', + '/samples/extension-module/jest.config.ts', + ], +} + +export default config diff --git a/network/add-did-from-seed.sh b/network/add-did-from-seed.sh index c8b1fea1fe..aea3512d68 100755 --- a/network/add-did-from-seed.sh +++ b/network/add-did-from-seed.sh @@ -1,11 +1,12 @@ #!/bin/bash -export SEED=${1?"Seed missing\nUsage: $0 SEED"} +export SEED=${1?"Seed missing\nUsage: $0 SEED ROLE"} +export ROLE=$2 echo " -wallet open afj-wallet key=password +wallet open credo-wallet key=password -pool connect afj-pool +pool connect credo-pool did new seed=${SEED}" >/etc/indy/command.txt @@ -15,4 +16,4 @@ IFS='"' read -r -a DID_PARTS <<<"$DID_STRING" export DID=${DID_PARTS[1]} export VERKEY=${DID_PARTS[3]} -add-did "$DID" "$VERKEY" +add-did "$DID" "$VERKEY" "$ROLE" diff --git a/network/add-did.sh b/network/add-did.sh index 619794c29a..1de9adb6ad 100755 --- a/network/add-did.sh +++ b/network/add-did.sh @@ -1,13 +1,18 @@ #!/bin/bash -export DID=${1?"Did missing\nUsage: $0 DID VERKEY"} -export VERKEY=${2?"Verkey missing\nUsage: $0 DID VERKEY"} +export DID=${1?"Did missing\nUsage: $0 DID VERKEY [ROLE]"} +export VERKEY=${2?"Verkey missing\nUsage: $0 DID VERKEY [ROLE]"} +export ROLE=$3 + +if [ -z "$ROLE" ]; then + ROLE=TRUST_ANCHOR +fi echo " -wallet open afj-wallet key=password -pool connect afj-pool +wallet open credo-wallet key=password +pool connect credo-pool did use V4SGRU86Z58d6TV7PBUe6f -ledger nym did=${DID} verkey=${VERKEY} role=TRUST_ANCHOR" >/etc/indy/command.txt +ledger nym did=${DID} verkey=${VERKEY} role=${ROLE}" >/etc/indy/command.txt indy-cli --config /etc/indy/indy-cli-config.json /etc/indy/command.txt diff --git a/network/indy-cli-setup.sh b/network/indy-cli-setup.sh index 53b283eeb8..e351828154 100755 --- a/network/indy-cli-setup.sh +++ b/network/indy-cli-setup.sh @@ -1,11 +1,11 @@ #!/bin/bash echo ' -wallet create afj-wallet key=password -wallet open afj-wallet key=password +wallet create credo-wallet key=password +wallet open credo-wallet key=password -pool create afj-pool gen_txn_file=/etc/indy/genesis.txn -pool connect afj-pool +pool create credo-pool gen_txn_file=/etc/indy/genesis.txn +pool connect credo-pool did new seed=000000000000000000000000Trustee1 did use V4SGRU86Z58d6TV7PBUe6f diff --git a/network/indy-pool-arm.dockerfile b/network/indy-pool-arm.dockerfile new file mode 100644 index 0000000000..31412840e0 --- /dev/null +++ b/network/indy-pool-arm.dockerfile @@ -0,0 +1,67 @@ +FROM snel/von-image:node-1.12-4-arm64 + +USER root + +# Install environment +RUN apt-get update -y && apt-get install -y supervisor + +# It is imporatnt the the lines are not indented. Some autformatters +# Indent the supervisord parameters. THIS WILL BREAK THE SETUP +RUN echo "[supervisord]\n\ +logfile = /tmp/supervisord.log\n\ +logfile_maxbytes = 50MB\n\ +logfile_backups=10\n\ +logLevel = error\n\ +pidfile = /tmp/supervisord.pid\n\ +nodaemon = true\n\ +minfds = 1024\n\ +minprocs = 200\n\ +umask = 022\n\ +user = indy\n\ +identifier = supervisor\n\ +directory = /tmp\n\ +nocleanup = true\n\ +childlogdir = /tmp\n\ +strip_ansi = false\n\ +\n\ +[program:node1]\n\ +command=start_indy_node Node1 0.0.0.0 9701 0.0.0.0 9702\n\ +directory=/home/indy\n\ +stdout_logfile=/tmp/node1.log\n\ +stderr_logfile=/tmp/node1.log\n\ +\n\ +[program:node2]\n\ +command=start_indy_node Node2 0.0.0.0 9703 0.0.0.0 9704\n\ +directory=/home/indy\n\ +stdout_logfile=/tmp/node2.log\n\ +stderr_logfile=/tmp/node2.log\n\ +\n\ +[program:node3]\n\ +command=start_indy_node Node3 0.0.0.0 9705 0.0.0.0 9706\n\ +directory=/home/indy\n\ +stdout_logfile=/tmp/node3.log\n\ +stderr_logfile=/tmp/node3.log\n\ +\n\ +[program:node4]\n\ +command=start_indy_node Node4 0.0.0.0 9707 0.0.0.0 9708\n\ +directory=/home/indy\n\ +stdout_logfile=/tmp/node4.log\n\ +stderr_logfile=/tmp/node4.log\n"\ +>> /etc/supervisord.conf + +USER indy + +COPY --chown=indy:indy network/indy_config.py /etc/indy/indy_config.py + +ARG pool_ip=127.0.0.1 +RUN generate_indy_pool_transactions --nodes 4 --clients 5 --nodeNum 1 2 3 4 --ips="$pool_ip,$pool_ip,$pool_ip,$pool_ip" + +COPY network/add-did.sh /usr/bin/add-did +COPY network/indy-cli-setup.sh /usr/bin/indy-cli-setup +COPY network/add-did-from-seed.sh /usr/bin/add-did-from-seed +COPY network/genesis/local-genesis.txn /etc/indy/genesis.txn +COPY network/indy-cli-config.json /etc/indy/indy-cli-config.json + +EXPOSE 9701 9702 9703 9704 9705 9706 9707 9708 + +CMD ["/usr/bin/supervisord"] \ No newline at end of file diff --git a/network/indy-pool.dockerfile b/network/indy-pool.dockerfile index 0c341de441..8705c8a79d 100644 --- a/network/indy-pool.dockerfile +++ b/network/indy-pool.dockerfile @@ -1,48 +1,9 @@ -FROM ubuntu:16.04 +FROM bcgovimages/von-image:node-1.12-6 -ARG uid=1000 +USER root # Install environment -RUN apt-get update -y && apt-get install -y \ - git \ - wget \ - python3.5 \ - python3-pip \ - python-setuptools \ - python3-nacl \ - apt-transport-https \ - ca-certificates \ - supervisor \ - gettext-base \ - software-properties-common - -RUN pip3 install -U \ - pip==9.0.3 \ - setuptools - -RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 || \ - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys CE7709D068DB5E88 -ARG indy_stream=stable -RUN echo "deb https://repo.sovrin.org/deb xenial $indy_stream" >> /etc/apt/sources.list -RUN add-apt-repository "deb https://repo.sovrin.org/sdk/deb xenial $indy_stream" - -RUN useradd -ms /bin/bash -u $uid indy - -ARG indy_plenum_ver=1.12.1 -ARG indy_node_ver=1.12.1 -ARG python3_indy_crypto_ver=0.4.5 -ARG indy_crypto_ver=0.4.5 -ARG python3_pyzmq_ver=18.1.0 - -RUN apt-get update -y && apt-get install -y \ - python3-pyzmq=${python3_pyzmq_ver} \ - indy-plenum=${indy_plenum_ver} \ - indy-node=${indy_node_ver} \ - python3-indy-crypto=${python3_indy_crypto_ver} \ - libindy-crypto=${indy_crypto_ver} \ - vim \ - libindy \ - indy-cli +RUN apt-get update -y && apt-get install -y supervisor # It is imporatnt the the lines are not indented. Some autformatters # Indent the supervisord parameters. THIS WILL BREAK THE SETUP @@ -90,11 +51,9 @@ stderr_logfile=/tmp/node4.log\n"\ USER indy -RUN awk '{if (index($1, "NETWORK_NAME") != 0) {print("NETWORK_NAME = \"sandbox\"")} else print($0)}' /etc/indy/indy_config.py> /tmp/indy_config.py -RUN mv /tmp/indy_config.py /etc/indy/indy_config.py +COPY --chown=indy:indy network/indy_config.py /etc/indy/indy_config.py ARG pool_ip=127.0.0.1 - RUN generate_indy_pool_transactions --nodes 4 --clients 5 --nodeNum 1 2 3 4 --ips="$pool_ip,$pool_ip,$pool_ip,$pool_ip" COPY network/add-did.sh /usr/bin/add-did diff --git a/network/indy_config.py b/network/indy_config.py new file mode 100644 index 0000000000..2ba64c1cb2 --- /dev/null +++ b/network/indy_config.py @@ -0,0 +1,12 @@ +NETWORK_NAME = 'sandbox' + +LEDGER_DIR = '/home/indy/ledger' +LOG_DIR = '/home/indy/log' +KEYS_DIR = LEDGER_DIR +GENESIS_DIR = LEDGER_DIR +BACKUP_DIR = '/home/indy/backup' +PLUGINS_DIR = '/home/indy/plugins' +NODE_INFO_DIR = LEDGER_DIR + +CLI_BASE_DIR = '/home/indy/.indy-cli/' +CLI_NETWORK_DIR = '/home/indy/.indy-cli/networks' diff --git a/package.json b/package.json index 51830f1930..e68f5483d7 100644 --- a/package.json +++ b/package.json @@ -1,62 +1,71 @@ { - "name": "aries-framework-javascript", - "version": "1.0.0", + "name": "credo-ts", + "private": true, "license": "Apache-2.0", - "main": "build/lib/index.js", - "types": "build/lib/index.d.ts", - "files": [ - "build/lib" + "workspaces": [ + "packages/*", + "demo", + "demo-openid", + "samples/*" ], - "scripts": { - "compile": "tsc", - "lint": "eslint --ignore-path .gitignore '**/*.+(js|ts)'", - "prettier": "prettier --ignore-path .gitignore '**/*.+(js|json|ts)'", - "format": "yarn prettier --write", - "check-format": "yarn prettier --list-different", - "test": "jest --verbose", - "dev": "ts-node-dev --respawn --transpile-only ./src/samples/mediator.ts", - "prod:start": "node ./build/samples/mediator.js", - "prod:build": "rm -rf build && yarn compile", - "validate": "npm-run-all --parallel lint compile", - "prepack": "rm -rf build && yarn compile", - "prepare": "husky install" + "repository": { + "url": "https://github.com/openwallet-foundation/credo-ts", + "type": "git" }, - "dependencies": { - "bn.js": "^5.2.0", - "buffer": "^6.0.3", - "class-transformer": "^0.4.0", - "class-validator": "^0.13.1", - "events": "^3.3.0", - "js-sha256": "^0.9.0", - "reflect-metadata": "^0.1.13", - "uuid": "^8.3.0" + "scripts": { + "check-types": "pnpm check-types:build && pnpm check-types:tests", + "check-types:tests": "tsc -p tsconfig.test.json --noEmit", + "check-types:build": "pnpm -r --parallel exec tsc --noEmit", + "prettier": "prettier --ignore-path .prettierignore '**/*.+(js|json|ts|md|yml|yaml)'", + "format": "pnpm prettier --write", + "check-format": "pnpm prettier --list-different", + "clean": "pnpm -r --parallel run clean", + "build": "pnpm -r --parallel run build", + "test:unit": "jest --testPathIgnorePatterns 'e2e.test.ts$'", + "test:e2e": "jest --testMatch '**/?(*.)e2e.test.ts'", + "test": "jest", + "lint": "eslint --ignore-path .gitignore .", + "validate": "pnpm lint && pnpm check-types && pnpm check-format", + "run-mediator": "ts-node ./samples/mediator.ts", + "release": "pnpm build && pnpm changeset publish" }, "devDependencies": { - "@types/bn.js": "^5.1.0", + "@changesets/cli": "^2.27.5", + "@hyperledger/aries-askar-nodejs": "^0.2.1", + "@jest/types": "^29.6.3", + "@types/bn.js": "^5.1.5", "@types/cors": "^2.8.10", - "@types/express": "4.17.8", - "@types/indy-sdk": "^1.15.2", - "@types/jest": "^26.0.20", - "@types/node-fetch": "^2.5.8", - "@types/uuid": "^8.3.0", - "@typescript-eslint/eslint-plugin": "^4.17.0", - "@typescript-eslint/parser": "^4.17.0", + "@types/eslint": "^8.21.2", + "@types/express": "^4.17.13", + "@types/jest": "^29.5.12", + "@types/node": "^18.18.8", + "@types/uuid": "^9.0.1", + "@types/varint": "^6.0.0", + "@types/ws": "^8.5.4", + "@typescript-eslint/eslint-plugin": "^7.14.1", + "@typescript-eslint/parser": "^7.14.1", + "bn.js": "^5.2.1", "cors": "^2.8.5", - "dotenv": "^8.2.0", - "eslint": "^7.21.0", - "eslint-config-prettier": "^8.1.0", - "eslint-plugin-prettier": "^3.3.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.3.0", + "eslint-import-resolver-typescript": "^3.5.3", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-workspaces": "^0.8.0", "express": "^4.17.1", - "husky": "^5.1.3", - "indy-sdk": "^1.16.0", - "jest": "^26.6.3", - "node-fetch": "^2.6.1", - "npm-run-all": "^4.1.5", - "prettier": "^2.2.1", - "rxjs": "^6.6.6", - "ts-jest": "^26.5.3", - "ts-node-dev": "^1.1.6", - "tslog": "^3.1.2", - "typescript": "^4.2.3" + "jest": "^29.7.0", + "prettier": "^2.3.1", + "rxjs": "^7.8.0", + "ts-jest": "^29.1.2", + "ts-node": "^10.0.0", + "tsyringe": "^4.8.0", + "typescript": "~5.5.2", + "ws": "^8.13.0" + }, + "resolutions": { + "@types/node": "18.18.8" + }, + "engines": { + "node": ">=18" } } diff --git a/packages/action-menu/CHANGELOG.md b/packages/action-menu/CHANGELOG.md new file mode 100644 index 0000000000..880a206b3d --- /dev/null +++ b/packages/action-menu/CHANGELOG.md @@ -0,0 +1,132 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release +- Updated dependencies [66e696d] + - @credo-ts/core@0.5.6 + +## 0.5.5 + +### Patch Changes + +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) +- Updated dependencies [3239ef3] +- Updated dependencies [d548fa4] +- Updated dependencies [482a630] + - @credo-ts/core@0.5.5 + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +**Note:** Version bump only for package @credo-ts/action-menu + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +**Note:** Version bump only for package @credo-ts/action-menu + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +**Note:** Version bump only for package @credo-ts/action-menu + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +### Features + +- **openid4vc:** persistance and events ([#1793](https://github.com/openwallet-foundation/credo-ts/issues/1793)) ([f4c386a](https://github.com/openwallet-foundation/credo-ts/commit/f4c386a6ccf8adb829cad30b81d524e6ffddb029)) + +## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) + +**Note:** Version bump only for package @credo-ts/action-menu + +## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) + +### Features + +- oob without handhsake improvements and routing ([#1511](https://github.com/hyperledger/aries-framework-javascript/issues/1511)) ([9e69cf4](https://github.com/hyperledger/aries-framework-javascript/commit/9e69cf441a75bf7a3c5556cf59e730ee3fce8c28)) + +# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) + +### Bug Fixes + +- expose indy pool configs and action menu messages ([#1333](https://github.com/hyperledger/aries-framework-javascript/issues/1333)) ([518e5e4](https://github.com/hyperledger/aries-framework-javascript/commit/518e5e4dfb59f9c0457bfd233409e9f4b3c429ee)) +- thread id improvements ([#1311](https://github.com/hyperledger/aries-framework-javascript/issues/1311)) ([229ed1b](https://github.com/hyperledger/aries-framework-javascript/commit/229ed1b9540ca0c9380b5cca6c763fefd6628960)) + +- refactor!: remove Dispatcher.registerMessageHandler (#1354) ([78ecf1e](https://github.com/hyperledger/aries-framework-javascript/commit/78ecf1ed959c9daba1c119d03f4596f1db16c57c)), closes [#1354](https://github.com/hyperledger/aries-framework-javascript/issues/1354) + +### Features + +- **openid4vc:** jwt format and more crypto ([#1472](https://github.com/hyperledger/aries-framework-javascript/issues/1472)) ([bd4932d](https://github.com/hyperledger/aries-framework-javascript/commit/bd4932d34f7314a6d49097b6460c7570e1ebc7a8)) +- outbound message send via session ([#1335](https://github.com/hyperledger/aries-framework-javascript/issues/1335)) ([582c711](https://github.com/hyperledger/aries-framework-javascript/commit/582c711728db12b7d38a0be2e9fa78dbf31b34c6)) + +### BREAKING CHANGES + +- `Dispatcher.registerMessageHandler` has been removed in favour of `MessageHandlerRegistry.registerMessageHandler`. If you want to register message handlers in an extension module, you can use directly `agentContext.dependencyManager.registerMessageHandlers`. + +Signed-off-by: Ariel Gentile + +## [0.3.3](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.2...v0.3.3) (2023-01-18) + +### Bug Fixes + +- fix typing issues with typescript 4.9 ([#1214](https://github.com/hyperledger/aries-framework-javascript/issues/1214)) ([087980f](https://github.com/hyperledger/aries-framework-javascript/commit/087980f1adf3ee0bc434ca9782243a62c6124444)) + +### Features + +- **indy-sdk:** add indy-sdk package ([#1200](https://github.com/hyperledger/aries-framework-javascript/issues/1200)) ([9933b35](https://github.com/hyperledger/aries-framework-javascript/commit/9933b35a6aa4524caef8a885e71b742cd0d7186b)) + +## [0.3.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.1...v0.3.2) (2023-01-04) + +**Note:** Version bump only for package @credo-ts/action-menu + +## [0.3.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.0...v0.3.1) (2022-12-27) + +**Note:** Version bump only for package @credo-ts/action-menu + +# [0.3.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.5...v0.3.0) (2022-12-22) + +- refactor!: rename Handler to MessageHandler (#1161) ([5e48696](https://github.com/hyperledger/aries-framework-javascript/commit/5e48696ec16d88321f225628e6cffab243718b4c)), closes [#1161](https://github.com/hyperledger/aries-framework-javascript/issues/1161) +- feat(action-menu)!: move to separate package (#1049) ([e0df0d8](https://github.com/hyperledger/aries-framework-javascript/commit/e0df0d884b1a7816c7c638406606e45f6e169ff4)), closes [#1049](https://github.com/hyperledger/aries-framework-javascript/issues/1049) + +### BREAKING CHANGES + +- Handler has been renamed to MessageHandler to be more descriptive, along with related types and methods. This means: + +Handler is now MessageHandler +HandlerInboundMessage is now MessageHandlerInboundMessage +Dispatcher.registerHandler is now Dispatcher.registerMessageHandlers + +- action-menu module has been removed from the core and moved to a separate package. To integrate it in an Agent instance, it can be injected in constructor like this: + +```ts +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + actionMenu: new ActionMenuModule(), + /* other custom modules */ + }, +}) +``` + +Then, module API can be accessed in `agent.modules.actionMenu`. diff --git a/packages/action-menu/README.md b/packages/action-menu/README.md new file mode 100644 index 0000000000..716d9204b5 --- /dev/null +++ b/packages/action-menu/README.md @@ -0,0 +1,64 @@ +

+
+ Credo Logo +

+

Credo Action Menu Module

+

+ License + typescript + @credo-ts/action-menu version + +

+
+ +Action Menu module for [Credo](https://github.com/openwallet-foundation/credo-ts.git). Implements [Aries RFC 0509](https://github.com/hyperledger/aries-rfcs/blob/1795d5c2d36f664f88f5e8045042ace8e573808c/features/0509-action-menu/README.md). + +### Quick start + +In order for this module to work, we have to inject it into the agent to access agent functionality. See the example for more information. + +### Example of usage + +```ts +import { ActionMenuModule } from '@credo-ts/action-menu' + +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + actionMenu: new ActionMenuModule(), + /* other custom modules */ + }, +}) + +await agent.initialize() + +// To request root menu to a given connection (menu will be received +// asynchronously in a ActionMenuStateChangedEvent) +await agent.modules.actionMenu.requestMenu({ connectionId }) + +// To select an option from the action menu +await agent.modules.actionMenu.performAction({ + connectionId, + performedAction: { name: 'option-1' }, +}) +``` diff --git a/packages/action-menu/jest.config.ts b/packages/action-menu/jest.config.ts new file mode 100644 index 0000000000..93c0197296 --- /dev/null +++ b/packages/action-menu/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/action-menu/package.json b/packages/action-menu/package.json new file mode 100644 index 0000000000..f7be7a29d1 --- /dev/null +++ b/packages/action-menu/package.json @@ -0,0 +1,39 @@ +{ + "name": "@credo-ts/action-menu", + "main": "src/index", + "types": "src/index", + "version": "0.5.6", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/action-menu", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/action-menu" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build", + "test": "jest" + }, + "dependencies": { + "@credo-ts/core": "workspace:*", + "class-transformer": "0.5.1", + "class-validator": "0.14.1", + "rxjs": "^7.8.0" + }, + "devDependencies": { + "reflect-metadata": "^0.1.13", + "rimraf": "^4.4.0", + "typescript": "~5.5.2" + } +} diff --git a/packages/action-menu/src/ActionMenuApi.ts b/packages/action-menu/src/ActionMenuApi.ts new file mode 100644 index 0000000000..0024aab32c --- /dev/null +++ b/packages/action-menu/src/ActionMenuApi.ts @@ -0,0 +1,167 @@ +import type { + ClearActiveMenuOptions, + FindActiveMenuOptions, + PerformActionOptions, + RequestMenuOptions, + SendMenuOptions, +} from './ActionMenuApiOptions' + +import { + AgentContext, + CredoError, + ConnectionService, + MessageSender, + injectable, + getOutboundMessageContext, +} from '@credo-ts/core' + +import { ActionMenuRole } from './ActionMenuRole' +import { + ActionMenuProblemReportHandler, + MenuMessageHandler, + MenuRequestMessageHandler, + PerformMessageHandler, +} from './handlers' +import { ActionMenuService } from './services' + +/** + * @public + */ +@injectable() +export class ActionMenuApi { + private connectionService: ConnectionService + private messageSender: MessageSender + private actionMenuService: ActionMenuService + private agentContext: AgentContext + + public constructor( + connectionService: ConnectionService, + messageSender: MessageSender, + actionMenuService: ActionMenuService, + agentContext: AgentContext + ) { + this.connectionService = connectionService + this.messageSender = messageSender + this.actionMenuService = actionMenuService + this.agentContext = agentContext + + this.agentContext.dependencyManager.registerMessageHandlers([ + new ActionMenuProblemReportHandler(this.actionMenuService), + new MenuMessageHandler(this.actionMenuService), + new MenuRequestMessageHandler(this.actionMenuService), + new PerformMessageHandler(this.actionMenuService), + ]) + } + + /** + * Start Action Menu protocol as requester, asking for root menu. Any active menu will be cleared. + * + * @param options options for requesting menu + * @returns Action Menu record associated to this new request + */ + public async requestMenu(options: RequestMenuOptions) { + const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + + const { message, record } = await this.actionMenuService.createRequest(this.agentContext, { + connection, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: record, + connectionRecord: connection, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + + return record + } + + /** + * Send a new Action Menu as responder. This menu will be sent as response if there is an + * existing menu thread. + * + * @param options options for sending menu + * @returns Action Menu record associated to this action + */ + public async sendMenu(options: SendMenuOptions) { + const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + + const { message, record } = await this.actionMenuService.createMenu(this.agentContext, { + connection, + menu: options.menu, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: record, + connectionRecord: connection, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + + return record + } + + /** + * Perform action in active Action Menu, as a requester. The related + * menu will be closed. + * + * @param options options for requesting menu + * @returns Action Menu record associated to this selection + */ + public async performAction(options: PerformActionOptions) { + const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + + const actionMenuRecord = await this.actionMenuService.find(this.agentContext, { + connectionId: connection.id, + role: ActionMenuRole.Requester, + }) + if (!actionMenuRecord) { + throw new CredoError(`No active menu found for connection id ${options.connectionId}`) + } + + const { message, record } = await this.actionMenuService.createPerform(this.agentContext, { + actionMenuRecord, + performedAction: options.performedAction, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: record, + connectionRecord: connection, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + + return record + } + + /** + * Find the current active menu for a given connection and the specified role. + * + * @param options options for requesting active menu + * @returns Active Action Menu record, or null if no active menu found + */ + public async findActiveMenu(options: FindActiveMenuOptions) { + return this.actionMenuService.find(this.agentContext, { + connectionId: options.connectionId, + role: options.role, + }) + } + + /** + * Clears the current active menu for a given connection and the specified role. + * + * @param options options for clearing active menu + * @returns Active Action Menu record, or null if no active menu record found + */ + public async clearActiveMenu(options: ClearActiveMenuOptions) { + const actionMenuRecord = await this.actionMenuService.find(this.agentContext, { + connectionId: options.connectionId, + role: options.role, + }) + + return actionMenuRecord ? await this.actionMenuService.clearMenu(this.agentContext, { actionMenuRecord }) : null + } +} diff --git a/packages/action-menu/src/ActionMenuApiOptions.ts b/packages/action-menu/src/ActionMenuApiOptions.ts new file mode 100644 index 0000000000..877e5fab74 --- /dev/null +++ b/packages/action-menu/src/ActionMenuApiOptions.ts @@ -0,0 +1,41 @@ +import type { ActionMenuRole } from './ActionMenuRole' +import type { ActionMenu, ActionMenuSelection } from './models' + +/** + * @public + */ +export interface FindActiveMenuOptions { + connectionId: string + role: ActionMenuRole +} + +/** + * @public + */ +export interface ClearActiveMenuOptions { + connectionId: string + role: ActionMenuRole +} + +/** + * @public + */ +export interface RequestMenuOptions { + connectionId: string +} + +/** + * @public + */ +export interface SendMenuOptions { + connectionId: string + menu: ActionMenu +} + +/** + * @public + */ +export interface PerformActionOptions { + connectionId: string + performedAction: ActionMenuSelection +} diff --git a/packages/action-menu/src/ActionMenuEvents.ts b/packages/action-menu/src/ActionMenuEvents.ts new file mode 100644 index 0000000000..31ae0c1774 --- /dev/null +++ b/packages/action-menu/src/ActionMenuEvents.ts @@ -0,0 +1,21 @@ +import type { ActionMenuState } from './ActionMenuState' +import type { ActionMenuRecord } from './repository' +import type { BaseEvent } from '@credo-ts/core' + +/** + * @public + */ +export enum ActionMenuEventTypes { + ActionMenuStateChanged = 'ActionMenuStateChanged', +} + +/** + * @public + */ +export interface ActionMenuStateChangedEvent extends BaseEvent { + type: typeof ActionMenuEventTypes.ActionMenuStateChanged + payload: { + actionMenuRecord: ActionMenuRecord + previousState: ActionMenuState | null + } +} diff --git a/packages/action-menu/src/ActionMenuModule.ts b/packages/action-menu/src/ActionMenuModule.ts new file mode 100644 index 0000000000..21beecf751 --- /dev/null +++ b/packages/action-menu/src/ActionMenuModule.ts @@ -0,0 +1,34 @@ +import type { DependencyManager, FeatureRegistry, Module } from '@credo-ts/core' + +import { Protocol } from '@credo-ts/core' + +import { ActionMenuApi } from './ActionMenuApi' +import { ActionMenuRole } from './ActionMenuRole' +import { ActionMenuRepository } from './repository' +import { ActionMenuService } from './services' + +/** + * @public + */ +export class ActionMenuModule implements Module { + public readonly api = ActionMenuApi + + /** + * Registers the dependencies of the question answer module on the dependency manager. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Services + dependencyManager.registerSingleton(ActionMenuService) + + // Repositories + dependencyManager.registerSingleton(ActionMenuRepository) + + // Feature Registry + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/action-menu/1.0', + roles: [ActionMenuRole.Requester, ActionMenuRole.Responder], + }) + ) + } +} diff --git a/packages/action-menu/src/ActionMenuRole.ts b/packages/action-menu/src/ActionMenuRole.ts new file mode 100644 index 0000000000..a73d9351aa --- /dev/null +++ b/packages/action-menu/src/ActionMenuRole.ts @@ -0,0 +1,10 @@ +/** + * Action Menu roles based on the flow defined in RFC 0509. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0509-action-menu#roles + * @public + */ +export enum ActionMenuRole { + Requester = 'requester', + Responder = 'responder', +} diff --git a/packages/action-menu/src/ActionMenuState.ts b/packages/action-menu/src/ActionMenuState.ts new file mode 100644 index 0000000000..da19f40686 --- /dev/null +++ b/packages/action-menu/src/ActionMenuState.ts @@ -0,0 +1,14 @@ +/** + * Action Menu states based on the flow defined in RFC 0509. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0509-action-menu#states + * @public + */ +export enum ActionMenuState { + Null = 'null', + AwaitingRootMenu = 'awaiting-root-menu', + PreparingRootMenu = 'preparing-root-menu', + PreparingSelection = 'preparing-selection', + AwaitingSelection = 'awaiting-selection', + Done = 'done', +} diff --git a/packages/action-menu/src/__tests__/ActionMenuModule.test.ts b/packages/action-menu/src/__tests__/ActionMenuModule.test.ts new file mode 100644 index 0000000000..c37554a827 --- /dev/null +++ b/packages/action-menu/src/__tests__/ActionMenuModule.test.ts @@ -0,0 +1,37 @@ +import type { DependencyManager, FeatureRegistry } from '@credo-ts/core' + +import { Protocol } from '@credo-ts/core' + +import { ActionMenuModule } from '../ActionMenuModule' +import { ActionMenuRole } from '../ActionMenuRole' +import { ActionMenuRepository } from '../repository' +import { ActionMenuService } from '../services' + +const dependencyManager = { + registerInstance: jest.fn(), + registerSingleton: jest.fn(), + registerContextScoped: jest.fn(), +} as unknown as DependencyManager + +const featureRegistry = { + register: jest.fn(), +} as unknown as FeatureRegistry + +describe('ActionMenuModule', () => { + test('registers dependencies on the dependency manager', () => { + const actionMenuModule = new ActionMenuModule() + actionMenuModule.register(dependencyManager, featureRegistry) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(ActionMenuService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(ActionMenuRepository) + + expect(featureRegistry.register).toHaveBeenCalledTimes(1) + expect(featureRegistry.register).toHaveBeenCalledWith( + new Protocol({ + id: 'https://didcomm.org/action-menu/1.0', + roles: [ActionMenuRole.Requester, ActionMenuRole.Responder], + }) + ) + }) +}) diff --git a/packages/action-menu/src/errors/ActionMenuProblemReportError.ts b/packages/action-menu/src/errors/ActionMenuProblemReportError.ts new file mode 100644 index 0000000000..f44d3d3c76 --- /dev/null +++ b/packages/action-menu/src/errors/ActionMenuProblemReportError.ts @@ -0,0 +1,30 @@ +import type { ActionMenuProblemReportReason } from './ActionMenuProblemReportReason' +import type { ProblemReportErrorOptions } from '@credo-ts/core' + +import { ProblemReportError } from '@credo-ts/core' + +import { ActionMenuProblemReportMessage } from '../messages' + +/** + * @internal + */ +interface ActionMenuProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: ActionMenuProblemReportReason +} + +/** + * @internal + */ +export class ActionMenuProblemReportError extends ProblemReportError { + public problemReport: ActionMenuProblemReportMessage + + public constructor(public message: string, { problemCode }: ActionMenuProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new ActionMenuProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/action-menu/src/errors/ActionMenuProblemReportReason.ts b/packages/action-menu/src/errors/ActionMenuProblemReportReason.ts new file mode 100644 index 0000000000..c015c5507f --- /dev/null +++ b/packages/action-menu/src/errors/ActionMenuProblemReportReason.ts @@ -0,0 +1,9 @@ +/** + * Action Menu errors discussed in RFC 0509. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0509-action-menu#unresolved-questions + * @internal + */ +export enum ActionMenuProblemReportReason { + Timeout = 'timeout', +} diff --git a/packages/action-menu/src/handlers/ActionMenuProblemReportHandler.ts b/packages/action-menu/src/handlers/ActionMenuProblemReportHandler.ts new file mode 100644 index 0000000000..a49b665098 --- /dev/null +++ b/packages/action-menu/src/handlers/ActionMenuProblemReportHandler.ts @@ -0,0 +1,20 @@ +import type { ActionMenuService } from '../services' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { ActionMenuProblemReportMessage } from '../messages' + +/** + * @internal + */ +export class ActionMenuProblemReportHandler implements MessageHandler { + private actionMenuService: ActionMenuService + public supportedMessages = [ActionMenuProblemReportMessage] + + public constructor(actionMenuService: ActionMenuService) { + this.actionMenuService = actionMenuService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.actionMenuService.processProblemReport(messageContext) + } +} diff --git a/packages/action-menu/src/handlers/MenuMessageHandler.ts b/packages/action-menu/src/handlers/MenuMessageHandler.ts new file mode 100644 index 0000000000..377bd07603 --- /dev/null +++ b/packages/action-menu/src/handlers/MenuMessageHandler.ts @@ -0,0 +1,22 @@ +import type { ActionMenuService } from '../services' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { MenuMessage } from '../messages' + +/** + * @internal + */ +export class MenuMessageHandler implements MessageHandler { + private actionMenuService: ActionMenuService + public supportedMessages = [MenuMessage] + + public constructor(actionMenuService: ActionMenuService) { + this.actionMenuService = actionMenuService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + inboundMessage.assertReadyConnection() + + await this.actionMenuService.processMenu(inboundMessage) + } +} diff --git a/packages/action-menu/src/handlers/MenuRequestMessageHandler.ts b/packages/action-menu/src/handlers/MenuRequestMessageHandler.ts new file mode 100644 index 0000000000..c214a9fc0e --- /dev/null +++ b/packages/action-menu/src/handlers/MenuRequestMessageHandler.ts @@ -0,0 +1,22 @@ +import type { ActionMenuService } from '../services' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { MenuRequestMessage } from '../messages' + +/** + * @internal + */ +export class MenuRequestMessageHandler implements MessageHandler { + private actionMenuService: ActionMenuService + public supportedMessages = [MenuRequestMessage] + + public constructor(actionMenuService: ActionMenuService) { + this.actionMenuService = actionMenuService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + inboundMessage.assertReadyConnection() + + await this.actionMenuService.processRequest(inboundMessage) + } +} diff --git a/packages/action-menu/src/handlers/PerformMessageHandler.ts b/packages/action-menu/src/handlers/PerformMessageHandler.ts new file mode 100644 index 0000000000..dd25103242 --- /dev/null +++ b/packages/action-menu/src/handlers/PerformMessageHandler.ts @@ -0,0 +1,22 @@ +import type { ActionMenuService } from '../services' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { PerformMessage } from '../messages' + +/** + * @internal + */ +export class PerformMessageHandler implements MessageHandler { + private actionMenuService: ActionMenuService + public supportedMessages = [PerformMessage] + + public constructor(actionMenuService: ActionMenuService) { + this.actionMenuService = actionMenuService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + inboundMessage.assertReadyConnection() + + await this.actionMenuService.processPerform(inboundMessage) + } +} diff --git a/packages/action-menu/src/handlers/index.ts b/packages/action-menu/src/handlers/index.ts new file mode 100644 index 0000000000..b7ba3b7117 --- /dev/null +++ b/packages/action-menu/src/handlers/index.ts @@ -0,0 +1,4 @@ +export * from './ActionMenuProblemReportHandler' +export * from './MenuMessageHandler' +export * from './MenuRequestMessageHandler' +export * from './PerformMessageHandler' diff --git a/packages/action-menu/src/index.ts b/packages/action-menu/src/index.ts new file mode 100644 index 0000000000..97c9a70ea7 --- /dev/null +++ b/packages/action-menu/src/index.ts @@ -0,0 +1,9 @@ +export * from './ActionMenuApi' +export * from './ActionMenuApiOptions' +export * from './ActionMenuModule' +export * from './ActionMenuEvents' +export * from './ActionMenuRole' +export * from './ActionMenuState' +export * from './models' +export * from './repository' +export * from './messages' diff --git a/packages/action-menu/src/messages/ActionMenuProblemReportMessage.ts b/packages/action-menu/src/messages/ActionMenuProblemReportMessage.ts new file mode 100644 index 0000000000..c62acbc17c --- /dev/null +++ b/packages/action-menu/src/messages/ActionMenuProblemReportMessage.ts @@ -0,0 +1,23 @@ +import type { ProblemReportMessageOptions } from '@credo-ts/core' + +import { IsValidMessageType, parseMessageType, ProblemReportMessage } from '@credo-ts/core' + +export type ActionMenuProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + * @internal + */ +export class ActionMenuProblemReportMessage extends ProblemReportMessage { + /** + * Create new ConnectionProblemReportMessage instance. + * @param options + */ + public constructor(options: ActionMenuProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(ActionMenuProblemReportMessage.type) + public readonly type = ActionMenuProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/action-menu/1.0/problem-report') +} diff --git a/packages/action-menu/src/messages/MenuMessage.ts b/packages/action-menu/src/messages/MenuMessage.ts new file mode 100644 index 0000000000..808b6ba465 --- /dev/null +++ b/packages/action-menu/src/messages/MenuMessage.ts @@ -0,0 +1,60 @@ +import type { ActionMenuOptionOptions } from '../models' + +import { AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' +import { Expose, Type } from 'class-transformer' +import { IsInstance, IsOptional, IsString } from 'class-validator' + +import { ActionMenuOption } from '../models' + +/** + * @internal + */ +export interface MenuMessageOptions { + id?: string + title: string + description: string + errorMessage?: string + options: ActionMenuOptionOptions[] + threadId?: string +} + +/** + * @internal + */ +export class MenuMessage extends AgentMessage { + public constructor(options: MenuMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.title = options.title + this.description = options.description + this.errorMessage = options.errorMessage + this.options = options.options.map((p) => new ActionMenuOption(p)) + if (options.threadId) { + this.setThread({ + threadId: options.threadId, + }) + } + } + } + + @IsValidMessageType(MenuMessage.type) + public readonly type = MenuMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/action-menu/1.0/menu') + + @IsString() + public title!: string + + @IsString() + public description!: string + + @Expose({ name: 'errormsg' }) + @IsString() + @IsOptional() + public errorMessage?: string + + @IsInstance(ActionMenuOption, { each: true }) + @Type(() => ActionMenuOption) + public options!: ActionMenuOption[] +} diff --git a/packages/action-menu/src/messages/MenuRequestMessage.ts b/packages/action-menu/src/messages/MenuRequestMessage.ts new file mode 100644 index 0000000000..6fb97857c5 --- /dev/null +++ b/packages/action-menu/src/messages/MenuRequestMessage.ts @@ -0,0 +1,25 @@ +import { AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' + +/** + * @internal + */ +export interface MenuRequestMessageOptions { + id?: string +} + +/** + * @internal + */ +export class MenuRequestMessage extends AgentMessage { + public constructor(options: MenuRequestMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + } + } + + @IsValidMessageType(MenuRequestMessage.type) + public readonly type = MenuRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/action-menu/1.0/menu-request') +} diff --git a/packages/action-menu/src/messages/PerformMessage.ts b/packages/action-menu/src/messages/PerformMessage.ts new file mode 100644 index 0000000000..a62331aa15 --- /dev/null +++ b/packages/action-menu/src/messages/PerformMessage.ts @@ -0,0 +1,41 @@ +import { AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' +import { IsOptional, IsString } from 'class-validator' + +/** + * @internal + */ +export interface PerformMessageOptions { + id?: string + name: string + params?: Record + threadId: string +} + +/** + * @internal + */ +export class PerformMessage extends AgentMessage { + public constructor(options: PerformMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.name = options.name + this.params = options.params + this.setThread({ + threadId: options.threadId, + }) + } + } + + @IsValidMessageType(PerformMessage.type) + public readonly type = PerformMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/action-menu/1.0/perform') + + @IsString() + public name!: string + + @IsString({ each: true }) + @IsOptional() + public params?: Record +} diff --git a/packages/action-menu/src/messages/index.ts b/packages/action-menu/src/messages/index.ts new file mode 100644 index 0000000000..ecf085a0cb --- /dev/null +++ b/packages/action-menu/src/messages/index.ts @@ -0,0 +1,4 @@ +export * from './ActionMenuProblemReportMessage' +export * from './MenuMessage' +export * from './MenuRequestMessage' +export * from './PerformMessage' diff --git a/packages/action-menu/src/models/ActionMenu.ts b/packages/action-menu/src/models/ActionMenu.ts new file mode 100644 index 0000000000..9e241693e3 --- /dev/null +++ b/packages/action-menu/src/models/ActionMenu.ts @@ -0,0 +1,38 @@ +import type { ActionMenuOptionOptions } from './ActionMenuOption' + +import { Type } from 'class-transformer' +import { IsInstance, IsString } from 'class-validator' + +import { ActionMenuOption } from './ActionMenuOption' + +/** + * @public + */ +export interface ActionMenuOptions { + title: string + description: string + options: ActionMenuOptionOptions[] +} + +/** + * @public + */ +export class ActionMenu { + public constructor(options: ActionMenuOptions) { + if (options) { + this.title = options.title + this.description = options.description + this.options = options.options.map((p) => new ActionMenuOption(p)) + } + } + + @IsString() + public title!: string + + @IsString() + public description!: string + + @IsInstance(ActionMenuOption, { each: true }) + @Type(() => ActionMenuOption) + public options!: ActionMenuOption[] +} diff --git a/packages/action-menu/src/models/ActionMenuOption.ts b/packages/action-menu/src/models/ActionMenuOption.ts new file mode 100644 index 0000000000..ce283c9473 --- /dev/null +++ b/packages/action-menu/src/models/ActionMenuOption.ts @@ -0,0 +1,52 @@ +import type { ActionMenuFormOptions } from './ActionMenuOptionForm' + +import { Type } from 'class-transformer' +import { IsBoolean, IsInstance, IsOptional, IsString } from 'class-validator' + +import { ActionMenuForm } from './ActionMenuOptionForm' + +/** + * @public + */ +export interface ActionMenuOptionOptions { + name: string + title: string + description: string + disabled?: boolean + form?: ActionMenuFormOptions +} + +/** + * @public + */ +export class ActionMenuOption { + public constructor(options: ActionMenuOptionOptions) { + if (options) { + this.name = options.name + this.title = options.title + this.description = options.description + this.disabled = options.disabled + if (options.form) { + this.form = new ActionMenuForm(options.form) + } + } + } + + @IsString() + public name!: string + + @IsString() + public title!: string + + @IsString() + public description!: string + + @IsBoolean() + @IsOptional() + public disabled?: boolean + + @IsInstance(ActionMenuForm) + @Type(() => ActionMenuForm) + @IsOptional() + public form?: ActionMenuForm +} diff --git a/packages/action-menu/src/models/ActionMenuOptionForm.ts b/packages/action-menu/src/models/ActionMenuOptionForm.ts new file mode 100644 index 0000000000..3c9f4e1e08 --- /dev/null +++ b/packages/action-menu/src/models/ActionMenuOptionForm.ts @@ -0,0 +1,39 @@ +import type { ActionMenuFormParameterOptions } from './ActionMenuOptionFormParameter' + +import { Expose, Type } from 'class-transformer' +import { IsInstance, IsString } from 'class-validator' + +import { ActionMenuFormParameter } from './ActionMenuOptionFormParameter' + +/** + * @public + */ +export interface ActionMenuFormOptions { + description: string + params: ActionMenuFormParameterOptions[] + submitLabel: string +} + +/** + * @public + */ +export class ActionMenuForm { + public constructor(options: ActionMenuFormOptions) { + if (options) { + this.description = options.description + this.params = options.params.map((p) => new ActionMenuFormParameter(p)) + this.submitLabel = options.submitLabel + } + } + + @IsString() + public description!: string + + @Expose({ name: 'submit-label' }) + @IsString() + public submitLabel!: string + + @IsInstance(ActionMenuFormParameter, { each: true }) + @Type(() => ActionMenuFormParameter) + public params!: ActionMenuFormParameter[] +} diff --git a/packages/action-menu/src/models/ActionMenuOptionFormParameter.ts b/packages/action-menu/src/models/ActionMenuOptionFormParameter.ts new file mode 100644 index 0000000000..dfcce82848 --- /dev/null +++ b/packages/action-menu/src/models/ActionMenuOptionFormParameter.ts @@ -0,0 +1,57 @@ +import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator' + +/** + * @public + */ +export enum ActionMenuFormInputType { + Text = 'text', +} + +/** + * @public + */ +export interface ActionMenuFormParameterOptions { + name: string + title: string + default?: string + description: string + required?: boolean + type?: ActionMenuFormInputType +} + +/** + * @public + */ +export class ActionMenuFormParameter { + public constructor(options: ActionMenuFormParameterOptions) { + if (options) { + this.name = options.name + this.title = options.title + this.default = options.default + this.description = options.description + this.required = options.required + this.type = options.type + } + } + + @IsString() + public name!: string + + @IsString() + public title!: string + + @IsString() + @IsOptional() + public default?: string + + @IsString() + public description!: string + + @IsBoolean() + @IsOptional() + public required?: boolean + + @IsEnum(ActionMenuFormInputType) + @IsOptional() + public type?: ActionMenuFormInputType +} diff --git a/packages/action-menu/src/models/ActionMenuSelection.ts b/packages/action-menu/src/models/ActionMenuSelection.ts new file mode 100644 index 0000000000..f25c361b41 --- /dev/null +++ b/packages/action-menu/src/models/ActionMenuSelection.ts @@ -0,0 +1,28 @@ +import { IsOptional, IsString } from 'class-validator' + +/** + * @public + */ +export interface ActionMenuSelectionOptions { + name: string + params?: Record +} + +/** + * @public + */ +export class ActionMenuSelection { + public constructor(options: ActionMenuSelectionOptions) { + if (options) { + this.name = options.name + this.params = options.params + } + } + + @IsString() + public name!: string + + @IsString({ each: true }) + @IsOptional() + public params?: Record +} diff --git a/packages/action-menu/src/models/index.ts b/packages/action-menu/src/models/index.ts new file mode 100644 index 0000000000..15c8673f52 --- /dev/null +++ b/packages/action-menu/src/models/index.ts @@ -0,0 +1,5 @@ +export * from './ActionMenu' +export * from './ActionMenuOption' +export * from './ActionMenuOptionForm' +export * from './ActionMenuOptionFormParameter' +export * from './ActionMenuSelection' diff --git a/packages/action-menu/src/repository/ActionMenuRecord.ts b/packages/action-menu/src/repository/ActionMenuRecord.ts new file mode 100644 index 0000000000..dec713d894 --- /dev/null +++ b/packages/action-menu/src/repository/ActionMenuRecord.ts @@ -0,0 +1,102 @@ +import type { ActionMenuRole } from '../ActionMenuRole' +import type { ActionMenuState } from '../ActionMenuState' +import type { TagsBase } from '@credo-ts/core' + +import { CredoError, BaseRecord, utils } from '@credo-ts/core' +import { Type } from 'class-transformer' + +import { ActionMenuSelection, ActionMenu } from '../models' + +/** + * @public + */ +export interface ActionMenuRecordProps { + id?: string + state: ActionMenuState + role: ActionMenuRole + createdAt?: Date + connectionId: string + threadId: string + menu?: ActionMenu + performedAction?: ActionMenuSelection + tags?: CustomActionMenuTags +} + +/** + * @public + */ +export type CustomActionMenuTags = TagsBase + +/** + * @public + */ +export type DefaultActionMenuTags = { + role: ActionMenuRole + connectionId: string + threadId: string +} + +/** + * @public + */ +export class ActionMenuRecord + extends BaseRecord + implements ActionMenuRecordProps +{ + public state!: ActionMenuState + public role!: ActionMenuRole + public connectionId!: string + public threadId!: string + + @Type(() => ActionMenu) + public menu?: ActionMenu + + @Type(() => ActionMenuSelection) + public performedAction?: ActionMenuSelection + + public static readonly type = 'ActionMenuRecord' + public readonly type = ActionMenuRecord.type + + public constructor(props: ActionMenuRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this.connectionId = props.connectionId + this.threadId = props.threadId + this.state = props.state + this.role = props.role + this.menu = props.menu + this.performedAction = props.performedAction + this._tags = props.tags ?? {} + } + } + + public getTags() { + return { + ...this._tags, + role: this.role, + connectionId: this.connectionId, + threadId: this.threadId, + } + } + + public assertState(expectedStates: ActionMenuState | ActionMenuState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new CredoError( + `Action Menu record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.` + ) + } + } + + public assertRole(expectedRole: ActionMenuRole) { + if (this.role !== expectedRole) { + throw new CredoError(`Action Menu record has invalid role ${this.role}. Expected role ${expectedRole}.`) + } + } +} diff --git a/packages/action-menu/src/repository/ActionMenuRepository.ts b/packages/action-menu/src/repository/ActionMenuRepository.ts new file mode 100644 index 0000000000..cc4e60fa5c --- /dev/null +++ b/packages/action-menu/src/repository/ActionMenuRepository.ts @@ -0,0 +1,16 @@ +import { EventEmitter, InjectionSymbols, inject, injectable, Repository, StorageService } from '@credo-ts/core' + +import { ActionMenuRecord } from './ActionMenuRecord' + +/** + * @internal + */ +@injectable() +export class ActionMenuRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(ActionMenuRecord, storageService, eventEmitter) + } +} diff --git a/packages/action-menu/src/repository/index.ts b/packages/action-menu/src/repository/index.ts new file mode 100644 index 0000000000..2c34741daf --- /dev/null +++ b/packages/action-menu/src/repository/index.ts @@ -0,0 +1,2 @@ +export * from './ActionMenuRepository' +export * from './ActionMenuRecord' diff --git a/packages/action-menu/src/services/ActionMenuService.ts b/packages/action-menu/src/services/ActionMenuService.ts new file mode 100644 index 0000000000..1bbf2b41b0 --- /dev/null +++ b/packages/action-menu/src/services/ActionMenuService.ts @@ -0,0 +1,367 @@ +import type { + ClearMenuOptions, + CreateMenuOptions, + CreatePerformOptions, + CreateRequestOptions, + FindMenuOptions, +} from './ActionMenuServiceOptions' +import type { ActionMenuStateChangedEvent } from '../ActionMenuEvents' +import type { ActionMenuProblemReportMessage } from '../messages' +import type { AgentContext, InboundMessageContext, Logger, Query, QueryOptions } from '@credo-ts/core' + +import { AgentConfig, EventEmitter, CredoError, injectable } from '@credo-ts/core' + +import { ActionMenuEventTypes } from '../ActionMenuEvents' +import { ActionMenuRole } from '../ActionMenuRole' +import { ActionMenuState } from '../ActionMenuState' +import { ActionMenuProblemReportError } from '../errors/ActionMenuProblemReportError' +import { ActionMenuProblemReportReason } from '../errors/ActionMenuProblemReportReason' +import { PerformMessage, MenuMessage, MenuRequestMessage } from '../messages' +import { ActionMenuSelection, ActionMenu } from '../models' +import { ActionMenuRepository, ActionMenuRecord } from '../repository' + +/** + * @internal + */ +@injectable() +export class ActionMenuService { + private actionMenuRepository: ActionMenuRepository + private eventEmitter: EventEmitter + private logger: Logger + + public constructor(actionMenuRepository: ActionMenuRepository, agentConfig: AgentConfig, eventEmitter: EventEmitter) { + this.actionMenuRepository = actionMenuRepository + this.eventEmitter = eventEmitter + this.logger = agentConfig.logger + } + + public async createRequest(agentContext: AgentContext, options: CreateRequestOptions) { + // Assert + options.connection.assertReady() + + // Create message + const menuRequestMessage = new MenuRequestMessage({}) + + // Create record if not existent for connection/role + let actionMenuRecord = await this.find(agentContext, { + connectionId: options.connection.id, + role: ActionMenuRole.Requester, + }) + + if (actionMenuRecord) { + // Protocol will be restarted and menu cleared + const previousState = actionMenuRecord.state + actionMenuRecord.state = ActionMenuState.AwaitingRootMenu + actionMenuRecord.threadId = menuRequestMessage.id + actionMenuRecord.menu = undefined + actionMenuRecord.performedAction = undefined + + await this.actionMenuRepository.update(agentContext, actionMenuRecord) + this.emitStateChangedEvent(agentContext, actionMenuRecord, previousState) + } else { + actionMenuRecord = new ActionMenuRecord({ + connectionId: options.connection.id, + role: ActionMenuRole.Requester, + state: ActionMenuState.AwaitingRootMenu, + threadId: menuRequestMessage.threadId, + }) + + await this.actionMenuRepository.save(agentContext, actionMenuRecord) + this.emitStateChangedEvent(agentContext, actionMenuRecord, null) + } + + return { message: menuRequestMessage, record: actionMenuRecord } + } + + public async processRequest(messageContext: InboundMessageContext) { + const { message: menuRequestMessage, agentContext } = messageContext + + this.logger.debug(`Processing menu request with id ${menuRequestMessage.id}`) + + // Assert + const connection = messageContext.assertReadyConnection() + + let actionMenuRecord = await this.find(agentContext, { + connectionId: connection.id, + role: ActionMenuRole.Responder, + }) + + if (actionMenuRecord) { + // Protocol will be restarted and menu cleared + const previousState = actionMenuRecord.state + actionMenuRecord.state = ActionMenuState.PreparingRootMenu + actionMenuRecord.threadId = menuRequestMessage.id + actionMenuRecord.menu = undefined + actionMenuRecord.performedAction = undefined + + await this.actionMenuRepository.update(agentContext, actionMenuRecord) + this.emitStateChangedEvent(agentContext, actionMenuRecord, previousState) + } else { + // Create record + actionMenuRecord = new ActionMenuRecord({ + connectionId: connection.id, + role: ActionMenuRole.Responder, + state: ActionMenuState.PreparingRootMenu, + threadId: menuRequestMessage.threadId, + }) + + await this.actionMenuRepository.save(agentContext, actionMenuRecord) + this.emitStateChangedEvent(agentContext, actionMenuRecord, null) + } + + return actionMenuRecord + } + + public async createMenu(agentContext: AgentContext, options: CreateMenuOptions) { + // Assert connection ready + options.connection.assertReady() + + const uniqueNames = new Set(options.menu.options.map((v) => v.name)) + if (uniqueNames.size < options.menu.options.length) { + throw new CredoError('Action Menu contains duplicated options') + } + + // Create message + const menuMessage = new MenuMessage({ + title: options.menu.title, + description: options.menu.description, + options: options.menu.options, + }) + + // Check if there is an existing menu for this connection and role + let actionMenuRecord = await this.find(agentContext, { + connectionId: options.connection.id, + role: ActionMenuRole.Responder, + }) + + // If so, continue existing flow + if (actionMenuRecord) { + actionMenuRecord.assertState([ActionMenuState.Null, ActionMenuState.PreparingRootMenu, ActionMenuState.Done]) + // The new menu will be bound to the existing thread + // unless it is in null state (protocol reset) + if (actionMenuRecord.state !== ActionMenuState.Null) { + menuMessage.setThread({ threadId: actionMenuRecord.threadId }) + } + + const previousState = actionMenuRecord.state + actionMenuRecord.menu = options.menu + actionMenuRecord.state = ActionMenuState.AwaitingSelection + actionMenuRecord.threadId = menuMessage.threadId + + await this.actionMenuRepository.update(agentContext, actionMenuRecord) + this.emitStateChangedEvent(agentContext, actionMenuRecord, previousState) + } else { + // Create record + actionMenuRecord = new ActionMenuRecord({ + connectionId: options.connection.id, + role: ActionMenuRole.Responder, + state: ActionMenuState.AwaitingSelection, + menu: options.menu, + threadId: menuMessage.threadId, + }) + + await this.actionMenuRepository.save(agentContext, actionMenuRecord) + this.emitStateChangedEvent(agentContext, actionMenuRecord, null) + } + + return { message: menuMessage, record: actionMenuRecord } + } + + public async processMenu(messageContext: InboundMessageContext) { + const { message: menuMessage, agentContext } = messageContext + + this.logger.debug(`Processing action menu with id ${menuMessage.id}`) + + // Assert + const connection = messageContext.assertReadyConnection() + + // Check if there is an existing menu for this connection and role + const record = await this.find(agentContext, { + connectionId: connection.id, + role: ActionMenuRole.Requester, + }) + + if (record) { + // Record found: update with menu details + const previousState = record.state + + record.state = ActionMenuState.PreparingSelection + record.menu = new ActionMenu({ + title: menuMessage.title, + description: menuMessage.description, + options: menuMessage.options, + }) + record.threadId = menuMessage.threadId + record.performedAction = undefined + + await this.actionMenuRepository.update(agentContext, record) + + this.emitStateChangedEvent(agentContext, record, previousState) + } else { + // Record not found: create it + const actionMenuRecord = new ActionMenuRecord({ + connectionId: connection.id, + role: ActionMenuRole.Requester, + state: ActionMenuState.PreparingSelection, + threadId: menuMessage.threadId, + menu: new ActionMenu({ + title: menuMessage.title, + description: menuMessage.description, + options: menuMessage.options, + }), + }) + + await this.actionMenuRepository.save(agentContext, actionMenuRecord) + + this.emitStateChangedEvent(agentContext, actionMenuRecord, null) + } + } + + public async createPerform(agentContext: AgentContext, options: CreatePerformOptions) { + const { actionMenuRecord: record, performedAction: performedSelection } = options + + // Assert + record.assertRole(ActionMenuRole.Requester) + record.assertState([ActionMenuState.PreparingSelection]) + + const validSelection = record.menu?.options.some((item) => item.name === performedSelection.name) + if (!validSelection) { + throw new CredoError('Selection does not match valid actions') + } + + const previousState = record.state + + // Create message + const menuMessage = new PerformMessage({ + name: performedSelection.name, + params: performedSelection.params, + threadId: record.threadId, + }) + + // Update record + record.performedAction = options.performedAction + record.state = ActionMenuState.Done + + await this.actionMenuRepository.update(agentContext, record) + + this.emitStateChangedEvent(agentContext, record, previousState) + + return { message: menuMessage, record } + } + + public async processPerform(messageContext: InboundMessageContext) { + const { message: performMessage, agentContext } = messageContext + + this.logger.debug(`Processing action menu perform with id ${performMessage.id}`) + + const connection = messageContext.assertReadyConnection() + + // Check if there is an existing menu for this connection and role + const record = await this.find(agentContext, { + connectionId: connection.id, + role: ActionMenuRole.Responder, + threadId: performMessage.threadId, + }) + + if (record) { + // Record found: check state and update with menu details + + // A Null state means that menu has been cleared by the responder. + // Requester should be informed in order to request another menu + if (record.state === ActionMenuState.Null) { + throw new ActionMenuProblemReportError('Action Menu has been cleared by the responder', { + problemCode: ActionMenuProblemReportReason.Timeout, + }) + } + record.assertState([ActionMenuState.AwaitingSelection]) + + const validSelection = record.menu?.options.some((item) => item.name === performMessage.name) + if (!validSelection) { + throw new CredoError('Selection does not match valid actions') + } + + const previousState = record.state + + record.state = ActionMenuState.Done + record.performedAction = new ActionMenuSelection({ name: performMessage.name, params: performMessage.params }) + + await this.actionMenuRepository.update(agentContext, record) + + this.emitStateChangedEvent(agentContext, record, previousState) + } else { + throw new CredoError(`No Action Menu found with thread id ${messageContext.message.threadId}`) + } + } + + public async clearMenu(agentContext: AgentContext, options: ClearMenuOptions) { + const { actionMenuRecord: record } = options + + const previousState = record.state + + // Update record + record.state = ActionMenuState.Null + record.menu = undefined + record.performedAction = undefined + + await this.actionMenuRepository.update(agentContext, record) + + this.emitStateChangedEvent(agentContext, record, previousState) + + return record + } + + public async processProblemReport( + messageContext: InboundMessageContext + ): Promise { + const { message: actionMenuProblemReportMessage, agentContext } = messageContext + + const connection = messageContext.assertReadyConnection() + + this.logger.debug(`Processing problem report with id ${actionMenuProblemReportMessage.id}`) + + const actionMenuRecord = await this.find(agentContext, { + role: ActionMenuRole.Requester, + connectionId: connection.id, + }) + + if (!actionMenuRecord) { + throw new CredoError(`Unable to process action menu problem: record not found for connection id ${connection.id}`) + } + // Clear menu to restart flow + return await this.clearMenu(agentContext, { actionMenuRecord }) + } + + public async findById(agentContext: AgentContext, actionMenuRecordId: string) { + return await this.actionMenuRepository.findById(agentContext, actionMenuRecordId) + } + + public async find(agentContext: AgentContext, options: FindMenuOptions) { + return await this.actionMenuRepository.findSingleByQuery(agentContext, { + connectionId: options.connectionId, + role: options.role, + threadId: options.threadId, + }) + } + + public async findAllByQuery( + agentContext: AgentContext, + options: Query, + queryOptions?: QueryOptions + ) { + return await this.actionMenuRepository.findByQuery(agentContext, options, queryOptions) + } + + private emitStateChangedEvent( + agentContext: AgentContext, + actionMenuRecord: ActionMenuRecord, + previousState: ActionMenuState | null + ) { + this.eventEmitter.emit(agentContext, { + type: ActionMenuEventTypes.ActionMenuStateChanged, + payload: { + actionMenuRecord: actionMenuRecord.clone(), + previousState: previousState, + }, + }) + } +} diff --git a/packages/action-menu/src/services/ActionMenuServiceOptions.ts b/packages/action-menu/src/services/ActionMenuServiceOptions.ts new file mode 100644 index 0000000000..dc2d85c0ad --- /dev/null +++ b/packages/action-menu/src/services/ActionMenuServiceOptions.ts @@ -0,0 +1,44 @@ +import type { ActionMenuRole } from '../ActionMenuRole' +import type { ActionMenuSelection } from '../models' +import type { ActionMenu } from '../models/ActionMenu' +import type { ActionMenuRecord } from '../repository' +import type { ConnectionRecord } from '@credo-ts/core' + +/** + * @internal + */ +export interface CreateRequestOptions { + connection: ConnectionRecord +} + +/** + * @internal + */ +export interface CreateMenuOptions { + connection: ConnectionRecord + menu: ActionMenu +} + +/** + * @internal + */ +export interface CreatePerformOptions { + actionMenuRecord: ActionMenuRecord + performedAction: ActionMenuSelection +} + +/** + * @internal + */ +export interface ClearMenuOptions { + actionMenuRecord: ActionMenuRecord +} + +/** + * @internal + */ +export interface FindMenuOptions { + connectionId: string + role: ActionMenuRole + threadId?: string +} diff --git a/packages/action-menu/src/services/__tests__/ActionMenuService.test.ts b/packages/action-menu/src/services/__tests__/ActionMenuService.test.ts new file mode 100644 index 0000000000..a186108661 --- /dev/null +++ b/packages/action-menu/src/services/__tests__/ActionMenuService.test.ts @@ -0,0 +1,890 @@ +import type { ActionMenuStateChangedEvent } from '../../ActionMenuEvents' +import type { ActionMenuSelection } from '../../models' +import type { AgentContext, AgentConfig, Repository } from '@credo-ts/core' + +import { DidExchangeState, EventEmitter, InboundMessageContext } from '@credo-ts/core' +import { Subject } from 'rxjs' + +import { + agentDependencies, + getAgentConfig, + getAgentContext, + getMockConnection, + mockFunction, +} from '../../../../core/tests/helpers' +import { ActionMenuEventTypes } from '../../ActionMenuEvents' +import { ActionMenuRole } from '../../ActionMenuRole' +import { ActionMenuState } from '../../ActionMenuState' +import { ActionMenuProblemReportError } from '../../errors/ActionMenuProblemReportError' +import { ActionMenuProblemReportReason } from '../../errors/ActionMenuProblemReportReason' +import { MenuMessage, MenuRequestMessage, PerformMessage } from '../../messages' +import { ActionMenu } from '../../models' +import { ActionMenuRecord, ActionMenuRepository } from '../../repository' +import { ActionMenuService } from '../ActionMenuService' + +jest.mock('../../repository/ActionMenuRepository') +const ActionMenuRepositoryMock = ActionMenuRepository as jest.Mock + +describe('ActionMenuService', () => { + const mockConnectionRecord = getMockConnection({ + id: 'd3849ac3-c981-455b-a1aa-a10bea6cead8', + did: 'did:sov:C2SsBf5QUQpqSAQfhu3sd2', + state: DidExchangeState.Completed, + }) + + let actionMenuRepository: Repository + let actionMenuService: ActionMenuService + let eventEmitter: EventEmitter + let agentConfig: AgentConfig + let agentContext: AgentContext + + const mockActionMenuRecord = (options: { + connectionId: string + role: ActionMenuRole + state: ActionMenuState + threadId: string + menu?: ActionMenu + performedAction?: ActionMenuSelection + }) => { + return new ActionMenuRecord({ + connectionId: options.connectionId, + role: options.role, + state: options.state, + threadId: options.threadId, + menu: options.menu, + performedAction: options.performedAction, + }) + } + + beforeAll(async () => { + agentConfig = getAgentConfig('ActionMenuServiceTest') + agentContext = getAgentContext() + }) + + beforeEach(async () => { + actionMenuRepository = new ActionMenuRepositoryMock() + eventEmitter = new EventEmitter(agentDependencies, new Subject()) + actionMenuService = new ActionMenuService(actionMenuRepository, agentConfig, eventEmitter) + }) + + describe('createMenu', () => { + let testMenu: ActionMenu + + beforeAll(() => { + testMenu = new ActionMenu({ + description: 'menu-description', + title: 'menu-title', + options: [{ name: 'opt1', title: 'opt1-title', description: 'opt1-desc' }], + }) + }) + + it(`throws an error when duplicated options are specified`, async () => { + expect( + actionMenuService.createMenu(agentContext, { + connection: mockConnectionRecord, + menu: { + title: 'menu-title', + description: 'menu-description', + options: [ + { name: 'opt1', description: 'desc1', title: 'title1' }, + { name: 'opt2', description: 'desc2', title: 'title2' }, + { name: 'opt1', description: 'desc3', title: 'title3' }, + { name: 'opt4', description: 'desc4', title: 'title4' }, + ], + }, + }) + ).rejects.toThrowError('Action Menu contains duplicated options') + }) + + it(`no previous menu: emits a menu with title, description and options`, async () => { + // No previous menu + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(null)) + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + await actionMenuService.createMenu(agentContext, { + connection: mockConnectionRecord, + menu: testMenu, + }) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: ActionMenuEventTypes.ActionMenuStateChanged, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: null, + actionMenuRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Responder, + state: ActionMenuState.AwaitingSelection, + menu: expect.objectContaining({ + description: 'menu-description', + title: 'menu-title', + options: [{ name: 'opt1', title: 'opt1-title', description: 'opt1-desc' }], + }), + }), + }, + }) + }) + + it(`existing menu: emits a menu with title, description, options and thread`, async () => { + // Previous menu is in Done state + const previousMenuDone = mockActionMenuRecord({ + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Responder, + state: ActionMenuState.Done, + threadId: 'threadId-1', + }) + + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(previousMenuDone)) + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + await actionMenuService.createMenu(agentContext, { + connection: mockConnectionRecord, + menu: testMenu, + }) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: ActionMenuEventTypes.ActionMenuStateChanged, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: ActionMenuState.Done, + actionMenuRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + threadId: 'threadId-1', + role: ActionMenuRole.Responder, + state: ActionMenuState.AwaitingSelection, + menu: expect.objectContaining({ + description: 'menu-description', + title: 'menu-title', + options: [{ name: 'opt1', title: 'opt1-title', description: 'opt1-desc' }], + }), + }), + }, + }) + }) + + it(`existing menu, cleared: emits a menu with title, description, options and new thread`, async () => { + // Previous menu is in Done state + const previousMenuClear = mockActionMenuRecord({ + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Responder, + state: ActionMenuState.Null, + threadId: 'threadId-1', + }) + + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(previousMenuClear)) + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + await actionMenuService.createMenu(agentContext, { + connection: mockConnectionRecord, + menu: testMenu, + }) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: ActionMenuEventTypes.ActionMenuStateChanged, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: ActionMenuState.Null, + actionMenuRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + threadId: expect.not.stringMatching('threadId-1'), + role: ActionMenuRole.Responder, + state: ActionMenuState.AwaitingSelection, + menu: expect.objectContaining({ + description: 'menu-description', + title: 'menu-title', + options: [{ name: 'opt1', title: 'opt1-title', description: 'opt1-desc' }], + }), + }), + }, + }) + }) + }) + + describe('createPerform', () => { + let mockRecord: ActionMenuRecord + + beforeEach(() => { + const testMenu = new ActionMenu({ + description: 'menu-description', + title: 'menu-title', + options: [ + { name: 'opt1', title: 'opt1-title', description: 'opt1-desc' }, + { name: 'opt2', title: 'opt2-title', description: 'opt2-desc' }, + ], + }) + + mockRecord = mockActionMenuRecord({ + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Requester, + state: ActionMenuState.PreparingSelection, + threadId: '123', + menu: testMenu, + }) + }) + + it(`throws an error when invalid selection is provided`, async () => { + expect( + actionMenuService.createPerform(agentContext, { + actionMenuRecord: mockRecord, + performedAction: { name: 'fake' }, + }) + ).rejects.toThrowError('Selection does not match valid actions') + }) + + it(`throws an error when state is not preparing-selection`, async () => { + for (const state of Object.values(ActionMenuState).filter( + (state) => state !== ActionMenuState.PreparingSelection + )) { + mockRecord.state = state + expect( + actionMenuService.createPerform(agentContext, { + actionMenuRecord: mockRecord, + performedAction: { name: 'opt1' }, + }) + ).rejects.toThrowError( + `Action Menu record is in invalid state ${state}. Valid states are: preparing-selection.` + ) + } + }) + + it(`emits a menu with a valid selection and action menu record`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + await actionMenuService.createPerform(agentContext, { + actionMenuRecord: mockRecord, + performedAction: { name: 'opt2' }, + }) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: ActionMenuEventTypes.ActionMenuStateChanged, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: ActionMenuState.PreparingSelection, + actionMenuRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Requester, + state: ActionMenuState.Done, + performedAction: { name: 'opt2' }, + }), + }, + }) + }) + }) + + describe('createRequest', () => { + let mockRecord: ActionMenuRecord + + beforeEach(() => { + const testMenu = new ActionMenu({ + description: 'menu-description', + title: 'menu-title', + options: [ + { name: 'opt1', title: 'opt1-title', description: 'opt1-desc' }, + { name: 'opt2', title: 'opt2-title', description: 'opt2-desc' }, + ], + }) + + mockRecord = mockActionMenuRecord({ + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Requester, + state: ActionMenuState.PreparingSelection, + threadId: '123', + menu: testMenu, + }) + }) + + it(`no existing record: emits event and creates new request and record`, async () => { + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(null)) + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + const { message, record } = await actionMenuService.createRequest(agentContext, { + connection: mockConnectionRecord, + }) + + const expectedRecord = { + id: expect.any(String), + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Requester, + threadId: message.threadId, + state: ActionMenuState.AwaitingRootMenu, + menu: undefined, + performedAction: undefined, + } + expect(record).toMatchObject(expectedRecord) + + expect(actionMenuRepository.save).toHaveBeenCalledWith(agentContext, expect.objectContaining(expectedRecord)) + expect(actionMenuRepository.update).not.toHaveBeenCalled() + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: ActionMenuEventTypes.ActionMenuStateChanged, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: null, + actionMenuRecord: expect.objectContaining(expectedRecord), + }, + }) + }) + + it(`already existing record: emits event, creates new request and updates record`, async () => { + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + const previousState = mockRecord.state + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + const { message, record } = await actionMenuService.createRequest(agentContext, { + connection: mockConnectionRecord, + }) + + const expectedRecord = { + id: expect.any(String), + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Requester, + threadId: message.threadId, + state: ActionMenuState.AwaitingRootMenu, + menu: undefined, + performedAction: undefined, + } + expect(record).toMatchObject(expectedRecord) + + expect(actionMenuRepository.update).toHaveBeenCalledWith(agentContext, expect.objectContaining(expectedRecord)) + expect(actionMenuRepository.save).not.toHaveBeenCalled() + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: ActionMenuEventTypes.ActionMenuStateChanged, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState, + actionMenuRecord: expect.objectContaining(expectedRecord), + }, + }) + }) + }) + + describe('clearMenu', () => { + let mockRecord: ActionMenuRecord + + beforeEach(() => { + const testMenu = new ActionMenu({ + description: 'menu-description', + title: 'menu-title', + options: [ + { name: 'opt1', title: 'opt1-title', description: 'opt1-desc' }, + { name: 'opt2', title: 'opt2-title', description: 'opt2-desc' }, + ], + }) + + mockRecord = mockActionMenuRecord({ + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Requester, + state: ActionMenuState.PreparingSelection, + threadId: '123', + menu: testMenu, + performedAction: { name: 'opt1' }, + }) + }) + + it(`requester role: emits a cleared menu`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + mockRecord.role = ActionMenuRole.Requester + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + await actionMenuService.clearMenu(agentContext, { + actionMenuRecord: mockRecord, + }) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: ActionMenuEventTypes.ActionMenuStateChanged, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: ActionMenuState.PreparingSelection, + actionMenuRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Requester, + state: ActionMenuState.Null, + menu: undefined, + performedAction: undefined, + }), + }, + }) + }) + + it(`responder role: emits a cleared menu`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + mockRecord.state = ActionMenuState.AwaitingSelection + mockRecord.role = ActionMenuRole.Responder + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + await actionMenuService.clearMenu(agentContext, { + actionMenuRecord: mockRecord, + }) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: ActionMenuEventTypes.ActionMenuStateChanged, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: ActionMenuState.AwaitingSelection, + actionMenuRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Responder, + state: ActionMenuState.Null, + menu: undefined, + performedAction: undefined, + }), + }, + }) + }) + }) + + describe('processMenu', () => { + let mockRecord: ActionMenuRecord + let mockMenuMessage: MenuMessage + + beforeEach(() => { + mockRecord = mockActionMenuRecord({ + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Requester, + state: ActionMenuState.PreparingSelection, + threadId: '123', + menu: new ActionMenu({ + description: 'menu-description', + title: 'menu-title', + options: [ + { name: 'opt1', title: 'opt1-title', description: 'opt1-desc' }, + { name: 'opt2', title: 'opt2-title', description: 'opt2-desc' }, + ], + }), + performedAction: { name: 'opt1' }, + }) + + mockMenuMessage = new MenuMessage({ + title: 'incoming title', + description: 'incoming description', + options: [ + { + title: 'incoming option 1 title', + description: 'incoming option 1 description', + name: 'incoming option 1 name', + }, + ], + }) + }) + + it(`emits event and creates record when no previous record`, async () => { + const messageContext = new InboundMessageContext(mockMenuMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(null)) + + await actionMenuService.processMenu(messageContext) + + const expectedRecord = { + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Requester, + state: ActionMenuState.PreparingSelection, + threadId: messageContext.message.threadId, + menu: expect.objectContaining({ + title: 'incoming title', + description: 'incoming description', + options: [ + { + title: 'incoming option 1 title', + description: 'incoming option 1 description', + name: 'incoming option 1 name', + }, + ], + }), + performedAction: undefined, + } + + expect(actionMenuRepository.save).toHaveBeenCalledWith(agentContext, expect.objectContaining(expectedRecord)) + expect(actionMenuRepository.update).not.toHaveBeenCalled() + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: ActionMenuEventTypes.ActionMenuStateChanged, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: null, + actionMenuRecord: expect.objectContaining(expectedRecord), + }, + }) + }) + + it(`emits event and updates record when existing record`, async () => { + const messageContext = new InboundMessageContext(mockMenuMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + // It should accept any previous state + for (const state of Object.values(ActionMenuState)) { + mockRecord.state = state + const previousState = state + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + await actionMenuService.processMenu(messageContext) + + const expectedRecord = { + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Requester, + state: ActionMenuState.PreparingSelection, + threadId: messageContext.message.threadId, + menu: expect.objectContaining({ + title: 'incoming title', + description: 'incoming description', + options: [ + { + title: 'incoming option 1 title', + description: 'incoming option 1 description', + name: 'incoming option 1 name', + }, + ], + }), + performedAction: undefined, + } + + expect(actionMenuRepository.update).toHaveBeenCalledWith(agentContext, expect.objectContaining(expectedRecord)) + expect(actionMenuRepository.save).not.toHaveBeenCalled() + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: ActionMenuEventTypes.ActionMenuStateChanged, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState, + actionMenuRecord: expect.objectContaining(expectedRecord), + }, + }) + } + }) + }) + + describe('processPerform', () => { + let mockRecord: ActionMenuRecord + + beforeEach(() => { + mockRecord = mockActionMenuRecord({ + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Responder, + state: ActionMenuState.AwaitingSelection, + threadId: '123', + menu: new ActionMenu({ + description: 'menu-description', + title: 'menu-title', + options: [ + { name: 'opt1', title: 'opt1-title', description: 'opt1-desc' }, + { name: 'opt2', title: 'opt2-title', description: 'opt2-desc' }, + ], + }), + }) + }) + + it(`emits event and saves record when valid selection and thread Id`, async () => { + const mockPerformMessage = new PerformMessage({ + name: 'opt1', + threadId: '123', + }) + + const messageContext = new InboundMessageContext(mockPerformMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + await actionMenuService.processPerform(messageContext) + + const expectedRecord = { + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Responder, + state: ActionMenuState.Done, + threadId: messageContext.message.threadId, + menu: expect.objectContaining({ + description: 'menu-description', + title: 'menu-title', + options: [ + { name: 'opt1', title: 'opt1-title', description: 'opt1-desc' }, + { name: 'opt2', title: 'opt2-title', description: 'opt2-desc' }, + ], + }), + performedAction: { name: 'opt1' }, + } + + expect(actionMenuRepository.findSingleByQuery).toHaveBeenCalledWith( + agentContext, + expect.objectContaining({ + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Responder, + threadId: messageContext.message.threadId, + }) + ) + expect(actionMenuRepository.update).toHaveBeenCalledWith(agentContext, expect.objectContaining(expectedRecord)) + expect(actionMenuRepository.save).not.toHaveBeenCalled() + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: ActionMenuEventTypes.ActionMenuStateChanged, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: ActionMenuState.AwaitingSelection, + actionMenuRecord: expect.objectContaining(expectedRecord), + }, + }) + }) + + it(`throws error when invalid selection`, async () => { + const mockPerformMessage = new PerformMessage({ + name: 'fake', + threadId: '123', + }) + + const messageContext = new InboundMessageContext(mockPerformMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + expect(actionMenuService.processPerform(messageContext)).rejects.toThrowError( + 'Selection does not match valid actions' + ) + + expect(actionMenuRepository.update).not.toHaveBeenCalled() + expect(actionMenuRepository.save).not.toHaveBeenCalled() + expect(eventListenerMock).not.toHaveBeenCalled() + }) + + it(`throws error when record not found`, async () => { + const mockPerformMessage = new PerformMessage({ + name: 'opt1', + threadId: '122', + }) + + const messageContext = new InboundMessageContext(mockPerformMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(null)) + + expect(actionMenuService.processPerform(messageContext)).rejects.toThrowError( + `No Action Menu found with thread id ${mockPerformMessage.threadId}` + ) + + expect(actionMenuRepository.update).not.toHaveBeenCalled() + expect(actionMenuRepository.save).not.toHaveBeenCalled() + expect(eventListenerMock).not.toHaveBeenCalled() + }) + + it(`throws error when invalid state`, async () => { + const mockPerformMessage = new PerformMessage({ + name: 'opt1', + threadId: '123', + }) + + const messageContext = new InboundMessageContext(mockPerformMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + mockRecord.state = ActionMenuState.Done + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + expect(actionMenuService.processPerform(messageContext)).rejects.toThrowError( + `Action Menu record is in invalid state ${mockRecord.state}. Valid states are: ${ActionMenuState.AwaitingSelection}.` + ) + + expect(actionMenuRepository.update).not.toHaveBeenCalled() + expect(actionMenuRepository.save).not.toHaveBeenCalled() + expect(eventListenerMock).not.toHaveBeenCalled() + }) + + it(`throws problem report error when menu has been cleared`, async () => { + const mockPerformMessage = new PerformMessage({ + name: 'opt1', + threadId: '123', + }) + + const messageContext = new InboundMessageContext(mockPerformMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + mockRecord.state = ActionMenuState.Null + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + expect(actionMenuService.processPerform(messageContext)).rejects.toThrowError( + new ActionMenuProblemReportError('Action Menu has been cleared by the responder', { + problemCode: ActionMenuProblemReportReason.Timeout, + }) + ) + + expect(actionMenuRepository.update).not.toHaveBeenCalled() + expect(actionMenuRepository.save).not.toHaveBeenCalled() + expect(eventListenerMock).not.toHaveBeenCalled() + }) + }) + + describe('processRequest', () => { + let mockRecord: ActionMenuRecord + let mockMenuRequestMessage: MenuRequestMessage + + beforeEach(() => { + mockRecord = mockActionMenuRecord({ + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Responder, + state: ActionMenuState.PreparingRootMenu, + threadId: '123', + menu: new ActionMenu({ + description: 'menu-description', + title: 'menu-title', + options: [ + { name: 'opt1', title: 'opt1-title', description: 'opt1-desc' }, + { name: 'opt2', title: 'opt2-title', description: 'opt2-desc' }, + ], + }), + performedAction: { name: 'opt1' }, + }) + + mockMenuRequestMessage = new MenuRequestMessage({}) + }) + + it(`emits event and creates record when no previous record`, async () => { + const messageContext = new InboundMessageContext(mockMenuRequestMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(null)) + + await actionMenuService.processRequest(messageContext) + + const expectedRecord = { + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Responder, + state: ActionMenuState.PreparingRootMenu, + threadId: messageContext.message.threadId, + menu: undefined, + performedAction: undefined, + } + + expect(actionMenuRepository.save).toHaveBeenCalledWith(agentContext, expect.objectContaining(expectedRecord)) + expect(actionMenuRepository.update).not.toHaveBeenCalled() + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: ActionMenuEventTypes.ActionMenuStateChanged, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: null, + actionMenuRecord: expect.objectContaining(expectedRecord), + }, + }) + }) + + it(`emits event and updates record when existing record`, async () => { + const messageContext = new InboundMessageContext(mockMenuRequestMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + const eventListenerMock = jest.fn() + eventEmitter.on(ActionMenuEventTypes.ActionMenuStateChanged, eventListenerMock) + + // It should accept any previous state + for (const state of Object.values(ActionMenuState)) { + mockRecord.state = state + const previousState = state + mockFunction(actionMenuRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + await actionMenuService.processRequest(messageContext) + + const expectedRecord = { + connectionId: mockConnectionRecord.id, + role: ActionMenuRole.Responder, + state: ActionMenuState.PreparingRootMenu, + threadId: messageContext.message.threadId, + menu: undefined, + performedAction: undefined, + } + + expect(actionMenuRepository.update).toHaveBeenCalledWith(agentContext, expect.objectContaining(expectedRecord)) + expect(actionMenuRepository.save).not.toHaveBeenCalled() + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: ActionMenuEventTypes.ActionMenuStateChanged, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState, + actionMenuRecord: expect.objectContaining(expectedRecord), + }, + }) + } + }) + }) +}) diff --git a/packages/action-menu/src/services/index.ts b/packages/action-menu/src/services/index.ts new file mode 100644 index 0000000000..83362466e7 --- /dev/null +++ b/packages/action-menu/src/services/index.ts @@ -0,0 +1,2 @@ +export * from './ActionMenuService' +export * from './ActionMenuServiceOptions' diff --git a/packages/action-menu/tests/action-menu.test.ts b/packages/action-menu/tests/action-menu.test.ts new file mode 100644 index 0000000000..134887830c --- /dev/null +++ b/packages/action-menu/tests/action-menu.test.ts @@ -0,0 +1,330 @@ +import type { ConnectionRecord } from '@credo-ts/core' + +import { Agent } from '@credo-ts/core' + +import { makeConnection, testLogger, setupSubjectTransports, getInMemoryAgentOptions } from '../../core/tests' + +import { waitForActionMenuRecord } from './helpers' + +import { ActionMenu, ActionMenuModule, ActionMenuRecord, ActionMenuRole, ActionMenuState } from '@credo-ts/action-menu' + +const modules = { + actionMenu: new ActionMenuModule(), +} + +const faberAgentOptions = getInMemoryAgentOptions( + 'Faber Action Menu', + { + endpoints: ['rxjs:faber'], + }, + modules +) + +const aliceAgentOptions = getInMemoryAgentOptions( + 'Alice Action Menu', + { + endpoints: ['rxjs:alice'], + }, + modules +) + +describe('Action Menu', () => { + let faberAgent: Agent + let aliceAgent: Agent + let faberConnection: ConnectionRecord + let aliceConnection: ConnectionRecord + + const rootMenu = new ActionMenu({ + title: 'Welcome', + description: 'This is the root menu', + options: [ + { + name: 'option-1', + description: 'Option 1 description', + title: 'Option 1', + }, + { + name: 'option-2', + description: 'Option 2 description', + title: 'Option 2', + }, + ], + }) + + const submenu1 = new ActionMenu({ + title: 'Menu 1', + description: 'This is first submenu', + options: [ + { + name: 'option-1-1', + description: '1-1 desc', + title: '1-1 title', + }, + { + name: 'option-1-2', + description: '1-1 desc', + title: '1-1 title', + }, + ], + }) + + beforeEach(async () => { + faberAgent = new Agent(faberAgentOptions) + aliceAgent = new Agent(aliceAgentOptions) + + setupSubjectTransports([faberAgent, aliceAgent]) + + await faberAgent.initialize() + await aliceAgent.initialize() + ;[aliceConnection, faberConnection] = await makeConnection(aliceAgent, faberAgent) + }) + + afterEach(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice requests menu to Faber and selects an option once received', async () => { + testLogger.test('Alice sends menu request to Faber') + let aliceActionMenuRecord = await aliceAgent.modules.actionMenu.requestMenu({ connectionId: aliceConnection.id }) + + testLogger.test('Faber waits for menu request from Alice') + await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.PreparingRootMenu, + }) + + testLogger.test('Faber sends root menu to Alice') + await faberAgent.modules.actionMenu.sendMenu({ connectionId: faberConnection.id, menu: rootMenu }) + + testLogger.test('Alice waits until she receives menu') + aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.PreparingSelection, + }) + + expect(aliceActionMenuRecord.menu).toEqual(rootMenu) + const faberActiveMenu = await faberAgent.modules.actionMenu.findActiveMenu({ + connectionId: faberConnection.id, + role: ActionMenuRole.Responder, + }) + expect(faberActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(faberActiveMenu?.state).toBe(ActionMenuState.AwaitingSelection) + + testLogger.test('Alice selects menu item') + await aliceAgent.modules.actionMenu.performAction({ + connectionId: aliceConnection.id, + performedAction: { name: 'option-1' }, + }) + + testLogger.test('Faber waits for menu selection from Alice') + await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.Done, + }) + + // As Alice has responded, menu should be closed (done state) + const aliceActiveMenu = await aliceAgent.modules.actionMenu.findActiveMenu({ + connectionId: aliceConnection.id, + role: ActionMenuRole.Requester, + }) + expect(aliceActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(aliceActiveMenu?.state).toBe(ActionMenuState.Done) + }) + + test('Faber sends root menu and Alice selects an option', async () => { + testLogger.test('Faber sends root menu to Alice') + await faberAgent.modules.actionMenu.sendMenu({ connectionId: faberConnection.id, menu: rootMenu }) + + testLogger.test('Alice waits until she receives menu') + const aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.PreparingSelection, + }) + + expect(aliceActionMenuRecord.menu).toEqual(rootMenu) + const faberActiveMenu = await faberAgent.modules.actionMenu.findActiveMenu({ + connectionId: faberConnection.id, + role: ActionMenuRole.Responder, + }) + expect(faberActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(faberActiveMenu?.state).toBe(ActionMenuState.AwaitingSelection) + + testLogger.test('Alice selects menu item') + await aliceAgent.modules.actionMenu.performAction({ + connectionId: aliceConnection.id, + performedAction: { name: 'option-1' }, + }) + + testLogger.test('Faber waits for menu selection from Alice') + await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.Done, + }) + + // As Alice has responded, menu should be closed (done state) + const aliceActiveMenu = await aliceAgent.modules.actionMenu.findActiveMenu({ + connectionId: aliceConnection.id, + role: ActionMenuRole.Requester, + }) + expect(aliceActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(aliceActiveMenu?.state).toBe(ActionMenuState.Done) + }) + + test('Menu navigation', async () => { + testLogger.test('Faber sends root menu ') + let faberActionMenuRecord = await faberAgent.modules.actionMenu.sendMenu({ + connectionId: faberConnection.id, + menu: rootMenu, + }) + + const rootThreadId = faberActionMenuRecord.threadId + + testLogger.test('Alice waits until she receives menu') + let aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.PreparingSelection, + }) + + expect(aliceActionMenuRecord.menu).toEqual(rootMenu) + expect(aliceActionMenuRecord.threadId).toEqual(rootThreadId) + + testLogger.test('Alice selects menu item 1') + await aliceAgent.modules.actionMenu.performAction({ + connectionId: aliceConnection.id, + performedAction: { name: 'option-1' }, + }) + + testLogger.test('Faber waits for menu selection from Alice') + faberActionMenuRecord = await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.Done, + }) + + // As Alice has responded, menu should be closed (done state) + let aliceActiveMenu = await aliceAgent.modules.actionMenu.findActiveMenu({ + connectionId: aliceConnection.id, + role: ActionMenuRole.Requester, + }) + expect(aliceActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(aliceActiveMenu?.state).toBe(ActionMenuState.Done) + expect(aliceActiveMenu?.threadId).toEqual(rootThreadId) + + testLogger.test('Faber sends submenu to Alice') + faberActionMenuRecord = await faberAgent.modules.actionMenu.sendMenu({ + connectionId: faberConnection.id, + menu: submenu1, + }) + + testLogger.test('Alice waits until she receives submenu') + aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.PreparingSelection, + }) + + expect(aliceActionMenuRecord.menu).toEqual(submenu1) + expect(aliceActionMenuRecord.threadId).toEqual(rootThreadId) + + testLogger.test('Alice selects menu item 1-1') + await aliceAgent.modules.actionMenu.performAction({ + connectionId: aliceConnection.id, + performedAction: { name: 'option-1-1' }, + }) + + testLogger.test('Faber waits for menu selection from Alice') + faberActionMenuRecord = await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.Done, + }) + + // As Alice has responded, menu should be closed (done state) + aliceActiveMenu = await aliceAgent.modules.actionMenu.findActiveMenu({ + connectionId: aliceConnection.id, + role: ActionMenuRole.Requester, + }) + expect(aliceActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(aliceActiveMenu?.state).toBe(ActionMenuState.Done) + expect(aliceActiveMenu?.threadId).toEqual(rootThreadId) + + testLogger.test('Alice sends menu request to Faber') + aliceActionMenuRecord = await aliceAgent.modules.actionMenu.requestMenu({ connectionId: aliceConnection.id }) + + testLogger.test('Faber waits for menu request from Alice') + faberActionMenuRecord = await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.PreparingRootMenu, + }) + + testLogger.test('This new menu request must have a different thread Id') + expect(faberActionMenuRecord.menu).toBeUndefined() + expect(aliceActionMenuRecord.threadId).not.toEqual(rootThreadId) + expect(faberActionMenuRecord.threadId).toEqual(aliceActionMenuRecord.threadId) + }) + + test('Menu clearing', async () => { + testLogger.test('Faber sends root menu to Alice') + await faberAgent.modules.actionMenu.sendMenu({ connectionId: faberConnection.id, menu: rootMenu }) + + testLogger.test('Alice waits until she receives menu') + let aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.PreparingSelection, + }) + + expect(aliceActionMenuRecord.menu).toEqual(rootMenu) + let faberActiveMenu = await faberAgent.modules.actionMenu.findActiveMenu({ + connectionId: faberConnection.id, + role: ActionMenuRole.Responder, + }) + expect(faberActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(faberActiveMenu?.state).toBe(ActionMenuState.AwaitingSelection) + + await faberAgent.modules.actionMenu.clearActiveMenu({ + connectionId: faberConnection.id, + role: ActionMenuRole.Responder, + }) + + testLogger.test('Alice selects menu item') + await aliceAgent.modules.actionMenu.performAction({ + connectionId: aliceConnection.id, + performedAction: { name: 'option-1' }, + }) + + // Exception + + testLogger.test('Faber rejects selection, as menu has been cleared') + // Faber sends error report to Alice, meaning that her Menu flow will be cleared + aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.Null, + role: ActionMenuRole.Requester, + }) + + testLogger.test('Alice request a new menu') + await aliceAgent.modules.actionMenu.requestMenu({ + connectionId: aliceConnection.id, + }) + + testLogger.test('Faber waits for menu request from Alice') + await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.PreparingRootMenu, + }) + + testLogger.test('Faber sends root menu to Alice') + await faberAgent.modules.actionMenu.sendMenu({ connectionId: faberConnection.id, menu: rootMenu }) + + testLogger.test('Alice waits until she receives menu') + aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.PreparingSelection, + }) + + expect(aliceActionMenuRecord.menu).toEqual(rootMenu) + faberActiveMenu = await faberAgent.modules.actionMenu.findActiveMenu({ + connectionId: faberConnection.id, + role: ActionMenuRole.Responder, + }) + expect(faberActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(faberActiveMenu?.state).toBe(ActionMenuState.AwaitingSelection) + + testLogger.test('Alice selects menu item') + await aliceAgent.modules.actionMenu.performAction({ + connectionId: aliceConnection.id, + performedAction: { name: 'option-1' }, + }) + + testLogger.test('Faber waits for menu selection from Alice') + await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.Done, + }) + }) +}) diff --git a/packages/action-menu/tests/helpers.ts b/packages/action-menu/tests/helpers.ts new file mode 100644 index 0000000000..d3176e2ed9 --- /dev/null +++ b/packages/action-menu/tests/helpers.ts @@ -0,0 +1,60 @@ +import type { ActionMenuStateChangedEvent, ActionMenuRole, ActionMenuState } from '@credo-ts/action-menu' +import type { Agent } from '@credo-ts/core' +import type { Observable } from 'rxjs' + +import { catchError, filter, firstValueFrom, map, ReplaySubject, timeout } from 'rxjs' + +import { ActionMenuEventTypes } from '@credo-ts/action-menu' + +export async function waitForActionMenuRecord( + agent: Agent, + options: { + threadId?: string + role?: ActionMenuRole + state?: ActionMenuState + previousState?: ActionMenuState | null + timeoutMs?: number + } +) { + const observable = agent.events.observable(ActionMenuEventTypes.ActionMenuStateChanged) + + return waitForActionMenuRecordSubject(observable, options) +} + +export function waitForActionMenuRecordSubject( + subject: ReplaySubject | Observable, + { + threadId, + role, + state, + previousState, + timeoutMs = 10000, + }: { + threadId?: string + role?: ActionMenuRole + state?: ActionMenuState + previousState?: ActionMenuState | null + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( + observable.pipe( + filter((e) => previousState === undefined || e.payload.previousState === previousState), + filter((e) => threadId === undefined || e.payload.actionMenuRecord.threadId === threadId), + filter((e) => role === undefined || e.payload.actionMenuRecord.role === role), + filter((e) => state === undefined || e.payload.actionMenuRecord.state === state), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `ActionMenuStateChangedEvent event not emitted within specified timeout: { + previousState: ${previousState}, + threadId: ${threadId}, + state: ${state} + }` + ) + }), + map((e) => e.payload.actionMenuRecord) + ) + ) +} diff --git a/packages/action-menu/tests/setup.ts b/packages/action-menu/tests/setup.ts new file mode 100644 index 0000000000..78143033f2 --- /dev/null +++ b/packages/action-menu/tests/setup.ts @@ -0,0 +1,3 @@ +import 'reflect-metadata' + +jest.setTimeout(120000) diff --git a/packages/action-menu/tsconfig.build.json b/packages/action-menu/tsconfig.build.json new file mode 100644 index 0000000000..2b75d0adab --- /dev/null +++ b/packages/action-menu/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build" + }, + "include": ["src/**/*"] +} diff --git a/packages/action-menu/tsconfig.json b/packages/action-menu/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/action-menu/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/packages/anoncreds/CHANGELOG.md b/packages/anoncreds/CHANGELOG.md new file mode 100644 index 0000000000..76ee38e749 --- /dev/null +++ b/packages/anoncreds/CHANGELOG.md @@ -0,0 +1,142 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release +- Updated dependencies [66e696d] + - @credo-ts/core@0.5.6 + +## 0.5.5 + +### Patch Changes + +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) +- Updated dependencies [3239ef3] +- Updated dependencies [d548fa4] +- Updated dependencies [482a630] + - @credo-ts/core@0.5.5 + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +### Bug Fixes + +- **anoncreds:** migration script credential id ([#1849](https://github.com/openwallet-foundation/credo-ts/issues/1849)) ([e58ec5b](https://github.com/openwallet-foundation/credo-ts/commit/e58ec5bd97043d57fcc3c5a4aee926943e6c5326)) + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +### Bug Fixes + +- **anoncreds:** credential exchange record migration ([#1844](https://github.com/openwallet-foundation/credo-ts/issues/1844)) ([93b3986](https://github.com/openwallet-foundation/credo-ts/commit/93b3986348a86365c3a2faf8023a51390528df93)) +- **anoncreds:** unqualified revocation registry processing ([#1833](https://github.com/openwallet-foundation/credo-ts/issues/1833)) ([edc5735](https://github.com/openwallet-foundation/credo-ts/commit/edc5735ccb663acabe8b8480f36cc3a72a1cf63d)) +- node-ffi-napi compatibility ([#1821](https://github.com/openwallet-foundation/credo-ts/issues/1821)) ([81d351b](https://github.com/openwallet-foundation/credo-ts/commit/81d351bc9d4d508ebfac9e7f2b2f10276ab1404a)) + +### Features + +- sort requested credentials ([#1839](https://github.com/openwallet-foundation/credo-ts/issues/1839)) ([b46c7fa](https://github.com/openwallet-foundation/credo-ts/commit/b46c7fa459d7e1a81744353bf595c754fad1b3a1)) + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +### Bug Fixes + +- anoncreds w3c migration metadata ([#1803](https://github.com/openwallet-foundation/credo-ts/issues/1803)) ([069c9c4](https://github.com/openwallet-foundation/credo-ts/commit/069c9c4fe362ee6c8af233df154d2d9b2c0f2d44)) + +### Features + +- **anoncreds:** expose methods and metadata ([#1797](https://github.com/openwallet-foundation/credo-ts/issues/1797)) ([5992c57](https://github.com/openwallet-foundation/credo-ts/commit/5992c57a34d3b48dfa86cb659c77af498b6e8708)) + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +### Bug Fixes + +- abandon proof protocol if presentation fails ([#1610](https://github.com/openwallet-foundation/credo-ts/issues/1610)) ([b2ba7c7](https://github.com/openwallet-foundation/credo-ts/commit/b2ba7c7197139e780cbb95eed77dc0a2ad3b3210)) +- **anoncreds:** allow for zero idx to be used for revocation ([#1742](https://github.com/openwallet-foundation/credo-ts/issues/1742)) ([a1b9901](https://github.com/openwallet-foundation/credo-ts/commit/a1b9901b8bb232560118c902d86464e28d8a73fa)) +- **anoncreds:** only store the revocation registry definition when the state is finished ([#1735](https://github.com/openwallet-foundation/credo-ts/issues/1735)) ([f7785c5](https://github.com/openwallet-foundation/credo-ts/commit/f7785c52b814dfa01c6d16dbecfcc937d533b710)) +- **anoncreds:** pass along options for registry and status list ([#1734](https://github.com/openwallet-foundation/credo-ts/issues/1734)) ([e4b99a8](https://github.com/openwallet-foundation/credo-ts/commit/e4b99a86c76a1a4a41aebb94da0b57f774dd6aaf)) +- **core:** query credential and proof records by correct DIDComm role ([#1780](https://github.com/openwallet-foundation/credo-ts/issues/1780)) ([add7e09](https://github.com/openwallet-foundation/credo-ts/commit/add7e091e845fdaddaf604335f19557f47a31079)) +- presentation submission format ([#1792](https://github.com/openwallet-foundation/credo-ts/issues/1792)) ([1a46e9f](https://github.com/openwallet-foundation/credo-ts/commit/1a46e9f02599ed8b2bf36f5b9d3951d143852f03)) +- query the record by credential and proof role ([#1784](https://github.com/openwallet-foundation/credo-ts/issues/1784)) ([d2b5cd9](https://github.com/openwallet-foundation/credo-ts/commit/d2b5cd9cbbfa95cbdcde9a4fed3305bab6161faf)) +- save AnonCredsCredentialRecord createdAt ([#1603](https://github.com/openwallet-foundation/credo-ts/issues/1603)) ([a1942f8](https://github.com/openwallet-foundation/credo-ts/commit/a1942f8a8dffb11558dcbb900cbeb052e7d0227e)) +- w3c anoncreds ([#1791](https://github.com/openwallet-foundation/credo-ts/issues/1791)) ([913596c](https://github.com/openwallet-foundation/credo-ts/commit/913596c4e843855f77a490428c55daac220bc8c6)) + +### Features + +- anoncreds w3c migration ([#1744](https://github.com/openwallet-foundation/credo-ts/issues/1744)) ([d7c2bbb](https://github.com/openwallet-foundation/credo-ts/commit/d7c2bbb4fde57cdacbbf1ed40c6bd1423f7ab015)) +- **anoncreds:** issue revocable credentials ([#1427](https://github.com/openwallet-foundation/credo-ts/issues/1427)) ([c59ad59](https://github.com/openwallet-foundation/credo-ts/commit/c59ad59fbe63b6d3760d19030e0f95fb2ea8488a)) +- **indy-vdr:** register revocation registry definitions and status list ([#1693](https://github.com/openwallet-foundation/credo-ts/issues/1693)) ([ee34fe7](https://github.com/openwallet-foundation/credo-ts/commit/ee34fe71780a0787db96e28575eeedce3b4704bd)) +- **openid4vc:** persistance and events ([#1793](https://github.com/openwallet-foundation/credo-ts/issues/1793)) ([f4c386a](https://github.com/openwallet-foundation/credo-ts/commit/f4c386a6ccf8adb829cad30b81d524e6ffddb029)) +- optional backup on storage migration ([#1745](https://github.com/openwallet-foundation/credo-ts/issues/1745)) ([81ff63c](https://github.com/openwallet-foundation/credo-ts/commit/81ff63ccf7c71eccf342899d298a780d66045534)) +- sped up lookup for revocation registries ([#1605](https://github.com/openwallet-foundation/credo-ts/issues/1605)) ([32ef8c5](https://github.com/openwallet-foundation/credo-ts/commit/32ef8c5a002c2cfe209c72e01f95b43337922fc6)) + +## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) + +### Bug Fixes + +- **oob:** support oob with connection and messages ([#1558](https://github.com/hyperledger/aries-framework-javascript/issues/1558)) ([9732ce4](https://github.com/hyperledger/aries-framework-javascript/commit/9732ce436a0ddee8760b02ac5182e216a75176c2)) + +## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) + +### Bug Fixes + +- **anoncreds:** wrong key name for predicates in proof object ([#1517](https://github.com/hyperledger/aries-framework-javascript/issues/1517)) ([d895c78](https://github.com/hyperledger/aries-framework-javascript/commit/d895c78e0e02954a95ad1fd7e2251ee9a02445dc)) + +### Features + +- **anoncreds:** auto create link secret ([#1521](https://github.com/hyperledger/aries-framework-javascript/issues/1521)) ([c6f03e4](https://github.com/hyperledger/aries-framework-javascript/commit/c6f03e49d79a33b1c4b459cef11add93dee051d0)) +- oob without handhsake improvements and routing ([#1511](https://github.com/hyperledger/aries-framework-javascript/issues/1511)) ([9e69cf4](https://github.com/hyperledger/aries-framework-javascript/commit/9e69cf441a75bf7a3c5556cf59e730ee3fce8c28)) + +# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) + +### Bug Fixes + +- add reflect-metadata ([#1409](https://github.com/hyperledger/aries-framework-javascript/issues/1409)) ([692defa](https://github.com/hyperledger/aries-framework-javascript/commit/692defa45ffcb4f36b0fa36970c4dc27aa75317c)) +- **anoncreds:** Buffer not imported from core ([#1367](https://github.com/hyperledger/aries-framework-javascript/issues/1367)) ([c133538](https://github.com/hyperledger/aries-framework-javascript/commit/c133538356471a6a0887322a3f6245aa5193e7e4)) +- **anoncreds:** include prover_did for legacy indy ([#1342](https://github.com/hyperledger/aries-framework-javascript/issues/1342)) ([d38ecb1](https://github.com/hyperledger/aries-framework-javascript/commit/d38ecb14cb58f1eb78e01c91699bb990d805dc08)) +- **anoncreds:** make revocation status list inline with the spec ([#1421](https://github.com/hyperledger/aries-framework-javascript/issues/1421)) ([644e860](https://github.com/hyperledger/aries-framework-javascript/commit/644e860a05f40166e26c497a2e8619c9a38df11d)) +- **askar:** anoncrypt messages unpacking ([#1332](https://github.com/hyperledger/aries-framework-javascript/issues/1332)) ([1c6aeae](https://github.com/hyperledger/aries-framework-javascript/commit/1c6aeae31ac57e83f4059f3dba35ccb1ca36926e)) +- incorrect type for anoncreds registration ([#1396](https://github.com/hyperledger/aries-framework-javascript/issues/1396)) ([9f0f8f2](https://github.com/hyperledger/aries-framework-javascript/commit/9f0f8f21e7436c0a422d8c3a42a4cb601bcf7c77)) +- issuance with unqualified identifiers ([#1431](https://github.com/hyperledger/aries-framework-javascript/issues/1431)) ([de90caf](https://github.com/hyperledger/aries-framework-javascript/commit/de90cafb8d12b7a940f881184cd745c4b5043cbc)) +- migration of link secret ([#1444](https://github.com/hyperledger/aries-framework-javascript/issues/1444)) ([9a43afe](https://github.com/hyperledger/aries-framework-javascript/commit/9a43afec7ea72a6fa8c6133f0fad05d8a3d2a595)) +- various anoncreds revocation fixes ([#1416](https://github.com/hyperledger/aries-framework-javascript/issues/1416)) ([d9cfc7d](https://github.com/hyperledger/aries-framework-javascript/commit/d9cfc7df6679d2008d66070a6c8a818440d066ab)) + +- feat!: add data, cache and temp dirs to FileSystem (#1306) ([ff5596d](https://github.com/hyperledger/aries-framework-javascript/commit/ff5596d0631e93746494c017797d0191b6bdb0b1)), closes [#1306](https://github.com/hyperledger/aries-framework-javascript/issues/1306) + +### Features + +- 0.4.0 migration script ([#1392](https://github.com/hyperledger/aries-framework-javascript/issues/1392)) ([bc5455f](https://github.com/hyperledger/aries-framework-javascript/commit/bc5455f7b42612a2b85e504bc6ddd36283a42bfa)) +- add anoncreds-rs package ([#1275](https://github.com/hyperledger/aries-framework-javascript/issues/1275)) ([efe0271](https://github.com/hyperledger/aries-framework-javascript/commit/efe0271198f21f1307df0f934c380f7a5c720b06)) +- **anoncreds:** add anoncreds API ([#1232](https://github.com/hyperledger/aries-framework-javascript/issues/1232)) ([3a4c5ec](https://github.com/hyperledger/aries-framework-javascript/commit/3a4c5ecd940e49d4d192eef1d41f2aaedb34d85a)) +- **anoncreds:** add AnonCreds format services ([#1385](https://github.com/hyperledger/aries-framework-javascript/issues/1385)) ([5f71dc2](https://github.com/hyperledger/aries-framework-javascript/commit/5f71dc2b403f6cb0fc9bb13f35051d377c2d1250)) +- **anoncreds:** add getCredential(s) methods ([#1386](https://github.com/hyperledger/aries-framework-javascript/issues/1386)) ([2efc009](https://github.com/hyperledger/aries-framework-javascript/commit/2efc0097138585391940fbb2eb504e50df57ec87)) +- **anoncreds:** add legacy indy credential format ([#1220](https://github.com/hyperledger/aries-framework-javascript/issues/1220)) ([13f3740](https://github.com/hyperledger/aries-framework-javascript/commit/13f374079262168f90ec7de7c3393beb9651295c)) +- **anoncreds:** legacy indy proof format service ([#1283](https://github.com/hyperledger/aries-framework-javascript/issues/1283)) ([c72fd74](https://github.com/hyperledger/aries-framework-javascript/commit/c72fd7416f2c1bc0497a84036e16adfa80585e49)) +- **anoncreds:** store method name in records ([#1387](https://github.com/hyperledger/aries-framework-javascript/issues/1387)) ([47636b4](https://github.com/hyperledger/aries-framework-javascript/commit/47636b4a08ffbfa9a3f2a5a3c5aebda44f7d16c8)) +- **anoncreds:** support credential attribute value and marker ([#1369](https://github.com/hyperledger/aries-framework-javascript/issues/1369)) ([5559996](https://github.com/hyperledger/aries-framework-javascript/commit/555999686a831e6988564fd5c9c937fc1023f567)) +- **anoncreds:** use legacy prover did ([#1374](https://github.com/hyperledger/aries-framework-javascript/issues/1374)) ([c17013c](https://github.com/hyperledger/aries-framework-javascript/commit/c17013c808a278d624210ce9e4333860cd78fc19)) +- default return route ([#1327](https://github.com/hyperledger/aries-framework-javascript/issues/1327)) ([dbfebb4](https://github.com/hyperledger/aries-framework-javascript/commit/dbfebb4720da731dbe11efdccdd061d1da3d1323)) +- **indy-vdr:** schema + credential definition endorsement ([#1451](https://github.com/hyperledger/aries-framework-javascript/issues/1451)) ([25b981b](https://github.com/hyperledger/aries-framework-javascript/commit/25b981b6e23d02409e90dabdccdccc8904d4e357)) +- outbound message send via session ([#1335](https://github.com/hyperledger/aries-framework-javascript/issues/1335)) ([582c711](https://github.com/hyperledger/aries-framework-javascript/commit/582c711728db12b7d38a0be2e9fa78dbf31b34c6)) + +### BREAKING CHANGES + +- Agent-produced files will now be divided in different system paths depending on their nature: data, temp and cache. Previously, they were located at a single location, defaulting to a temporary directory. + +If you specified a custom path in `FileSystem` object constructor, you now must provide an object containing `baseDataPath`, `baseTempPath` and `baseCachePath`. They can point to the same path, although it's recommended to specify different path to avoid future file clashes. diff --git a/packages/anoncreds/README.md b/packages/anoncreds/README.md new file mode 100644 index 0000000000..703ea1a963 --- /dev/null +++ b/packages/anoncreds/README.md @@ -0,0 +1,31 @@ +

+
+ Credo Logo +

+

Credo AnonCreds Module

+

+ License + typescript + @credo-ts/anoncreds version + +

+
+ +Credo AnonCreds provides AnonCreds capabilities of Credo. See the [AnonCreds Setup](https://credo.js.org/guides/getting-started/set-up/anoncreds) for installation instructions. diff --git a/packages/anoncreds/jest.config.ts b/packages/anoncreds/jest.config.ts new file mode 100644 index 0000000000..93c0197296 --- /dev/null +++ b/packages/anoncreds/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json new file mode 100644 index 0000000000..5f9f5aff2a --- /dev/null +++ b/packages/anoncreds/package.json @@ -0,0 +1,49 @@ +{ + "name": "@credo-ts/anoncreds", + "main": "build/index", + "types": "build/index", + "version": "0.5.6-revocation", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/anoncreds", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/anoncreds" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build", + "test": "jest" + }, + "dependencies": { + "@astronautlabs/jsonpath": "^1.1.2", + "@credo-ts/core": "0.5.6-revocation", + "@sphereon/pex-models": "^2.2.4", + "big-integer": "^1.6.51", + "bn.js": "^5.2.1", + "class-transformer": "0.5.1", + "class-validator": "0.14.1", + "reflect-metadata": "^0.1.13" + }, + "devDependencies": { + "@credo-ts/node": "workspace:*", + "@hyperledger/anoncreds-nodejs": "^0.2.2", + "@hyperledger/anoncreds-shared": "^0.2.2", + "rimraf": "^4.4.0", + "rxjs": "^7.8.0", + "typescript": "~5.5.2" + }, + "peerDependencies": { + "@hyperledger/anoncreds-shared": "^0.2.2" + } +} diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts new file mode 100644 index 0000000000..d4ddbf9073 --- /dev/null +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -0,0 +1,796 @@ +import type { + AnonCredsCreateLinkSecretOptions, + AnonCredsRegisterCredentialDefinitionOptions, + AnonCredsRegisterRevocationRegistryDefinitionOptions, + AnonCredsRegisterRevocationStatusListOptions, + AnonCredsUpdateRevocationStatusListOptions, +} from './AnonCredsApiOptions' +import type { AnonCredsCredentialDefinition, AnonCredsSchema } from './models' +import type { + AnonCredsRegistry, + GetCredentialDefinitionReturn, + GetCredentialsOptions, + GetRevocationRegistryDefinitionReturn, + GetRevocationStatusListReturn, + GetSchemaReturn, + RegisterCredentialDefinitionReturn, + RegisterSchemaReturn, + RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationStatusListReturn, +} from './services' +import type { Extensible } from './services/registry/base' +import type { SimpleQuery } from '@credo-ts/core' + +import { AgentContext, inject, injectable } from '@credo-ts/core' + +import { AnonCredsModuleConfig } from './AnonCredsModuleConfig' +import { AnonCredsStoreRecordError } from './error' +import { + AnonCredsRevocationRegistryDefinitionPrivateRecord, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRepository, + AnonCredsCredentialDefinitionPrivateRecord, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsKeyCorrectnessProofRecord, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsLinkSecretRepository, + AnonCredsRevocationRegistryDefinitionRecord, + AnonCredsRevocationRegistryState, +} from './repository' +import { AnonCredsCredentialDefinitionRecord } from './repository/AnonCredsCredentialDefinitionRecord' +import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' +import { AnonCredsSchemaRecord } from './repository/AnonCredsSchemaRecord' +import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' +import { AnonCredsCredentialDefinitionRecordMetadataKeys } from './repository/anonCredsCredentialDefinitionRecordMetadataTypes' +import { AnonCredsRevocationRegistryDefinitionRecordMetadataKeys } from './repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes' +import { + AnonCredsHolderService, + AnonCredsHolderServiceSymbol, + AnonCredsIssuerService, + AnonCredsIssuerServiceSymbol, +} from './services' +import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' +import { dateToTimestamp, storeLinkSecret } from './utils' + +@injectable() +export class AnonCredsApi { + public config: AnonCredsModuleConfig + + private agentContext: AgentContext + private anonCredsRegistryService: AnonCredsRegistryService + private anonCredsSchemaRepository: AnonCredsSchemaRepository + private anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository + private anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository + private anonCredsRevocationRegistryDefinitionRepository: AnonCredsRevocationRegistryDefinitionRepository + private anonCredsRevocationRegistryDefinitionPrivateRepository: AnonCredsRevocationRegistryDefinitionPrivateRepository + private anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository + private anonCredsLinkSecretRepository: AnonCredsLinkSecretRepository + private anonCredsIssuerService: AnonCredsIssuerService + private anonCredsHolderService: AnonCredsHolderService + + public constructor( + agentContext: AgentContext, + anonCredsRegistryService: AnonCredsRegistryService, + config: AnonCredsModuleConfig, + @inject(AnonCredsIssuerServiceSymbol) anonCredsIssuerService: AnonCredsIssuerService, + @inject(AnonCredsHolderServiceSymbol) anonCredsHolderService: AnonCredsHolderService, + anonCredsSchemaRepository: AnonCredsSchemaRepository, + anonCredsRevocationRegistryDefinitionRepository: AnonCredsRevocationRegistryDefinitionRepository, + anonCredsRevocationRegistryDefinitionPrivateRepository: AnonCredsRevocationRegistryDefinitionPrivateRepository, + anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository, + anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository, + anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository, + anonCredsLinkSecretRepository: AnonCredsLinkSecretRepository + ) { + this.agentContext = agentContext + this.anonCredsRegistryService = anonCredsRegistryService + this.config = config + this.anonCredsIssuerService = anonCredsIssuerService + this.anonCredsHolderService = anonCredsHolderService + this.anonCredsSchemaRepository = anonCredsSchemaRepository + this.anonCredsRevocationRegistryDefinitionRepository = anonCredsRevocationRegistryDefinitionRepository + this.anonCredsRevocationRegistryDefinitionPrivateRepository = anonCredsRevocationRegistryDefinitionPrivateRepository + this.anonCredsCredentialDefinitionRepository = anonCredsCredentialDefinitionRepository + this.anonCredsCredentialDefinitionPrivateRepository = anonCredsCredentialDefinitionPrivateRepository + this.anonCredsKeyCorrectnessProofRepository = anonCredsKeyCorrectnessProofRepository + this.anonCredsLinkSecretRepository = anonCredsLinkSecretRepository + } + + /** + * Create a Link Secret, optionally indicating its ID and if it will be the default one + * If there is no default Link Secret, this will be set as default (even if setAsDefault is false). + * + */ + public async createLinkSecret(options?: AnonCredsCreateLinkSecretOptions): Promise { + const { linkSecretId, linkSecretValue } = await this.anonCredsHolderService.createLinkSecret(this.agentContext, { + linkSecretId: options?.linkSecretId, + }) + + await storeLinkSecret(this.agentContext, { + linkSecretId, + linkSecretValue, + setAsDefault: options?.setAsDefault, + }) + + return linkSecretId + } + + /** + * Get a list of ids for the created link secrets + */ + public async getLinkSecretIds(): Promise { + const linkSecrets = await this.anonCredsLinkSecretRepository.getAll(this.agentContext) + + return linkSecrets.map((linkSecret) => linkSecret.linkSecretId) + } + + /** + * Retrieve a {@link AnonCredsSchema} from the registry associated + * with the {@link schemaId} + */ + public async getSchema(schemaId: string): Promise { + const failedReturnBase = { + resolutionMetadata: { + error: 'error', + message: `Unable to resolve schema ${schemaId}`, + }, + schemaId, + schemaMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(schemaId) + if (!registry) { + failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod' + failedReturnBase.resolutionMetadata.message = `Unable to resolve schema ${schemaId}: No registry found for identifier ${schemaId}` + return failedReturnBase + } + + try { + const result = await registry.getSchema(this.agentContext, schemaId) + return result + } catch (error) { + failedReturnBase.resolutionMetadata.message = `Unable to resolve schema ${schemaId}: ${error.message}` + return failedReturnBase + } + } + + public async registerSchema( + options: AnonCredsRegisterSchema + ): Promise { + const failedReturnBase = { + schemaState: { + state: 'failed' as const, + schema: options.schema, + reason: `Error registering schema for issuerId ${options.schema.issuerId}`, + }, + registrationMetadata: {}, + schemaMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(options.schema.issuerId) + if (!registry) { + failedReturnBase.schemaState.reason = `Unable to register schema. No registry found for issuerId ${options.schema.issuerId}` + return failedReturnBase + } + + try { + const result = await registry.registerSchema(this.agentContext, options) + if (result.schemaState.state === 'finished') { + await this.storeSchemaRecord(registry, result) + } + + return result + } catch (error) { + // Storage failed + if (error instanceof AnonCredsStoreRecordError) { + failedReturnBase.schemaState.reason = `Error storing schema record: ${error.message}` + return failedReturnBase + } + + // In theory registerSchema SHOULD NOT throw, but we can't know for sure + failedReturnBase.schemaState.reason = `Error registering schema: ${error.message}` + return failedReturnBase + } + } + + public async getCreatedSchemas(query: SimpleQuery) { + return this.anonCredsSchemaRepository.findByQuery(this.agentContext, query) + } + + /** + * Retrieve a {@link GetCredentialDefinitionReturn} from the registry associated + * with the {@link credentialDefinitionId} + */ + public async getCredentialDefinition(credentialDefinitionId: string): Promise { + const failedReturnBase = { + resolutionMetadata: { + error: 'error', + message: `Unable to resolve credential definition ${credentialDefinitionId}`, + }, + credentialDefinitionId, + credentialDefinitionMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(credentialDefinitionId) + if (!registry) { + failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod' + failedReturnBase.resolutionMetadata.message = `Unable to resolve credential definition ${credentialDefinitionId}: No registry found for identifier ${credentialDefinitionId}` + return failedReturnBase + } + + try { + const result = await registry.getCredentialDefinition(this.agentContext, credentialDefinitionId) + return result + } catch (error) { + failedReturnBase.resolutionMetadata.message = `Unable to resolve credential definition ${credentialDefinitionId}: ${error.message}` + return failedReturnBase + } + } + + public async registerCredentialDefinition( + options: AnonCredsRegisterCredentialDefinition + ): Promise { + const failedReturnBase = { + credentialDefinitionState: { + state: 'failed' as const, + reason: `Error registering credential definition for issuerId ${options.credentialDefinition.issuerId}`, + }, + registrationMetadata: {}, + credentialDefinitionMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(options.credentialDefinition.issuerId) + if (!registry) { + failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for issuerId ${options.credentialDefinition.issuerId}` + return failedReturnBase + } + + let credentialDefinition: AnonCredsCredentialDefinition + let credentialDefinitionPrivate: Record | undefined = undefined + let keyCorrectnessProof: Record | undefined = undefined + + try { + if (isFullCredentialDefinitionInput(options.credentialDefinition)) { + credentialDefinition = options.credentialDefinition + } else { + // If the input credential definition is not a full credential definition, we need to create one first + // There's a caveat to when the input contains a full credential, that the credential definition private + // and key correctness proof must already be stored in the wallet + const schemaRegistry = this.findRegistryForIdentifier(options.credentialDefinition.schemaId) + if (!schemaRegistry) { + failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for schemaId ${options.credentialDefinition.schemaId}` + return failedReturnBase + } + + const schemaResult = await schemaRegistry.getSchema(this.agentContext, options.credentialDefinition.schemaId) + + if (!schemaResult.schema) { + failedReturnBase.credentialDefinitionState.reason = `error resolving schema with id ${options.credentialDefinition.schemaId}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}` + return failedReturnBase + } + + const createCredentialDefinitionResult = await this.anonCredsIssuerService.createCredentialDefinition( + this.agentContext, + { + issuerId: options.credentialDefinition.issuerId, + schemaId: options.credentialDefinition.schemaId, + tag: options.credentialDefinition.tag, + supportRevocation: options.options.supportRevocation, + schema: schemaResult.schema, + }, + // NOTE: indy-sdk support has been removed from main repo, but keeping + // this in place to allow the indy-sdk to still be used as a custom package for some time + // FIXME: Indy SDK requires the schema seq no to be passed in here. This is not ideal. + { + indyLedgerSchemaSeqNo: schemaResult.schemaMetadata.indyLedgerSeqNo, + } + ) + + credentialDefinition = createCredentialDefinitionResult.credentialDefinition + credentialDefinitionPrivate = createCredentialDefinitionResult.credentialDefinitionPrivate + keyCorrectnessProof = createCredentialDefinitionResult.keyCorrectnessProof + } + + const result = await registry.registerCredentialDefinition(this.agentContext, { + credentialDefinition, + options: options.options, + }) + + // Once a credential definition is created, the credential definition private and the key correctness proof must be stored because they change even if they the credential is recreated with the same arguments. + // To avoid having unregistered credential definitions in the wallet, the credential definitions itself are stored only when the credential definition status is finished, meaning that the credential definition has been successfully registered. + await this.storeCredentialDefinitionPrivateAndKeyCorrectnessRecord( + result, + credentialDefinitionPrivate, + keyCorrectnessProof + ) + if (result.credentialDefinitionState.state === 'finished') { + await this.storeCredentialDefinitionRecord(registry, result) + } + + return result + } catch (error) { + // Storage failed + if (error instanceof AnonCredsStoreRecordError) { + failedReturnBase.credentialDefinitionState.reason = `Error storing credential definition records: ${error.message}` + return failedReturnBase + } + + // In theory registerCredentialDefinition SHOULD NOT throw, but we can't know for sure + failedReturnBase.credentialDefinitionState.reason = `Error registering credential definition: ${error.message}` + return failedReturnBase + } + } + + public async getCreatedCredentialDefinitions(query: SimpleQuery) { + return this.anonCredsCredentialDefinitionRepository.findByQuery(this.agentContext, query) + } + + /** + * Retrieve a {@link AnonCredsRevocationRegistryDefinition} from the registry associated + * with the {@link revocationRegistryDefinitionId} + */ + public async getRevocationRegistryDefinition( + revocationRegistryDefinitionId: string + ): Promise { + const failedReturnBase = { + resolutionMetadata: { + error: 'error', + message: `Unable to resolve revocation registry ${revocationRegistryDefinitionId}`, + }, + revocationRegistryDefinitionId, + revocationRegistryDefinitionMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId) + if (!registry) { + failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod' + failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation registry ${revocationRegistryDefinitionId}: No registry found for identifier ${revocationRegistryDefinitionId}` + return failedReturnBase + } + + try { + const result = await registry.getRevocationRegistryDefinition(this.agentContext, revocationRegistryDefinitionId) + return result + } catch (error) { + failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation registry ${revocationRegistryDefinitionId}: ${error.message}` + return failedReturnBase + } + } + + public async registerRevocationRegistryDefinition( + options: AnonCredsRegisterRevocationRegistryDefinition + ): Promise { + const { issuerId, tag, credentialDefinitionId, maximumCredentialNumber } = options.revocationRegistryDefinition + + const tailsFileService = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + + const tailsDirectoryPath = await tailsFileService.getTailsBasePath(this.agentContext) + + const failedReturnBase = { + revocationRegistryDefinitionState: { + state: 'failed' as const, + reason: `Error registering revocation registry definition for issuerId ${issuerId}`, + }, + registrationMetadata: {}, + revocationRegistryDefinitionMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(issuerId) + if (!registry) { + failedReturnBase.revocationRegistryDefinitionState.reason = `Unable to register revocation registry definition. No registry found for issuerId ${issuerId}` + return failedReturnBase + } + + const { credentialDefinition } = await registry.getCredentialDefinition(this.agentContext, credentialDefinitionId) + + if (!credentialDefinition) { + failedReturnBase.revocationRegistryDefinitionState.reason = `Unable to register revocation registry definition. No credential definition found for id ${credentialDefinitionId}` + return failedReturnBase + } + try { + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = + await this.anonCredsIssuerService.createRevocationRegistryDefinition(this.agentContext, { + issuerId, + tag, + credentialDefinitionId, + credentialDefinition, + maximumCredentialNumber, + tailsDirectoryPath, + }) + + // At this moment, tails file should be published and a valid public URL will be received + const localTailsLocation = revocationRegistryDefinition.value.tailsLocation + + const { tailsFileUrl } = await tailsFileService.uploadTailsFile(this.agentContext, { + revocationRegistryDefinition, + }) + revocationRegistryDefinition.value.tailsLocation = tailsFileUrl + + const result = await registry.registerRevocationRegistryDefinition(this.agentContext, { + revocationRegistryDefinition, + options: options.options, + }) + + // To avoid having unregistered revocation registry definitions in the wallet, the revocation registry definition itself are stored only when the revocation registry definition status is finished, meaning that the revocation registry definition has been successfully registered. + if (result.revocationRegistryDefinitionState.state === 'finished') { + await this.storeRevocationRegistryDefinitionRecord(result, revocationRegistryDefinitionPrivate) + } + + return { + ...result, + revocationRegistryDefinitionMetadata: { ...result.revocationRegistryDefinitionMetadata, localTailsLocation }, + } + } catch (error) { + // Storage failed + if (error instanceof AnonCredsStoreRecordError) { + failedReturnBase.revocationRegistryDefinitionState.reason = `Error storing revocation registry definition records: ${error.message}` + return failedReturnBase + } + + failedReturnBase.revocationRegistryDefinitionState.reason = `Error registering revocation registry definition: ${error.message}` + return failedReturnBase + } + } + + /** + * Retrieve the {@link AnonCredsRevocationStatusList} for the given {@link timestamp} from the registry associated + * with the {@link revocationRegistryDefinitionId} + */ + public async getRevocationStatusList( + revocationRegistryDefinitionId: string, + timestamp: number + ): Promise { + const failedReturnBase = { + resolutionMetadata: { + error: 'error', + message: `Unable to resolve revocation status list for revocation registry ${revocationRegistryDefinitionId}`, + }, + revocationStatusListMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId) + if (!registry) { + failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod' + failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation status list for revocation registry ${revocationRegistryDefinitionId}: No registry found for identifier ${revocationRegistryDefinitionId}` + return failedReturnBase + } + + try { + const result = await registry.getRevocationStatusList( + this.agentContext, + revocationRegistryDefinitionId, + timestamp + ) + return result + } catch (error) { + failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation status list for revocation registry ${revocationRegistryDefinitionId}: ${error.message}` + return failedReturnBase + } + } + + public async registerRevocationStatusList( + options: AnonCredsRegisterRevocationStatusList + ): Promise { + const { issuerId, revocationRegistryDefinitionId } = options.revocationStatusList + + const failedReturnBase = { + revocationStatusListState: { + state: 'failed' as const, + reason: `Error registering revocation status list for issuerId ${issuerId}`, + }, + registrationMetadata: {}, + revocationStatusListMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(issuerId) + if (!registry) { + failedReturnBase.revocationStatusListState.reason = `Unable to register revocation status list. No registry found for issuerId ${issuerId}` + return failedReturnBase + } + + const { revocationRegistryDefinition } = await registry.getRevocationRegistryDefinition( + this.agentContext, + revocationRegistryDefinitionId + ) + + if (!revocationRegistryDefinition) { + failedReturnBase.revocationStatusListState.reason = `Unable to register revocation status list. No revocation registry definition found for ${revocationRegistryDefinitionId}` + return failedReturnBase + } + const tailsFileService = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + const { tailsFilePath } = await tailsFileService.getTailsFile(this.agentContext, { + revocationRegistryDefinition, + }) + + try { + const revocationStatusList = await this.anonCredsIssuerService.createRevocationStatusList(this.agentContext, { + issuerId, + revocationRegistryDefinition, + revocationRegistryDefinitionId, + tailsFilePath, + }) + + const result = await registry.registerRevocationStatusList(this.agentContext, { + revocationStatusList, + options: options.options, + }) + + return result + } catch (error) { + // Storage failed + if (error instanceof AnonCredsStoreRecordError) { + failedReturnBase.revocationStatusListState.reason = `Error storing revocation status list records: ${error.message}` + return failedReturnBase + } + + failedReturnBase.revocationStatusListState.reason = `Error registering revocation status list: ${error.message}` + return failedReturnBase + } + } + + public async updateRevocationStatusList( + options: AnonCredsUpdateRevocationStatusList + ): Promise { + const { issuedCredentialIndexes, revokedCredentialIndexes, revocationRegistryDefinitionId } = + options.revocationStatusList + + const failedReturnBase = { + revocationStatusListState: { + state: 'failed' as const, + reason: `Error updating revocation status list for revocation registry definition id ${options.revocationStatusList.revocationRegistryDefinitionId}`, + }, + registrationMetadata: {}, + revocationStatusListMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(options.revocationStatusList.revocationRegistryDefinitionId) + if (!registry) { + failedReturnBase.revocationStatusListState.reason = `Unable to update revocation status list. No registry found for id ${options.revocationStatusList.revocationRegistryDefinitionId}` + return failedReturnBase + } + + const { revocationRegistryDefinition } = await registry.getRevocationRegistryDefinition( + this.agentContext, + revocationRegistryDefinitionId + ) + + if (!revocationRegistryDefinition) { + failedReturnBase.revocationStatusListState.reason = `Unable to update revocation status list. No revocation registry definition found for ${revocationRegistryDefinitionId}` + return failedReturnBase + } + + const { revocationStatusList: previousRevocationStatusList } = await this.getRevocationStatusList( + revocationRegistryDefinitionId, + dateToTimestamp(new Date()) + ) + + if (!previousRevocationStatusList) { + failedReturnBase.revocationStatusListState.reason = `Unable to update revocation status list. No previous revocation status list found for ${options.revocationStatusList.revocationRegistryDefinitionId}` + return failedReturnBase + } + + const tailsFileService = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + const { tailsFilePath } = await tailsFileService.getTailsFile(this.agentContext, { + revocationRegistryDefinition, + }) + + try { + const revocationStatusList = await this.anonCredsIssuerService.updateRevocationStatusList(this.agentContext, { + issued: issuedCredentialIndexes, + revoked: revokedCredentialIndexes, + revocationStatusList: previousRevocationStatusList, + revocationRegistryDefinition, + tailsFilePath, + }) + + const result = await registry.registerRevocationStatusList(this.agentContext, { + revocationStatusList, + options: options.options, + }) + + return result + } catch (error) { + // Storage failed + if (error instanceof AnonCredsStoreRecordError) { + failedReturnBase.revocationStatusListState.reason = `Error storing revocation status list records: ${error.message}` + return failedReturnBase + } + + failedReturnBase.revocationStatusListState.reason = `Error registering revocation status list: ${error.message}` + return failedReturnBase + } + } + + public async getCredential(id: string) { + return this.anonCredsHolderService.getCredential(this.agentContext, { id }) + } + + public async getCredentials(options: GetCredentialsOptions) { + return this.anonCredsHolderService.getCredentials(this.agentContext, options) + } + + private async storeRevocationRegistryDefinitionRecord( + result: RegisterRevocationRegistryDefinitionReturn, + revocationRegistryDefinitionPrivate?: Record + ): Promise { + try { + // If we have both the revocationRegistryDefinition and the revocationRegistryDefinitionId we will store a copy + // of the credential definition. We may need to handle an edge case in the future where we e.g. don't have the + // id yet, and it is registered through a different channel + if ( + result.revocationRegistryDefinitionState.revocationRegistryDefinition && + result.revocationRegistryDefinitionState.revocationRegistryDefinitionId + ) { + const revocationRegistryDefinitionRecord = new AnonCredsRevocationRegistryDefinitionRecord({ + revocationRegistryDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinitionId, + revocationRegistryDefinition: result.revocationRegistryDefinitionState.revocationRegistryDefinition, + }) + + // TODO: do we need to store this metadata? For indy, the registration metadata contains e.g. + // the indyLedgerSeqNo and the didIndyNamespace, but it can get quite big if complete transactions + // are stored in the metadata + revocationRegistryDefinitionRecord.metadata.set( + AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.RevocationRegistryDefinitionMetadata, + result.revocationRegistryDefinitionMetadata + ) + revocationRegistryDefinitionRecord.metadata.set( + AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.RevocationRegistryDefinitionRegistrationMetadata, + result.registrationMetadata + ) + + await this.anonCredsRevocationRegistryDefinitionRepository.save( + this.agentContext, + revocationRegistryDefinitionRecord + ) + + // Store Revocation Registry Definition private data (if provided by issuer service) + if (revocationRegistryDefinitionPrivate) { + const revocationRegistryDefinitionPrivateRecord = new AnonCredsRevocationRegistryDefinitionPrivateRecord({ + revocationRegistryDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinitionId, + credentialDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinition.credDefId, + value: revocationRegistryDefinitionPrivate, + state: AnonCredsRevocationRegistryState.Active, + }) + await this.anonCredsRevocationRegistryDefinitionPrivateRepository.save( + this.agentContext, + revocationRegistryDefinitionPrivateRecord + ) + } + } + } catch (error) { + throw new AnonCredsStoreRecordError(`Error storing revocation registry definition records`, { cause: error }) + } + } + + private async storeCredentialDefinitionPrivateAndKeyCorrectnessRecord( + result: RegisterCredentialDefinitionReturn, + credentialDefinitionPrivate?: Record, + keyCorrectnessProof?: Record + ): Promise { + try { + if (!result.credentialDefinitionState.credentialDefinitionId) return + + // Store Credential Definition private data (if provided by issuer service) + if (credentialDefinitionPrivate) { + const credentialDefinitionPrivateRecord = new AnonCredsCredentialDefinitionPrivateRecord({ + credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, + value: credentialDefinitionPrivate, + }) + await this.anonCredsCredentialDefinitionPrivateRepository.save( + this.agentContext, + credentialDefinitionPrivateRecord + ) + } + + if (keyCorrectnessProof) { + const keyCorrectnessProofRecord = new AnonCredsKeyCorrectnessProofRecord({ + credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, + value: keyCorrectnessProof, + }) + await this.anonCredsKeyCorrectnessProofRepository.save(this.agentContext, keyCorrectnessProofRecord) + } + } catch (error) { + throw new AnonCredsStoreRecordError(`Error storing credential definition key-correctness-proof and private`, { + cause: error, + }) + } + } + + private async storeCredentialDefinitionRecord( + registry: AnonCredsRegistry, + result: RegisterCredentialDefinitionReturn + ): Promise { + try { + // If we have both the credentialDefinition and the credentialDefinitionId we will store a copy of the credential definition. We may need to handle an + // edge case in the future where we e.g. don't have the id yet, and it is registered through a different channel + if ( + !result.credentialDefinitionState.credentialDefinition || + !result.credentialDefinitionState.credentialDefinitionId + ) { + return + } + const credentialDefinitionRecord = new AnonCredsCredentialDefinitionRecord({ + credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, + credentialDefinition: result.credentialDefinitionState.credentialDefinition, + methodName: registry.methodName, + }) + + // TODO: do we need to store this metadata? For indy, the registration metadata contains e.g. + // the indyLedgerSeqNo and the didIndyNamespace, but it can get quite big if complete transactions + // are stored in the metadata + credentialDefinitionRecord.metadata.set( + AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionMetadata, + result.credentialDefinitionMetadata + ) + credentialDefinitionRecord.metadata.set( + AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionRegistrationMetadata, + result.registrationMetadata + ) + + await this.anonCredsCredentialDefinitionRepository.save(this.agentContext, credentialDefinitionRecord) + } catch (error) { + throw new AnonCredsStoreRecordError(`Error storing credential definition records`, { cause: error }) + } + } + + private async storeSchemaRecord(registry: AnonCredsRegistry, result: RegisterSchemaReturn): Promise { + try { + // If we have both the schema and the schemaId we will store a copy of the schema. We may need to handle an + // edge case in the future where we e.g. don't have the id yet, and it is registered through a different channel + if (result.schemaState.schema && result.schemaState.schemaId) { + const schemaRecord = new AnonCredsSchemaRecord({ + schemaId: result.schemaState.schemaId, + schema: result.schemaState.schema, + methodName: registry.methodName, + }) + + await this.anonCredsSchemaRepository.save(this.agentContext, schemaRecord) + } + } catch (error) { + throw new AnonCredsStoreRecordError(`Error storing schema record`, { cause: error }) + } + } + + private findRegistryForIdentifier(identifier: string) { + try { + return this.anonCredsRegistryService.getRegistryForIdentifier(this.agentContext, identifier) + } catch { + return null + } + } +} + +export interface AnonCredsRegisterCredentialDefinitionApiOptions { + supportRevocation: boolean +} + +interface AnonCredsRegisterCredentialDefinition { + credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions + options: T & AnonCredsRegisterCredentialDefinitionApiOptions +} + +interface AnonCredsRegisterSchema { + schema: AnonCredsSchema + options: T +} + +interface AnonCredsRegisterRevocationRegistryDefinition { + revocationRegistryDefinition: AnonCredsRegisterRevocationRegistryDefinitionOptions + options: T +} + +interface AnonCredsRegisterRevocationStatusList { + revocationStatusList: AnonCredsRegisterRevocationStatusListOptions + options: T +} + +interface AnonCredsUpdateRevocationStatusList { + revocationStatusList: AnonCredsUpdateRevocationStatusListOptions + options: T +} + +function isFullCredentialDefinitionInput( + credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions +): credentialDefinition is AnonCredsCredentialDefinition { + return 'value' in credentialDefinition +} diff --git a/packages/anoncreds/src/AnonCredsApiOptions.ts b/packages/anoncreds/src/AnonCredsApiOptions.ts new file mode 100644 index 0000000000..64bb1e1d61 --- /dev/null +++ b/packages/anoncreds/src/AnonCredsApiOptions.ts @@ -0,0 +1,28 @@ +export interface AnonCredsCreateLinkSecretOptions { + linkSecretId?: string + setAsDefault?: boolean +} + +export interface AnonCredsRegisterCredentialDefinitionOptions { + issuerId: string + schemaId: string + tag: string +} + +export interface AnonCredsRegisterRevocationRegistryDefinitionOptions { + issuerId: string + tag: string + credentialDefinitionId: string + maximumCredentialNumber: number +} + +export interface AnonCredsRegisterRevocationStatusListOptions { + issuerId: string + revocationRegistryDefinitionId: string +} + +export interface AnonCredsUpdateRevocationStatusListOptions { + revokedCredentialIndexes?: number[] + issuedCredentialIndexes?: number[] + revocationRegistryDefinitionId: string +} diff --git a/packages/anoncreds/src/AnonCredsModule.ts b/packages/anoncreds/src/AnonCredsModule.ts new file mode 100644 index 0000000000..daa5b927e0 --- /dev/null +++ b/packages/anoncreds/src/AnonCredsModule.ts @@ -0,0 +1,70 @@ +import type { AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig' +import type { DependencyManager, Module, Update } from '@credo-ts/core' + +import { AnonCredsDataIntegrityServiceSymbol } from '@credo-ts/core' + +import { AnonCredsApi } from './AnonCredsApi' +import { AnonCredsModuleConfig } from './AnonCredsModuleConfig' +import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from './anoncreds-rs' +import { AnonCredsDataIntegrityService } from './anoncreds-rs/AnonCredsDataIntegrityService' +import { + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsLinkSecretRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRepository, +} from './repository' +import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' +import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' +import { AnonCredsHolderServiceSymbol, AnonCredsIssuerServiceSymbol, AnonCredsVerifierServiceSymbol } from './services' +import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' +import { updateAnonCredsModuleV0_3_1ToV0_4 } from './updates/0.3.1-0.4' +import { updateAnonCredsModuleV0_4ToV0_5 } from './updates/0.4-0.5' + +/** + * @public + */ +export class AnonCredsModule implements Module { + public readonly config: AnonCredsModuleConfig + public api = AnonCredsApi + + public constructor(config: AnonCredsModuleConfigOptions) { + this.config = new AnonCredsModuleConfig(config) + } + + public register(dependencyManager: DependencyManager) { + // Config + dependencyManager.registerInstance(AnonCredsModuleConfig, this.config) + + dependencyManager.registerSingleton(AnonCredsRegistryService) + + // Repositories + dependencyManager.registerSingleton(AnonCredsSchemaRepository) + dependencyManager.registerSingleton(AnonCredsCredentialDefinitionRepository) + dependencyManager.registerSingleton(AnonCredsCredentialDefinitionPrivateRepository) + dependencyManager.registerSingleton(AnonCredsKeyCorrectnessProofRepository) + dependencyManager.registerSingleton(AnonCredsLinkSecretRepository) + dependencyManager.registerSingleton(AnonCredsRevocationRegistryDefinitionRepository) + dependencyManager.registerSingleton(AnonCredsRevocationRegistryDefinitionPrivateRepository) + + // TODO: should we allow to override the service? + dependencyManager.registerSingleton(AnonCredsHolderServiceSymbol, AnonCredsRsHolderService) + dependencyManager.registerSingleton(AnonCredsIssuerServiceSymbol, AnonCredsRsIssuerService) + dependencyManager.registerSingleton(AnonCredsVerifierServiceSymbol, AnonCredsRsVerifierService) + + dependencyManager.registerSingleton(AnonCredsDataIntegrityServiceSymbol, AnonCredsDataIntegrityService) + } + + public updates = [ + { + fromVersion: '0.3.1', + toVersion: '0.4', + doUpdate: updateAnonCredsModuleV0_3_1ToV0_4, + }, + { + fromVersion: '0.4', + toVersion: '0.5', + doUpdate: updateAnonCredsModuleV0_4ToV0_5, + }, + ] satisfies Update[] +} diff --git a/packages/anoncreds/src/AnonCredsModuleConfig.ts b/packages/anoncreds/src/AnonCredsModuleConfig.ts new file mode 100644 index 0000000000..4eebff5e66 --- /dev/null +++ b/packages/anoncreds/src/AnonCredsModuleConfig.ts @@ -0,0 +1,94 @@ +import type { AnonCredsRegistry } from './services' +import type { TailsFileService } from './services/tails' +import type { Anoncreds } from '@hyperledger/anoncreds-shared' + +import { BasicTailsFileService } from './services/tails' + +/** + * @public + * AnonCredsModuleConfigOptions defines the interface for the options of the AnonCredsModuleConfig class. + */ +export interface AnonCredsModuleConfigOptions { + /** + * A list of AnonCreds registries to make available to the AnonCreds module. + */ + registries: [AnonCredsRegistry, ...AnonCredsRegistry[]] + + /** + * Tails file service for download/uploading tails files + * @default BasicTailsFileService (only for downloading tails files) + */ + tailsFileService?: TailsFileService + + /** + * + * ## Node.JS + * + * ```ts + * import { anoncreds } from '@hyperledger/anoncreds-nodejs' + * + * const agent = new Agent({ + * config: {}, + * dependencies: agentDependencies, + * modules: { + * anoncreds: new AnoncredsModule({ + * anoncreds, + * }) + * } + * }) + * ``` + * + * ## React Native + * + * ```ts + * import { anoncreds } from '@hyperledger/anoncreds-react-native' + * + * const agent = new Agent({ + * config: {}, + * dependencies: agentDependencies, + * modules: { + * anoncreds: new AnoncredsModule({ + * anoncreds, + * }) + * } + * }) + * ``` + */ + anoncreds: Anoncreds + + /** + * Create a default link secret if there are no created link secrets. + * @defaultValue true + */ + autoCreateLinkSecret?: boolean +} + +/** + * @public + */ +export class AnonCredsModuleConfig { + private options: AnonCredsModuleConfigOptions + + public constructor(options: AnonCredsModuleConfigOptions) { + this.options = options + } + + /** See {@link AnonCredsModuleConfigOptions.registries} */ + public get registries() { + return this.options.registries + } + + /** See {@link AnonCredsModuleConfigOptions.tailsFileService} */ + public get tailsFileService() { + return this.options.tailsFileService ?? new BasicTailsFileService() + } + + public get anoncreds() { + return this.options.anoncreds + } + + /** See {@link AnonCredsModuleConfigOptions.autoCreateLinkSecret} */ + public get autoCreateLinkSecret() { + return this.options.autoCreateLinkSecret ?? true + } +} diff --git a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts new file mode 100644 index 0000000000..c0be4691a5 --- /dev/null +++ b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts @@ -0,0 +1,71 @@ +import type { AnonCredsRegistry } from '../services' +import type { DependencyManager } from '@credo-ts/core' + +import { AnonCredsDataIntegrityServiceSymbol } from '@credo-ts/core' + +import { anoncreds } from '../../tests/helpers' +import { AnonCredsModule } from '../AnonCredsModule' +import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' +import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from '../anoncreds-rs' +import { AnonCredsDataIntegrityService } from '../anoncreds-rs/AnonCredsDataIntegrityService' +import { + AnonCredsSchemaRepository, + AnonCredsCredentialDefinitionRepository, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsLinkSecretRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRepository, +} from '../repository' +import { AnonCredsHolderServiceSymbol, AnonCredsIssuerServiceSymbol, AnonCredsVerifierServiceSymbol } from '../services' +import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' + +const dependencyManager = { + registerInstance: jest.fn(), + registerSingleton: jest.fn(), +} as unknown as DependencyManager + +const registry = {} as AnonCredsRegistry + +describe('AnonCredsModule', () => { + test('registers dependencies on the dependency manager', () => { + const anonCredsModule = new AnonCredsModule({ + registries: [registry], + anoncreds, + }) + anonCredsModule.register(dependencyManager) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(12) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsRegistryService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsSchemaRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsCredentialDefinitionRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsCredentialDefinitionPrivateRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsKeyCorrectnessProofRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsLinkSecretRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsRevocationRegistryDefinitionRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( + AnonCredsRevocationRegistryDefinitionPrivateRepository + ) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( + AnonCredsHolderServiceSymbol, + AnonCredsRsHolderService + ) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( + AnonCredsIssuerServiceSymbol, + AnonCredsRsIssuerService + ) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( + AnonCredsVerifierServiceSymbol, + AnonCredsRsVerifierService + ) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( + AnonCredsDataIntegrityServiceSymbol, + AnonCredsDataIntegrityService + ) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(AnonCredsModuleConfig, anonCredsModule.config) + }) +}) diff --git a/packages/anoncreds/src/__tests__/AnonCredsModuleConfig.test.ts b/packages/anoncreds/src/__tests__/AnonCredsModuleConfig.test.ts new file mode 100644 index 0000000000..04fa236497 --- /dev/null +++ b/packages/anoncreds/src/__tests__/AnonCredsModuleConfig.test.ts @@ -0,0 +1,17 @@ +import type { AnonCredsRegistry } from '../services' + +import { anoncreds } from '../../tests/helpers' +import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' + +describe('AnonCredsModuleConfig', () => { + test('sets values', () => { + const registry = {} as AnonCredsRegistry + + const config = new AnonCredsModuleConfig({ + registries: [registry], + anoncreds, + }) + + expect(config.registries).toEqual([registry]) + }) +}) diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsDataIntegrityService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsDataIntegrityService.ts new file mode 100644 index 0000000000..9894f41702 --- /dev/null +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsDataIntegrityService.ts @@ -0,0 +1,333 @@ +import type { AnonCredsRsVerifierService } from './AnonCredsRsVerifierService' +import type { AnonCredsProofRequest, AnonCredsRequestedPredicate } from '../models' +import type { CredentialWithRevocationMetadata } from '../models/utils' +import type { AnonCredsCredentialProve, CreateW3cPresentationOptions, AnonCredsHolderService } from '../services' +import type { + AgentContext, + IAnonCredsDataIntegrityService, + AnoncredsDataIntegrityVerifyPresentation, + DifPresentationExchangeDefinition, + DifPresentationExchangeSubmission, + W3cCredentialRecord, + W3cJsonLdVerifiableCredential, +} from '@credo-ts/core' +import type { Descriptor, FieldV2, InputDescriptorV1, InputDescriptorV2 } from '@sphereon/pex-models' + +import { JSONPath } from '@astronautlabs/jsonpath' +import { + CredoError, + Hasher, + JsonTransformer, + TypedArrayEncoder, + ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE, + deepEquality, + injectable, + ClaimFormat, +} from '@credo-ts/core' +import BigNumber from 'bn.js' + +import { AnonCredsHolderServiceSymbol, AnonCredsVerifierServiceSymbol } from '../services' +import { fetchCredentialDefinitions, fetchSchemas } from '../utils/anonCredsObjects' +import { assertLinkSecretsMatch } from '../utils/linkSecret' +import { getAnonCredsTagsFromRecord } from '../utils/w3cAnonCredsUtils' + +import { getW3cAnonCredsCredentialMetadata } from './utils' + +export type PathComponent = string | number + +@injectable() +export class AnonCredsDataIntegrityService implements IAnonCredsDataIntegrityService { + private getDataIntegrityProof(credential: W3cJsonLdVerifiableCredential) { + const cryptosuite = ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE + if (Array.isArray(credential.proof)) { + const proof = credential.proof.find( + (proof) => proof.type === 'DataIntegrityProof' && 'cryptosuite' in proof && proof.cryptosuite === cryptosuite + ) + if (!proof) throw new CredoError(`Could not find ${ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE} proof`) + return proof + } + + if ( + credential.proof.type !== 'DataIntegrityProof' || + !('cryptosuite' in credential.proof && credential.proof.cryptosuite === cryptosuite) + ) { + throw new CredoError(`Could not find ${ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE} proof`) + } + + return credential.proof + } + private extractPathNodes(obj: unknown, paths: string[]): { value: unknown; path: PathComponent[] }[] { + let result: { value: unknown; path: PathComponent[] }[] = [] + if (paths) { + for (const path of paths) { + result = JSONPath.nodes(obj, path) + if (result.length) break + } + } + return result + } + + private async getCredentialMetadataForDescriptor( + agentContext: AgentContext, + descriptorMapObject: Descriptor, + selectedCredentials: W3cJsonLdVerifiableCredential[] + ) { + const credentialExtractionResult = this.extractPathNodes({ verifiableCredential: selectedCredentials }, [ + descriptorMapObject.path, + ]) + + if (credentialExtractionResult.length === 0 || credentialExtractionResult.length > 1) { + throw new Error('Could not extract credential from presentation submission') + } + + const w3cJsonLdVerifiableCredential = credentialExtractionResult[0].value as W3cJsonLdVerifiableCredential + const w3cJsonLdVerifiableCredentialJson = JsonTransformer.toJSON(w3cJsonLdVerifiableCredential) + + const entryIndex = selectedCredentials.findIndex((credential) => + deepEquality(JsonTransformer.toJSON(credential), w3cJsonLdVerifiableCredentialJson) + ) + if (entryIndex === -1) throw new CredoError('Could not find selected credential') + + return { + entryIndex, + credential: selectedCredentials[entryIndex], + ...getW3cAnonCredsCredentialMetadata(w3cJsonLdVerifiableCredential), + } + } + + private descriptorRequiresRevocationStatus(descriptor: InputDescriptorV1 | InputDescriptorV2) { + const statuses = descriptor.constraints?.statuses + if (!statuses) return false + if ( + statuses?.active?.directive && + (statuses.active.directive === 'allowed' || statuses.active.directive === 'required') + ) { + return true + } else { + throw new CredoError('Unsupported status directive') + } + } + + private getPredicateTypeAndValues(predicateFilter: NonNullable) { + const predicates: { + predicateType: AnonCredsRequestedPredicate['p_type'] + predicateValue: AnonCredsRequestedPredicate['p_value'] + }[] = [] + + const supportedJsonSchemaNumericRangeProperties: Record = { + exclusiveMinimum: '>', + exclusiveMaximum: '<', + minimum: '>=', + maximum: '<=', + } + + for (const [key, value] of Object.entries(predicateFilter)) { + if (key === 'type') continue + + const predicateType = supportedJsonSchemaNumericRangeProperties[key] + if (!predicateType) throw new CredoError(`Unsupported predicate filter property '${key}'`) + predicates.push({ + predicateType, + predicateValue: value, + }) + } + + return predicates + } + + private getClaimNameForField(field: FieldV2) { + if (!field.path) throw new CredoError('Field path is required') + // fixme: could the path start otherwise? + const baseClaimPath = '$.credentialSubject.' + const claimPaths = field.path.filter((path) => path.startsWith(baseClaimPath)) + if (claimPaths.length === 0) return undefined + + // FIXME: we should iterate over all attributes of the schema here and check if the path is valid + // see https://identity.foundation/presentation-exchange/#presentation-definition + const claimNames = claimPaths.map((path) => path.slice(baseClaimPath.length)) + const propertyName = claimNames[0] + + return propertyName + } + + public createAnonCredsProofRequestAndMetadata = async ( + agentContext: AgentContext, + presentationDefinition: DifPresentationExchangeDefinition, + presentationSubmission: DifPresentationExchangeSubmission, + credentials: W3cJsonLdVerifiableCredential[], + challenge: string + ) => { + const credentialsProve: AnonCredsCredentialProve[] = [] + const schemaIds = new Set() + const credentialDefinitionIds = new Set() + const credentialsWithMetadata: CredentialWithRevocationMetadata[] = [] + + const hash = Hasher.hash(TypedArrayEncoder.fromString(challenge), 'sha-256') + const nonce = new BigNumber(hash).toString().slice(0, 20) + + const anonCredsProofRequest: AnonCredsProofRequest = { + version: '1.0', + name: presentationDefinition.name ?? 'Proof request', + nonce, + requested_attributes: {}, + requested_predicates: {}, + } + + const nonRevoked = Math.floor(Date.now() / 1000) + const nonRevokedInterval = { from: nonRevoked, to: nonRevoked } + + for (const descriptorMapObject of presentationSubmission.descriptor_map) { + const descriptor: InputDescriptorV1 | InputDescriptorV2 | undefined = ( + presentationDefinition.input_descriptors as InputDescriptorV2[] + ).find((descriptor) => descriptor.id === descriptorMapObject.id) + + if (!descriptor) { + throw new Error(`Descriptor with id ${descriptorMapObject.id} not found in presentation definition`) + } + + const referent = descriptorMapObject.id + const attributeReferent = `${referent}_attribute` + const predicateReferentBase = `${referent}_predicate` + let predicateReferentIndex = 0 + + const fields = descriptor.constraints?.fields + if (!fields) throw new CredoError('Unclear mapping of constraint with no fields.') + + const { entryIndex, schemaId, credentialDefinitionId, revocationRegistryId, credential } = + await this.getCredentialMetadataForDescriptor(agentContext, descriptorMapObject, credentials) + + schemaIds.add(schemaId) + credentialDefinitionIds.add(credentialDefinitionId) + + const requiresRevocationStatus = this.descriptorRequiresRevocationStatus(descriptor) + if (requiresRevocationStatus && !revocationRegistryId) { + throw new CredoError('Selected credentials must be revocable but are not') + } + + credentialsWithMetadata.push({ + credential, + nonRevoked: requiresRevocationStatus ? nonRevokedInterval : undefined, + }) + + for (const field of fields) { + const propertyName = this.getClaimNameForField(field) + if (!propertyName) continue + + if (field.predicate) { + if (!field.filter) throw new CredoError('Missing required predicate filter property.') + const predicateTypeAndValues = this.getPredicateTypeAndValues(field.filter) + for (const { predicateType, predicateValue } of predicateTypeAndValues) { + const predicateReferent = `${predicateReferentBase}_${predicateReferentIndex++}` + anonCredsProofRequest.requested_predicates[predicateReferent] = { + name: propertyName, + p_type: predicateType, + p_value: predicateValue, + restrictions: [{ cred_def_id: credentialDefinitionId }], + non_revoked: requiresRevocationStatus ? nonRevokedInterval : undefined, + } + + credentialsProve.push({ entryIndex, referent: predicateReferent, isPredicate: true, reveal: true }) + } + } else { + if (!anonCredsProofRequest.requested_attributes[attributeReferent]) { + anonCredsProofRequest.requested_attributes[attributeReferent] = { + names: [propertyName], + restrictions: [{ cred_def_id: credentialDefinitionId }], + non_revoked: requiresRevocationStatus ? nonRevokedInterval : undefined, + } + } else { + const names = anonCredsProofRequest.requested_attributes[attributeReferent].names ?? [] + anonCredsProofRequest.requested_attributes[attributeReferent].names = [...names, propertyName] + } + + credentialsProve.push({ entryIndex, referent: attributeReferent, isPredicate: false, reveal: true }) + } + } + } + + return { anonCredsProofRequest, credentialsWithMetadata, credentialsProve, schemaIds, credentialDefinitionIds } + } + + public async createPresentation( + agentContext: AgentContext, + options: { + presentationDefinition: DifPresentationExchangeDefinition + presentationSubmission: DifPresentationExchangeSubmission + selectedCredentialRecords: W3cCredentialRecord[] + challenge: string + } + ) { + const { presentationDefinition, presentationSubmission, selectedCredentialRecords, challenge } = options + + const linkSecrets = selectedCredentialRecords + .map((record) => getAnonCredsTagsFromRecord(record)?.anonCredsLinkSecretId) + .filter((linkSecretId): linkSecretId is string => linkSecretId !== undefined) + + const linkSecretId = assertLinkSecretsMatch(agentContext, linkSecrets) + + const { anonCredsProofRequest, credentialDefinitionIds, schemaIds, credentialsProve, credentialsWithMetadata } = + await this.createAnonCredsProofRequestAndMetadata( + agentContext, + presentationDefinition, + presentationSubmission, + selectedCredentialRecords.map((record) => record.credential) as W3cJsonLdVerifiableCredential[], + challenge + ) + + const createPresentationOptions: CreateW3cPresentationOptions = { + linkSecretId, + proofRequest: anonCredsProofRequest, + credentialsProve, + credentialsWithRevocationMetadata: credentialsWithMetadata, + schemas: await fetchSchemas(agentContext, schemaIds), + credentialDefinitions: await fetchCredentialDefinitions(agentContext, credentialDefinitionIds), + } + + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + const w3cPresentation = await anonCredsHolderService.createW3cPresentation(agentContext, createPresentationOptions) + return w3cPresentation + } + + public async verifyPresentation(agentContext: AgentContext, options: AnoncredsDataIntegrityVerifyPresentation) { + const { presentation, presentationDefinition, presentationSubmission, challenge } = options + + const credentialDefinitionIds = new Set() + + const verifiableCredentials = Array.isArray(presentation.verifiableCredential) + ? presentation.verifiableCredential + : [presentation.verifiableCredential] + + for (const verifiableCredential of verifiableCredentials) { + if (verifiableCredential.claimFormat === ClaimFormat.LdpVc) { + const proof = this.getDataIntegrityProof(verifiableCredential) + credentialDefinitionIds.add(proof.verificationMethod) + } else { + throw new CredoError('Unsupported credential type') + } + } + + const { anonCredsProofRequest, credentialsWithMetadata } = await this.createAnonCredsProofRequestAndMetadata( + agentContext, + presentationDefinition, + presentationSubmission, + verifiableCredentials as W3cJsonLdVerifiableCredential[], + challenge + ) + + const credentialDefinitions = await fetchCredentialDefinitions(agentContext, credentialDefinitionIds) + const schemaIds = new Set(Object.values(credentialDefinitions).map((cd) => cd.schemaId)) + const schemas = await fetchSchemas(agentContext, schemaIds) + + const anonCredsVerifierService = + agentContext.dependencyManager.resolve(AnonCredsVerifierServiceSymbol) + + return await anonCredsVerifierService.verifyW3cPresentation(agentContext, { + credentialsWithRevocationMetadata: credentialsWithMetadata, + presentation, + proofRequest: anonCredsProofRequest, + schemas, + credentialDefinitions, + }) + } +} diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts new file mode 100644 index 0000000000..d253f33267 --- /dev/null +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts @@ -0,0 +1,897 @@ +import type { + AnonCredsCredentialDefinition, + AnonCredsProof, + AnonCredsRequestedAttributeMatch, + AnonCredsRequestedPredicateMatch, + AnonCredsRevocationRegistryDefinition, + AnonCredsSchema, + AnonCredsCredentialRequest, + AnonCredsCredential, + AnonCredsCredentialInfo, + AnonCredsProofRequestRestriction, +} from '../models' +import type { CredentialWithRevocationMetadata } from '../models/utils' +import type { AnonCredsCredentialRecord } from '../repository' +import type { + GetCredentialsForProofRequestOptions, + GetCredentialsForProofRequestReturn, + AnonCredsHolderService, + CreateLinkSecretOptions, + CreateLinkSecretReturn, + CreateProofOptions, + CreateCredentialRequestOptions, + CreateCredentialRequestReturn, + GetCredentialOptions, + GetCredentialsOptions, + StoreCredentialOptions, +} from '../services' +import type { + AnonCredsCredentialProve, + CreateW3cPresentationOptions, + LegacyToW3cCredentialOptions, + W3cToLegacyCredentialOptions, +} from '../services/AnonCredsHolderServiceOptions' +import type { AnonCredsCredentialRequestMetadata, W3cAnonCredsCredentialMetadata } from '../utils/metadata' +import type { AgentContext, Query, SimpleQuery } from '@credo-ts/core' +import type { + CredentialEntry, + CredentialProve, + CredentialRequestMetadata, + JsonObject, + W3cCredentialEntry, +} from '@hyperledger/anoncreds-shared' + +import { + CredoError, + JsonTransformer, + W3cCredentialRecord, + TypedArrayEncoder, + W3cCredentialRepository, + W3cCredentialService, + W3cJsonLdVerifiableCredential, + injectable, + utils, + W3cJsonLdVerifiablePresentation, +} from '@credo-ts/core' +import { + Credential, + W3cPresentation as W3cAnonCredsPresentation, + W3cCredential as W3cAnonCredsCredential, + CredentialRequest, + CredentialRevocationState, + LinkSecret, + Presentation, + RevocationRegistryDefinition, + RevocationStatusList, + anoncreds, +} from '@hyperledger/anoncreds-shared' + +import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' +import { AnonCredsRsError } from '../error' +import { AnonCredsRestrictionWrapper } from '../models' +import { AnonCredsCredentialRepository, AnonCredsLinkSecretRepository } from '../repository' +import { AnonCredsRegistryService } from '../services' +import { storeLinkSecret, unqualifiedCredentialDefinitionIdRegex } from '../utils' +import { + isUnqualifiedCredentialDefinitionId, + isUnqualifiedIndyDid, + isUnqualifiedSchemaId, +} from '../utils/indyIdentifiers' +import { assertLinkSecretsMatch, getLinkSecret } from '../utils/linkSecret' +import { W3cAnonCredsCredentialMetadataKey } from '../utils/metadata' +import { proofRequestUsesUnqualifiedIdentifiers } from '../utils/proofRequest' +import { getAnoncredsCredentialInfoFromRecord, getW3cRecordAnonCredsTags } from '../utils/w3cAnonCredsUtils' + +import { getRevocationMetadata } from './utils' + +@injectable() +export class AnonCredsRsHolderService implements AnonCredsHolderService { + public async createLinkSecret( + agentContext: AgentContext, + options?: CreateLinkSecretOptions + ): Promise { + return { + linkSecretId: options?.linkSecretId ?? utils.uuid(), + linkSecretValue: LinkSecret.create(), + } + } + + public async createProof(agentContext: AgentContext, options: CreateProofOptions): Promise { + const { credentialDefinitions, proofRequest, selectedCredentials, schemas } = options + + let presentation: Presentation | undefined + try { + const rsCredentialDefinitions: Record = {} + for (const credDefId in credentialDefinitions) { + rsCredentialDefinitions[credDefId] = credentialDefinitions[credDefId] as unknown as JsonObject + } + + const rsSchemas: Record = {} + for (const schemaId in schemas) { + rsSchemas[schemaId] = schemas[schemaId] as unknown as JsonObject + } + + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const anoncredsCredentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) + + // Cache retrieved credentials in order to minimize storage calls + const retrievedCredentials = new Map() + + const credentialEntryFromAttribute = async ( + attribute: AnonCredsRequestedAttributeMatch | AnonCredsRequestedPredicateMatch + ): Promise<{ linkSecretId: string; credentialEntry: CredentialEntry; credentialId: string }> => { + let credentialRecord = retrievedCredentials.get(attribute.credentialId) + + if (!credentialRecord) { + const w3cCredentialRecord = await w3cCredentialRepository.findById(agentContext, attribute.credentialId) + + if (w3cCredentialRecord) { + credentialRecord = w3cCredentialRecord + retrievedCredentials.set(attribute.credentialId, w3cCredentialRecord) + } else { + credentialRecord = await anoncredsCredentialRepository.getByCredentialId( + agentContext, + attribute.credentialId + ) + + agentContext.config.logger.warn( + [ + `Creating AnonCreds proof with legacy credential ${attribute.credentialId}.`, + `Please run the migration script to migrate credentials to the new w3c format. See https://credo.js.org/guides/updating/versions/0.4-to-0.5 for information on how to migrate.`, + ].join('\n') + ) + } + } + + const { linkSecretId, revocationRegistryId, credentialRevocationId } = getAnoncredsCredentialInfoFromRecord( + credentialRecord, + proofRequestUsesUnqualifiedIdentifiers(proofRequest) + ) + + // TODO: Check if credential has a revocation registry id (check response from anoncreds-rs API, as it is + // sending back a mandatory string in Credential.revocationRegistryId) + const timestamp = attribute.timestamp + + let revocationState: CredentialRevocationState | undefined + let revocationRegistryDefinition: RevocationRegistryDefinition | undefined + try { + if (timestamp && credentialRevocationId && revocationRegistryId) { + if (!options.revocationRegistries[revocationRegistryId]) { + throw new AnonCredsRsError(`Revocation Registry ${revocationRegistryId} not found`) + } + + const { definition, revocationStatusLists, tailsFilePath } = + options.revocationRegistries[revocationRegistryId] + + // Extract revocation status list for the given timestamp + const revocationStatusList = revocationStatusLists[timestamp] + if (!revocationStatusList) { + throw new CredoError( + `Revocation status list for revocation registry ${revocationRegistryId} and timestamp ${timestamp} not found in revocation status lists. All revocation status lists must be present.` + ) + } + + revocationRegistryDefinition = RevocationRegistryDefinition.fromJson(definition as unknown as JsonObject) + revocationState = CredentialRevocationState.create({ + revocationRegistryIndex: Number(credentialRevocationId), + revocationRegistryDefinition, + tailsPath: tailsFilePath, + revocationStatusList: RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject), + }) + } + + const credential = + credentialRecord instanceof W3cCredentialRecord + ? await this.w3cToLegacyCredential(agentContext, { + credential: credentialRecord.credential as W3cJsonLdVerifiableCredential, + }) + : (credentialRecord.credential as AnonCredsCredential) + + return { + linkSecretId, + credentialId: attribute.credentialId, + credentialEntry: { + credential: credential as unknown as JsonObject, + revocationState: revocationState?.toJson(), + timestamp, + }, + } + } finally { + revocationState?.handle.clear() + revocationRegistryDefinition?.handle.clear() + } + } + + const credentialsProve: CredentialProve[] = [] + const credentials: { linkSecretId: string; credentialEntry: CredentialEntry; credentialId: string }[] = [] + + let entryIndex = 0 + for (const referent in selectedCredentials.attributes) { + const attribute = selectedCredentials.attributes[referent] + + // If the credentialId with the same timestamp is already present, we will use the existing entry, so that the proof is created + // showing the attributes come from the same cred, rather than different ones. + const existingCredentialIndex = credentials.findIndex( + (credential) => + credential.credentialId === attribute.credentialId && + attribute.timestamp === credential.credentialEntry.timestamp + ) + + if (existingCredentialIndex !== -1) { + credentialsProve.push({ + entryIndex: existingCredentialIndex, + isPredicate: false, + referent, + reveal: attribute.revealed, + }) + } else { + credentials.push(await credentialEntryFromAttribute(attribute)) + credentialsProve.push({ entryIndex, isPredicate: false, referent, reveal: attribute.revealed }) + entryIndex = entryIndex + 1 + } + } + + for (const referent in selectedCredentials.predicates) { + const predicate = selectedCredentials.predicates[referent] + + // If the credentialId with the same timestamp is already present, we will use the existing entry, so that the proof is created + // showing the attributes come from the same cred, rather than different ones. + const existingCredentialIndex = credentials.findIndex( + (credential) => + credential.credentialId === predicate.credentialId && + predicate.timestamp === credential.credentialEntry.timestamp + ) + + if (existingCredentialIndex !== -1) { + credentialsProve.push({ entryIndex: existingCredentialIndex, isPredicate: true, referent, reveal: true }) + } else { + credentials.push(await credentialEntryFromAttribute(predicate)) + credentialsProve.push({ entryIndex, isPredicate: true, referent, reveal: true }) + entryIndex = entryIndex + 1 + } + } + + const linkSecretIds = credentials.map((item) => item.linkSecretId) + const linkSecretId = assertLinkSecretsMatch(agentContext, linkSecretIds) + const linkSecret = await getLinkSecret(agentContext, linkSecretId) + + presentation = Presentation.create({ + credentialDefinitions: rsCredentialDefinitions, + schemas: rsSchemas, + presentationRequest: proofRequest as unknown as JsonObject, + credentials: credentials.map((entry) => entry.credentialEntry), + credentialsProve, + selfAttest: selectedCredentials.selfAttestedAttributes, + linkSecret, + }) + + return presentation.toJson() as unknown as AnonCredsProof + } finally { + presentation?.handle.clear() + } + } + + public async createCredentialRequest( + agentContext: AgentContext, + options: CreateCredentialRequestOptions + ): Promise { + const { useLegacyProverDid, credentialDefinition, credentialOffer } = options + let createReturnObj: + | { credentialRequest: CredentialRequest; credentialRequestMetadata: CredentialRequestMetadata } + | undefined + try { + const linkSecretRepository = agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository) + + // If a link secret is specified, use it. Otherwise, attempt to use default link secret + let linkSecretRecord = options.linkSecretId + ? await linkSecretRepository.getByLinkSecretId(agentContext, options.linkSecretId) + : await linkSecretRepository.findDefault(agentContext) + + // No default link secret. Automatically create one if set on module config + if (!linkSecretRecord) { + const moduleConfig = agentContext.dependencyManager.resolve(AnonCredsModuleConfig) + if (!moduleConfig.autoCreateLinkSecret) { + throw new AnonCredsRsError( + 'No link secret provided to createCredentialRequest and no default link secret has been found' + ) + } + const { linkSecretId, linkSecretValue } = await this.createLinkSecret(agentContext, {}) + linkSecretRecord = await storeLinkSecret(agentContext, { linkSecretId, linkSecretValue, setAsDefault: true }) + } + + if (!linkSecretRecord.value) { + throw new AnonCredsRsError('Link Secret value not stored') + } + + const isLegacyIdentifier = credentialOffer.cred_def_id.match(unqualifiedCredentialDefinitionIdRegex) + if (!isLegacyIdentifier && useLegacyProverDid) { + throw new CredoError('Cannot use legacy prover_did with non-legacy identifiers') + } + createReturnObj = CredentialRequest.create({ + entropy: !useLegacyProverDid || !isLegacyIdentifier ? anoncreds.generateNonce() : undefined, + proverDid: useLegacyProverDid + ? TypedArrayEncoder.toBase58(TypedArrayEncoder.fromString(anoncreds.generateNonce().slice(0, 16))) + : undefined, + credentialDefinition: credentialDefinition as unknown as JsonObject, + credentialOffer: credentialOffer as unknown as JsonObject, + linkSecret: linkSecretRecord.value, + linkSecretId: linkSecretRecord.linkSecretId, + }) + + return { + credentialRequest: createReturnObj.credentialRequest.toJson() as unknown as AnonCredsCredentialRequest, + credentialRequestMetadata: + createReturnObj.credentialRequestMetadata.toJson() as unknown as AnonCredsCredentialRequestMetadata, + } + } finally { + createReturnObj?.credentialRequest.handle.clear() + createReturnObj?.credentialRequestMetadata.handle.clear() + } + } + + public async w3cToLegacyCredential(agentContext: AgentContext, options: W3cToLegacyCredentialOptions) { + const credentialJson = JsonTransformer.toJSON(options.credential) + const w3cAnonCredsCredentialObj = W3cAnonCredsCredential.fromJson(credentialJson) + const w3cCredentialObj = w3cAnonCredsCredentialObj.toLegacy() + const legacyCredential = w3cCredentialObj.toJson() as unknown as AnonCredsCredential + return legacyCredential + } + + public async processW3cCredential( + agentContext: AgentContext, + credential: W3cJsonLdVerifiableCredential, + processOptions: { + credentialDefinition: AnonCredsCredentialDefinition + credentialRequestMetadata: AnonCredsCredentialRequestMetadata + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition | undefined + } + ) { + const { credentialRequestMetadata, revocationRegistryDefinition, credentialDefinition } = processOptions + + const processCredentialOptions = { + credentialRequestMetadata: credentialRequestMetadata as unknown as JsonObject, + linkSecret: await getLinkSecret(agentContext, credentialRequestMetadata.link_secret_name), + revocationRegistryDefinition: revocationRegistryDefinition as unknown as JsonObject, + credentialDefinition: credentialDefinition as unknown as JsonObject, + } + + const credentialJson = JsonTransformer.toJSON(credential) + const w3cAnonCredsCredential = W3cAnonCredsCredential.fromJson(credentialJson) + const processedW3cAnonCredsCredential = w3cAnonCredsCredential.process(processCredentialOptions) + + const processedW3cJsonLdVerifiableCredential = JsonTransformer.fromJSON( + processedW3cAnonCredsCredential.toJson(), + W3cJsonLdVerifiableCredential + ) + return processedW3cJsonLdVerifiableCredential + } + + public async legacyToW3cCredential(agentContext: AgentContext, options: LegacyToW3cCredentialOptions) { + const { credential, issuerId, processOptions } = options + let w3cCredential: W3cJsonLdVerifiableCredential + + let anonCredsCredential: Credential | undefined + let w3cCredentialObj: W3cAnonCredsCredential | undefined + try { + anonCredsCredential = Credential.fromJson(credential as unknown as JsonObject) + w3cCredentialObj = anonCredsCredential.toW3c({ issuerId, w3cVersion: '1.1' }) + + const w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON( + w3cCredentialObj.toJson(), + W3cJsonLdVerifiableCredential + ) + + w3cCredential = processOptions + ? await this.processW3cCredential(agentContext, w3cJsonLdVerifiableCredential, processOptions) + : w3cJsonLdVerifiableCredential + } finally { + anonCredsCredential?.handle?.clear() + w3cCredentialObj?.handle?.clear() + } + + return w3cCredential + } + + public async storeW3cCredential( + agentContext: AgentContext, + options: { + credential: W3cJsonLdVerifiableCredential + credentialDefinitionId: string + schema: AnonCredsSchema + credentialDefinition: AnonCredsCredentialDefinition + revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition + revocationRegistryId?: string + credentialRequestMetadata: AnonCredsCredentialRequestMetadata + } + ) { + const { + credential, + credentialRequestMetadata, + schema, + credentialDefinition, + credentialDefinitionId, + revocationRegistryId, + } = options + + const methodName = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, credential.issuerId).methodName + + // this thows an error if the link secret is not found + await getLinkSecret(agentContext, credentialRequestMetadata.link_secret_name) + + const { revocationRegistryIndex } = W3cAnonCredsCredential.fromJson(JsonTransformer.toJSON(credential)) + + if (Array.isArray(credential.credentialSubject)) { + throw new CredoError('Credential subject must be an object, not an array.') + } + + const anonCredsTags = getW3cRecordAnonCredsTags({ + credentialSubject: credential.credentialSubject, + issuerId: credential.issuerId, + schema, + schemaId: credentialDefinition.schemaId, + credentialDefinitionId, + revocationRegistryId, + credentialRevocationId: revocationRegistryIndex?.toString(), + linkSecretId: credentialRequestMetadata.link_secret_name, + methodName, + }) + + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const w3cCredentialRecord = await w3cCredentialService.storeCredential(agentContext, { credential }) + + const anonCredsCredentialMetadata: W3cAnonCredsCredentialMetadata = { + credentialRevocationId: anonCredsTags.anonCredsCredentialRevocationId, + linkSecretId: anonCredsTags.anonCredsLinkSecretId, + methodName: anonCredsTags.anonCredsMethodName, + } + + w3cCredentialRecord.setTags(anonCredsTags) + w3cCredentialRecord.metadata.set(W3cAnonCredsCredentialMetadataKey, anonCredsCredentialMetadata) + + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + await w3cCredentialRepository.update(agentContext, w3cCredentialRecord) + + return w3cCredentialRecord + } + + public async storeCredential(agentContext: AgentContext, options: StoreCredentialOptions): Promise { + const { + credential, + credentialDefinition, + credentialDefinitionId, + credentialRequestMetadata, + schema, + revocationRegistry, + } = options + + const w3cJsonLdCredential = + credential instanceof W3cJsonLdVerifiableCredential + ? credential + : await this.legacyToW3cCredential(agentContext, { + credential, + issuerId: credentialDefinition.issuerId, + processOptions: { + credentialRequestMetadata, + credentialDefinition, + revocationRegistryDefinition: revocationRegistry?.definition, + }, + }) + + const w3cCredentialRecord = await this.storeW3cCredential(agentContext, { + credentialRequestMetadata, + credential: w3cJsonLdCredential, + credentialDefinitionId, + schema, + credentialDefinition, + revocationRegistryDefinition: revocationRegistry?.definition, + revocationRegistryId: revocationRegistry?.id, + }) + + return w3cCredentialRecord.id + } + + public async getCredential( + agentContext: AgentContext, + options: GetCredentialOptions + ): Promise { + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const w3cCredentialRecord = await w3cCredentialRepository.findById(agentContext, options.id) + if (w3cCredentialRecord) { + return getAnoncredsCredentialInfoFromRecord(w3cCredentialRecord, options.useUnqualifiedIdentifiersIfPresent) + } + + const anonCredsCredentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) + const anonCredsCredentialRecord = await anonCredsCredentialRepository.getByCredentialId(agentContext, options.id) + + agentContext.config.logger.warn( + [ + `Querying legacy credential repository for credential with id ${options.id}.`, + `Please run the migration script to migrate credentials to the new w3c format.`, + ].join('\n') + ) + + return getAnoncredsCredentialInfoFromRecord(anonCredsCredentialRecord) + } + + private async getLegacyCredentials( + agentContext: AgentContext, + options: GetCredentialsOptions + ): Promise { + const credentialRecords = await agentContext.dependencyManager + .resolve(AnonCredsCredentialRepository) + .findByQuery(agentContext, { + credentialDefinitionId: options.credentialDefinitionId, + schemaId: options.schemaId, + issuerId: options.issuerId, + schemaName: options.schemaName, + schemaVersion: options.schemaVersion, + schemaIssuerId: options.schemaIssuerId, + methodName: options.methodName, + }) + + return credentialRecords.map((credentialRecord) => getAnoncredsCredentialInfoFromRecord(credentialRecord)) + } + + public async getCredentials( + agentContext: AgentContext, + options: GetCredentialsOptions + ): Promise { + const credentialRecords = await agentContext.dependencyManager + .resolve(W3cCredentialRepository) + .findByQuery(agentContext, { + issuerId: !options.issuerId || isUnqualifiedIndyDid(options.issuerId) ? undefined : options.issuerId, + anonCredsCredentialDefinitionId: + !options.credentialDefinitionId || isUnqualifiedCredentialDefinitionId(options.credentialDefinitionId) + ? undefined + : options.credentialDefinitionId, + anonCredsSchemaId: !options.schemaId || isUnqualifiedSchemaId(options.schemaId) ? undefined : options.schemaId, + anonCredsSchemaName: options.schemaName, + anonCredsSchemaVersion: options.schemaVersion, + anonCredsSchemaIssuerId: + !options.schemaIssuerId || isUnqualifiedIndyDid(options.schemaIssuerId) ? undefined : options.schemaIssuerId, + + anonCredsMethodName: options.methodName, + anonCredsUnqualifiedSchemaId: + options.schemaId && isUnqualifiedSchemaId(options.schemaId) ? options.schemaId : undefined, + anonCredsUnqualifiedIssuerId: + options.issuerId && isUnqualifiedIndyDid(options.issuerId) ? options.issuerId : undefined, + anonCredsUnqualifiedSchemaIssuerId: + options.schemaIssuerId && isUnqualifiedIndyDid(options.schemaIssuerId) ? options.schemaIssuerId : undefined, + anonCredsUnqualifiedCredentialDefinitionId: + options.credentialDefinitionId && isUnqualifiedCredentialDefinitionId(options.credentialDefinitionId) + ? options.credentialDefinitionId + : undefined, + }) + + const credentials = credentialRecords.map((credentialRecord) => + getAnoncredsCredentialInfoFromRecord(credentialRecord) + ) + const legacyCredentials = await this.getLegacyCredentials(agentContext, options) + + if (legacyCredentials.length > 0) { + agentContext.config.logger.warn( + `Queried credentials include legacy credentials. Please run the migration script to migrate credentials to the new w3c format.` + ) + } + return [...legacyCredentials, ...credentials] + } + + public async deleteCredential(agentContext: AgentContext, id: string): Promise { + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const w3cCredentialRecord = await w3cCredentialRepository.findById(agentContext, id) + + if (w3cCredentialRecord) { + await w3cCredentialRepository.delete(agentContext, w3cCredentialRecord) + return + } + + const anoncredsCredentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) + const anoncredsCredentialRecord = await anoncredsCredentialRepository.getByCredentialId(agentContext, id) + await anoncredsCredentialRepository.delete(agentContext, anoncredsCredentialRecord) + } + private async getLegacyCredentialsForProofRequest( + agentContext: AgentContext, + options: GetCredentialsForProofRequestOptions + ): Promise { + const proofRequest = options.proofRequest + const referent = options.attributeReferent + + const requestedAttribute = + proofRequest.requested_attributes[referent] ?? proofRequest.requested_predicates[referent] + + if (!requestedAttribute) { + throw new AnonCredsRsError(`Referent not found in proof request`) + } + + const $and = [] + + // Make sure the attribute(s) that are requested are present using the marker tag + const attributes = requestedAttribute.names ?? [requestedAttribute.name] + const attributeQuery: SimpleQuery = {} + for (const attribute of attributes) { + attributeQuery[`anonCredsAttr::${attribute}::marker`] = true + } + $and.push(attributeQuery) + + // Add query for proof request restrictions + if (requestedAttribute.restrictions) { + const restrictionQuery = this.queryLegacyFromRestrictions(requestedAttribute.restrictions) + $and.push(restrictionQuery) + } + + // Add extra query + // TODO: we're not really typing the extraQuery, and it will work differently based on the anoncreds implmentation + // We should make the allowed properties more strict + if (options.extraQuery) { + $and.push(options.extraQuery) + } + + const credentials = await agentContext.dependencyManager + .resolve(AnonCredsCredentialRepository) + .findByQuery(agentContext, { + $and, + }) + + return credentials.map((credentialRecord) => { + return { + credentialInfo: getAnoncredsCredentialInfoFromRecord(credentialRecord), + interval: proofRequest.non_revoked, + } + }) + } + + public async getCredentialsForProofRequest( + agentContext: AgentContext, + options: GetCredentialsForProofRequestOptions + ): Promise { + const proofRequest = options.proofRequest + const referent = options.attributeReferent + + const requestedAttribute = + proofRequest.requested_attributes[referent] ?? proofRequest.requested_predicates[referent] + + if (!requestedAttribute) { + throw new AnonCredsRsError(`Referent not found in proof request`) + } + + const $and = [] + + const useUnqualifiedIdentifiers = proofRequestUsesUnqualifiedIdentifiers(proofRequest) + + // Make sure the attribute(s) that are requested are present using the marker tag + const attributes = requestedAttribute.names ?? [requestedAttribute.name] + const attributeQuery: SimpleQuery = {} + for (const attribute of attributes) { + attributeQuery[`anonCredsAttr::${attribute}::marker`] = true + } + $and.push(attributeQuery) + + // Add query for proof request restrictions + if (requestedAttribute.restrictions) { + const restrictionQuery = this.queryFromRestrictions(requestedAttribute.restrictions) + $and.push(restrictionQuery) + } + + // Add extra query + // TODO: we're not really typing the extraQuery, and it will work differently based on the anoncreds implmentation + // We should make the allowed properties more strict + if (options.extraQuery) { + $and.push(options.extraQuery) + } + + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const credentials = await w3cCredentialRepository.findByQuery(agentContext, { $and }) + const legacyCredentialWithMetadata = await this.getLegacyCredentialsForProofRequest(agentContext, options) + + if (legacyCredentialWithMetadata.length > 0) { + agentContext.config.logger.warn( + [ + `Including legacy credentials in proof request.`, + `Please run the migration script to migrate credentials to the new w3c format.`, + ].join('\n') + ) + } + + const credentialWithMetadata = credentials.map((credentialRecord) => { + return { + credentialInfo: getAnoncredsCredentialInfoFromRecord(credentialRecord, useUnqualifiedIdentifiers), + interval: proofRequest.non_revoked, + } + }) + + return [...credentialWithMetadata, ...legacyCredentialWithMetadata] + } + + private queryFromRestrictions(restrictions: AnonCredsProofRequestRestriction[]) { + const query: Query[] = [] + + const { restrictions: parsedRestrictions } = JsonTransformer.fromJSON({ restrictions }, AnonCredsRestrictionWrapper) + + for (const restriction of parsedRestrictions) { + const queryElements: SimpleQuery = {} + + if (restriction.credentialDefinitionId) { + if (isUnqualifiedCredentialDefinitionId(restriction.credentialDefinitionId)) { + queryElements.anonCredsUnqualifiedCredentialDefinitionId = restriction.credentialDefinitionId + } else { + queryElements.anonCredsCredentialDefinitionId = restriction.credentialDefinitionId + } + } + + if (restriction.issuerId || restriction.issuerDid) { + const issuerId = (restriction.issuerId ?? restriction.issuerDid) as string + if (isUnqualifiedIndyDid(issuerId)) { + queryElements.anonCredsUnqualifiedIssuerId = issuerId + } else { + queryElements.issuerId = issuerId + } + } + + if (restriction.schemaId) { + if (isUnqualifiedSchemaId(restriction.schemaId)) { + queryElements.anonCredsUnqualifiedSchemaId = restriction.schemaId + } else { + queryElements.anonCredsSchemaId = restriction.schemaId + } + } + + if (restriction.schemaIssuerId || restriction.schemaIssuerDid) { + const schemaIssuerId = (restriction.schemaIssuerId ?? restriction.schemaIssuerDid) as string + if (isUnqualifiedIndyDid(schemaIssuerId)) { + queryElements.anonCredsUnqualifiedSchemaIssuerId = schemaIssuerId + } else { + queryElements.anonCredsSchemaIssuerId = schemaIssuerId + } + } + + if (restriction.schemaName) { + queryElements.anonCredsSchemaName = restriction.schemaName + } + + if (restriction.schemaVersion) { + queryElements.anonCredsSchemaVersion = restriction.schemaVersion + } + + for (const [attributeName, attributeValue] of Object.entries(restriction.attributeValues)) { + queryElements[`anonCredsAttr::${attributeName}::value`] = attributeValue + } + + for (const [attributeName, isAvailable] of Object.entries(restriction.attributeMarkers)) { + if (isAvailable) { + queryElements[`anonCredsAttr::${attributeName}::marker`] = isAvailable + } + } + + query.push(queryElements) + } + + return query.length === 1 ? query[0] : { $or: query } + } + + private queryLegacyFromRestrictions(restrictions: AnonCredsProofRequestRestriction[]) { + const query: Query[] = [] + + const { restrictions: parsedRestrictions } = JsonTransformer.fromJSON({ restrictions }, AnonCredsRestrictionWrapper) + + for (const restriction of parsedRestrictions) { + const queryElements: SimpleQuery = {} + const additionalQueryElements: SimpleQuery = {} + + if (restriction.credentialDefinitionId) { + queryElements.credentialDefinitionId = restriction.credentialDefinitionId + if (isUnqualifiedCredentialDefinitionId(restriction.credentialDefinitionId)) { + additionalQueryElements.credentialDefinitionId = restriction.credentialDefinitionId + } + } + + if (restriction.issuerId || restriction.issuerDid) { + const issuerId = (restriction.issuerId ?? restriction.issuerDid) as string + queryElements.issuerId = issuerId + if (isUnqualifiedIndyDid(issuerId)) { + additionalQueryElements.issuerId = issuerId + } + } + + if (restriction.schemaId) { + queryElements.schemaId = restriction.schemaId + if (isUnqualifiedSchemaId(restriction.schemaId)) { + additionalQueryElements.schemaId = restriction.schemaId + } + } + + if (restriction.schemaIssuerId || restriction.schemaIssuerDid) { + const issuerId = (restriction.schemaIssuerId ?? restriction.schemaIssuerDid) as string + queryElements.schemaIssuerId = issuerId + if (isUnqualifiedIndyDid(issuerId)) { + additionalQueryElements.schemaIssuerId = issuerId + } + } + + if (restriction.schemaName) { + queryElements.schemaName = restriction.schemaName + } + + if (restriction.schemaVersion) { + queryElements.schemaVersion = restriction.schemaVersion + } + + for (const [attributeName, attributeValue] of Object.entries(restriction.attributeValues)) { + queryElements[`attr::${attributeName}::value`] = attributeValue + } + + for (const [attributeName, isAvailable] of Object.entries(restriction.attributeMarkers)) { + if (isAvailable) { + queryElements[`attr::${attributeName}::marker`] = isAvailable + } + } + + query.push(queryElements) + if (Object.keys(additionalQueryElements).length > 0) { + query.push(additionalQueryElements) + } + } + + return query.length === 1 ? query[0] : { $or: query } + } + + private getPresentationMetadata = async ( + agentContext: AgentContext, + options: { + credentialsWithMetadata: CredentialWithRevocationMetadata[] + credentialsProve: AnonCredsCredentialProve[] + } + ) => { + const { credentialsWithMetadata, credentialsProve } = options + + const credentials: W3cCredentialEntry[] = await Promise.all( + credentialsWithMetadata.map(async ({ credential, nonRevoked }) => { + const credentialJson = JsonTransformer.toJSON(credential) + const { revocationRegistryIndex, revocationRegistryId, timestamp } = + W3cAnonCredsCredential.fromJson(credentialJson) + + if (!nonRevoked) return { credential: credentialJson, revocationState: undefined, timestamp: undefined } + + if (!revocationRegistryId || !revocationRegistryIndex) throw new CredoError('Missing revocation metadata') + + const { revocationState, updatedTimestamp } = await getRevocationMetadata(agentContext, { + nonRevokedInterval: nonRevoked, + timestamp, + revocationRegistryIndex, + revocationRegistryId, + }) + + return { credential: credentialJson, revocationState, timestamp: updatedTimestamp } + }) + ) + + return { credentialsProve, credentials } + } + + public async createW3cPresentation(agentContext: AgentContext, options: CreateW3cPresentationOptions) { + const { credentialsProve, credentials } = await this.getPresentationMetadata(agentContext, { + credentialsWithMetadata: options.credentialsWithRevocationMetadata, + credentialsProve: options.credentialsProve, + }) + + let w3cAnonCredsPresentation: W3cAnonCredsPresentation | undefined + let w3cPresentation: W3cJsonLdVerifiablePresentation + try { + w3cAnonCredsPresentation = W3cAnonCredsPresentation.create({ + credentials, + credentialsProve, + schemas: options.schemas as unknown as Record, + credentialDefinitions: options.credentialDefinitions as unknown as Record, + presentationRequest: options.proofRequest as unknown as JsonObject, + linkSecret: await getLinkSecret(agentContext, options.linkSecretId), + }) + const presentationJson = w3cAnonCredsPresentation.toJson() as unknown as JsonObject + w3cPresentation = JsonTransformer.fromJSON(presentationJson, W3cJsonLdVerifiablePresentation) + } finally { + w3cAnonCredsPresentation?.handle.clear() + } + + return w3cPresentation + } +} diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsIssuerService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsIssuerService.ts new file mode 100644 index 0000000000..9636c1c4ea --- /dev/null +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsIssuerService.ts @@ -0,0 +1,372 @@ +import type { + AnonCredsSchema, + AnonCredsCredentialDefinition, + AnonCredsRevocationRegistryDefinition, + AnonCredsRevocationStatusList, + AnonCredsCredentialOffer, + AnonCredsCredential, +} from '../models' +import type { + AnonCredsIssuerService, + CreateSchemaOptions, + CreateCredentialDefinitionOptions, + CreateCredentialDefinitionReturn, + CreateRevocationRegistryDefinitionOptions, + CreateRevocationRegistryDefinitionReturn, + CreateRevocationStatusListOptions, + UpdateRevocationStatusListOptions, + CreateCredentialOptions, + CreateCredentialReturn, + CreateCredentialOfferOptions, +} from '../services' +import type { AgentContext } from '@credo-ts/core' +import type { CredentialDefinitionPrivate, JsonObject, KeyCorrectnessProof } from '@hyperledger/anoncreds-shared' + +import { injectable, CredoError } from '@credo-ts/core' +import { + RevocationStatusList, + RevocationRegistryDefinitionPrivate, + RevocationRegistryDefinition, + CredentialRevocationConfig, + Credential, + CredentialDefinition, + CredentialOffer, + Schema, +} from '@hyperledger/anoncreds-shared' + +import { AnonCredsRsError } from '../error' +import { + AnonCredsCredentialDefinitionRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRepository, + AnonCredsRevocationRegistryState, +} from '../repository' +import { + isUnqualifiedCredentialDefinitionId, + parseIndySchemaId, + getUnqualifiedSchemaId, + parseIndyDid, +} from '../utils/indyIdentifiers' + +@injectable() +export class AnonCredsRsIssuerService implements AnonCredsIssuerService { + public async createSchema(agentContext: AgentContext, options: CreateSchemaOptions): Promise { + const { issuerId, name, version, attrNames: attributeNames } = options + + let schema: Schema | undefined + try { + const schema = Schema.create({ + issuerId, + name, + version, + attributeNames, + }) + + return schema.toJson() as unknown as AnonCredsSchema + } finally { + schema?.handle.clear() + } + } + + public async createCredentialDefinition( + agentContext: AgentContext, + options: CreateCredentialDefinitionOptions + ): Promise { + const { tag, supportRevocation, schema, issuerId, schemaId } = options + + let createReturnObj: + | { + credentialDefinition: CredentialDefinition + credentialDefinitionPrivate: CredentialDefinitionPrivate + keyCorrectnessProof: KeyCorrectnessProof + } + | undefined + try { + createReturnObj = CredentialDefinition.create({ + schema: schema as unknown as JsonObject, + issuerId, + schemaId, + tag, + supportRevocation, + signatureType: 'CL', + }) + + return { + credentialDefinition: createReturnObj.credentialDefinition.toJson() as unknown as AnonCredsCredentialDefinition, + credentialDefinitionPrivate: createReturnObj.credentialDefinitionPrivate.toJson(), + keyCorrectnessProof: createReturnObj.keyCorrectnessProof.toJson(), + } + } finally { + createReturnObj?.credentialDefinition.handle.clear() + createReturnObj?.credentialDefinitionPrivate.handle.clear() + createReturnObj?.keyCorrectnessProof.handle.clear() + } + } + + public async createRevocationRegistryDefinition( + agentContext: AgentContext, + options: CreateRevocationRegistryDefinitionOptions + ): Promise { + const { tag, issuerId, credentialDefinition, credentialDefinitionId, maximumCredentialNumber, tailsDirectoryPath } = + options + + let createReturnObj: + | { + revocationRegistryDefinition: RevocationRegistryDefinition + revocationRegistryDefinitionPrivate: RevocationRegistryDefinitionPrivate + } + | undefined + try { + createReturnObj = RevocationRegistryDefinition.create({ + credentialDefinition: credentialDefinition as unknown as JsonObject, + credentialDefinitionId, + issuerId, + maximumCredentialNumber, + revocationRegistryType: 'CL_ACCUM', + tag, + tailsDirectoryPath, + }) + + return { + revocationRegistryDefinition: + createReturnObj.revocationRegistryDefinition.toJson() as unknown as AnonCredsRevocationRegistryDefinition, + revocationRegistryDefinitionPrivate: createReturnObj.revocationRegistryDefinitionPrivate.toJson(), + } + } finally { + createReturnObj?.revocationRegistryDefinition.handle.clear() + createReturnObj?.revocationRegistryDefinitionPrivate.handle.clear() + } + } + + public async createRevocationStatusList( + agentContext: AgentContext, + options: CreateRevocationStatusListOptions + ): Promise { + const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition } = options + + const credentialDefinitionRecord = await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, revocationRegistryDefinition.credDefId) + + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) + + let revocationStatusList: RevocationStatusList | undefined + try { + revocationStatusList = RevocationStatusList.create({ + issuanceByDefault: true, + revocationRegistryDefinitionId, + credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject, + revocationRegistryDefinition: revocationRegistryDefinition as unknown as JsonObject, + revocationRegistryDefinitionPrivate: revocationRegistryDefinitionPrivateRecord.value as unknown as JsonObject, + issuerId, + }) + + return revocationStatusList.toJson() as unknown as AnonCredsRevocationStatusList + } finally { + revocationStatusList?.handle.clear() + } + } + + public async updateRevocationStatusList( + agentContext: AgentContext, + options: UpdateRevocationStatusListOptions + ): Promise { + const { revocationStatusList, revocationRegistryDefinition, issued, revoked, timestamp, tailsFilePath } = options + + let updatedRevocationStatusList: RevocationStatusList | undefined + let revocationRegistryDefinitionObj: RevocationRegistryDefinition | undefined + + try { + updatedRevocationStatusList = RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject) + + if (timestamp && !issued && !revoked) { + updatedRevocationStatusList.updateTimestamp({ + timestamp, + }) + } else { + const credentialDefinitionRecord = await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, revocationRegistryDefinition.credDefId) + + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationStatusList.revRegDefId) + + revocationRegistryDefinitionObj = RevocationRegistryDefinition.fromJson({ + ...revocationRegistryDefinition, + value: { ...revocationRegistryDefinition.value, tailsLocation: tailsFilePath }, + } as unknown as JsonObject) + updatedRevocationStatusList.update({ + credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject, + revocationRegistryDefinition: revocationRegistryDefinitionObj, + revocationRegistryDefinitionPrivate: revocationRegistryDefinitionPrivateRecord.value, + issued: options.issued, + revoked: options.revoked, + timestamp: timestamp ?? -1, // FIXME: this should be fixed in anoncreds-rs wrapper + }) + } + + return updatedRevocationStatusList.toJson() as unknown as AnonCredsRevocationStatusList + } finally { + updatedRevocationStatusList?.handle.clear() + revocationRegistryDefinitionObj?.handle.clear() + } + } + + public async createCredentialOffer( + agentContext: AgentContext, + options: CreateCredentialOfferOptions + ): Promise { + const { credentialDefinitionId } = options + + let credentialOffer: CredentialOffer | undefined + try { + // The getByCredentialDefinitionId supports both qualified and unqualified identifiers, even though the + // record is always stored using the qualified identifier. + const credentialDefinitionRecord = await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, options.credentialDefinitionId) + + // We fetch the keyCorrectnessProof based on the credential definition record id, as the + // credential definition id passed to this module could be unqualified, and the key correctness + // proof is only stored using the qualified identifier. + const keyCorrectnessProofRecord = await agentContext.dependencyManager + .resolve(AnonCredsKeyCorrectnessProofRepository) + .getByCredentialDefinitionId(agentContext, credentialDefinitionRecord.credentialDefinitionId) + + if (!credentialDefinitionRecord) { + throw new AnonCredsRsError(`Credential Definition ${credentialDefinitionId} not found`) + } + + let schemaId = credentialDefinitionRecord.credentialDefinition.schemaId + + // if the credentialDefinitionId is not qualified, we need to transform the schemaId to also be unqualified + if (isUnqualifiedCredentialDefinitionId(options.credentialDefinitionId)) { + const { namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(schemaId) + schemaId = getUnqualifiedSchemaId(namespaceIdentifier, schemaName, schemaVersion) + } + + credentialOffer = CredentialOffer.create({ + credentialDefinitionId, + keyCorrectnessProof: keyCorrectnessProofRecord?.value, + schemaId, + }) + + return credentialOffer.toJson() as unknown as AnonCredsCredentialOffer + } finally { + credentialOffer?.handle.clear() + } + } + + public async createCredential( + agentContext: AgentContext, + options: CreateCredentialOptions + ): Promise { + const { + credentialOffer, + credentialRequest, + credentialValues, + revocationRegistryDefinitionId, + revocationStatusList, + revocationRegistryIndex, + } = options + + const definedRevocationOptions = [ + revocationRegistryDefinitionId, + revocationStatusList, + revocationRegistryIndex, + ].filter((e) => e !== undefined) + if (definedRevocationOptions.length > 0 && definedRevocationOptions.length < 3) { + throw new CredoError( + 'Revocation requires all of revocationRegistryDefinitionId, revocationRegistryIndex and revocationStatusList' + ) + } + + let credential: Credential | undefined + try { + const attributeRawValues: Record = {} + const attributeEncodedValues: Record = {} + + Object.keys(credentialValues).forEach((key) => { + attributeRawValues[key] = credentialValues[key].raw + attributeEncodedValues[key] = credentialValues[key].encoded + }) + + const credentialDefinitionRecord = await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, options.credentialRequest.cred_def_id) + + // We fetch the private record based on the cred def id from the cred def record, as the + // credential definition id passed to this module could be unqualified, and the private record + // is only stored using the qualified identifier. + const credentialDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionPrivateRepository) + .getByCredentialDefinitionId(agentContext, credentialDefinitionRecord.credentialDefinitionId) + + let credentialDefinition = credentialDefinitionRecord.credentialDefinition + + if (isUnqualifiedCredentialDefinitionId(options.credentialRequest.cred_def_id)) { + const { namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(credentialDefinition.schemaId) + const { namespaceIdentifier: unqualifiedDid } = parseIndyDid(credentialDefinition.issuerId) + credentialDefinition = { + ...credentialDefinition, + schemaId: getUnqualifiedSchemaId(namespaceIdentifier, schemaName, schemaVersion), + issuerId: unqualifiedDid, + } + } + + let revocationConfiguration: CredentialRevocationConfig | undefined + if (revocationRegistryDefinitionId && revocationStatusList && revocationRegistryIndex) { + const revocationRegistryDefinitionRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) + + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) + + if ( + revocationRegistryIndex >= revocationRegistryDefinitionRecord.revocationRegistryDefinition.value.maxCredNum + ) { + revocationRegistryDefinitionPrivateRecord.state = AnonCredsRevocationRegistryState.Full + } + + revocationConfiguration = new CredentialRevocationConfig({ + registryDefinition: RevocationRegistryDefinition.fromJson( + revocationRegistryDefinitionRecord.revocationRegistryDefinition as unknown as JsonObject + ), + registryDefinitionPrivate: RevocationRegistryDefinitionPrivate.fromJson( + revocationRegistryDefinitionPrivateRecord.value + ), + statusList: RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject), + registryIndex: revocationRegistryIndex, + }) + } + credential = Credential.create({ + credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject, + credentialOffer: credentialOffer as unknown as JsonObject, + credentialRequest: credentialRequest as unknown as JsonObject, + revocationRegistryId: revocationRegistryDefinitionId, + attributeEncodedValues, + attributeRawValues, + credentialDefinitionPrivate: credentialDefinitionPrivateRecord.value, + revocationConfiguration, + // FIXME: duplicated input parameter? + revocationStatusList: revocationStatusList + ? RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject) + : undefined, + }) + + return { + credential: credential.toJson() as unknown as AnonCredsCredential, + credentialRevocationId: credential.revocationRegistryIndex?.toString(), + } + } finally { + credential?.handle.clear() + } + } +} diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts new file mode 100644 index 0000000000..56c2293ea5 --- /dev/null +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts @@ -0,0 +1,206 @@ +import type { AnonCredsProof, AnonCredsProofRequest, AnonCredsNonRevokedInterval } from '../models' +import type { CredentialWithRevocationMetadata } from '../models/utils' +import type { AnonCredsVerifierService, VerifyProofOptions, VerifyW3cPresentationOptions } from '../services' +import type { AgentContext } from '@credo-ts/core' +import type { + JsonObject, + NonRevokedIntervalOverride, + RevocationRegistryDefinition, + VerifyW3cPresentationOptions as VerifyAnonCredsW3cPresentationOptions, +} from '@hyperledger/anoncreds-shared' + +import { JsonTransformer, injectable } from '@credo-ts/core' +import { Presentation, W3cPresentation, W3cCredential as AnonCredsW3cCredential } from '@hyperledger/anoncreds-shared' + +import { fetchRevocationStatusList } from '../utils' + +import { getRevocationMetadata } from './utils' + +@injectable() +export class AnonCredsRsVerifierService implements AnonCredsVerifierService { + public async verifyProof(agentContext: AgentContext, options: VerifyProofOptions): Promise { + const { credentialDefinitions, proof, proofRequest, revocationRegistries, schemas } = options + + let presentation: Presentation | undefined + try { + // Check that provided timestamps correspond to the active ones from the VDR. If they are and differ from the originally + // requested ones, create overrides for anoncreds-rs to consider them valid + const { verified, nonRevokedIntervalOverrides } = await this.verifyTimestamps(agentContext, proof, proofRequest) + + // No need to call anoncreds-rs as we already know that the proof will not be valid + if (!verified) { + agentContext.config.logger.debug('Invalid timestamps for provided identifiers') + return false + } + + presentation = Presentation.fromJson(proof as unknown as JsonObject) + + const rsCredentialDefinitions: Record = {} + for (const credDefId in credentialDefinitions) { + rsCredentialDefinitions[credDefId] = credentialDefinitions[credDefId] as unknown as JsonObject + } + + const rsSchemas: Record = {} + for (const schemaId in schemas) { + rsSchemas[schemaId] = schemas[schemaId] as unknown as JsonObject + } + + const revocationRegistryDefinitions: Record = {} + const lists: JsonObject[] = [] + + for (const revocationRegistryDefinitionId in revocationRegistries) { + const { definition, revocationStatusLists } = options.revocationRegistries[revocationRegistryDefinitionId] + + revocationRegistryDefinitions[revocationRegistryDefinitionId] = definition as unknown as JsonObject + + lists.push(...(Object.values(revocationStatusLists) as unknown as Array)) + } + + return presentation.verify({ + presentationRequest: proofRequest as unknown as JsonObject, + credentialDefinitions: rsCredentialDefinitions, + schemas: rsSchemas, + revocationRegistryDefinitions, + revocationStatusLists: lists, + nonRevokedIntervalOverrides, + }) + } finally { + presentation?.handle.clear() + } + } + + private async verifyTimestamps( + agentContext: AgentContext, + proof: AnonCredsProof, + proofRequest: AnonCredsProofRequest + ): Promise<{ verified: boolean; nonRevokedIntervalOverrides?: NonRevokedIntervalOverride[] }> { + const nonRevokedIntervalOverrides: NonRevokedIntervalOverride[] = [] + + // Override expected timestamps if the requested ones don't exacly match the values from VDR + const globalNonRevokedInterval = proofRequest.non_revoked + + const requestedNonRevokedRestrictions: { + nonRevokedInterval: AnonCredsNonRevokedInterval + schemaId?: string + credentialDefinitionId?: string + revocationRegistryDefinitionId?: string + }[] = [] + + for (const value of [ + ...Object.values(proofRequest.requested_attributes), + ...Object.values(proofRequest.requested_predicates), + ]) { + const nonRevokedInterval = value.non_revoked ?? globalNonRevokedInterval + if (nonRevokedInterval) { + value.restrictions?.forEach((restriction) => + requestedNonRevokedRestrictions.push({ + nonRevokedInterval, + schemaId: restriction.schema_id, + credentialDefinitionId: restriction.cred_def_id, + revocationRegistryDefinitionId: restriction.rev_reg_id, + }) + ) + } + } + + for (const identifier of proof.identifiers) { + if (!identifier.timestamp || !identifier.rev_reg_id) { + continue + } + const relatedNonRevokedRestrictionItem = requestedNonRevokedRestrictions.find( + (item) => + item.revocationRegistryDefinitionId === item.revocationRegistryDefinitionId || + item.credentialDefinitionId === identifier.cred_def_id || + item.schemaId === item.schemaId + ) + + const requestedFrom = relatedNonRevokedRestrictionItem?.nonRevokedInterval.from + if (requestedFrom && requestedFrom > identifier.timestamp) { + // Check VDR if the active revocation status list at requestedFrom was the one from provided timestamp. + // If it matches, add to the override list + const { revocationStatusList } = await fetchRevocationStatusList( + agentContext, + identifier.rev_reg_id, + requestedFrom + ) + + const vdrTimestamp = revocationStatusList?.timestamp + if (vdrTimestamp && vdrTimestamp === identifier.timestamp) { + nonRevokedIntervalOverrides.push({ + overrideRevocationStatusListTimestamp: identifier.timestamp, + requestedFromTimestamp: requestedFrom, + revocationRegistryDefinitionId: identifier.rev_reg_id, + }) + } else { + agentContext.config.logger.debug( + `VDR timestamp for ${requestedFrom} does not correspond to the one provided in proof identifiers. Expected: ${identifier.timestamp} and received ${vdrTimestamp}` + ) + return { verified: false } + } + } + } + + return { + verified: true, + nonRevokedIntervalOverrides: nonRevokedIntervalOverrides.length ? nonRevokedIntervalOverrides : undefined, + } + } + + private getRevocationMetadataForCredentials = async ( + agentContext: AgentContext, + credentialsWithMetadata: CredentialWithRevocationMetadata[] + ) => { + const revocationMetadataFetchPromises = credentialsWithMetadata + .filter((cwm) => cwm.nonRevoked) + .map(async (credentialWithMetadata) => { + const w3cJsonLdVerifiableCredential = JsonTransformer.toJSON(credentialWithMetadata.credential) + const { revocationRegistryIndex, revocationRegistryId, timestamp } = + AnonCredsW3cCredential.fromJson(w3cJsonLdVerifiableCredential) + + return await getRevocationMetadata(agentContext, { + nonRevokedInterval: credentialWithMetadata.nonRevoked as AnonCredsNonRevokedInterval, + timestamp: timestamp, + revocationRegistryId, + revocationRegistryIndex, + }) + }) + + return await Promise.all(revocationMetadataFetchPromises) + } + + public async verifyW3cPresentation(agentContext: AgentContext, options: VerifyW3cPresentationOptions) { + const revocationMetadata = await this.getRevocationMetadataForCredentials( + agentContext, + options.credentialsWithRevocationMetadata + ) + + const revocationRegistryDefinitions: Record = {} + revocationMetadata.forEach( + (rm) => (revocationRegistryDefinitions[rm.revocationRegistryId] = rm.revocationRegistryDefinition) + ) + + const verificationOptions: VerifyAnonCredsW3cPresentationOptions = { + presentationRequest: options.proofRequest as unknown as JsonObject, + schemas: options.schemas as unknown as Record, + credentialDefinitions: options.credentialDefinitions as unknown as Record, + revocationRegistryDefinitions, + revocationStatusLists: revocationMetadata.map((rm) => rm.revocationStatusList), + nonRevokedIntervalOverrides: revocationMetadata + .filter((rm) => rm.nonRevokedIntervalOverride) + .map((rm) => rm.nonRevokedIntervalOverride as NonRevokedIntervalOverride), + } + + let result = false + const presentationJson = JsonTransformer.toJSON(options.presentation) + if ('presentation_submission' in presentationJson) delete presentationJson.presentation_submission + + let w3cPresentation: W3cPresentation | undefined + try { + w3cPresentation = W3cPresentation.fromJson(presentationJson) + result = w3cPresentation.verify(verificationOptions) + } finally { + w3cPresentation?.handle.clear() + } + return result + } +} diff --git a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts new file mode 100644 index 0000000000..9b7fb040cc --- /dev/null +++ b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts @@ -0,0 +1,653 @@ +import type { W3cAnonCredsCredentialMetadata } from '../../utils/metadata' +import type { AnonCredsCredentialTags } from '../../utils/w3cAnonCredsUtils' +import type { + AnonCredsCredentialDefinition, + AnonCredsProofRequest, + AnonCredsRevocationStatusList, + AnonCredsSchema, + AnonCredsSelectedCredentials, +} from '@credo-ts/anoncreds' +import type { DidRepository } from '@credo-ts/core' +import type { JsonObject } from '@hyperledger/anoncreds-shared' + +import { + DidResolverService, + DidsModuleConfig, + InjectionSymbols, + SignatureSuiteToken, + W3cCredentialRecord, + W3cCredentialRepository, + W3cCredentialSubject, + W3cCredentialsModuleConfig, + W3cJsonLdVerifiableCredential, +} from '@credo-ts/core' +import { anoncreds } from '@hyperledger/anoncreds-nodejs' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' +import { AnonCredsCredentialDefinitionRepository } from '../../../../anoncreds/src/repository/AnonCredsCredentialDefinitionRepository' +import { AnonCredsLinkSecretRepository } from '../../../../anoncreds/src/repository/AnonCredsLinkSecretRepository' +import { InMemoryAnonCredsRegistry } from '../../../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { testLogger } from '../../../../core/tests' +import { agentDependencies, getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { W3cAnonCredsCredentialMetadataKey } from '../../utils/metadata' +import { AnonCredsRsHolderService } from '../AnonCredsRsHolderService' + +import { InMemoryWallet } from './../../../../../tests/InMemoryWallet' +import { + createCredentialDefinition, + createCredentialForHolder, + createCredentialOffer, + createLinkSecret, + storeCredential, +} from './helpers' + +import { + AnonCredsCredentialRepository, + AnonCredsModuleConfig, + AnonCredsHolderServiceSymbol, + AnonCredsLinkSecretRecord, +} from '@credo-ts/anoncreds' + +const agentConfig = getAgentConfig('AnonCredsRsHolderServiceTest') +const anonCredsHolderService = new AnonCredsRsHolderService() + +jest.mock('../../../../anoncreds/src/repository/AnonCredsCredentialDefinitionRepository') +const CredentialDefinitionRepositoryMock = + AnonCredsCredentialDefinitionRepository as jest.Mock +const credentialDefinitionRepositoryMock = new CredentialDefinitionRepositoryMock() + +jest.mock('../../../../anoncreds/src/repository/AnonCredsLinkSecretRepository') +const AnonCredsLinkSecretRepositoryMock = AnonCredsLinkSecretRepository as jest.Mock +const anoncredsLinkSecretRepositoryMock = new AnonCredsLinkSecretRepositoryMock() + +jest.mock('../../../../core/src/modules/vc/repository/W3cCredentialRepository') +const W3cCredentialRepositoryMock = W3cCredentialRepository as jest.Mock +const w3cCredentialRepositoryMock = new W3cCredentialRepositoryMock() + +jest.mock('../../../../anoncreds/src/repository/AnonCredsCredentialRepository') +const AnonCredsCredentialRepositoryMock = AnonCredsCredentialRepository as jest.Mock +const anoncredsCredentialRepositoryMock = new AnonCredsCredentialRepositoryMock() + +const inMemoryStorageService = new InMemoryStorageService() + +const wallet = new InMemoryWallet() + +const agentContext = getAgentContext({ + registerInstances: [ + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.StorageService, inMemoryStorageService], + [InjectionSymbols.Stop$, new Subject()], + [AnonCredsCredentialDefinitionRepository, credentialDefinitionRepositoryMock], + [AnonCredsLinkSecretRepository, anoncredsLinkSecretRepositoryMock], + [W3cCredentialRepository, w3cCredentialRepositoryMock], + [AnonCredsCredentialRepository, anoncredsCredentialRepositoryMock], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [ + AnonCredsModuleConfig, + new AnonCredsModuleConfig({ + registries: [new InMemoryAnonCredsRegistry({})], + anoncreds, + }), + ], + [InjectionSymbols.Logger, testLogger], + [DidResolverService, new DidResolverService(testLogger, new DidsModuleConfig(), {} as unknown as DidRepository)], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [SignatureSuiteToken, 'default'], + ], + agentConfig, + wallet, +}) + +describe('AnonCredsRsHolderService', () => { + const getByCredentialIdMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'getByCredentialId') + const findByIdMock = jest.spyOn(w3cCredentialRepositoryMock, 'findById') + const findByQueryMock = jest.spyOn(w3cCredentialRepositoryMock, 'findByQuery') + + beforeEach(() => { + findByIdMock.mockClear() + getByCredentialIdMock.mockClear() + findByQueryMock.mockClear() + }) + + test('createCredentialRequest', async () => { + mockFunction(anoncredsLinkSecretRepositoryMock.getByLinkSecretId).mockResolvedValue( + new AnonCredsLinkSecretRecord({ linkSecretId: 'linkSecretId', value: createLinkSecret() }) + ) + + const { credentialDefinition, keyCorrectnessProof } = createCredentialDefinition({ + attributeNames: ['phoneNumber'], + issuerId: 'issuer:uri', + }) + const credentialOffer = createCredentialOffer(keyCorrectnessProof) + + const { credentialRequest } = await anonCredsHolderService.createCredentialRequest(agentContext, { + credentialDefinition, + credentialOffer, + linkSecretId: 'linkSecretId', + }) + + expect(credentialRequest.cred_def_id).toBe('creddef:uri') + expect(credentialRequest.prover_did).toBeUndefined() + }) + + test('createLinkSecret', async () => { + let linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { + linkSecretId: 'linkSecretId', + }) + + expect(linkSecret.linkSecretId).toBe('linkSecretId') + expect(linkSecret.linkSecretValue).toBeDefined() + + linkSecret = await anonCredsHolderService.createLinkSecret(agentContext) + + expect(linkSecret.linkSecretId).toBeDefined() + expect(linkSecret.linkSecretValue).toBeDefined() + }) + + test('createProof', async () => { + const proofRequest: AnonCredsProofRequest = { + nonce: anoncreds.generateNonce(), + name: 'pres_req_1', + version: '0.1', + requested_attributes: { + attr1_referent: { + name: 'name', + restrictions: [{ issuer_did: 'issuer:uri' }], + }, + attr2_referent: { + name: 'phoneNumber', + }, + attr4_referent: { + names: ['name', 'height'], + }, + attr5_referent: { + name: 'favouriteSport', + }, + }, + requested_predicates: { + predicate1_referent: { name: 'age', p_type: '>=' as const, p_value: 18 }, + }, + //non_revoked: { from: 10, to: 200 }, + } + + const { + schema: personSchema, + credentialDefinition: personCredentialDefinition, + credentialDefinitionPrivate: personCredentialDefinitionPrivate, + keyCorrectnessProof: personKeyCorrectnessProof, + } = createCredentialDefinition({ + attributeNames: ['name', 'age', 'sex', 'height'], + issuerId: 'issuer:uri', + }) + + const { + schema: phoneSchema, + credentialDefinition: phoneCredentialDefinition, + credentialDefinitionPrivate: phoneCredentialDefinitionPrivate, + keyCorrectnessProof: phoneKeyCorrectnessProof, + } = createCredentialDefinition({ + attributeNames: ['phoneNumber'], + issuerId: 'issuer:uri', + }) + + const linkSecret = createLinkSecret() + + mockFunction(anoncredsLinkSecretRepositoryMock.getByLinkSecretId).mockResolvedValue( + new AnonCredsLinkSecretRecord({ linkSecretId: 'linkSecretId', value: linkSecret }) + ) + + const { + credential: personCredential, + credentialInfo: personCredentialInfo, + revocationRegistryDefinition: personRevRegDef, + tailsPath: personTailsPath, + } = await createCredentialForHolder({ + agentContext, + attributes: { + name: 'John', + sex: 'M', + height: '179', + age: '19', + }, + credentialDefinition: personCredentialDefinition as unknown as JsonObject, + schemaId: 'personschema:uri', + credentialDefinitionId: 'personcreddef:uri', + credentialDefinitionPrivate: personCredentialDefinitionPrivate, + keyCorrectnessProof: personKeyCorrectnessProof, + linkSecret, + linkSecretId: 'linkSecretId', + revocationRegistryDefinitionId: 'personrevregid:uri', + }) + const personRecord = await storeCredential(agentContext, personCredential, { + credentialDefinitionId: 'personcreddef:uri', + schemaId: 'personschema:uri', + schema: personSchema as unknown as AnonCredsSchema, + linkSecretId: 'linkSecretId', + }) + + const { + credential: phoneCredential, + credentialInfo: phoneCredentialInfo, + revocationRegistryDefinition: phoneRevRegDef, + tailsPath: phoneTailsPath, + } = await createCredentialForHolder({ + agentContext, + attributes: { + phoneNumber: 'linkSecretId56', + }, + credentialDefinition: phoneCredentialDefinition as unknown as JsonObject, + schemaId: 'phoneschema:uri', + credentialDefinitionId: 'phonecreddef:uri', + credentialDefinitionPrivate: phoneCredentialDefinitionPrivate, + keyCorrectnessProof: phoneKeyCorrectnessProof, + linkSecret, + linkSecretId: 'linkSecretId', + revocationRegistryDefinitionId: 'phonerevregid:uri', + }) + + const phoneRecord = await storeCredential(agentContext, phoneCredential, { + credentialDefinitionId: 'phonecreddef:uri', + schemaId: 'phoneschema:uri', + schema: phoneSchema as unknown as AnonCredsSchema, + linkSecretId: 'linkSecretId', + }) + + const selectedCredentials: AnonCredsSelectedCredentials = { + selfAttestedAttributes: { attr5_referent: 'football' }, + attributes: { + attr1_referent: { + credentialId: personRecord.id, + credentialInfo: { ...personCredentialInfo, credentialId: personRecord.id }, + revealed: true, + }, + attr2_referent: { + credentialId: phoneRecord.id, + credentialInfo: { ...phoneCredentialInfo, credentialId: phoneRecord.id }, + revealed: true, + }, + attr4_referent: { + credentialId: personRecord.id, + credentialInfo: { ...personCredentialInfo, credentialId: personRecord.id }, + revealed: true, + }, + }, + predicates: { + predicate1_referent: { + credentialId: personRecord.id, + credentialInfo: { ...personCredentialInfo, credentialId: personRecord.id }, + }, + }, + } + + findByIdMock.mockResolvedValueOnce(personRecord) + findByIdMock.mockResolvedValueOnce(phoneRecord) + + const revocationRegistries = { + 'personrevregid:uri': { + tailsFilePath: personTailsPath, + definition: JSON.parse(anoncreds.getJson({ objectHandle: personRevRegDef })), + revocationStatusLists: { '1': {} as AnonCredsRevocationStatusList }, + }, + 'phonerevregid:uri': { + tailsFilePath: phoneTailsPath, + definition: JSON.parse(anoncreds.getJson({ objectHandle: phoneRevRegDef })), + revocationStatusLists: { '1': {} as AnonCredsRevocationStatusList }, + }, + } + + await anonCredsHolderService.createProof(agentContext, { + credentialDefinitions: { + 'personcreddef:uri': personCredentialDefinition as AnonCredsCredentialDefinition, + 'phonecreddef:uri': phoneCredentialDefinition as AnonCredsCredentialDefinition, + }, + proofRequest, + selectedCredentials, + schemas: { + 'phoneschema:uri': { attrNames: ['phoneNumber'], issuerId: 'issuer:uri', name: 'phoneschema', version: '1' }, + 'personschema:uri': { + attrNames: ['name', 'sex', 'height', 'age'], + issuerId: 'issuer:uri', + name: 'personschema', + version: '1', + }, + }, + revocationRegistries, + }) + + expect(findByIdMock).toHaveBeenCalledTimes(2) + // TODO: check proof object + }) + + describe('getCredentialsForProofRequest', () => { + const findByQueryMock = jest.spyOn(w3cCredentialRepositoryMock, 'findByQuery') + const anonCredsFindByQueryMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'findByQuery') + + const proofRequest: AnonCredsProofRequest = { + nonce: anoncreds.generateNonce(), + name: 'pres_req_1', + version: '0.1', + requested_attributes: { + attr1_referent: { + name: 'name', + restrictions: [{ issuer_did: 'issuer:uri' }], + }, + attr2_referent: { + name: 'phoneNumber', + }, + attr4_referent: { + names: ['name', 'height'], + restrictions: [{ cred_def_id: 'crededefid:uri', issuer_id: 'issuerid:uri' }, { schema_version: '1.0' }], + }, + attr5_referent: { + name: 'name', + restrictions: [{ 'attr::name::value': 'Alice', 'attr::name::marker': '1' }], + }, + }, + requested_predicates: { + predicate1_referent: { name: 'age', p_type: '>=' as const, p_value: 18 }, + }, + } + + beforeEach(() => { + findByQueryMock.mockResolvedValue([]) + anonCredsFindByQueryMock.mockResolvedValue([]) + }) + + afterEach(() => { + findByQueryMock.mockClear() + anonCredsFindByQueryMock.mockClear() + }) + + test('invalid referent', async () => { + await expect( + anonCredsHolderService.getCredentialsForProofRequest(agentContext, { + proofRequest, + attributeReferent: 'name', + }) + ).rejects.toThrowError() + }) + + test('referent with single restriction', async () => { + await anonCredsHolderService.getCredentialsForProofRequest(agentContext, { + proofRequest, + attributeReferent: 'attr1_referent', + }) + + expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { + $and: [ + { + 'anonCredsAttr::name::marker': true, + }, + { + issuerId: 'issuer:uri', + }, + ], + }) + }) + + test('referent without restrictions', async () => { + await anonCredsHolderService.getCredentialsForProofRequest(agentContext, { + proofRequest, + attributeReferent: 'attr2_referent', + }) + + expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { + $and: [ + { + 'anonCredsAttr::phoneNumber::marker': true, + }, + ], + }) + }) + + test('referent with multiple names and restrictions', async () => { + await anonCredsHolderService.getCredentialsForProofRequest(agentContext, { + proofRequest, + attributeReferent: 'attr4_referent', + }) + + expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { + $and: [ + { + 'anonCredsAttr::name::marker': true, + 'anonCredsAttr::height::marker': true, + }, + { + $or: [ + { + anonCredsCredentialDefinitionId: 'crededefid:uri', + issuerId: 'issuerid:uri', + }, + { + anonCredsSchemaVersion: '1.0', + }, + ], + }, + ], + }) + }) + + test('referent with attribute values and marker restriction', async () => { + await anonCredsHolderService.getCredentialsForProofRequest(agentContext, { + proofRequest, + attributeReferent: 'attr5_referent', + }) + + expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { + $and: [ + { + 'anonCredsAttr::name::marker': true, + }, + { + 'anonCredsAttr::name::value': 'Alice', + 'anonCredsAttr::name::marker': true, + }, + ], + }) + }) + + test('predicate referent', async () => { + await anonCredsHolderService.getCredentialsForProofRequest(agentContext, { + proofRequest, + attributeReferent: 'predicate1_referent', + }) + + expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { + $and: [ + { + 'anonCredsAttr::age::marker': true, + }, + ], + }) + }) + }) + + test('deleteCredential', async () => { + const record = new W3cCredentialRecord({ + credential: {} as W3cJsonLdVerifiableCredential, + tags: {}, + }) + findByIdMock.mockResolvedValueOnce(null).mockResolvedValueOnce(record) + getByCredentialIdMock.mockRejectedValueOnce(new Error()) + + await expect(anonCredsHolderService.deleteCredential(agentContext, 'credentialId')).rejects.toThrow() + await anonCredsHolderService.deleteCredential(agentContext, 'credentialId') + expect(findByIdMock).toHaveBeenCalledWith(agentContext, 'credentialId') + }) + + test('get single Credential', async () => { + const record = new W3cCredentialRecord({ + credential: new W3cJsonLdVerifiableCredential({ + credentialSubject: new W3cCredentialSubject({ claims: { attr1: 'value1', attr2: 'value2' } }), + issuer: 'test', + issuanceDate: Date.now().toString(), + type: ['VerifiableCredential'], + proof: { + created: Date.now().toString(), + type: 'test', + proofPurpose: 'test', + verificationMethod: 'test', + }, + }), + tags: {}, + }) + + const tags: AnonCredsCredentialTags = { + anonCredsLinkSecretId: 'linkSecretId', + anonCredsCredentialDefinitionId: 'credDefId', + anonCredsSchemaId: 'schemaId', + anonCredsSchemaName: 'schemaName', + anonCredsSchemaIssuerId: 'schemaIssuerId', + anonCredsSchemaVersion: 'schemaVersion', + anonCredsMethodName: 'methodName', + anonCredsCredentialRevocationId: 'credentialRevocationId', + anonCredsRevocationRegistryId: 'revRegId', + } + + const anonCredsCredentialMetadata: W3cAnonCredsCredentialMetadata = { + credentialRevocationId: tags.anonCredsCredentialRevocationId, + linkSecretId: tags.anonCredsLinkSecretId, + methodName: tags.anonCredsMethodName, + } + + record.setTags(tags) + record.metadata.set(W3cAnonCredsCredentialMetadataKey, anonCredsCredentialMetadata) + + findByIdMock.mockResolvedValueOnce(null).mockResolvedValueOnce(record) + getByCredentialIdMock.mockRejectedValueOnce(new Error()) + + await expect(anonCredsHolderService.getCredential(agentContext, { id: 'myCredentialId' })).rejects.toThrowError() + + const credentialInfo = await anonCredsHolderService.getCredential(agentContext, { id: 'myCredentialId' }) + + expect(credentialInfo).toMatchObject({ + attributes: { attr1: 'value1', attr2: 'value2' }, + credentialDefinitionId: 'credDefId', + credentialId: record.id, + revocationRegistryId: 'revRegId', + schemaId: 'schemaId', + credentialRevocationId: 'credentialRevocationId', + }) + }) + + test('getCredentials', async () => { + const record = new W3cCredentialRecord({ + credential: new W3cJsonLdVerifiableCredential({ + credentialSubject: new W3cCredentialSubject({ claims: { attr1: 'value1', attr2: 'value2' } }), + issuer: 'test', + issuanceDate: Date.now().toString(), + type: ['VerifiableCredential'], + proof: { + created: Date.now().toString(), + type: 'test', + proofPurpose: 'test', + verificationMethod: 'test', + }, + }), + tags: {}, + }) + const records = [record] + + const tags: AnonCredsCredentialTags = { + anonCredsLinkSecretId: 'linkSecretId', + anonCredsCredentialDefinitionId: 'credDefId', + anonCredsSchemaId: 'schemaId', + anonCredsSchemaName: 'schemaName', + anonCredsSchemaIssuerId: 'schemaIssuerId', + anonCredsSchemaVersion: 'schemaVersion', + anonCredsMethodName: 'methodName', + anonCredsCredentialRevocationId: 'credentialRevocationId', + anonCredsRevocationRegistryId: 'revRegId', + } + + const anonCredsCredentialMetadata: W3cAnonCredsCredentialMetadata = { + credentialRevocationId: tags.anonCredsCredentialRevocationId, + linkSecretId: tags.anonCredsLinkSecretId, + methodName: tags.anonCredsMethodName, + } + + record.setTags(tags) + record.metadata.set(W3cAnonCredsCredentialMetadataKey, anonCredsCredentialMetadata) + + findByQueryMock.mockResolvedValueOnce(records) + + const credentialInfo = await anonCredsHolderService.getCredentials(agentContext, { + credentialDefinitionId: 'credDefId', + schemaId: 'schemaId', + schemaIssuerId: 'schemaIssuerDid', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + issuerId: 'issuerDid', + methodName: 'inMemory', + }) + + expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { + anonCredsCredentialDefinitionId: 'credDefId', + anonCredsSchemaId: 'schemaId', + anonCredsSchemaIssuerId: 'schemaIssuerDid', + issuerId: 'issuerDid', + anonCredsSchemaName: 'schemaName', + anonCredsSchemaVersion: 'schemaVersion', + anonCredsMethodName: 'inMemory', + }) + expect(credentialInfo).toMatchObject([ + { + attributes: { attr1: 'value1', attr2: 'value2' }, + credentialDefinitionId: 'credDefId', + credentialId: record.id, + revocationRegistryId: 'revRegId', + schemaId: 'schemaId', + credentialRevocationId: 'credentialRevocationId', + }, + ]) + }) + + test('storeCredential', async () => { + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = createCredentialDefinition({ + attributeNames: ['name', 'age', 'sex', 'height'], + issuerId: 'issuer:uri', + }) + + const linkSecret = createLinkSecret() + + mockFunction(anoncredsLinkSecretRepositoryMock.getByLinkSecretId).mockResolvedValue( + new AnonCredsLinkSecretRecord({ linkSecretId: 'linkSecretId', value: linkSecret }) + ) + + const saveCredentialMock = jest.spyOn(w3cCredentialRepositoryMock, 'save') + + const { credential } = await createCredentialForHolder({ + agentContext, + attributes: { + name: 'John', + sex: 'M', + height: '179', + age: '19', + }, + credentialDefinition: credentialDefinition as unknown as JsonObject, + schemaId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/SCHEMA/credentialDefinition-name/1.0', + credentialDefinitionId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/CLAIM_DEF/104/default', + credentialDefinitionPrivate, + keyCorrectnessProof, + linkSecret, + linkSecretId: 'linkSecretId', + revocationRegistryDefinitionId: 'did:indy:sovrin:test:12345/anoncreds/v0/REV_REG_DEF/420/someTag/anotherTag', + }) + + await storeCredential(agentContext, credential, { + schema: { + name: 'schemaname', + attrNames: ['name', 'age', 'height', 'sex'], + issuerId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH', + version: '1.0', + }, + + linkSecretId: 'linkSecretId', + schemaId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/SCHEMA/credentialDefinition-name/1.0', + credentialDefinitionId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/CLAIM_DEF/104/default', + }) + + expect(saveCredentialMock).toHaveBeenCalledWith(agentContext, expect.objectContaining({ credential })) + }) +}) diff --git a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts new file mode 100644 index 0000000000..328f34e250 --- /dev/null +++ b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts @@ -0,0 +1,456 @@ +import type { AnonCredsProofRequest } from '@credo-ts/anoncreds' +import type { DidRepository } from '@credo-ts/core' + +import { + DidResolverService, + DidsModuleConfig, + InjectionSymbols, + SignatureSuiteToken, + W3cCredentialsModuleConfig, +} from '@credo-ts/core' +import { anoncreds } from '@hyperledger/anoncreds-nodejs' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' +import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' +import { InMemoryAnonCredsRegistry } from '../../../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { testLogger } from '../../../../core/tests' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' +import { encodeCredentialValue } from '../../utils/credential' +import { AnonCredsRsHolderService } from '../AnonCredsRsHolderService' +import { AnonCredsRsIssuerService } from '../AnonCredsRsIssuerService' +import { AnonCredsRsVerifierService } from '../AnonCredsRsVerifierService' + +import { + AnonCredsCredentialDefinitionPrivateRecord, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsCredentialDefinitionRecord, + AnonCredsCredentialDefinitionRepository, + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsKeyCorrectnessProofRecord, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsLinkSecretRecord, + AnonCredsLinkSecretRepository, + AnonCredsModuleConfig, + AnonCredsSchemaRecord, + AnonCredsSchemaRepository, + AnonCredsVerifierServiceSymbol, + getUnqualifiedCredentialDefinitionId, + getUnqualifiedSchemaId, + parseIndyCredentialDefinitionId, + parseIndySchemaId, +} from '@credo-ts/anoncreds' + +const agentConfig = getAgentConfig('AnonCredsCredentialFormatServiceTest') +const anonCredsVerifierService = new AnonCredsRsVerifierService() +const anonCredsHolderService = new AnonCredsRsHolderService() +const anonCredsIssuerService = new AnonCredsRsIssuerService() +const storageService = new InMemoryStorageService() +const wallet = new InMemoryWallet() +const registry = new InMemoryAnonCredsRegistry() + +const agentContext = getAgentContext({ + wallet, + registerInstances: [ + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.StorageService, storageService], + [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [ + AnonCredsModuleConfig, + new AnonCredsModuleConfig({ + registries: [registry], + anoncreds, + }), + ], + + [InjectionSymbols.Logger, testLogger], + [DidResolverService, new DidResolverService(testLogger, new DidsModuleConfig(), {} as unknown as DidRepository)], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [SignatureSuiteToken, 'default'], + ], + agentConfig, +}) + +describe('AnonCredsRsServices', () => { + test('issuance flow without revocation', async () => { + const issuerId = 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt' + + const schema = await anonCredsIssuerService.createSchema(agentContext, { + attrNames: ['name', 'age'], + issuerId, + name: 'Employee Credential', + version: '1.0.0', + }) + + const { schemaState } = await registry.registerSchema(agentContext, { + schema, + options: {}, + }) + + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await anonCredsIssuerService.createCredentialDefinition(agentContext, { + issuerId, + schemaId: schemaState.schemaId as string, + schema, + tag: 'Employee Credential', + supportRevocation: false, + }) + + const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition, + options: {}, + }) + + if ( + !credentialDefinitionState.credentialDefinition || + !credentialDefinitionState.credentialDefinitionId || + !schemaState.schema || + !schemaState.schemaId + ) { + throw new Error('Failed to create schema or credential definition') + } + + if (!credentialDefinitionPrivate || !keyCorrectnessProof) { + throw new Error('Failed to get private part of credential definition') + } + + await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( + agentContext, + new AnonCredsSchemaRecord({ + schema: schemaState.schema, + schemaId: schemaState.schemaId, + methodName: 'inMemory', + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition: credentialDefinitionState.credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + methodName: 'inMemory', + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( + agentContext, + new AnonCredsCredentialDefinitionPrivateRecord({ + value: credentialDefinitionPrivate, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( + agentContext, + new AnonCredsKeyCorrectnessProofRecord({ + value: keyCorrectnessProof, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + const credentialOffer = await anonCredsIssuerService.createCredentialOffer(agentContext, { + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + + const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' }) + expect(linkSecret.linkSecretId).toBe('linkSecretId') + + await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save( + agentContext, + new AnonCredsLinkSecretRecord({ + value: linkSecret.linkSecretValue, + linkSecretId: linkSecret.linkSecretId, + }) + ) + + const credentialRequestState = await anonCredsHolderService.createCredentialRequest(agentContext, { + credentialDefinition: credentialDefinitionState.credentialDefinition, + credentialOffer, + linkSecretId: linkSecret.linkSecretId, + }) + + const { credential } = await anonCredsIssuerService.createCredential(agentContext, { + credentialOffer, + credentialRequest: credentialRequestState.credentialRequest, + credentialValues: { + name: { raw: 'John', encoded: encodeCredentialValue('John') }, + age: { raw: '25', encoded: encodeCredentialValue('25') }, + }, + }) + + const credentialId = await anonCredsHolderService.storeCredential(agentContext, { + credential, + credentialDefinition, + schema, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + credentialRequestMetadata: credentialRequestState.credentialRequestMetadata, + }) + + const credentialInfo = await anonCredsHolderService.getCredential(agentContext, { + id: credentialId, + }) + + expect(credentialInfo).toEqual({ + credentialId, + attributes: { + age: 25, + name: 'John', + }, + linkSecretId: 'linkSecretId', + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: null, + credentialRevocationId: null, + methodName: 'inMemory', + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + const proofRequest: AnonCredsProofRequest = { + nonce: anoncreds.generateNonce(), + name: 'pres_req_1', + version: '0.1', + requested_attributes: { + attr1_referent: { + name: 'name', + }, + }, + requested_predicates: { + predicate1_referent: { name: 'age', p_type: '>=' as const, p_value: 18 }, + }, + } + + const proof = await anonCredsHolderService.createProof(agentContext, { + credentialDefinitions: { [credentialDefinitionState.credentialDefinitionId]: credentialDefinition }, + proofRequest, + selectedCredentials: { + attributes: { + attr1_referent: { credentialId, credentialInfo, revealed: true }, + }, + predicates: { + predicate1_referent: { credentialId, credentialInfo }, + }, + selfAttestedAttributes: {}, + }, + schemas: { [schemaState.schemaId]: schema }, + revocationRegistries: {}, + }) + + const verifiedProof = await anonCredsVerifierService.verifyProof(agentContext, { + credentialDefinitions: { [credentialDefinitionState.credentialDefinitionId]: credentialDefinition }, + proof, + proofRequest, + schemas: { [schemaState.schemaId]: schema }, + revocationRegistries: {}, + }) + + expect(verifiedProof).toBeTruthy() + }) + + test('issuance flow with unqualified identifiers', async () => { + // Use qualified identifiers to create schema and credential definition (we only support qualified identifiers for these) + const issuerId = 'did:indy:pool:localtest:A4CYPASJYRZRt98YWrac3H' + + const schema = await anonCredsIssuerService.createSchema(agentContext, { + attrNames: ['name', 'age'], + issuerId, + name: 'Employee Credential', + version: '1.0.0', + }) + + const { schemaState } = await registry.registerSchema(agentContext, { + schema, + options: {}, + }) + + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await anonCredsIssuerService.createCredentialDefinition(agentContext, { + issuerId, + schemaId: schemaState.schemaId as string, + schema, + tag: 'Employee Credential', + supportRevocation: false, + }) + + const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition, + options: {}, + }) + + if ( + !credentialDefinitionState.credentialDefinition || + !credentialDefinitionState.credentialDefinitionId || + !schemaState.schema || + !schemaState.schemaId + ) { + throw new Error('Failed to create schema or credential definition') + } + + if (!credentialDefinitionPrivate || !keyCorrectnessProof) { + throw new Error('Failed to get private part of credential definition') + } + + await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( + agentContext, + new AnonCredsSchemaRecord({ + schema: schemaState.schema, + schemaId: schemaState.schemaId, + methodName: 'inMemory', + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition: credentialDefinitionState.credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + methodName: 'inMemory', + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( + agentContext, + new AnonCredsCredentialDefinitionPrivateRecord({ + value: credentialDefinitionPrivate, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( + agentContext, + new AnonCredsKeyCorrectnessProofRecord({ + value: keyCorrectnessProof, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + const { namespaceIdentifier, schemaSeqNo, tag } = parseIndyCredentialDefinitionId( + credentialDefinitionState.credentialDefinitionId + ) + const unqualifiedCredentialDefinitionId = getUnqualifiedCredentialDefinitionId( + namespaceIdentifier, + schemaSeqNo, + tag + ) + + const parsedSchema = parseIndySchemaId(schemaState.schemaId) + const unqualifiedSchemaId = getUnqualifiedSchemaId( + parsedSchema.namespaceIdentifier, + parsedSchema.schemaName, + parsedSchema.schemaVersion + ) + + // Create offer with unqualified credential definition id + const credentialOffer = await anonCredsIssuerService.createCredentialOffer(agentContext, { + credentialDefinitionId: unqualifiedCredentialDefinitionId, + }) + + const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'someLinkSecretId' }) + expect(linkSecret.linkSecretId).toBe('someLinkSecretId') + + await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save( + agentContext, + new AnonCredsLinkSecretRecord({ + value: linkSecret.linkSecretValue, + linkSecretId: linkSecret.linkSecretId, + }) + ) + + const unqualifiedCredentialDefinition = await registry.getCredentialDefinition( + agentContext, + credentialOffer.cred_def_id + ) + const unqualifiedSchema = await registry.getSchema(agentContext, credentialOffer.schema_id) + if (!unqualifiedCredentialDefinition.credentialDefinition || !unqualifiedSchema.schema) { + throw new Error('unable to fetch credential definition or schema') + } + + const credentialRequestState = await anonCredsHolderService.createCredentialRequest(agentContext, { + credentialDefinition: unqualifiedCredentialDefinition.credentialDefinition, + credentialOffer, + linkSecretId: linkSecret.linkSecretId, + }) + + const { credential } = await anonCredsIssuerService.createCredential(agentContext, { + credentialOffer, + credentialRequest: credentialRequestState.credentialRequest, + credentialValues: { + name: { raw: 'John', encoded: encodeCredentialValue('John') }, + age: { raw: '25', encoded: encodeCredentialValue('25') }, + }, + }) + + // store credential now requires qualified identifiers + const credentialId = await anonCredsHolderService.storeCredential(agentContext, { + credential, + credentialDefinition, + schema, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + credentialRequestMetadata: credentialRequestState.credentialRequestMetadata, + }) + + const credentialInfo = await anonCredsHolderService.getCredential(agentContext, { + id: credentialId, + }) + + expect(credentialInfo).toEqual({ + credentialId, + attributes: { + age: 25, + name: 'John', + }, + linkSecretId: 'someLinkSecretId', + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: null, + credentialRevocationId: null, + methodName: 'inMemory', + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + const proofRequest: AnonCredsProofRequest = { + nonce: anoncreds.generateNonce(), + name: 'pres_req_1', + version: '0.1', + requested_attributes: { + attr1_referent: { + name: 'name', + }, + }, + requested_predicates: { + predicate1_referent: { name: 'age', p_type: '>=' as const, p_value: 18 }, + }, + } + + const proof = await anonCredsHolderService.createProof(agentContext, { + credentialDefinitions: { [unqualifiedCredentialDefinitionId]: credentialDefinition }, + proofRequest, + selectedCredentials: { + attributes: { + attr1_referent: { credentialId, credentialInfo, revealed: true }, + }, + predicates: { + predicate1_referent: { credentialId, credentialInfo }, + }, + selfAttestedAttributes: {}, + }, + schemas: { [unqualifiedSchemaId]: schema }, + revocationRegistries: {}, + }) + + const verifiedProof = await anonCredsVerifierService.verifyProof(agentContext, { + credentialDefinitions: { [unqualifiedCredentialDefinitionId]: credentialDefinition }, + proof, + proofRequest, + schemas: { [unqualifiedSchemaId]: schema }, + revocationRegistries: {}, + }) + + expect(verifiedProof).toBeTruthy() + }) +}) diff --git a/packages/anoncreds/src/anoncreds-rs/__tests__/helpers.ts b/packages/anoncreds/src/anoncreds-rs/__tests__/helpers.ts new file mode 100644 index 0000000000..6369e11a39 --- /dev/null +++ b/packages/anoncreds/src/anoncreds-rs/__tests__/helpers.ts @@ -0,0 +1,258 @@ +import type { W3cAnonCredsCredentialMetadata } from '../../utils/metadata' +import type { AnonCredsCredentialTags } from '../../utils/w3cAnonCredsUtils' +import type { + AnonCredsCredentialDefinition, + AnonCredsCredentialInfo, + AnonCredsCredentialOffer, + AnonCredsSchema, +} from '@credo-ts/anoncreds' +import type { AgentContext } from '@credo-ts/core' +import type { JsonObject } from '@hyperledger/anoncreds-shared' + +import { + JsonTransformer, + W3cCredentialRepository, + W3cCredentialService, + W3cJsonLdVerifiableCredential, +} from '@credo-ts/core' +import { + CredentialDefinition, + CredentialOffer, + CredentialRequest, + CredentialRevocationConfig, + LinkSecret, + RevocationRegistryDefinition, + RevocationRegistryDefinitionPrivate, + RevocationStatusList, + Schema, + W3cCredential, + anoncreds, +} from '@hyperledger/anoncreds-shared' + +import { W3cAnonCredsCredentialMetadataKey } from '../../utils/metadata' + +/** + * Creates a valid credential definition and returns its public and + * private part, including its key correctness proof + */ +export function createCredentialDefinition(options: { attributeNames: string[]; issuerId: string }) { + const { attributeNames, issuerId } = options + + const schema = Schema.create({ + issuerId, + attributeNames, + name: 'schema1', + version: '1', + }) + + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = CredentialDefinition.create({ + issuerId, + schema, + schemaId: 'schema:uri', + signatureType: 'CL', + supportRevocation: true, // FIXME: Revocation should not be mandatory but current anoncreds-rs is requiring it + tag: 'TAG', + }) + + const returnObj = { + credentialDefinition: credentialDefinition.toJson() as unknown as AnonCredsCredentialDefinition, + credentialDefinitionPrivate: credentialDefinitionPrivate.toJson() as unknown as JsonObject, + keyCorrectnessProof: keyCorrectnessProof.toJson() as unknown as JsonObject, + schema: schema.toJson() as unknown as Schema, + } + + credentialDefinition.handle.clear() + credentialDefinitionPrivate.handle.clear() + keyCorrectnessProof.handle.clear() + schema.handle.clear() + + return returnObj +} + +/** + * Creates a valid credential offer and returns itsf + */ +export function createCredentialOffer(keyCorrectnessProof: Record) { + const credentialOffer = CredentialOffer.create({ + credentialDefinitionId: 'creddef:uri', + keyCorrectnessProof, + schemaId: 'schema:uri', + }) + const credentialOfferJson = credentialOffer.toJson() as unknown as AnonCredsCredentialOffer + credentialOffer.handle.clear() + return credentialOfferJson +} + +/** + * + * @returns Creates a valid link secret value for anoncreds-rs + */ +export function createLinkSecret() { + return LinkSecret.create() +} + +export async function createCredentialForHolder(options: { + agentContext: AgentContext + credentialDefinition: JsonObject + credentialDefinitionPrivate: JsonObject + keyCorrectnessProof: JsonObject + schemaId: string + credentialDefinitionId: string + attributes: Record + linkSecret: string + linkSecretId: string + revocationRegistryDefinitionId: string +}) { + const { + credentialDefinition, + credentialDefinitionPrivate, + keyCorrectnessProof, + schemaId, + credentialDefinitionId, + attributes, + linkSecret, + linkSecretId, + revocationRegistryDefinitionId, + } = options + + const credentialOffer = CredentialOffer.create({ + credentialDefinitionId, + keyCorrectnessProof, + schemaId, + }) + + const { credentialRequest, credentialRequestMetadata } = CredentialRequest.create({ + entropy: 'some-entropy', + credentialDefinition, + credentialOffer, + linkSecret, + linkSecretId: linkSecretId, + }) + + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate, tailsPath } = + createRevocationRegistryDefinition({ + credentialDefinitionId, + credentialDefinition, + }) + + const timeCreateRevStatusList = 12 + const revocationStatusList = RevocationStatusList.create({ + credentialDefinition, + revocationRegistryDefinitionPrivate: new RevocationRegistryDefinitionPrivate( + revocationRegistryDefinitionPrivate.handle + ), + issuerId: credentialDefinition.issuerId as string, + timestamp: timeCreateRevStatusList, + issuanceByDefault: true, + revocationRegistryDefinition: new RevocationRegistryDefinition(revocationRegistryDefinition.handle), + revocationRegistryDefinitionId: 'mock:uri', + }) + + const credentialObj = W3cCredential.create({ + credentialDefinition, + credentialDefinitionPrivate, + credentialOffer, + credentialRequest, + attributeRawValues: attributes, + revocationRegistryId: revocationRegistryDefinitionId, + revocationStatusList, + revocationConfiguration: new CredentialRevocationConfig({ + statusList: revocationStatusList, + registryDefinition: new RevocationRegistryDefinition(revocationRegistryDefinition.handle), + registryDefinitionPrivate: new RevocationRegistryDefinitionPrivate(revocationRegistryDefinitionPrivate.handle), + registryIndex: 9, + }), + }) + + const w3cJsonLdCredential = JsonTransformer.fromJSON(credentialObj.toJson(), W3cJsonLdVerifiableCredential) + + const credentialInfo: Omit = { + attributes, + credentialDefinitionId, + linkSecretId, + schemaId, + methodName: 'inMemory', + credentialRevocationId: null, + revocationRegistryId: null, + createdAt: new Date('2024-01-01T00:00:00Z'), + updatedAt: new Date('2024-01-01T00:00:00Z'), + } + const returnObj = { + credential: w3cJsonLdCredential, + credentialInfo, + revocationRegistryDefinition, + tailsPath, + credentialRequestMetadata, + } + + credentialObj.handle.clear() + credentialOffer.handle.clear() + credentialRequest.handle.clear() + revocationRegistryDefinitionPrivate.clear() + revocationStatusList.handle.clear() + + return returnObj +} + +export function createRevocationRegistryDefinition(options: { + credentialDefinitionId: string + credentialDefinition: Record +}) { + const { credentialDefinitionId, credentialDefinition } = options + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = + anoncreds.createRevocationRegistryDefinition({ + credentialDefinitionId, + credentialDefinition: CredentialDefinition.fromJson(credentialDefinition).handle, + issuerId: credentialDefinition.issuerId as string, + tag: 'some_tag', + revocationRegistryType: 'CL_ACCUM', + maximumCredentialNumber: 10, + }) + + const tailsPath = anoncreds.revocationRegistryDefinitionGetAttribute({ + objectHandle: revocationRegistryDefinition, + name: 'tails_location', + }) + + return { revocationRegistryDefinition, revocationRegistryDefinitionPrivate, tailsPath } +} + +export async function storeCredential( + agentContext: AgentContext, + w3cJsonLdCredential: W3cJsonLdVerifiableCredential, + options: { + linkSecretId: string + credentialDefinitionId: string + schemaId: string + schema: AnonCredsSchema + } +) { + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const record = await w3cCredentialService.storeCredential(agentContext, { + credential: w3cJsonLdCredential, + }) + + const anonCredsCredentialRecordTags: AnonCredsCredentialTags = { + anonCredsLinkSecretId: options.linkSecretId, + anonCredsCredentialDefinitionId: options.credentialDefinitionId, + anonCredsSchemaId: options.schemaId, + anonCredsSchemaName: options.schema.name, + anonCredsSchemaIssuerId: options.schema.issuerId, + anonCredsSchemaVersion: options.schema.version, + anonCredsMethodName: 'method', + } + + const anonCredsCredentialMetadata: W3cAnonCredsCredentialMetadata = { + credentialRevocationId: anonCredsCredentialRecordTags.anonCredsCredentialRevocationId, + linkSecretId: anonCredsCredentialRecordTags.anonCredsLinkSecretId, + methodName: anonCredsCredentialRecordTags.anonCredsMethodName, + } + + record.setTags(anonCredsCredentialRecordTags) + record.metadata.set(W3cAnonCredsCredentialMetadataKey, anonCredsCredentialMetadata) + + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + await w3cCredentialRepository.update(agentContext, record) + + return record +} diff --git a/packages/anoncreds/src/anoncreds-rs/index.ts b/packages/anoncreds/src/anoncreds-rs/index.ts new file mode 100644 index 0000000000..b675ab0025 --- /dev/null +++ b/packages/anoncreds/src/anoncreds-rs/index.ts @@ -0,0 +1,3 @@ +export { AnonCredsRsHolderService } from './AnonCredsRsHolderService' +export { AnonCredsRsIssuerService } from './AnonCredsRsIssuerService' +export { AnonCredsRsVerifierService } from './AnonCredsRsVerifierService' diff --git a/packages/anoncreds/src/anoncreds-rs/utils.ts b/packages/anoncreds/src/anoncreds-rs/utils.ts new file mode 100644 index 0000000000..94d05a247f --- /dev/null +++ b/packages/anoncreds/src/anoncreds-rs/utils.ts @@ -0,0 +1,118 @@ +import type { AnonCredsNonRevokedInterval } from '../models' +import type { AgentContext, JsonObject, W3cJsonLdVerifiableCredential } from '@credo-ts/core' +import type { NonRevokedIntervalOverride } from '@hyperledger/anoncreds-shared' + +import { CredoError, JsonTransformer } from '@credo-ts/core' +import { + W3cCredential as AnonCredsW3cCredential, + RevocationRegistryDefinition, + RevocationStatusList, + CredentialRevocationState, +} from '@hyperledger/anoncreds-shared' + +import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' +import { + assertBestPracticeRevocationInterval, + fetchRevocationRegistryDefinition, + fetchRevocationStatusList, +} from '../utils' + +export interface CredentialRevocationMetadata { + timestamp?: number + revocationRegistryId: string + revocationRegistryIndex?: number + nonRevokedInterval: AnonCredsNonRevokedInterval +} + +export async function getRevocationMetadata( + agentContext: AgentContext, + credentialRevocationMetadata: CredentialRevocationMetadata, + mustHaveTimeStamp = false +) { + let nonRevokedIntervalOverride: NonRevokedIntervalOverride | undefined + + const { revocationRegistryId, revocationRegistryIndex, nonRevokedInterval, timestamp } = credentialRevocationMetadata + if (!revocationRegistryId || !nonRevokedInterval || (mustHaveTimeStamp && !timestamp)) { + throw new CredoError('Invalid revocation metadata') + } + + // Make sure the revocation interval follows best practices from Aries RFC 0441 + assertBestPracticeRevocationInterval(nonRevokedInterval) + + const { revocationRegistryDefinition: anonCredsRevocationRegistryDefinition } = + await fetchRevocationRegistryDefinition(agentContext, revocationRegistryId) + + const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + const { tailsFilePath } = await tailsFileService.getTailsFile(agentContext, { + revocationRegistryDefinition: anonCredsRevocationRegistryDefinition, + }) + + const timestampToFetch = timestamp ?? nonRevokedInterval.to + if (!timestampToFetch) throw new CredoError('Timestamp to fetch is required') + + const { revocationStatusList: _revocationStatusList } = await fetchRevocationStatusList( + agentContext, + revocationRegistryId, + timestampToFetch + ) + const updatedTimestamp = timestamp ?? _revocationStatusList.timestamp + + const revocationRegistryDefinition = RevocationRegistryDefinition.fromJson( + anonCredsRevocationRegistryDefinition as unknown as JsonObject + ) + + const revocationStatusList = RevocationStatusList.fromJson(_revocationStatusList as unknown as JsonObject) + const revocationState = revocationRegistryIndex + ? CredentialRevocationState.create({ + revocationRegistryIndex: Number(revocationRegistryIndex), + revocationRegistryDefinition: revocationRegistryDefinition, + tailsPath: tailsFilePath, + revocationStatusList, + }) + : undefined + + const requestedFrom = nonRevokedInterval.from + if (requestedFrom && requestedFrom > timestampToFetch) { + const { revocationStatusList: overrideRevocationStatusList } = await fetchRevocationStatusList( + agentContext, + revocationRegistryId, + requestedFrom + ) + + const vdrTimestamp = overrideRevocationStatusList?.timestamp + if (vdrTimestamp && vdrTimestamp === timestampToFetch) { + nonRevokedIntervalOverride = { + overrideRevocationStatusListTimestamp: timestampToFetch, + requestedFromTimestamp: requestedFrom, + revocationRegistryDefinitionId: revocationRegistryId, + } + } else { + throw new CredoError( + `VDR timestamp for ${requestedFrom} does not correspond to the one provided in proof identifiers. Expected: ${updatedTimestamp} and received ${vdrTimestamp}` + ) + } + } + + return { + updatedTimestamp, + revocationRegistryId, + revocationRegistryDefinition, + revocationStatusList, + nonRevokedIntervalOverride, + revocationState, + } +} + +export const getW3cAnonCredsCredentialMetadata = (w3cJsonLdVerifiableCredential: W3cJsonLdVerifiableCredential) => { + const w3cJsonLdVerifiableCredentialJson = JsonTransformer.toJSON(w3cJsonLdVerifiableCredential) + + const { schemaId, credentialDefinitionId, revocationRegistryId } = AnonCredsW3cCredential.fromJson( + w3cJsonLdVerifiableCredentialJson + ) + + return { + schemaId, + credentialDefinitionId, + revocationRegistryId, + } +} diff --git a/packages/anoncreds/src/error/AnonCredsError.ts b/packages/anoncreds/src/error/AnonCredsError.ts new file mode 100644 index 0000000000..d5b5f3ac80 --- /dev/null +++ b/packages/anoncreds/src/error/AnonCredsError.ts @@ -0,0 +1,7 @@ +import { CredoError } from '@credo-ts/core' + +export class AnonCredsError extends CredoError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/anoncreds/src/error/AnonCredsRsError.ts b/packages/anoncreds/src/error/AnonCredsRsError.ts new file mode 100644 index 0000000000..bb21921897 --- /dev/null +++ b/packages/anoncreds/src/error/AnonCredsRsError.ts @@ -0,0 +1,7 @@ +import { AnonCredsError } from './AnonCredsError' + +export class AnonCredsRsError extends AnonCredsError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/anoncreds/src/error/AnonCredsStoreRecordError.ts b/packages/anoncreds/src/error/AnonCredsStoreRecordError.ts new file mode 100644 index 0000000000..11437d7b64 --- /dev/null +++ b/packages/anoncreds/src/error/AnonCredsStoreRecordError.ts @@ -0,0 +1,7 @@ +import { AnonCredsError } from './AnonCredsError' + +export class AnonCredsStoreRecordError extends AnonCredsError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/anoncreds/src/error/index.ts b/packages/anoncreds/src/error/index.ts new file mode 100644 index 0000000000..5fdab9fb38 --- /dev/null +++ b/packages/anoncreds/src/error/index.ts @@ -0,0 +1,3 @@ +export * from './AnonCredsError' +export * from './AnonCredsStoreRecordError' +export * from './AnonCredsRsError' diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts new file mode 100644 index 0000000000..869acd44a1 --- /dev/null +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts @@ -0,0 +1,97 @@ +import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '../models' +import type { CredentialPreviewAttributeOptions, CredentialFormat, LinkedAttachment } from '@credo-ts/core' + +export interface AnonCredsCredentialProposalFormat { + schema_issuer_id?: string + schema_name?: string + schema_version?: string + schema_id?: string + + cred_def_id?: string + issuer_id?: string + + // TODO: we don't necessarily need to include these in the AnonCreds Format RFC + // as it's a new one and we can just forbid the use of legacy properties + schema_issuer_did?: string + issuer_did?: string +} + +/** + * This defines the module payload for calling CredentialsApi.createProposal + * or CredentialsApi.negotiateOffer + */ +export interface AnonCredsProposeCredentialFormat { + schemaIssuerId?: string + schemaId?: string + schemaName?: string + schemaVersion?: string + + credentialDefinitionId?: string + issuerId?: string + + attributes?: CredentialPreviewAttributeOptions[] + linkedAttachments?: LinkedAttachment[] + + // Kept for backwards compatibility + schemaIssuerDid?: string + issuerDid?: string +} + +/** + * This defines the module payload for calling CredentialsApi.acceptProposal + */ +export interface AnonCredsAcceptProposalFormat { + credentialDefinitionId?: string + revocationRegistryDefinitionId?: string + revocationRegistryIndex?: number + attributes?: CredentialPreviewAttributeOptions[] + linkedAttachments?: LinkedAttachment[] +} + +/** + * This defines the module payload for calling CredentialsApi.acceptOffer. No options are available for this + * method, so it's an empty object + */ +export interface AnonCredsAcceptOfferFormat { + linkSecretId?: string +} + +/** + * This defines the module payload for calling CredentialsApi.offerCredential + * or CredentialsApi.negotiateProposal + */ +export interface AnonCredsOfferCredentialFormat { + credentialDefinitionId: string + revocationRegistryDefinitionId?: string + revocationRegistryIndex?: number + attributes: CredentialPreviewAttributeOptions[] + linkedAttachments?: LinkedAttachment[] +} + +/** + * This defines the module payload for calling CredentialsApi.acceptRequest. No options are available for this + * method, so it's an empty object + */ +export type AnonCredsAcceptRequestFormat = Record + +export interface AnonCredsCredentialFormat extends CredentialFormat { + formatKey: 'anoncreds' + credentialRecordType: 'w3c' + credentialFormats: { + createProposal: AnonCredsProposeCredentialFormat + acceptProposal: AnonCredsAcceptProposalFormat + createOffer: AnonCredsOfferCredentialFormat + acceptOffer: AnonCredsAcceptOfferFormat + createRequest: never // cannot start from createRequest + acceptRequest: AnonCredsAcceptRequestFormat + } + // TODO: update to new RFC once available + // Format data is based on RFC 0592 + // https://github.com/hyperledger/aries-rfcs/tree/main/features/0592-indy-attachments + formatData: { + proposal: AnonCredsCredentialProposalFormat + offer: AnonCredsCredentialOffer + request: AnonCredsCredentialRequest + credential: AnonCredsCredential + } +} diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts new file mode 100644 index 0000000000..0d308f87b2 --- /dev/null +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -0,0 +1,678 @@ +import type { AnonCredsCredentialFormat, AnonCredsCredentialProposalFormat } from './AnonCredsCredentialFormat' +import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '../models' +import type { AnonCredsIssuerService, AnonCredsHolderService } from '../services' +import type { AnonCredsCredentialMetadata, AnonCredsCredentialRequestMetadata } from '../utils/metadata' +import type { + CredentialFormatService, + AgentContext, + CredentialFormatCreateProposalOptions, + CredentialFormatCreateProposalReturn, + CredentialFormatProcessOptions, + CredentialFormatAcceptProposalOptions, + CredentialFormatCreateOfferReturn, + CredentialFormatCreateOfferOptions, + CredentialFormatAcceptOfferOptions, + CredentialFormatCreateReturn, + CredentialFormatAcceptRequestOptions, + CredentialFormatProcessCredentialOptions, + CredentialFormatAutoRespondProposalOptions, + CredentialFormatAutoRespondOfferOptions, + CredentialFormatAutoRespondRequestOptions, + CredentialFormatAutoRespondCredentialOptions, + CredentialExchangeRecord, + CredentialPreviewAttributeOptions, + LinkedAttachment, +} from '@credo-ts/core' + +import { + ProblemReportError, + MessageValidator, + CredentialFormatSpec, + CredoError, + Attachment, + JsonEncoder, + utils, + CredentialProblemReportReason, + JsonTransformer, +} from '@credo-ts/core' + +import { AnonCredsCredentialProposal } from '../models/AnonCredsCredentialProposal' +import { + AnonCredsCredentialDefinitionRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryState, +} from '../repository' +import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' +import { + dateToTimestamp, + fetchCredentialDefinition, + fetchRevocationRegistryDefinition, + fetchRevocationStatusList, + fetchSchema, +} from '../utils' +import { + convertAttributesToCredentialValues, + assertCredentialValuesMatch, + checkCredentialValuesMatch, + assertAttributesMatch, + createAndLinkAttachmentsToPreview, +} from '../utils/credential' +import { AnonCredsCredentialMetadataKey, AnonCredsCredentialRequestMetadataKey } from '../utils/metadata' +import { getStoreCredentialOptions } from '../utils/w3cAnonCredsUtils' + +const ANONCREDS_CREDENTIAL_OFFER = 'anoncreds/credential-offer@v1.0' +const ANONCREDS_CREDENTIAL_REQUEST = 'anoncreds/credential-request@v1.0' +const ANONCREDS_CREDENTIAL_FILTER = 'anoncreds/credential-filter@v1.0' +const ANONCREDS_CREDENTIAL = 'anoncreds/credential@v1.0' + +export class AnonCredsCredentialFormatService implements CredentialFormatService { + /** formatKey is the key used when calling agent.credentials.xxx with credentialFormats.anoncreds */ + public readonly formatKey = 'anoncreds' as const + + /** + * credentialRecordType is the type of record that stores the credential. It is stored in the credential + * record binding in the credential exchange record. + */ + public readonly credentialRecordType = 'w3c' as const + + /** + * Create a {@link AttachmentFormats} object dependent on the message type. + * + * @param options The object containing all the options for the proposed credential + * @returns object containing associated attachment, format and optionally the credential preview + * + */ + public async createProposal( + agentContext: AgentContext, + { credentialFormats, credentialRecord }: CredentialFormatCreateProposalOptions + ): Promise { + const format = new CredentialFormatSpec({ + format: ANONCREDS_CREDENTIAL_FILTER, + }) + + const anoncredsFormat = credentialFormats.anoncreds + + if (!anoncredsFormat) { + throw new CredoError('Missing anoncreds payload in createProposal') + } + + // We want all properties except for `attributes` and `linkedAttachments` attributes. + // The easiest way is to destructure and use the spread operator. But that leaves the other properties unused + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { attributes, linkedAttachments, ...anoncredsCredentialProposal } = anoncredsFormat + const proposal = new AnonCredsCredentialProposal(anoncredsCredentialProposal) + + try { + MessageValidator.validateSync(proposal) + } catch (error) { + throw new CredoError(`Invalid proposal supplied: ${anoncredsCredentialProposal} in AnonCredsFormatService`) + } + + const attachment = this.getFormatData(JsonTransformer.toJSON(proposal), format.attachmentId) + + const { previewAttributes } = this.getCredentialLinkedAttachments( + anoncredsFormat.attributes, + anoncredsFormat.linkedAttachments + ) + + // Set the metadata + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { + schemaId: proposal.schemaId, + credentialDefinitionId: proposal.credentialDefinitionId, + }) + + return { format, attachment, previewAttributes } + } + + public async processProposal( + agentContext: AgentContext, + { attachment }: CredentialFormatProcessOptions + ): Promise { + const proposalJson = attachment.getDataAsJson() + + JsonTransformer.fromJSON(proposalJson, AnonCredsCredentialProposal) + } + + public async acceptProposal( + agentContext: AgentContext, + { + attachmentId, + credentialFormats, + credentialRecord, + proposalAttachment, + }: CredentialFormatAcceptProposalOptions + ): Promise { + const anoncredsFormat = credentialFormats?.anoncreds + + const proposalJson = proposalAttachment.getDataAsJson() + const credentialDefinitionId = anoncredsFormat?.credentialDefinitionId ?? proposalJson.cred_def_id + + const attributes = anoncredsFormat?.attributes ?? credentialRecord.credentialAttributes + + if (!credentialDefinitionId) { + throw new CredoError('No credential definition id in proposal or provided as input to accept proposal method.') + } + + if (!attributes) { + throw new CredoError('No attributes in proposal or provided as input to accept proposal method.') + } + + const { format, attachment, previewAttributes } = await this.createAnonCredsOffer(agentContext, { + credentialRecord, + attachmentId, + attributes, + credentialDefinitionId, + revocationRegistryDefinitionId: anoncredsFormat?.revocationRegistryDefinitionId, + revocationRegistryIndex: anoncredsFormat?.revocationRegistryIndex, + linkedAttachments: anoncredsFormat?.linkedAttachments, + }) + + return { format, attachment, previewAttributes } + } + + /** + * Create a credential attachment format for a credential request. + * + * @param options The object containing all the options for the credential offer + * @returns object containing associated attachment, formats and offersAttach elements + * + */ + public async createOffer( + agentContext: AgentContext, + { credentialFormats, credentialRecord, attachmentId }: CredentialFormatCreateOfferOptions + ): Promise { + const anoncredsFormat = credentialFormats.anoncreds + + if (!anoncredsFormat) { + throw new CredoError('Missing anoncreds credential format data') + } + + const { format, attachment, previewAttributes } = await this.createAnonCredsOffer(agentContext, { + credentialRecord, + attachmentId, + attributes: anoncredsFormat.attributes, + credentialDefinitionId: anoncredsFormat.credentialDefinitionId, + revocationRegistryDefinitionId: anoncredsFormat.revocationRegistryDefinitionId, + revocationRegistryIndex: anoncredsFormat.revocationRegistryIndex, + linkedAttachments: anoncredsFormat.linkedAttachments, + }) + + return { format, attachment, previewAttributes } + } + + public async processOffer( + agentContext: AgentContext, + { attachment, credentialRecord }: CredentialFormatProcessOptions + ) { + agentContext.config.logger.debug( + `Processing anoncreds credential offer for credential record ${credentialRecord.id}` + ) + + const credOffer = attachment.getDataAsJson() + + if (!credOffer.schema_id || !credOffer.cred_def_id) { + throw new ProblemReportError('Invalid credential offer', { + problemCode: CredentialProblemReportReason.IssuanceAbandoned, + }) + } + } + + public async acceptOffer( + agentContext: AgentContext, + { + credentialRecord, + attachmentId, + offerAttachment, + credentialFormats, + }: CredentialFormatAcceptOfferOptions + ): Promise { + const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const credentialOffer = offerAttachment.getDataAsJson() + + // Get credential definition + const { credentialDefinition } = await fetchCredentialDefinition(agentContext, credentialOffer.cred_def_id) + + const { credentialRequest, credentialRequestMetadata } = await holderService.createCredentialRequest(agentContext, { + credentialOffer, + credentialDefinition, + linkSecretId: credentialFormats?.anoncreds?.linkSecretId, + }) + + credentialRecord.metadata.set( + AnonCredsCredentialRequestMetadataKey, + credentialRequestMetadata + ) + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { + credentialDefinitionId: credentialOffer.cred_def_id, + schemaId: credentialOffer.schema_id, + }) + + const format = new CredentialFormatSpec({ + attachmentId, + format: ANONCREDS_CREDENTIAL_REQUEST, + }) + + const attachment = this.getFormatData(credentialRequest, format.attachmentId) + return { format, attachment } + } + + /** + * Starting from a request is not supported for anoncreds credentials, this method only throws an error. + */ + public async createRequest(): Promise { + throw new CredoError('Starting from a request is not supported for anoncreds credentials') + } + + /** + * We don't have any models to validate an anoncreds request object, for now this method does nothing + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async processRequest(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise { + // not needed for anoncreds + } + + public async acceptRequest( + agentContext: AgentContext, + { + credentialRecord, + attachmentId, + offerAttachment, + requestAttachment, + }: CredentialFormatAcceptRequestOptions + ): Promise { + // Assert credential attributes + const credentialAttributes = credentialRecord.credentialAttributes + if (!credentialAttributes) { + throw new CredoError( + `Missing required credential attribute values on credential record with id ${credentialRecord.id}` + ) + } + + const anonCredsIssuerService = + agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol) + + const credentialOffer = offerAttachment?.getDataAsJson() + if (!credentialOffer) throw new CredoError('Missing anoncreds credential offer in createCredential') + + const credentialRequest = requestAttachment.getDataAsJson() + if (!credentialRequest) throw new CredoError('Missing anoncreds credential request in createCredential') + + // We check locally for credential definition info. If it supports revocation, we need to search locally for + // an active revocation registry + const credentialDefinition = ( + await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, credentialRequest.cred_def_id) + ).credentialDefinition.value + + let revocationRegistryDefinitionId + let revocationRegistryIndex + let revocationStatusList + + if (credentialDefinition.revocation) { + const credentialMetadata = + credentialRecord.metadata.get(AnonCredsCredentialMetadataKey) + revocationRegistryDefinitionId = credentialMetadata?.revocationRegistryId + if (credentialMetadata?.credentialRevocationId) { + revocationRegistryIndex = Number(credentialMetadata.credentialRevocationId) + } + + if (!revocationRegistryDefinitionId || !revocationRegistryIndex) { + throw new CredoError( + 'Revocation registry definition id and revocation index are mandatory to issue AnonCreds revocable credentials' + ) + } + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) + + if (revocationRegistryDefinitionPrivateRecord.state !== AnonCredsRevocationRegistryState.Active) { + throw new CredoError( + `Revocation registry ${revocationRegistryDefinitionId} is in ${revocationRegistryDefinitionPrivateRecord.state} state` + ) + } + + const revocationStatusListResult = await fetchRevocationStatusList( + agentContext, + revocationRegistryDefinitionId, + dateToTimestamp(new Date()) + ) + revocationStatusList = revocationStatusListResult.revocationStatusList + } + + const { credential, credentialRevocationId } = await anonCredsIssuerService.createCredential(agentContext, { + credentialOffer, + credentialRequest, + credentialValues: convertAttributesToCredentialValues(credentialAttributes), + revocationRegistryDefinitionId, + revocationRegistryIndex, + revocationStatusList, + }) + + // If the credential is revocable, store the revocation identifiers in the credential record + if (credential.rev_reg_id) { + credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, { + revocationRegistryId: revocationRegistryDefinitionId ?? undefined, + credentialRevocationId: credentialRevocationId ?? undefined, + }) + credentialRecord.setTags({ + anonCredsRevocationRegistryId: revocationRegistryDefinitionId, + anonCredsCredentialRevocationId: credentialRevocationId, + }) + } + + const format = new CredentialFormatSpec({ + attachmentId, + format: ANONCREDS_CREDENTIAL, + }) + + const attachment = this.getFormatData(credential, format.attachmentId) + return { format, attachment } + } + + /** + * Processes an incoming credential - retrieve metadata, retrieve payload and store it in wallet + * @param options the issue credential message wrapped inside this object + * @param credentialRecord the credential exchange record for this credential + */ + public async processCredential( + agentContext: AgentContext, + { credentialRecord, attachment }: CredentialFormatProcessCredentialOptions + ): Promise { + const credentialRequestMetadata = credentialRecord.metadata.get( + AnonCredsCredentialRequestMetadataKey + ) + + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + if (!credentialRequestMetadata) { + throw new CredoError( + `Missing required request metadata for credential exchange with thread id with id ${credentialRecord.id}` + ) + } + + if (!credentialRecord.credentialAttributes) { + throw new CredoError('Missing credential attributes on credential record. Unable to check credential attributes') + } + + const anonCredsCredential = attachment.getDataAsJson() + + const { credentialDefinition, credentialDefinitionId } = await fetchCredentialDefinition( + agentContext, + anonCredsCredential.cred_def_id + ) + const { schema, indyNamespace } = await fetchSchema(agentContext, anonCredsCredential.schema_id) + + // Resolve revocation registry if credential is revocable + const revocationRegistryResult = anonCredsCredential.rev_reg_id + ? await fetchRevocationRegistryDefinition(agentContext, anonCredsCredential.rev_reg_id) + : undefined + + // assert the credential values match the offer values + const recordCredentialValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes) + assertCredentialValuesMatch(anonCredsCredential.values, recordCredentialValues) + + const storeCredentialOptions = getStoreCredentialOptions( + { + credentialId: utils.uuid(), + credentialRequestMetadata, + credential: anonCredsCredential, + credentialDefinitionId, + credentialDefinition, + schema, + revocationRegistry: revocationRegistryResult?.revocationRegistryDefinition + ? { + definition: revocationRegistryResult.revocationRegistryDefinition, + id: revocationRegistryResult.revocationRegistryDefinitionId, + } + : undefined, + }, + indyNamespace + ) + + const credentialId = await anonCredsHolderService.storeCredential(agentContext, storeCredentialOptions) + + // If the credential is revocable, store the revocation identifiers in the credential record + if (anonCredsCredential.rev_reg_id) { + const credential = await anonCredsHolderService.getCredential(agentContext, { id: credentialId }) + + credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, { + credentialRevocationId: credential.credentialRevocationId ?? undefined, + revocationRegistryId: credential.revocationRegistryId ?? undefined, + }) + credentialRecord.setTags({ + anonCredsRevocationRegistryId: credential.revocationRegistryId, + anonCredsCredentialRevocationId: credential.credentialRevocationId, + }) + } + + credentialRecord.credentials.push({ + credentialRecordType: this.credentialRecordType, + credentialRecordId: credentialId, + }) + } + + public supportsFormat(format: string): boolean { + const supportedFormats = [ + ANONCREDS_CREDENTIAL_REQUEST, + ANONCREDS_CREDENTIAL_OFFER, + ANONCREDS_CREDENTIAL_FILTER, + ANONCREDS_CREDENTIAL, + ] + + return supportedFormats.includes(format) + } + + /** + * Gets the attachment object for a given attachmentId. We need to get out the correct attachmentId for + * anoncreds and then find the corresponding attachment (if there is one) + * @param formats the formats object containing the attachmentId + * @param messageAttachments the attachments containing the payload + * @returns The Attachment if found or undefined + * + */ + public getAttachment(formats: CredentialFormatSpec[], messageAttachments: Attachment[]): Attachment | undefined { + const supportedAttachmentIds = formats.filter((f) => this.supportsFormat(f.format)).map((f) => f.attachmentId) + const supportedAttachment = messageAttachments.find((attachment) => supportedAttachmentIds.includes(attachment.id)) + + return supportedAttachment + } + + public async deleteCredentialById(agentContext: AgentContext, credentialRecordId: string): Promise { + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + await anonCredsHolderService.deleteCredential(agentContext, credentialRecordId) + } + + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + { offerAttachment, proposalAttachment }: CredentialFormatAutoRespondProposalOptions + ) { + const proposalJson = proposalAttachment.getDataAsJson() + const offerJson = offerAttachment.getDataAsJson() + + // We want to make sure the credential definition matches. + // TODO: If no credential definition is present on the proposal, we could check whether the other fields + // of the proposal match with the credential definition id. + return proposalJson.cred_def_id === offerJson.cred_def_id + } + + public async shouldAutoRespondToOffer( + agentContext: AgentContext, + { offerAttachment, proposalAttachment }: CredentialFormatAutoRespondOfferOptions + ) { + const proposalJson = proposalAttachment.getDataAsJson() + const offerJson = offerAttachment.getDataAsJson() + + // We want to make sure the credential definition matches. + // TODO: If no credential definition is present on the proposal, we could check whether the other fields + // of the proposal match with the credential definition id. + return proposalJson.cred_def_id === offerJson.cred_def_id + } + + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + { offerAttachment, requestAttachment }: CredentialFormatAutoRespondRequestOptions + ) { + const credentialOfferJson = offerAttachment.getDataAsJson() + const credentialRequestJson = requestAttachment.getDataAsJson() + + return credentialOfferJson.cred_def_id === credentialRequestJson.cred_def_id + } + + public async shouldAutoRespondToCredential( + agentContext: AgentContext, + { credentialRecord, requestAttachment, credentialAttachment }: CredentialFormatAutoRespondCredentialOptions + ) { + const credentialJson = credentialAttachment.getDataAsJson() + const credentialRequestJson = requestAttachment.getDataAsJson() + + // make sure the credential definition matches + if (credentialJson.cred_def_id !== credentialRequestJson.cred_def_id) return false + + // If we don't have any attributes stored we can't compare so always return false. + if (!credentialRecord.credentialAttributes) return false + const attributeValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes) + + // check whether the values match the values in the record + return checkCredentialValuesMatch(attributeValues, credentialJson.values) + } + + private async createAnonCredsOffer( + agentContext: AgentContext, + { + credentialRecord, + attachmentId, + credentialDefinitionId, + revocationRegistryDefinitionId, + revocationRegistryIndex, + attributes, + linkedAttachments, + }: { + credentialDefinitionId: string + revocationRegistryDefinitionId?: string + revocationRegistryIndex?: number + credentialRecord: CredentialExchangeRecord + attachmentId?: string + attributes: CredentialPreviewAttributeOptions[] + linkedAttachments?: LinkedAttachment[] + } + ): Promise { + const anonCredsIssuerService = + agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol) + + // if the proposal has an attachment Id use that, otherwise the generated id of the formats object + const format = new CredentialFormatSpec({ + attachmentId: attachmentId, + format: ANONCREDS_CREDENTIAL, + }) + + const offer = await anonCredsIssuerService.createCredentialOffer(agentContext, { + credentialDefinitionId, + }) + + const { previewAttributes } = this.getCredentialLinkedAttachments(attributes, linkedAttachments) + if (!previewAttributes) { + throw new CredoError('Missing required preview attributes for anoncreds offer') + } + + await this.assertPreviewAttributesMatchSchemaAttributes(agentContext, offer, previewAttributes) + + // We check locally for credential definition info. If it supports revocation, revocationRegistryIndex + // and revocationRegistryDefinitionId are mandatory + const credentialDefinition = ( + await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, offer.cred_def_id) + ).credentialDefinition.value + + if (credentialDefinition.revocation) { + if (!revocationRegistryDefinitionId || revocationRegistryIndex === undefined) { + throw new CredoError( + 'AnonCreds revocable credentials require revocationRegistryDefinitionId and revocationRegistryIndex' + ) + } + + // Set revocation tags + credentialRecord.setTags({ + anonCredsRevocationRegistryId: revocationRegistryDefinitionId, + anonCredsCredentialRevocationId: revocationRegistryIndex.toString(), + }) + } + + // Set the metadata + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { + schemaId: offer.schema_id, + credentialDefinitionId: offer.cred_def_id, + credentialRevocationId: revocationRegistryIndex?.toString(), + revocationRegistryId: revocationRegistryDefinitionId, + }) + + const attachment = this.getFormatData(offer, format.attachmentId) + + return { format, attachment, previewAttributes } + } + + private async assertPreviewAttributesMatchSchemaAttributes( + agentContext: AgentContext, + offer: AnonCredsCredentialOffer, + attributes: CredentialPreviewAttributeOptions[] + ): Promise { + const { schema } = await fetchSchema(agentContext, offer.schema_id) + + assertAttributesMatch(schema, attributes) + } + + /** + * Get linked attachments for anoncreds format from a proposal message. This allows attachments + * to be copied across to old style credential records + * + * @param options ProposeCredentialOptions object containing (optionally) the linked attachments + * @return array of linked attachments or undefined if none present + */ + private getCredentialLinkedAttachments( + attributes?: CredentialPreviewAttributeOptions[], + linkedAttachments?: LinkedAttachment[] + ): { + attachments?: Attachment[] + previewAttributes?: CredentialPreviewAttributeOptions[] + } { + if (!linkedAttachments && !attributes) { + return {} + } + + let previewAttributes = attributes ?? [] + let attachments: Attachment[] | undefined + + if (linkedAttachments) { + // there are linked attachments so transform into the attribute field of the CredentialPreview object for + // this proposal + previewAttributes = createAndLinkAttachmentsToPreview(linkedAttachments, previewAttributes) + attachments = linkedAttachments.map((linkedAttachment) => linkedAttachment.attachment) + } + + return { attachments, previewAttributes } + } + + /** + * Returns an object of type {@link Attachment} for use in credential exchange messages. + * It looks up the correct format identifier and encodes the data as a base64 attachment. + * + * @param data The data to include in the attach object + * @param id the attach id from the formats component of the message + */ + public getFormatData(data: unknown, id: string): Attachment { + const attachment = new Attachment({ + id, + mimeType: 'application/json', + data: { + base64: JsonEncoder.toBase64(data), + }, + }) + + return attachment + } +} diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormat.ts b/packages/anoncreds/src/formats/AnonCredsProofFormat.ts new file mode 100644 index 0000000000..2ed36dcaba --- /dev/null +++ b/packages/anoncreds/src/formats/AnonCredsProofFormat.ts @@ -0,0 +1,90 @@ +import type { + AnonCredsNonRevokedInterval, + AnonCredsPredicateType, + AnonCredsProof, + AnonCredsProofRequest, + AnonCredsRequestedAttribute, + AnonCredsRequestedAttributeMatch, + AnonCredsRequestedPredicate, + AnonCredsRequestedPredicateMatch, + AnonCredsSelectedCredentials, +} from '../models' +import type { ProofFormat } from '@credo-ts/core' + +export interface AnonCredsPresentationPreviewAttribute { + name: string + credentialDefinitionId?: string + mimeType?: string + value?: string + referent?: string +} + +export interface AnonCredsPresentationPreviewPredicate { + name: string + credentialDefinitionId: string + predicate: AnonCredsPredicateType + threshold: number +} + +/** + * Interface for creating an anoncreds proof proposal. + */ +export interface AnonCredsProposeProofFormat { + name?: string + version?: string + attributes?: AnonCredsPresentationPreviewAttribute[] + predicates?: AnonCredsPresentationPreviewPredicate[] + nonRevokedInterval?: AnonCredsNonRevokedInterval +} + +/** + * Interface for creating an anoncreds proof request. + */ +export interface AnonCredsRequestProofFormat { + name: string + version: string + non_revoked?: AnonCredsNonRevokedInterval + requested_attributes?: Record + requested_predicates?: Record +} + +/** + * Interface for getting credentials for an indy proof request. + */ +export interface AnonCredsCredentialsForProofRequest { + attributes: Record + predicates: Record +} + +export interface AnonCredsGetCredentialsForProofRequestOptions { + filterByNonRevocationRequirements?: boolean +} + +export interface AnonCredsProofFormat extends ProofFormat { + formatKey: 'anoncreds' + + proofFormats: { + createProposal: AnonCredsProposeProofFormat + acceptProposal: { + name?: string + version?: string + } + createRequest: AnonCredsRequestProofFormat + acceptRequest: AnonCredsSelectedCredentials + + getCredentialsForRequest: { + input: AnonCredsGetCredentialsForProofRequestOptions + output: AnonCredsCredentialsForProofRequest + } + selectCredentialsForRequest: { + input: AnonCredsGetCredentialsForProofRequestOptions + output: AnonCredsSelectedCredentials + } + } + + formatData: { + proposal: AnonCredsProofRequest + request: AnonCredsProofRequest + presentation: AnonCredsProof + } +} diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts new file mode 100644 index 0000000000..09a6526970 --- /dev/null +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -0,0 +1,445 @@ +import type { AnonCredsProofFormat, AnonCredsGetCredentialsForProofRequestOptions } from './AnonCredsProofFormat' +import type { + AnonCredsCredentialDefinition, + AnonCredsProof, + AnonCredsSchema, + AnonCredsSelectedCredentials, + AnonCredsProofRequest, +} from '../models' +import type { AnonCredsHolderService, AnonCredsVerifierService } from '../services' +import type { + ProofFormatService, + AgentContext, + ProofFormatCreateReturn, + FormatCreateRequestOptions, + ProofFormatCreateProposalOptions, + ProofFormatProcessOptions, + ProofFormatAcceptProposalOptions, + ProofFormatAcceptRequestOptions, + ProofFormatProcessPresentationOptions, + ProofFormatGetCredentialsForRequestOptions, + ProofFormatGetCredentialsForRequestReturn, + ProofFormatSelectCredentialsForRequestOptions, + ProofFormatSelectCredentialsForRequestReturn, + ProofFormatAutoRespondProposalOptions, + ProofFormatAutoRespondRequestOptions, + ProofFormatAutoRespondPresentationOptions, +} from '@credo-ts/core' + +import { CredoError, Attachment, AttachmentData, JsonEncoder, ProofFormatSpec, JsonTransformer } from '@credo-ts/core' + +import { AnonCredsProofRequest as AnonCredsProofRequestClass } from '../models/AnonCredsProofRequest' +import { AnonCredsVerifierServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' +import { + createRequestFromPreview, + areAnonCredsProofRequestsEqual, + checkValidCredentialValueEncoding, + assertNoDuplicateGroupsNamesInProofRequest, + getRevocationRegistriesForRequest, + getRevocationRegistriesForProof, + fetchSchema, + fetchCredentialDefinition, +} from '../utils' +import { encodeCredentialValue } from '../utils/credential' +import { getCredentialsForAnonCredsProofRequest } from '../utils/getCredentialsForAnonCredsRequest' + +const ANONCREDS_PRESENTATION_PROPOSAL = 'anoncreds/proof-request@v1.0' +const ANONCREDS_PRESENTATION_REQUEST = 'anoncreds/proof-request@v1.0' +const ANONCREDS_PRESENTATION = 'anoncreds/proof@v1.0' + +export class AnonCredsProofFormatService implements ProofFormatService { + public readonly formatKey = 'anoncreds' as const + + public async createProposal( + agentContext: AgentContext, + { attachmentId, proofFormats }: ProofFormatCreateProposalOptions + ): Promise { + const format = new ProofFormatSpec({ + format: ANONCREDS_PRESENTATION_PROPOSAL, + attachmentId, + }) + + const anoncredsFormat = proofFormats.anoncreds + if (!anoncredsFormat) { + throw Error('Missing anoncreds format to create proposal attachment format') + } + + const proofRequest = createRequestFromPreview({ + attributes: anoncredsFormat.attributes ?? [], + predicates: anoncredsFormat.predicates ?? [], + name: anoncredsFormat.name ?? 'Proof request', + version: anoncredsFormat.version ?? '1.0', + nonce: await agentContext.wallet.generateNonce(), + nonRevokedInterval: anoncredsFormat.nonRevokedInterval, + }) + const attachment = this.getFormatData(proofRequest, format.attachmentId) + + return { attachment, format } + } + + public async processProposal(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const proposalJson = attachment.getDataAsJson() + + // fromJson also validates + JsonTransformer.fromJSON(proposalJson, AnonCredsProofRequestClass) + + // Assert attribute and predicate (group) names do not match + assertNoDuplicateGroupsNamesInProofRequest(proposalJson) + } + + public async acceptProposal( + agentContext: AgentContext, + { proposalAttachment, attachmentId }: ProofFormatAcceptProposalOptions + ): Promise { + const format = new ProofFormatSpec({ + format: ANONCREDS_PRESENTATION_REQUEST, + attachmentId, + }) + + const proposalJson = proposalAttachment.getDataAsJson() + + const request = { + ...proposalJson, + // We never want to reuse the nonce from the proposal, as this will allow replay attacks + nonce: await agentContext.wallet.generateNonce(), + } + + const attachment = this.getFormatData(request, format.attachmentId) + + return { attachment, format } + } + + public async createRequest( + agentContext: AgentContext, + { attachmentId, proofFormats }: FormatCreateRequestOptions + ): Promise { + const format = new ProofFormatSpec({ + format: ANONCREDS_PRESENTATION_REQUEST, + attachmentId, + }) + + const anoncredsFormat = proofFormats.anoncreds + if (!anoncredsFormat) { + throw Error('Missing anoncreds format in create request attachment format') + } + + const request = { + name: anoncredsFormat.name, + version: anoncredsFormat.version, + nonce: await agentContext.wallet.generateNonce(), + requested_attributes: anoncredsFormat.requested_attributes ?? {}, + requested_predicates: anoncredsFormat.requested_predicates ?? {}, + non_revoked: anoncredsFormat.non_revoked, + } satisfies AnonCredsProofRequest + + // Assert attribute and predicate (group) names do not match + assertNoDuplicateGroupsNamesInProofRequest(request) + + const attachment = this.getFormatData(request, format.attachmentId) + + return { attachment, format } + } + + public async processRequest(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const requestJson = attachment.getDataAsJson() + + // fromJson also validates + JsonTransformer.fromJSON(requestJson, AnonCredsProofRequestClass) + + // Assert attribute and predicate (group) names do not match + assertNoDuplicateGroupsNamesInProofRequest(requestJson) + } + + public async acceptRequest( + agentContext: AgentContext, + { proofFormats, requestAttachment, attachmentId }: ProofFormatAcceptRequestOptions + ): Promise { + const format = new ProofFormatSpec({ + format: ANONCREDS_PRESENTATION, + attachmentId, + }) + const requestJson = requestAttachment.getDataAsJson() + + const anoncredsFormat = proofFormats?.anoncreds + + const selectedCredentials = + anoncredsFormat ?? + (await this._selectCredentialsForRequest(agentContext, requestJson, { + filterByNonRevocationRequirements: true, + })) + + const proof = await this.createProof(agentContext, requestJson, selectedCredentials) + const attachment = this.getFormatData(proof, format.attachmentId) + + return { + attachment, + format, + } + } + + public async processPresentation( + agentContext: AgentContext, + { requestAttachment, attachment }: ProofFormatProcessPresentationOptions + ): Promise { + const verifierService = + agentContext.dependencyManager.resolve(AnonCredsVerifierServiceSymbol) + + const proofRequestJson = requestAttachment.getDataAsJson() + + // NOTE: we don't do validation here, as this is handled by the AnonCreds implementation, however + // this can lead to confusing error messages. We should consider doing validation here as well. + // Defining a class-transformer/class-validator class seems a bit overkill, and the usage of interfaces + // for the anoncreds package keeps things simple. Maybe we can try to use something like zod to validate + const proofJson = attachment.getDataAsJson() + + for (const [referent, attribute] of Object.entries(proofJson.requested_proof.revealed_attrs)) { + if (!checkValidCredentialValueEncoding(attribute.raw, attribute.encoded)) { + throw new CredoError( + `The encoded value for '${referent}' is invalid. ` + + `Expected '${encodeCredentialValue(attribute.raw)}'. ` + + `Actual '${attribute.encoded}'` + ) + } + } + + for (const [, attributeGroup] of Object.entries(proofJson.requested_proof.revealed_attr_groups ?? {})) { + for (const [attributeName, attribute] of Object.entries(attributeGroup.values)) { + if (!checkValidCredentialValueEncoding(attribute.raw, attribute.encoded)) { + throw new CredoError( + `The encoded value for '${attributeName}' is invalid. ` + + `Expected '${encodeCredentialValue(attribute.raw)}'. ` + + `Actual '${attribute.encoded}'` + ) + } + } + } + + const schemas = await this.getSchemas(agentContext, new Set(proofJson.identifiers.map((i) => i.schema_id))) + const credentialDefinitions = await this.getCredentialDefinitions( + agentContext, + new Set(proofJson.identifiers.map((i) => i.cred_def_id)) + ) + + const revocationRegistries = await getRevocationRegistriesForProof(agentContext, proofJson) + + const verified = await verifierService.verifyProof(agentContext, { + proofRequest: proofRequestJson, + proof: proofJson, + schemas, + credentialDefinitions, + revocationRegistries, + }) + + return verified + } + + public async getCredentialsForRequest( + agentContext: AgentContext, + { requestAttachment, proofFormats }: ProofFormatGetCredentialsForRequestOptions + ): Promise> { + const proofRequestJson = requestAttachment.getDataAsJson() + + // Set default values + const { filterByNonRevocationRequirements = true } = proofFormats?.anoncreds ?? {} + + const credentialsForRequest = await getCredentialsForAnonCredsProofRequest(agentContext, proofRequestJson, { + filterByNonRevocationRequirements, + }) + + return credentialsForRequest + } + + public async selectCredentialsForRequest( + agentContext: AgentContext, + { requestAttachment, proofFormats }: ProofFormatSelectCredentialsForRequestOptions + ): Promise> { + const proofRequestJson = requestAttachment.getDataAsJson() + + // Set default values + const { filterByNonRevocationRequirements = true } = proofFormats?.anoncreds ?? {} + + const selectedCredentials = this._selectCredentialsForRequest(agentContext, proofRequestJson, { + filterByNonRevocationRequirements, + }) + + return selectedCredentials + } + + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + { proposalAttachment, requestAttachment }: ProofFormatAutoRespondProposalOptions + ): Promise { + const proposalJson = proposalAttachment.getDataAsJson() + const requestJson = requestAttachment.getDataAsJson() + + const areRequestsEqual = areAnonCredsProofRequestsEqual(proposalJson, requestJson) + agentContext.config.logger.debug(`AnonCreds request and proposal are are equal: ${areRequestsEqual}`, { + proposalJson, + requestJson, + }) + + return areRequestsEqual + } + + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + { proposalAttachment, requestAttachment }: ProofFormatAutoRespondRequestOptions + ): Promise { + const proposalJson = proposalAttachment.getDataAsJson() + const requestJson = requestAttachment.getDataAsJson() + + return areAnonCredsProofRequestsEqual(proposalJson, requestJson) + } + + public async shouldAutoRespondToPresentation( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _options: ProofFormatAutoRespondPresentationOptions + ): Promise { + // The presentation is already verified in processPresentation, so we can just return true here. + // It's only an ack, so it's just that we received the presentation. + return true + } + + public supportsFormat(formatIdentifier: string): boolean { + const supportedFormats = [ANONCREDS_PRESENTATION_PROPOSAL, ANONCREDS_PRESENTATION_REQUEST, ANONCREDS_PRESENTATION] + return supportedFormats.includes(formatIdentifier) + } + + private async _selectCredentialsForRequest( + agentContext: AgentContext, + proofRequest: AnonCredsProofRequest, + options: AnonCredsGetCredentialsForProofRequestOptions + ): Promise { + const credentialsForRequest = await getCredentialsForAnonCredsProofRequest(agentContext, proofRequest, options) + + const selectedCredentials: AnonCredsSelectedCredentials = { + attributes: {}, + predicates: {}, + selfAttestedAttributes: {}, + } + + Object.keys(credentialsForRequest.attributes).forEach((attributeName) => { + const attributeArray = credentialsForRequest.attributes[attributeName] + + if (attributeArray.length === 0) { + throw new CredoError('Unable to automatically select requested attributes.') + } + + selectedCredentials.attributes[attributeName] = attributeArray[0] + }) + + Object.keys(credentialsForRequest.predicates).forEach((attributeName) => { + if (credentialsForRequest.predicates[attributeName].length === 0) { + throw new CredoError('Unable to automatically select requested predicates.') + } else { + selectedCredentials.predicates[attributeName] = credentialsForRequest.predicates[attributeName][0] + } + }) + + return selectedCredentials + } + + /** + * Build schemas object needed to create and verify proof objects. + * + * Creates object with `{ schemaId: AnonCredsSchema }` mapping + * + * @param schemaIds List of schema ids + * @returns Object containing schemas for specified schema ids + * + */ + private async getSchemas(agentContext: AgentContext, schemaIds: Set) { + const schemas: { [key: string]: AnonCredsSchema } = {} + + for (const schemaId of schemaIds) { + const { schema } = await fetchSchema(agentContext, schemaId) + schemas[schemaId] = schema + } + + return schemas + } + + /** + * Build credential definitions object needed to create and verify proof objects. + * + * Creates object with `{ credentialDefinitionId: AnonCredsCredentialDefinition }` mapping + * + * @param credentialDefinitionIds List of credential definition ids + * @returns Object containing credential definitions for specified credential definition ids + * + */ + private async getCredentialDefinitions(agentContext: AgentContext, credentialDefinitionIds: Set) { + const credentialDefinitions: { [key: string]: AnonCredsCredentialDefinition } = {} + + for (const credentialDefinitionId of credentialDefinitionIds) { + const { credentialDefinition } = await fetchCredentialDefinition(agentContext, credentialDefinitionId) + credentialDefinitions[credentialDefinitionId] = credentialDefinition + } + + return credentialDefinitions + } + + /** + * Create anoncreds proof from a given proof request and requested credential object. + * + * @param proofRequest The proof request to create the proof for + * @param requestedCredentials The requested credentials object specifying which credentials to use for the proof + * @returns anoncreds proof object + */ + private async createProof( + agentContext: AgentContext, + proofRequest: AnonCredsProofRequest, + selectedCredentials: AnonCredsSelectedCredentials + ): Promise { + const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const credentialObjects = await Promise.all( + [...Object.values(selectedCredentials.attributes), ...Object.values(selectedCredentials.predicates)].map( + async (c) => c.credentialInfo ?? holderService.getCredential(agentContext, { id: c.credentialId }) + ) + ) + + const schemas = await this.getSchemas(agentContext, new Set(credentialObjects.map((c) => c.schemaId))) + const credentialDefinitions = await this.getCredentialDefinitions( + agentContext, + new Set(credentialObjects.map((c) => c.credentialDefinitionId)) + ) + + // selectedCredentials are overridden with specified timestamps of the revocation status list that + // should be used for the selected credentials. + const { revocationRegistries, updatedSelectedCredentials } = await getRevocationRegistriesForRequest( + agentContext, + proofRequest, + selectedCredentials + ) + + return await holderService.createProof(agentContext, { + proofRequest, + selectedCredentials: updatedSelectedCredentials, + schemas, + credentialDefinitions, + revocationRegistries, + }) + } + + /** + * Returns an object of type {@link Attachment} for use in credential exchange messages. + * It looks up the correct format identifier and encodes the data as a base64 attachment. + * + * @param data The data to include in the attach object + * @param id the attach id from the formats component of the message + */ + private getFormatData(data: unknown, id: string): Attachment { + const attachment = new Attachment({ + id, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(data), + }), + }) + + return attachment + } +} diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts new file mode 100644 index 0000000000..ce365aea0b --- /dev/null +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -0,0 +1,1178 @@ +import type { AnonCredsRevocationStatusList } from '../models' +import type { AnonCredsIssuerService, AnonCredsHolderService } from '../services' +import type { AnonCredsClaimRecord } from '../utils/credential' +import type { AnonCredsCredentialMetadata, AnonCredsCredentialRequestMetadata } from '../utils/metadata' +import type { + DataIntegrityCredentialRequest, + AnonCredsLinkSecretBindingMethod, + DidCommSignedAttachmentBindingMethod, + DataIntegrityCredentialRequestBindingProof, + W3C_VC_DATA_MODEL_VERSION, + DataIntegrityCredential, + AnonCredsLinkSecretDataIntegrityBindingProof, + DidCommSignedAttachmentDataIntegrityBindingProof, + DataIntegrityOfferCredentialFormat, + DataIntegrityCredentialFormat, + CredentialFormatService, + AgentContext, + CredentialFormatCreateProposalOptions, + CredentialFormatCreateProposalReturn, + CredentialFormatProcessOptions, + CredentialFormatAcceptProposalOptions, + CredentialFormatCreateOfferReturn, + CredentialFormatCreateOfferOptions, + CredentialFormatAcceptOfferOptions, + CredentialFormatCreateReturn, + CredentialFormatAcceptRequestOptions, + CredentialFormatProcessCredentialOptions, + CredentialFormatAutoRespondProposalOptions, + CredentialFormatAutoRespondOfferOptions, + CredentialFormatAutoRespondRequestOptions, + CredentialFormatAutoRespondCredentialOptions, + CredentialExchangeRecord, + CredentialPreviewAttributeOptions, + JsonObject, + JwaSignatureAlgorithm, + JwsDetachedFormat, + VerificationMethod, + W3cCredentialRecord, +} from '@credo-ts/core' + +import { + ProblemReportError, + CredentialFormatSpec, + Attachment, + JsonEncoder, + CredentialProblemReportReason, + JsonTransformer, + W3cCredential, + DidsApi, + W3cCredentialService, + W3cJsonLdVerifiableCredential, + getJwkClassFromKeyType, + AttachmentData, + JwsService, + getKeyFromVerificationMethod, + getJwkFromKey, + ClaimFormat, + JwtPayload, + SignatureSuiteRegistry, + CredentialPreviewAttribute, + CredoError, + deepEquality, + DataIntegrityCredentialOffer, + W3cCredentialSubject, +} from '@credo-ts/core' + +import { + AnonCredsCredentialDefinitionRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryState, +} from '../repository' +import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' +import { + dateToTimestamp, + fetchCredentialDefinition, + fetchRevocationRegistryDefinition, + fetchRevocationStatusList, + fetchSchema, +} from '../utils' +import { + convertAttributesToCredentialValues, + assertAttributesMatch as assertAttributesMatchSchema, +} from '../utils/credential' +import { AnonCredsCredentialMetadataKey, AnonCredsCredentialRequestMetadataKey } from '../utils/metadata' +import { getAnonCredsTagsFromRecord } from '../utils/w3cAnonCredsUtils' + +const W3C_DATA_INTEGRITY_CREDENTIAL_OFFER = 'didcomm/w3c-di-vc-offer@v0.1' +const W3C_DATA_INTEGRITY_CREDENTIAL_REQUEST = 'didcomm/w3c-di-vc-request@v0.1' +const W3C_DATA_INTEGRITY_CREDENTIAL = 'didcomm/w3c-di-vc@v0.1' + +export class DataIntegrityCredentialFormatService implements CredentialFormatService { + /** formatKey is the key used when calling agent.credentials.xxx with credentialFormats.anoncreds */ + public readonly formatKey = 'dataIntegrity' as const + + /** + * credentialRecordType is the type of record that stores the credential. It is stored in the credential + * record binding in the credential exchange record. + */ + public readonly credentialRecordType = 'w3c' as const + + /** + * Create a {@link AttachmentFormats} object dependent on the message type. + * + * @param options The object containing all the options for the proposed credential + * @returns object containing associated attachment, format and optionally the credential preview + * + */ + public async createProposal( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { credentialFormats, credentialRecord }: CredentialFormatCreateProposalOptions + ): Promise { + throw new CredoError('Not defined') + } + + public async processProposal( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { attachment }: CredentialFormatProcessOptions + ): Promise { + throw new CredoError('Not defined') + } + + public async acceptProposal( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + input: CredentialFormatAcceptProposalOptions + ): Promise { + throw new CredoError('Not defined') + } + + /** + * Create a credential attachment format for a credential request. + * + * @param options The object containing all the options for the credential offer + * @returns object containing associated attachment, formats and offersAttach elements + * + */ + public async createOffer( + agentContext: AgentContext, + { + credentialFormats, + credentialRecord, + attachmentId, + }: CredentialFormatCreateOfferOptions + ): Promise { + const dataIntegrityFormat = credentialFormats.dataIntegrity + if (!dataIntegrityFormat) throw new CredoError('Missing data integrity credential format data') + + const format = new CredentialFormatSpec({ + attachmentId: attachmentId, + format: W3C_DATA_INTEGRITY_CREDENTIAL_OFFER, + }) + + const credential = dataIntegrityFormat.credential + if ('proof' in credential) throw new CredoError('The offered credential MUST NOT contain any proofs.') + + const { dataIntegrityCredentialOffer, previewAttributes } = await this.createDataIntegrityCredentialOffer( + agentContext, + credentialRecord, + dataIntegrityFormat + ) + + const attachment = this.getFormatData(JsonTransformer.toJSON(dataIntegrityCredentialOffer), format.attachmentId) + return { format, attachment, previewAttributes } + } + + private getCredentialVersion(credentialJson: JsonObject): W3C_VC_DATA_MODEL_VERSION { + const context = credentialJson['@context'] + if (!context || !Array.isArray(context)) throw new CredoError('Invalid @context in credential offer') + + const isV1Credential = context.find((c) => c === 'https://www.w3.org/2018/credentials/v1') + const isV2Credential = context.find((c) => c === 'https://www.w3.org/ns/credentials/v2') + + if (isV1Credential) return '1.1' + else if (isV2Credential) throw new CredoError('Received w3c credential with unsupported version 2.0.') + else throw new CredoError('Cannot determine credential version from @context') + } + + public async processOffer( + agentContext: AgentContext, + { attachment, credentialRecord }: CredentialFormatProcessOptions + ) { + agentContext.config.logger.debug( + `Processing data integrity credential offer for credential record ${credentialRecord.id}` + ) + + const dataIntegrityCredentialOffer = JsonTransformer.fromJSON( + attachment.getDataAsJson(), + DataIntegrityCredentialOffer + ) + + const credentialJson = dataIntegrityCredentialOffer.credential + const credentialVersion = this.getCredentialVersion(credentialJson) + + const credentialToBeValidated = { + ...credentialJson, + issuer: credentialJson.issuer ?? 'https://example.com', + ...(credentialVersion === '1.1' + ? { issuanceDate: new Date().toISOString() } + : { validFrom: new Date().toISOString() }), + } + + JsonTransformer.fromJSON(credentialToBeValidated, W3cCredential) + + const missingBindingMethod = + dataIntegrityCredentialOffer.bindingRequired && + !dataIntegrityCredentialOffer.bindingMethod?.anoncredsLinkSecret && + !dataIntegrityCredentialOffer.bindingMethod?.didcommSignedAttachment + + if (missingBindingMethod) { + throw new ProblemReportError('Invalid credential offer. Missing binding method.', { + problemCode: CredentialProblemReportReason.IssuanceAbandoned, + }) + } + } + + private async createSignedAttachment( + agentContext: AgentContext, + data: { nonce: string }, + options: { alg?: string; kid: string }, + issuerSupportedAlgs: string[] + ) { + const { alg, kid } = options + + if (!kid.startsWith('did:')) { + throw new CredoError(`kid '${kid}' is not a DID. Only dids are supported for kid`) + } else if (!kid.includes('#')) { + throw new CredoError( + `kid '${kid}' does not contain a fragment. kid MUST point to a specific key in the did document.` + ) + } + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(kid) + const verificationMethod = didDocument.dereferenceKey(kid) + const key = getKeyFromVerificationMethod(verificationMethod) + const jwk = getJwkFromKey(key) + + if (alg && !jwk.supportsSignatureAlgorithm(alg)) { + throw new CredoError(`key type '${jwk.keyType}', does not support the JWS signature alg '${alg}'`) + } + + const signingAlg = issuerSupportedAlgs.find( + (supportedAlg) => jwk.supportsSignatureAlgorithm(supportedAlg) && (alg === undefined || alg === supportedAlg) + ) + if (!signingAlg) throw new CredoError('No signing algorithm supported by the issuer found') + + const jwsService = agentContext.dependencyManager.resolve(JwsService) + const jws = await jwsService.createJws(agentContext, { + key, + header: {}, + payload: new JwtPayload({ additionalClaims: { nonce: data.nonce } }), + protectedHeaderOptions: { alg: signingAlg, kid }, + }) + + const signedAttach = new Attachment({ + mimeType: 'application/json', + data: new AttachmentData({ base64: jws.payload }), + }) + + signedAttach.addJws(jws) + + return signedAttach + } + + private async getSignedAttachmentPayload(agentContext: AgentContext, signedAttachment: Attachment) { + const jws = signedAttachment.data.jws as JwsDetachedFormat + if (!jws) throw new CredoError('Missing jws in signed attachment') + if (!jws.protected) throw new CredoError('Missing protected header in signed attachment') + if (!signedAttachment.data.base64) throw new CredoError('Missing payload in signed attachment') + + const jwsService = agentContext.dependencyManager.resolve(JwsService) + const { isValid } = await jwsService.verifyJws(agentContext, { + jws: { + header: jws.header, + protected: jws.protected, + signature: jws.signature, + payload: signedAttachment.data.base64, + }, + jwkResolver: async ({ protectedHeader: { kid } }) => { + if (!kid || typeof kid !== 'string') throw new CredoError('Missing kid in protected header.') + if (!kid.startsWith('did:')) throw new CredoError('Only did is supported for kid identifier') + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(kid) + const verificationMethod = didDocument.dereferenceKey(kid) + const key = getKeyFromVerificationMethod(verificationMethod) + return getJwkFromKey(key) + }, + }) + + if (!isValid) throw new CredoError('Failed to validate signature of signed attachment') + const payload = JsonEncoder.fromBase64(signedAttachment.data.base64) as { nonce: string } + if (!payload.nonce || typeof payload.nonce !== 'string') { + throw new CredoError('Invalid payload in signed attachment') + } + + return payload + } + + public async acceptOffer( + agentContext: AgentContext, + { + credentialRecord, + attachmentId, + offerAttachment, + credentialFormats, + }: CredentialFormatAcceptOfferOptions + ): Promise { + const dataIntegrityFormat = credentialFormats?.dataIntegrity + + const credentialOffer = JsonTransformer.fromJSON(offerAttachment.getDataAsJson(), DataIntegrityCredentialOffer) + + let anonCredsLinkSecretDataIntegrityBindingProof: AnonCredsLinkSecretDataIntegrityBindingProof | undefined = + undefined + const autoAcceptOfferWithAnonCredsLinkSecretMethod = + credentialOffer.bindingRequired && + !dataIntegrityFormat?.didCommSignedAttachment && + credentialOffer.bindingMethod?.anoncredsLinkSecret + + if (dataIntegrityFormat?.anonCredsLinkSecret || autoAcceptOfferWithAnonCredsLinkSecretMethod) { + if (!credentialOffer.bindingMethod?.anoncredsLinkSecret) { + throw new CredoError('Cannot request credential with a binding method that was not offered.') + } + + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const credentialDefinitionId = credentialOffer.bindingMethod.anoncredsLinkSecret.credentialDefinitionId + const credentialDefinitionReturn = await fetchCredentialDefinition(agentContext, credentialDefinitionId) + + const { + credentialRequest: anonCredsCredentialRequest, + credentialRequestMetadata: anonCredsCredentialRequestMetadata, + } = await anonCredsHolderService.createCredentialRequest(agentContext, { + credentialOffer: { + schema_id: credentialDefinitionReturn.credentialDefinition.schemaId, + cred_def_id: credentialOffer.bindingMethod.anoncredsLinkSecret.credentialDefinitionId, + key_correctness_proof: credentialOffer.bindingMethod.anoncredsLinkSecret.keyCorrectnessProof, + nonce: credentialOffer.bindingMethod.anoncredsLinkSecret.nonce, + }, + credentialDefinition: credentialDefinitionReturn.credentialDefinition, + linkSecretId: dataIntegrityFormat?.anonCredsLinkSecret?.linkSecretId, + }) + + if (!anonCredsCredentialRequest.entropy) throw new CredoError('Missing entropy for anonCredsCredentialRequest') + anonCredsLinkSecretDataIntegrityBindingProof = + anonCredsCredentialRequest as AnonCredsLinkSecretDataIntegrityBindingProof + + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { + credentialDefinitionId: credentialOffer.bindingMethod.anoncredsLinkSecret.credentialDefinitionId, + schemaId: credentialDefinitionReturn.credentialDefinition.schemaId, + }) + credentialRecord.metadata.set( + AnonCredsCredentialRequestMetadataKey, + anonCredsCredentialRequestMetadata + ) + } + + let didCommSignedAttachmentBindingProof: DidCommSignedAttachmentDataIntegrityBindingProof | undefined = undefined + let didCommSignedAttachment: Attachment | undefined = undefined + if (dataIntegrityFormat?.didCommSignedAttachment) { + if (!credentialOffer.bindingMethod?.didcommSignedAttachment) { + throw new CredoError('Cannot request credential with a binding method that was not offered.') + } + + didCommSignedAttachment = await this.createSignedAttachment( + agentContext, + { nonce: credentialOffer.bindingMethod.didcommSignedAttachment.nonce }, + dataIntegrityFormat.didCommSignedAttachment, + credentialOffer.bindingMethod.didcommSignedAttachment.algsSupported + ) + + didCommSignedAttachmentBindingProof = { attachment_id: didCommSignedAttachment.id } + } + + const bindingProof: DataIntegrityCredentialRequestBindingProof | undefined = + !anonCredsLinkSecretDataIntegrityBindingProof && !didCommSignedAttachmentBindingProof + ? undefined + : { + anoncreds_link_secret: anonCredsLinkSecretDataIntegrityBindingProof, + didcomm_signed_attachment: didCommSignedAttachmentBindingProof, + } + + if (credentialOffer.bindingRequired && !bindingProof) throw new CredoError('Missing required binding proof') + + const dataModelVersion = dataIntegrityFormat?.dataModelVersion ?? credentialOffer.dataModelVersionsSupported[0] + if (!credentialOffer.dataModelVersionsSupported.includes(dataModelVersion)) { + throw new CredoError('Cannot request credential with a data model version that was not offered.') + } + + const credentialRequest: DataIntegrityCredentialRequest = { + data_model_version: dataModelVersion, + binding_proof: bindingProof, + } + + const format = new CredentialFormatSpec({ + attachmentId, + format: W3C_DATA_INTEGRITY_CREDENTIAL_REQUEST, + }) + + const attachment = this.getFormatData(credentialRequest, format.attachmentId) + return { format, attachment, appendAttachments: didCommSignedAttachment ? [didCommSignedAttachment] : undefined } + } + + /** + * Starting from a request is not supported for anoncreds credentials, this method only throws an error. + */ + public async createRequest(): Promise { + throw new CredoError('Starting from a request is not supported for w3c credentials') + } + + /** + * We don't have any models to validate an anoncreds request object, for now this method does nothing + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async processRequest(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise { + // not needed for dataIntegrity + } + + private async createCredentialWithAnonCredsDataIntegrityProof( + agentContext: AgentContext, + input: { + credentialRecord: CredentialExchangeRecord + anonCredsLinkSecretBindingMethod: AnonCredsLinkSecretBindingMethod + anonCredsLinkSecretBindingProof: AnonCredsLinkSecretDataIntegrityBindingProof + linkSecretMetadata: AnonCredsCredentialMetadata + credentialSubjectId?: string + } + ): Promise { + const { + credentialRecord, + anonCredsLinkSecretBindingMethod, + anonCredsLinkSecretBindingProof, + linkSecretMetadata, + credentialSubjectId, + } = input + + const credentialAttributes = credentialRecord.credentialAttributes + if (!credentialAttributes) { + throw new CredoError( + `Missing required credential attribute values on credential record with id ${credentialRecord.id}` + ) + } + + const credentialSubjectIdAttribute = credentialAttributes.find((ca) => ca.name === 'id') + if ( + credentialSubjectId && + credentialSubjectIdAttribute && + credentialSubjectIdAttribute.value !== credentialSubjectId + ) { + throw new CredoError('Invalid credential subject id.') + } else if (!credentialSubjectIdAttribute && credentialSubjectId) { + credentialAttributes.push(new CredentialPreviewAttribute({ name: 'id', value: credentialSubjectId })) + } + + const anonCredsIssuerService = + agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol) + + const credentialDefinition = ( + await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, linkSecretMetadata.credentialDefinitionId as string) + ).credentialDefinition.value + + // We check locally for credential definition info. If it supports revocation, we need to search locally for + // an active revocation registry + let revocationRegistryDefinitionId: string | undefined = undefined + let revocationRegistryIndex: number | undefined = undefined + let revocationStatusList: AnonCredsRevocationStatusList | undefined = undefined + + if (credentialDefinition.revocation) { + const { credentialRevocationId, revocationRegistryId } = linkSecretMetadata + + if (!credentialRevocationId || !revocationRegistryId) { + throw new CredoError( + 'Revocation registry definition id and revocation index are mandatory to issue AnonCreds revocable credentials' + ) + } + + revocationRegistryDefinitionId = revocationRegistryId + revocationRegistryIndex = Number(credentialRevocationId) + + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) + + if (revocationRegistryDefinitionPrivateRecord.state !== AnonCredsRevocationRegistryState.Active) { + throw new CredoError( + `Revocation registry ${revocationRegistryDefinitionId} is in ${revocationRegistryDefinitionPrivateRecord.state} state` + ) + } + + const revocationStatusListResult = await fetchRevocationStatusList( + agentContext, + revocationRegistryDefinitionId, + dateToTimestamp(new Date()) + ) + + revocationStatusList = revocationStatusListResult.revocationStatusList + } + + const { credential } = await anonCredsIssuerService.createCredential(agentContext, { + credentialOffer: { + schema_id: linkSecretMetadata.schemaId as string, + cred_def_id: anonCredsLinkSecretBindingMethod.credentialDefinitionId, + key_correctness_proof: anonCredsLinkSecretBindingMethod.keyCorrectnessProof, + nonce: anonCredsLinkSecretBindingMethod.nonce, + }, + credentialRequest: anonCredsLinkSecretBindingProof, + credentialValues: convertAttributesToCredentialValues(credentialAttributes), + revocationRegistryDefinitionId, + revocationRegistryIndex, + revocationStatusList, + }) + + const { credentialDefinition: anoncredsCredentialDefinition } = await fetchCredentialDefinition( + agentContext, + credential.cred_def_id + ) + + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + return await anonCredsHolderService.legacyToW3cCredential(agentContext, { + credential, + issuerId: anoncredsCredentialDefinition.issuerId, + }) + } + + private async getSignatureMetadata( + agentContext: AgentContext, + offeredCredential: W3cCredential, + issuerVerificationMethod?: string + ) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(offeredCredential.issuerId) + + let verificationMethod: VerificationMethod + if (issuerVerificationMethod) { + verificationMethod = didDocument.dereferenceKey(issuerVerificationMethod, ['authentication', 'assertionMethod']) + } else { + const vms = didDocument.authentication ?? didDocument.assertionMethod ?? didDocument.verificationMethod + if (!vms || vms.length === 0) { + throw new CredoError('Missing authenticationMethod, assertionMethod, and verificationMethods in did document') + } + + if (typeof vms[0] === 'string') { + verificationMethod = didDocument.dereferenceVerificationMethod(vms[0]) + } else { + verificationMethod = vms[0] + } + } + + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + const signatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) + if (!signatureSuite) { + throw new CredoError(`Could not find signature suite for verification method type ${verificationMethod.type}`) + } + + return { verificationMethod, signatureSuite, offeredCredential } + } + + private async assertAndSetCredentialSubjectId(credential: W3cCredential, credentialSubjectId: string | undefined) { + if (!credentialSubjectId) return credential + + if (Array.isArray(credential.credentialSubject)) { + throw new CredoError('Invalid credential subject relation. Cannot determine the subject to be updated.') + } + + const subjectId = credential.credentialSubject.id + if (subjectId && credentialSubjectId !== subjectId) { + throw new CredoError('Invalid credential subject id.') + } + + if (!subjectId) credential.credentialSubject.id = credentialSubjectId + + return credential + } + + private async signCredential( + agentContext: AgentContext, + credential: W3cCredential | W3cJsonLdVerifiableCredential, + issuerVerificationMethod?: string + ) { + const { signatureSuite, verificationMethod } = await this.getSignatureMetadata( + agentContext, + credential, + issuerVerificationMethod + ) + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + + let credentialToBeSigned = credential + if (credential instanceof W3cJsonLdVerifiableCredential) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { proof, ..._credentialToBeSigned } = credential + credentialToBeSigned = _credentialToBeSigned as W3cCredential + } + + const w3cJsonLdVerifiableCredential = (await w3cCredentialService.signCredential(agentContext, { + format: ClaimFormat.LdpVc, + credential: credentialToBeSigned as W3cCredential, + proofType: signatureSuite.proofType, + verificationMethod: verificationMethod.id, + })) as W3cJsonLdVerifiableCredential + + if (Array.isArray(w3cJsonLdVerifiableCredential.proof)) { + throw new CredoError('A newly signed credential can not have multiple proofs') + } + + if (credential instanceof W3cJsonLdVerifiableCredential) { + const combinedProofs = Array.isArray(credential.proof) ? credential.proof : [credential.proof] + combinedProofs.push(w3cJsonLdVerifiableCredential.proof) + w3cJsonLdVerifiableCredential.proof = combinedProofs + } + return w3cJsonLdVerifiableCredential + } + + public async acceptRequest( + agentContext: AgentContext, + { + credentialFormats, + credentialRecord, + attachmentId, + offerAttachment, + requestAttachment, + requestAppendAttachments, + }: CredentialFormatAcceptRequestOptions + ): Promise { + const dataIntegrityFormat = credentialFormats?.dataIntegrity + + const credentialOffer = JsonTransformer.fromJSON(offerAttachment?.getDataAsJson(), DataIntegrityCredentialOffer) + + const assertedCredential = await this.assertAndSetCredentialSubjectId( + JsonTransformer.fromJSON(credentialOffer.credential, W3cCredential), + dataIntegrityFormat?.credentialSubjectId + ) + + const credentialRequest = requestAttachment.getDataAsJson() + if (!credentialRequest) throw new CredoError('Missing data integrity credential request in createCredential') + + let signedCredential: W3cJsonLdVerifiableCredential | undefined + if (credentialRequest.binding_proof?.anoncreds_link_secret) { + if (!credentialOffer.bindingMethod?.anoncredsLinkSecret) { + throw new CredoError('Cannot issue credential with a binding method that was not offered') + } + + const linkSecretMetadata = + credentialRecord.metadata.get(AnonCredsCredentialMetadataKey) + if (!linkSecretMetadata) throw new CredoError('Missing anoncreds link secret metadata') + + signedCredential = await this.createCredentialWithAnonCredsDataIntegrityProof(agentContext, { + credentialRecord, + anonCredsLinkSecretBindingMethod: credentialOffer.bindingMethod.anoncredsLinkSecret, + linkSecretMetadata, + anonCredsLinkSecretBindingProof: credentialRequest.binding_proof.anoncreds_link_secret, + credentialSubjectId: dataIntegrityFormat?.credentialSubjectId, + }) + } + + if (credentialRequest.binding_proof?.didcomm_signed_attachment) { + if (!credentialOffer.bindingMethod?.didcommSignedAttachment) { + throw new CredoError('Cannot issue credential with a binding method that was not offered') + } + + const bindingProofAttachment = requestAppendAttachments?.find( + (attachments) => attachments.id === credentialRequest.binding_proof?.didcomm_signed_attachment?.attachment_id + ) + if (!bindingProofAttachment) throw new CredoError('Missing binding proof attachment') + + const { nonce } = await this.getSignedAttachmentPayload(agentContext, bindingProofAttachment) + if (nonce !== credentialOffer.bindingMethod.didcommSignedAttachment.nonce) { + throw new CredoError('Invalid nonce in signed attachment') + } + + signedCredential = await this.signCredential( + agentContext, + signedCredential ?? assertedCredential, + dataIntegrityFormat?.issuerVerificationMethod + ) + } + + if ( + !credentialRequest.binding_proof?.anoncreds_link_secret && + !credentialRequest.binding_proof?.didcomm_signed_attachment + ) { + signedCredential = await this.signCredential(agentContext, assertedCredential) + } + + const format = new CredentialFormatSpec({ + attachmentId, + format: W3C_DATA_INTEGRITY_CREDENTIAL, + }) + + const attachment = this.getFormatData({ credential: JsonTransformer.toJSON(signedCredential) }, format.attachmentId) + return { format, attachment } + } + + private async storeAnonCredsCredential( + agentContext: AgentContext, + credentialJson: JsonObject, + credentialRecord: CredentialExchangeRecord, + linkSecretRequestMetadata: AnonCredsCredentialRequestMetadata + ) { + if (!credentialRecord.credentialAttributes) { + throw new CredoError('Missing credential attributes on credential record. Unable to check credential attributes') + } + + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const legacyAnonCredsCredential = await anonCredsHolderService.w3cToLegacyCredential(agentContext, { + credential: JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential), + }) + + const { + schema_id: schemaId, + cred_def_id: credentialDefinitionId, + rev_reg_id: revocationRegistryId, + } = legacyAnonCredsCredential + + const schemaReturn = await fetchSchema(agentContext, schemaId) + const credentialDefinitionReturn = await fetchCredentialDefinition(agentContext, credentialDefinitionId) + const revocationRegistryDefinitionReturn = revocationRegistryId + ? await fetchRevocationRegistryDefinition(agentContext, revocationRegistryId) + : undefined + + // This is required to process the credential + const w3cJsonLdVerifiableCredential = await anonCredsHolderService.legacyToW3cCredential(agentContext, { + credential: legacyAnonCredsCredential, + issuerId: credentialJson.issuer as string, + processOptions: { + credentialRequestMetadata: linkSecretRequestMetadata, + credentialDefinition: credentialDefinitionReturn.credentialDefinition, + revocationRegistryDefinition: revocationRegistryDefinitionReturn?.revocationRegistryDefinition, + }, + }) + + const w3cCredentialRecordId = await anonCredsHolderService.storeCredential(agentContext, { + credential: w3cJsonLdVerifiableCredential, + schema: schemaReturn.schema, + credentialDefinitionId, + credentialDefinition: credentialDefinitionReturn.credentialDefinition, + credentialRequestMetadata: linkSecretRequestMetadata, + revocationRegistry: revocationRegistryDefinitionReturn + ? { + id: revocationRegistryId as string, + definition: revocationRegistryDefinitionReturn?.revocationRegistryDefinition, + } + : undefined, + }) + + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const w3cCredentialRecord = await w3cCredentialService.getCredentialRecordById(agentContext, w3cCredentialRecordId) + + // If the credential is revocable, store the revocation identifiers in the credential record + if (revocationRegistryId) { + const linkSecretMetadata = + credentialRecord.metadata.get(AnonCredsCredentialMetadataKey) + if (!linkSecretMetadata) throw new CredoError('Missing link secret metadata') + + const anonCredsTags = await getAnonCredsTagsFromRecord(w3cCredentialRecord) + if (!anonCredsTags) throw new CredoError('Missing anoncreds tags on credential record.') + + linkSecretMetadata.revocationRegistryId = revocationRegistryDefinitionReturn?.revocationRegistryDefinitionId + linkSecretMetadata.credentialRevocationId = anonCredsTags.anonCredsCredentialRevocationId?.toString() + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, linkSecretMetadata) + } + + return w3cCredentialRecord + } + + /** + * Processes an incoming credential - retrieve metadata, retrieve payload and store it in wallet + * @param options the issue credential message wrapped inside this object + * @param credentialRecord the credential exchange record for this credential + */ + public async processCredential( + agentContext: AgentContext, + { credentialRecord, attachment, requestAttachment, offerAttachment }: CredentialFormatProcessCredentialOptions + ): Promise { + const credentialOffer = JsonTransformer.fromJSON(offerAttachment.getDataAsJson(), DataIntegrityCredentialOffer) + const offeredCredentialJson = credentialOffer.credential + + const credentialRequest = requestAttachment.getDataAsJson() + if (!credentialRequest) throw new CredoError('Missing data integrity credential request in createCredential') + + if (!credentialRecord.credentialAttributes) { + throw new CredoError('Missing credential attributes on credential record.') + } + + const { credential: credentialJson } = attachment.getDataAsJson() + + if (Array.isArray(offeredCredentialJson.credentialSubject)) { + throw new CredoError('Invalid credential subject. Multiple credential subjects are not yet supported.') + } + + const credentialSubjectMatches = Object.entries(offeredCredentialJson.credentialSubject as JsonObject).every( + ([key, offeredValue]) => { + const receivedValue = (credentialJson.credentialSubject as JsonObject)[key] + if (!offeredValue || !receivedValue) return false + + if (typeof offeredValue === 'number' || typeof receivedValue === 'number') { + return offeredValue.toString() === receivedValue.toString() + } + + return deepEquality(offeredValue, receivedValue) + } + ) + + if (!credentialSubjectMatches) { + throw new CredoError( + 'Received invalid credential. Received credential subject does not match the offered credential subject.' + ) + } + + const credentialVersion = this.getCredentialVersion(credentialJson) + const expectedReceivedCredential = { + ...offeredCredentialJson, + '@context': credentialJson['@context'], + issuer: offeredCredentialJson.issuer ?? credentialJson.issuer, + credentialSubject: credentialJson.credentialSubject, + ...(credentialVersion === '1.1' && { issuanceDate: credentialJson.issuanceDate }), + ...(credentialVersion === '2.0' && { validFrom: credentialJson.validFrom }), + ...(offeredCredentialJson.credentialStatus && { credentialStatus: credentialJson.credentialStatus }), + proof: credentialJson.proof, + } + + if (!deepEquality(credentialJson, expectedReceivedCredential)) { + throw new CredoError('Received invalid credential. Received credential does not match the offered credential') + } + + let w3cCredentialRecord: W3cCredentialRecord + if (credentialRequest.binding_proof?.anoncreds_link_secret) { + const linkSecretRequestMetadata = credentialRecord.metadata.get( + AnonCredsCredentialRequestMetadataKey + ) + if (!linkSecretRequestMetadata) { + throw new CredoError('Missing link secret request metadata') + } + + const integrityProtectedFields = ['@context', 'issuer', 'type', 'credentialSubject', 'validFrom', 'issuanceDate'] + if ( + Object.keys(offeredCredentialJson).some((key) => !integrityProtectedFields.includes(key) && key !== 'proof') + ) { + throw new CredoError('Credential offer contains non anoncreds integrity protected fields.') + } + + if (!Array.isArray(offeredCredentialJson.type) || offeredCredentialJson?.type.length !== 1) { + throw new CredoError(`Invalid credential type. Only single credential type 'VerifiableCredential' is supported`) + } + + w3cCredentialRecord = await this.storeAnonCredsCredential( + agentContext, + credentialJson, + credentialRecord, + linkSecretRequestMetadata + ) + + await this.assertCredentialAttributesMatchSchemaAttributes( + agentContext, + w3cCredentialRecord.credential, + getAnonCredsTagsFromRecord(w3cCredentialRecord)?.anonCredsSchemaId as string, + true + ) + } else { + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential) + w3cCredentialRecord = await w3cCredentialService.storeCredential(agentContext, { + credential: w3cJsonLdVerifiableCredential, + }) + } + + credentialRecord.credentials.push({ + credentialRecordType: this.credentialRecordType, + credentialRecordId: w3cCredentialRecord.id, + }) + } + + public supportsFormat(format: string): boolean { + const supportedFormats = [ + W3C_DATA_INTEGRITY_CREDENTIAL_REQUEST, + W3C_DATA_INTEGRITY_CREDENTIAL_OFFER, + W3C_DATA_INTEGRITY_CREDENTIAL, + ] + + return supportedFormats.includes(format) + } + + /** + * Gets the attachment object for a given attachmentId. We need to get out the correct attachmentId for + * anoncreds and then find the corresponding attachment (if there is one) + * @param formats the formats object containing the attachmentId + * @param messageAttachments the attachments containing the payload + * @returns The Attachment if found or undefined + * + */ + public getAttachment(formats: CredentialFormatSpec[], messageAttachments: Attachment[]): Attachment | undefined { + const supportedAttachmentIds = formats.filter((f) => this.supportsFormat(f.format)).map((f) => f.attachmentId) + const supportedAttachment = messageAttachments.find((attachment) => supportedAttachmentIds.includes(attachment.id)) + + return supportedAttachment + } + + public async deleteCredentialById(agentContext: AgentContext, credentialRecordId: string): Promise { + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + await anonCredsHolderService.deleteCredential(agentContext, credentialRecordId) + } + + public async shouldAutoRespondToProposal( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { offerAttachment, proposalAttachment }: CredentialFormatAutoRespondProposalOptions + ) { + throw new CredoError('Not implemented') + return false + } + + public async shouldAutoRespondToOffer( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { offerAttachment }: CredentialFormatAutoRespondOfferOptions + ) { + const credentialOffer = JsonTransformer.fromJSON(offerAttachment.getDataAsJson(), DataIntegrityCredentialOffer) + if (!credentialOffer.bindingRequired) return true + return false + } + + public async shouldAutoRespondToRequest( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + { offerAttachment, requestAttachment }: CredentialFormatAutoRespondRequestOptions + ) { + const credentialOffer = JsonTransformer.fromJSON(offerAttachment?.getDataAsJson(), DataIntegrityCredentialOffer) + const credentialRequest = requestAttachment.getDataAsJson() + + if ( + !credentialOffer.bindingRequired && + !credentialRequest.binding_proof?.anoncreds_link_secret && + !credentialRequest.binding_proof?.didcomm_signed_attachment + ) { + return true + } + + if ( + credentialOffer.bindingRequired && + !credentialRequest.binding_proof?.anoncreds_link_secret && + !credentialRequest.binding_proof?.didcomm_signed_attachment + ) { + return false + } + + // cannot auto response credential subject id must be set manually + if (credentialRequest.binding_proof?.didcomm_signed_attachment) { + try { + const subjectJson = credentialOffer.credential.credentialSubject + const credentialSubject = JsonTransformer.fromJSON(subjectJson, W3cCredentialSubject) + if (credentialSubject.id === undefined) return false + } catch (e) { + return false + } + } + + const validLinkSecretRequest = + !credentialRequest.binding_proof?.anoncreds_link_secret || + (credentialRequest.binding_proof?.anoncreds_link_secret && credentialOffer.bindingMethod?.anoncredsLinkSecret) + + const validDidCommSignedAttachmetRequest = + !credentialRequest.binding_proof?.didcomm_signed_attachment || + (credentialRequest.binding_proof?.didcomm_signed_attachment && + credentialOffer.bindingMethod?.didcommSignedAttachment) + + return Boolean(validLinkSecretRequest && validDidCommSignedAttachmetRequest) + } + + public async shouldAutoRespondToCredential( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { credentialRecord, requestAttachment, credentialAttachment }: CredentialFormatAutoRespondCredentialOptions + ) { + return true + } + + private async createDataIntegrityCredentialOffer( + agentContext: AgentContext, + credentialRecord: CredentialExchangeRecord, + options: DataIntegrityOfferCredentialFormat + ): Promise<{ + dataIntegrityCredentialOffer: DataIntegrityCredentialOffer + previewAttributes: CredentialPreviewAttributeOptions[] + }> { + const { + bindingRequired, + credential, + anonCredsLinkSecretBinding: anonCredsLinkSecretBindingMethodOptions, + didCommSignedAttachmentBinding: didCommSignedAttachmentBindingMethodOptions, + } = options + + const dataModelVersionsSupported: W3C_VC_DATA_MODEL_VERSION[] = ['1.1'] + + // validate the credential and get the preview attributes + const credentialJson = credential instanceof W3cCredential ? JsonTransformer.toJSON(credential) : credential + const validW3cCredential = JsonTransformer.fromJSON(credentialJson, W3cCredential) + const previewAttributes = this.previewAttributesFromCredential(validW3cCredential) + + let anonCredsLinkSecretBindingMethod: AnonCredsLinkSecretBindingMethod | undefined = undefined + if (anonCredsLinkSecretBindingMethodOptions) { + const { credentialDefinitionId, revocationRegistryDefinitionId, revocationRegistryIndex } = + anonCredsLinkSecretBindingMethodOptions + + const anoncredsCredentialOffer = await agentContext.dependencyManager + .resolve(AnonCredsIssuerServiceSymbol) + .createCredentialOffer(agentContext, { credentialDefinitionId }) + + // We check locally for credential definition info. If it supports revocation, revocationRegistryIndex + // and revocationRegistryDefinitionId are mandatory + const { credentialDefinition } = await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, anoncredsCredentialOffer.cred_def_id) + + if (credentialDefinition.value.revocation) { + if (!revocationRegistryDefinitionId || !revocationRegistryIndex) { + throw new CredoError( + 'AnonCreds revocable credentials require revocationRegistryDefinitionId and revocationRegistryIndex' + ) + } + + // Set revocation tags + credentialRecord.setTags({ + anonCredsRevocationRegistryId: revocationRegistryDefinitionId, + anonCredsCredentialRevocationId: revocationRegistryIndex.toString(), + }) + } + + await this.assertCredentialAttributesMatchSchemaAttributes( + agentContext, + validW3cCredential, + credentialDefinition.schemaId, + false + ) + + anonCredsLinkSecretBindingMethod = { + credentialDefinitionId: anoncredsCredentialOffer.cred_def_id, + keyCorrectnessProof: anoncredsCredentialOffer.key_correctness_proof, + nonce: anoncredsCredentialOffer.nonce, + } + + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { + schemaId: anoncredsCredentialOffer.schema_id, + credentialDefinitionId: credentialDefinitionId, + credentialRevocationId: revocationRegistryIndex?.toString(), + revocationRegistryId: revocationRegistryDefinitionId, + }) + } + + let didCommSignedAttachmentBindingMethod: DidCommSignedAttachmentBindingMethod | undefined = undefined + if (didCommSignedAttachmentBindingMethodOptions) { + const { didMethodsSupported, algsSupported } = didCommSignedAttachmentBindingMethodOptions + didCommSignedAttachmentBindingMethod = { + didMethodsSupported: + didMethodsSupported ?? agentContext.dependencyManager.resolve(DidsApi).supportedResolverMethods, + algsSupported: algsSupported ?? this.getSupportedJwaSignatureAlgorithms(agentContext), + nonce: await agentContext.wallet.generateNonce(), + } + + if (didCommSignedAttachmentBindingMethod.algsSupported.length === 0) { + throw new CredoError('No supported JWA signature algorithms found.') + } + + if (didCommSignedAttachmentBindingMethod.didMethodsSupported.length === 0) { + throw new CredoError('No supported DID methods found.') + } + } + + if (bindingRequired && !anonCredsLinkSecretBindingMethod && !didCommSignedAttachmentBindingMethod) { + throw new CredoError('Missing required binding method.') + } + + const dataIntegrityCredentialOffer = new DataIntegrityCredentialOffer({ + dataModelVersionsSupported, + bindingRequired: bindingRequired, + bindingMethod: { + anoncredsLinkSecret: anonCredsLinkSecretBindingMethod, + didcommSignedAttachment: didCommSignedAttachmentBindingMethod, + }, + credential: credentialJson, + }) + + return { dataIntegrityCredentialOffer, previewAttributes } + } + + private previewAttributesFromCredential(credential: W3cCredential): CredentialPreviewAttributeOptions[] { + if (Array.isArray(credential.credentialSubject)) { + throw new CredoError('Credential subject must be an object.') + } + + const claims = { + ...credential.credentialSubject.claims, + ...(credential.credentialSubject.id && { id: credential.credentialSubject.id }), + } as AnonCredsClaimRecord + const attributes = Object.entries(claims).map(([key, value]): CredentialPreviewAttributeOptions => { + return { name: key, value: value.toString() } + }) + return attributes + } + + private async assertCredentialAttributesMatchSchemaAttributes( + agentContext: AgentContext, + credential: W3cCredential, + schemaId: string, + credentialSubjectIdMustBeSet: boolean + ) { + const attributes = this.previewAttributesFromCredential(credential) + + const schemaReturn = await fetchSchema(agentContext, schemaId) + + const enhancedAttributes = [...attributes] + if ( + !credentialSubjectIdMustBeSet && + schemaReturn.schema.attrNames.includes('id') && + attributes.find((attr) => attr.name === 'id') === undefined + ) + enhancedAttributes.push({ name: 'id', value: 'mock' }) + assertAttributesMatchSchema(schemaReturn.schema, enhancedAttributes) + + return { attributes } + } + + /** + * Returns an object of type {@link Attachment} for use in credential exchange messages. + * It looks up the correct format identifier and encodes the data as a base64 attachment. + * + * @param data The data to include in the attach object + * @param id the attach id from the formats component of the message + */ + public getFormatData(data: unknown, id: string): Attachment { + const attachment = new Attachment({ + id, + mimeType: 'application/json', + data: { + base64: JsonEncoder.toBase64(data), + }, + }) + + return attachment + } + + /** + * Returns the JWA Signature Algorithms that are supported by the wallet. + * + * This is an approximation based on the supported key types of the wallet. + * This is not 100% correct as a supporting a key type does not mean you support + * all the algorithms for that key type. However, this needs refactoring of the wallet + * that is planned for the 0.5.0 release. + */ + private getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { + const supportedKeyTypes = agentContext.wallet.supportedKeyTypes + + // Extract the supported JWS algs based on the key types the wallet support. + const supportedJwaSignatureAlgorithms = supportedKeyTypes + // Map the supported key types to the supported JWK class + .map(getJwkClassFromKeyType) + // Filter out the undefined values + .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) + // Extract the supported JWA signature algorithms from the JWK class + .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) + + return supportedJwaSignatureAlgorithms + } +} diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts new file mode 100644 index 0000000000..5e7ec57ae3 --- /dev/null +++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts @@ -0,0 +1,55 @@ +import type { + AnonCredsAcceptOfferFormat, + AnonCredsAcceptProposalFormat, + AnonCredsAcceptRequestFormat, + AnonCredsCredentialProposalFormat, + AnonCredsOfferCredentialFormat, + AnonCredsProposeCredentialFormat, +} from './AnonCredsCredentialFormat' +import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '../models' +import type { CredentialFormat } from '@credo-ts/core' + +// Legacy indy credential proposal doesn't support _id properties +export type LegacyIndyCredentialProposalFormat = Omit< + AnonCredsCredentialProposalFormat, + 'schema_issuer_id' | 'issuer_id' +> + +/** + * This defines the module payload for calling CredentialsApi.createProposal + * or CredentialsApi.negotiateOffer + * + * NOTE: This doesn't include the `issuerId` and `schemaIssuerId` properties that are present in the newer format. + */ +export type LegacyIndyProposeCredentialFormat = Omit + +export interface LegacyIndyCredentialRequest extends AnonCredsCredentialRequest { + // prover_did is optional in AnonCreds credential request, but required in legacy format + prover_did: string +} + +export interface LegacyIndyCredentialFormat extends CredentialFormat { + formatKey: 'indy' + + credentialRecordType: 'w3c' + + // credential formats are the same as the AnonCreds credential format + credentialFormats: { + // The createProposal interface is different between the interfaces + createProposal: LegacyIndyProposeCredentialFormat + acceptProposal: AnonCredsAcceptProposalFormat + createOffer: AnonCredsOfferCredentialFormat + acceptOffer: AnonCredsAcceptOfferFormat + createRequest: never // cannot start from createRequest + acceptRequest: AnonCredsAcceptRequestFormat + } + + // Format data is based on RFC 0592 + // https://github.com/hyperledger/aries-rfcs/tree/main/features/0592-indy-attachments + formatData: { + proposal: LegacyIndyCredentialProposalFormat + offer: AnonCredsCredentialOffer + request: LegacyIndyCredentialRequest + credential: AnonCredsCredential + } +} diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts new file mode 100644 index 0000000000..07ead68328 --- /dev/null +++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts @@ -0,0 +1,588 @@ +import type { LegacyIndyCredentialFormat, LegacyIndyCredentialProposalFormat } from './LegacyIndyCredentialFormat' +import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '../models' +import type { AnonCredsIssuerService, AnonCredsHolderService } from '../services' +import type { AnonCredsCredentialMetadata, AnonCredsCredentialRequestMetadata } from '../utils/metadata' +import type { + CredentialFormatService, + AgentContext, + CredentialFormatCreateProposalOptions, + CredentialFormatCreateProposalReturn, + CredentialFormatProcessOptions, + CredentialFormatAcceptProposalOptions, + CredentialFormatCreateOfferReturn, + CredentialFormatCreateOfferOptions, + CredentialFormatAcceptOfferOptions, + CredentialFormatCreateReturn, + CredentialFormatAcceptRequestOptions, + CredentialFormatProcessCredentialOptions, + CredentialFormatAutoRespondProposalOptions, + CredentialFormatAutoRespondOfferOptions, + CredentialFormatAutoRespondRequestOptions, + CredentialFormatAutoRespondCredentialOptions, + CredentialExchangeRecord, + CredentialPreviewAttributeOptions, + LinkedAttachment, +} from '@credo-ts/core' + +import { + ProblemReportError, + MessageValidator, + CredentialFormatSpec, + CredoError, + Attachment, + JsonEncoder, + CredentialProblemReportReason, + JsonTransformer, +} from '@credo-ts/core' + +import { AnonCredsCredentialProposal } from '../models/AnonCredsCredentialProposal' +import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' +import { fetchCredentialDefinition, fetchRevocationRegistryDefinition, fetchSchema } from '../utils' +import { + convertAttributesToCredentialValues, + assertCredentialValuesMatch, + checkCredentialValuesMatch, + assertAttributesMatch, + createAndLinkAttachmentsToPreview, +} from '../utils/credential' +import { isUnqualifiedCredentialDefinitionId, isUnqualifiedSchemaId } from '../utils/indyIdentifiers' +import { AnonCredsCredentialMetadataKey, AnonCredsCredentialRequestMetadataKey } from '../utils/metadata' +import { generateLegacyProverDidLikeString } from '../utils/proverDid' +import { getStoreCredentialOptions } from '../utils/w3cAnonCredsUtils' + +const INDY_CRED_ABSTRACT = 'hlindy/cred-abstract@v2.0' +const INDY_CRED_REQUEST = 'hlindy/cred-req@v2.0' +const INDY_CRED_FILTER = 'hlindy/cred-filter@v2.0' +const INDY_CRED = 'hlindy/cred@v2.0' + +export class LegacyIndyCredentialFormatService implements CredentialFormatService { + /** formatKey is the key used when calling agent.credentials.xxx with credentialFormats.indy */ + public readonly formatKey = 'indy' as const + + /** + * credentialRecordType is the type of record that stores the credential. It is stored in the credential + * record binding in the credential exchange record. + */ + public readonly credentialRecordType = 'w3c' as const + + /** + * Create a {@link AttachmentFormats} object dependent on the message type. + * + * @param options The object containing all the options for the proposed credential + * @returns object containing associated attachment, format and optionally the credential preview + * + */ + public async createProposal( + agentContext: AgentContext, + { credentialFormats, credentialRecord }: CredentialFormatCreateProposalOptions + ): Promise { + const format = new CredentialFormatSpec({ + format: INDY_CRED_FILTER, + }) + + const indyFormat = credentialFormats.indy + + if (!indyFormat) { + throw new CredoError('Missing indy payload in createProposal') + } + + // We want all properties except for `attributes` and `linkedAttachments` attributes. + // The easiest way is to destructure and use the spread operator. But that leaves the other properties unused + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { attributes, linkedAttachments, ...indyCredentialProposal } = indyFormat + const proposal = new AnonCredsCredentialProposal(indyCredentialProposal) + + try { + MessageValidator.validateSync(proposal) + } catch (error) { + throw new CredoError(`Invalid proposal supplied: ${indyCredentialProposal} in Indy Format Service`) + } + + const attachment = this.getFormatData(JsonTransformer.toJSON(proposal), format.attachmentId) + + const { previewAttributes } = this.getCredentialLinkedAttachments( + indyFormat.attributes, + indyFormat.linkedAttachments + ) + + // Set the metadata + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { + schemaId: proposal.schemaId, + credentialDefinitionId: proposal.credentialDefinitionId, + }) + + return { format, attachment, previewAttributes } + } + + public async processProposal( + agentContext: AgentContext, + { attachment }: CredentialFormatProcessOptions + ): Promise { + const proposalJson = attachment.getDataAsJson() + + JsonTransformer.fromJSON(proposalJson, AnonCredsCredentialProposal) + } + + public async acceptProposal( + agentContext: AgentContext, + { + attachmentId, + credentialFormats, + credentialRecord, + proposalAttachment, + }: CredentialFormatAcceptProposalOptions + ): Promise { + const indyFormat = credentialFormats?.indy + + const proposalJson = proposalAttachment.getDataAsJson() + const credentialDefinitionId = indyFormat?.credentialDefinitionId ?? proposalJson.cred_def_id + + const attributes = indyFormat?.attributes ?? credentialRecord.credentialAttributes + + if (!credentialDefinitionId) { + throw new CredoError('No credential definition id in proposal or provided as input to accept proposal method.') + } + + if (!isUnqualifiedCredentialDefinitionId(credentialDefinitionId)) { + throw new CredoError(`${credentialDefinitionId} is not a valid legacy indy credential definition id`) + } + + if (!attributes) { + throw new CredoError('No attributes in proposal or provided as input to accept proposal method.') + } + + const { format, attachment, previewAttributes } = await this.createIndyOffer(agentContext, { + credentialRecord, + attachmentId, + attributes, + credentialDefinitionId, + linkedAttachments: indyFormat?.linkedAttachments, + }) + + return { format, attachment, previewAttributes } + } + + /** + * Create a credential attachment format for a credential request. + * + * @param options The object containing all the options for the credential offer + * @returns object containing associated attachment, formats and offersAttach elements + * + */ + public async createOffer( + agentContext: AgentContext, + { + credentialFormats, + credentialRecord, + attachmentId, + }: CredentialFormatCreateOfferOptions + ): Promise { + const indyFormat = credentialFormats.indy + + if (!indyFormat) { + throw new CredoError('Missing indy credentialFormat data') + } + + const { format, attachment, previewAttributes } = await this.createIndyOffer(agentContext, { + credentialRecord, + attachmentId, + attributes: indyFormat.attributes, + credentialDefinitionId: indyFormat.credentialDefinitionId, + linkedAttachments: indyFormat.linkedAttachments, + }) + + return { format, attachment, previewAttributes } + } + + public async processOffer( + agentContext: AgentContext, + { attachment, credentialRecord }: CredentialFormatProcessOptions + ) { + agentContext.config.logger.debug(`Processing indy credential offer for credential record ${credentialRecord.id}`) + + const credOffer = attachment.getDataAsJson() + + if (!isUnqualifiedSchemaId(credOffer.schema_id) || !isUnqualifiedCredentialDefinitionId(credOffer.cred_def_id)) { + throw new ProblemReportError('Invalid credential offer', { + problemCode: CredentialProblemReportReason.IssuanceAbandoned, + }) + } + } + + public async acceptOffer( + agentContext: AgentContext, + { + credentialRecord, + attachmentId, + offerAttachment, + credentialFormats, + }: CredentialFormatAcceptOfferOptions + ): Promise { + const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const credentialOffer = offerAttachment.getDataAsJson() + + if (!isUnqualifiedCredentialDefinitionId(credentialOffer.cred_def_id)) { + throw new CredoError(`${credentialOffer.cred_def_id} is not a valid legacy indy credential definition id`) + } + // Get credential definition + const { credentialDefinition } = await fetchCredentialDefinition(agentContext, credentialOffer.cred_def_id) + + const { credentialRequest, credentialRequestMetadata } = await holderService.createCredentialRequest(agentContext, { + credentialOffer, + credentialDefinition, + linkSecretId: credentialFormats?.indy?.linkSecretId, + useLegacyProverDid: true, + }) + + if (!credentialRequest.prover_did) { + // We just generate a prover did like string, as it's not used for anything and we don't need + // to prove ownership of the did. It's deprecated in AnonCreds v1, but kept for backwards compatibility + credentialRequest.prover_did = generateLegacyProverDidLikeString() + } + + credentialRecord.metadata.set( + AnonCredsCredentialRequestMetadataKey, + credentialRequestMetadata + ) + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { + credentialDefinitionId: credentialOffer.cred_def_id, + schemaId: credentialOffer.schema_id, + }) + + const format = new CredentialFormatSpec({ + attachmentId, + format: INDY_CRED_REQUEST, + }) + + const attachment = this.getFormatData(credentialRequest, format.attachmentId) + return { format, attachment } + } + + /** + * Starting from a request is not supported for indy credentials, this method only throws an error. + */ + public async createRequest(): Promise { + throw new CredoError('Starting from a request is not supported for indy credentials') + } + + /** + * We don't have any models to validate an indy request object, for now this method does nothing + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async processRequest(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise { + // not needed for Indy + } + + public async acceptRequest( + agentContext: AgentContext, + { + credentialRecord, + attachmentId, + offerAttachment, + requestAttachment, + }: CredentialFormatAcceptRequestOptions + ): Promise { + // Assert credential attributes + const credentialAttributes = credentialRecord.credentialAttributes + if (!credentialAttributes) { + throw new CredoError( + `Missing required credential attribute values on credential record with id ${credentialRecord.id}` + ) + } + + const anonCredsIssuerService = + agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol) + + const credentialOffer = offerAttachment?.getDataAsJson() + if (!credentialOffer) throw new CredoError('Missing indy credential offer in createCredential') + + const credentialRequest = requestAttachment.getDataAsJson() + if (!credentialRequest) throw new CredoError('Missing indy credential request in createCredential') + + const { credential } = await anonCredsIssuerService.createCredential(agentContext, { + credentialOffer, + credentialRequest, + credentialValues: convertAttributesToCredentialValues(credentialAttributes), + }) + + const format = new CredentialFormatSpec({ + attachmentId, + format: INDY_CRED, + }) + + const attachment = this.getFormatData(credential, format.attachmentId) + return { format, attachment } + } + + /** + * Processes an incoming credential - retrieve metadata, retrieve payload and store it in the Indy wallet + * @param options the issue credential message wrapped inside this object + * @param credentialRecord the credential exchange record for this credential + */ + public async processCredential( + agentContext: AgentContext, + { credentialRecord, attachment }: CredentialFormatProcessCredentialOptions + ): Promise { + const credentialRequestMetadata = credentialRecord.metadata.get( + AnonCredsCredentialRequestMetadataKey + ) + + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + if (!credentialRequestMetadata) { + throw new CredoError( + `Missing required request metadata for credential exchange with thread id with id ${credentialRecord.id}` + ) + } + + if (!credentialRecord.credentialAttributes) { + throw new CredoError('Missing credential attributes on credential record. Unable to check credential attributes') + } + + const anonCredsCredential = attachment.getDataAsJson() + + const { credentialDefinition, credentialDefinitionId } = await fetchCredentialDefinition( + agentContext, + anonCredsCredential.cred_def_id + ) + + const { schema, indyNamespace } = await fetchSchema(agentContext, anonCredsCredential.schema_id) + + // Resolve revocation registry if credential is revocable + const revocationRegistryResult = anonCredsCredential.rev_reg_id + ? await fetchRevocationRegistryDefinition(agentContext, anonCredsCredential.rev_reg_id) + : undefined + + // assert the credential values match the offer values + const recordCredentialValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes) + assertCredentialValuesMatch(anonCredsCredential.values, recordCredentialValues) + + const storeCredentialOptions = getStoreCredentialOptions( + { + credential: anonCredsCredential, + credentialRequestMetadata, + credentialDefinition, + schema, + credentialDefinitionId, + revocationRegistry: revocationRegistryResult?.revocationRegistryDefinition + ? { + id: revocationRegistryResult.revocationRegistryDefinitionId, + definition: revocationRegistryResult.revocationRegistryDefinition, + } + : undefined, + }, + indyNamespace + ) + + const credentialId = await anonCredsHolderService.storeCredential(agentContext, storeCredentialOptions) + + // If the credential is revocable, store the revocation identifiers in the credential record + if (anonCredsCredential.rev_reg_id) { + const credential = await anonCredsHolderService.getCredential(agentContext, { id: credentialId }) + + credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, { + credentialRevocationId: credential.credentialRevocationId ?? undefined, + revocationRegistryId: credential.revocationRegistryId ?? undefined, + }) + credentialRecord.setTags({ + anonCredsRevocationRegistryId: credential.revocationRegistryId, + anonCredsUnqualifiedRevocationRegistryId: anonCredsCredential.rev_reg_id, + anonCredsCredentialRevocationId: credential.credentialRevocationId, + }) + } + + credentialRecord.credentials.push({ + credentialRecordType: this.credentialRecordType, + credentialRecordId: credentialId, + }) + } + + public supportsFormat(format: string): boolean { + const supportedFormats = [INDY_CRED_ABSTRACT, INDY_CRED_REQUEST, INDY_CRED_FILTER, INDY_CRED] + + return supportedFormats.includes(format) + } + + /** + * Gets the attachment object for a given attachmentId. We need to get out the correct attachmentId for + * indy and then find the corresponding attachment (if there is one) + * @param formats the formats object containing the attachmentId + * @param messageAttachments the attachments containing the payload + * @returns The Attachment if found or undefined + * + */ + public getAttachment(formats: CredentialFormatSpec[], messageAttachments: Attachment[]): Attachment | undefined { + const supportedAttachmentIds = formats.filter((f) => this.supportsFormat(f.format)).map((f) => f.attachmentId) + const supportedAttachment = messageAttachments.find((attachment) => supportedAttachmentIds.includes(attachment.id)) + + return supportedAttachment + } + + public async deleteCredentialById(agentContext: AgentContext, credentialRecordId: string): Promise { + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + await anonCredsHolderService.deleteCredential(agentContext, credentialRecordId) + } + + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + { offerAttachment, proposalAttachment }: CredentialFormatAutoRespondProposalOptions + ) { + const proposalJson = proposalAttachment.getDataAsJson() + const offerJson = offerAttachment.getDataAsJson() + + // We want to make sure the credential definition matches. + // TODO: If no credential definition is present on the proposal, we could check whether the other fields + // of the proposal match with the credential definition id. + return proposalJson.cred_def_id === offerJson.cred_def_id + } + + public async shouldAutoRespondToOffer( + agentContext: AgentContext, + { offerAttachment, proposalAttachment }: CredentialFormatAutoRespondOfferOptions + ) { + const proposalJson = proposalAttachment.getDataAsJson() + const offerJson = offerAttachment.getDataAsJson() + + // We want to make sure the credential definition matches. + // TODO: If no credential definition is present on the proposal, we could check whether the other fields + // of the proposal match with the credential definition id. + return proposalJson.cred_def_id === offerJson.cred_def_id + } + + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + { offerAttachment, requestAttachment }: CredentialFormatAutoRespondRequestOptions + ) { + const credentialOfferJson = offerAttachment.getDataAsJson() + const credentialRequestJson = requestAttachment.getDataAsJson() + + return credentialOfferJson.cred_def_id === credentialRequestJson.cred_def_id + } + + public async shouldAutoRespondToCredential( + agentContext: AgentContext, + { credentialRecord, requestAttachment, credentialAttachment }: CredentialFormatAutoRespondCredentialOptions + ) { + const credentialJson = credentialAttachment.getDataAsJson() + const credentialRequestJson = requestAttachment.getDataAsJson() + + // make sure the credential definition matches + if (credentialJson.cred_def_id !== credentialRequestJson.cred_def_id) return false + + // If we don't have any attributes stored we can't compare so always return false. + if (!credentialRecord.credentialAttributes) return false + const attributeValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes) + + // check whether the values match the values in the record + return checkCredentialValuesMatch(attributeValues, credentialJson.values) + } + + private async createIndyOffer( + agentContext: AgentContext, + { + credentialRecord, + attachmentId, + credentialDefinitionId, + attributes, + linkedAttachments, + }: { + credentialDefinitionId: string + credentialRecord: CredentialExchangeRecord + attachmentId?: string + attributes: CredentialPreviewAttributeOptions[] + linkedAttachments?: LinkedAttachment[] + } + ): Promise { + const anonCredsIssuerService = + agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol) + + // if the proposal has an attachment Id use that, otherwise the generated id of the formats object + const format = new CredentialFormatSpec({ + attachmentId: attachmentId, + format: INDY_CRED_ABSTRACT, + }) + + const offer = await anonCredsIssuerService.createCredentialOffer(agentContext, { + credentialDefinitionId, + }) + + const { previewAttributes } = this.getCredentialLinkedAttachments(attributes, linkedAttachments) + if (!previewAttributes) { + throw new CredoError('Missing required preview attributes for indy offer') + } + + await this.assertPreviewAttributesMatchSchemaAttributes(agentContext, offer, previewAttributes) + + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { + schemaId: offer.schema_id, + credentialDefinitionId: offer.cred_def_id, + }) + + const attachment = this.getFormatData(offer, format.attachmentId) + + return { format, attachment, previewAttributes } + } + + private async assertPreviewAttributesMatchSchemaAttributes( + agentContext: AgentContext, + offer: AnonCredsCredentialOffer, + attributes: CredentialPreviewAttributeOptions[] + ): Promise { + const { schema } = await fetchSchema(agentContext, offer.schema_id) + assertAttributesMatch(schema, attributes) + } + + /** + * Get linked attachments for indy format from a proposal message. This allows attachments + * to be copied across to old style credential records + * + * @param options ProposeCredentialOptions object containing (optionally) the linked attachments + * @return array of linked attachments or undefined if none present + */ + private getCredentialLinkedAttachments( + attributes?: CredentialPreviewAttributeOptions[], + linkedAttachments?: LinkedAttachment[] + ): { + attachments?: Attachment[] + previewAttributes?: CredentialPreviewAttributeOptions[] + } { + if (!linkedAttachments && !attributes) { + return {} + } + + let previewAttributes = attributes ?? [] + let attachments: Attachment[] | undefined + + if (linkedAttachments) { + // there are linked attachments so transform into the attribute field of the CredentialPreview object for + // this proposal + previewAttributes = createAndLinkAttachmentsToPreview(linkedAttachments, previewAttributes) + attachments = linkedAttachments.map((linkedAttachment) => linkedAttachment.attachment) + } + + return { attachments, previewAttributes } + } + + /** + * Returns an object of type {@link Attachment} for use in credential exchange messages. + * It looks up the correct format identifier and encodes the data as a base64 attachment. + * + * @param data The data to include in the attach object + * @param id the attach id from the formats component of the message + */ + public getFormatData(data: unknown, id: string): Attachment { + const attachment = new Attachment({ + id, + mimeType: 'application/json', + data: { + base64: JsonEncoder.toBase64(data), + }, + }) + + return attachment + } +} diff --git a/packages/anoncreds/src/formats/LegacyIndyProofFormat.ts b/packages/anoncreds/src/formats/LegacyIndyProofFormat.ts new file mode 100644 index 0000000000..448ba49a87 --- /dev/null +++ b/packages/anoncreds/src/formats/LegacyIndyProofFormat.ts @@ -0,0 +1,40 @@ +import type { + AnonCredsProposeProofFormat, + AnonCredsRequestProofFormat, + AnonCredsGetCredentialsForProofRequestOptions, + AnonCredsCredentialsForProofRequest, +} from './AnonCredsProofFormat' +import type { AnonCredsProof, AnonCredsProofRequest, AnonCredsSelectedCredentials } from '../models' +import type { ProofFormat } from '@credo-ts/core' + +// TODO: Custom restrictions to remove `_id` from restrictions? +export type LegacyIndyProofRequest = AnonCredsProofRequest + +export interface LegacyIndyProofFormat extends ProofFormat { + formatKey: 'indy' + + proofFormats: { + createProposal: AnonCredsProposeProofFormat + acceptProposal: { + name?: string + version?: string + } + createRequest: AnonCredsRequestProofFormat + acceptRequest: AnonCredsSelectedCredentials + + getCredentialsForRequest: { + input: AnonCredsGetCredentialsForProofRequestOptions + output: AnonCredsCredentialsForProofRequest + } + selectCredentialsForRequest: { + input: AnonCredsGetCredentialsForProofRequestOptions + output: AnonCredsSelectedCredentials + } + } + + formatData: { + proposal: LegacyIndyProofRequest + request: LegacyIndyProofRequest + presentation: AnonCredsProof + } +} diff --git a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts new file mode 100644 index 0000000000..e9fc51f452 --- /dev/null +++ b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts @@ -0,0 +1,611 @@ +import type { + AnonCredsCredentialsForProofRequest, + AnonCredsGetCredentialsForProofRequestOptions, +} from './AnonCredsProofFormat' +import type { LegacyIndyProofFormat } from './LegacyIndyProofFormat' +import type { + AnonCredsCredentialDefinition, + AnonCredsCredentialInfo, + AnonCredsProof, + AnonCredsRequestedAttribute, + AnonCredsRequestedAttributeMatch, + AnonCredsRequestedPredicate, + AnonCredsRequestedPredicateMatch, + AnonCredsSchema, + AnonCredsSelectedCredentials, + AnonCredsProofRequest, +} from '../models' +import type { AnonCredsHolderService, AnonCredsVerifierService, GetCredentialsForProofRequestReturn } from '../services' +import type { + ProofFormatService, + AgentContext, + ProofFormatCreateReturn, + FormatCreateRequestOptions, + ProofFormatCreateProposalOptions, + ProofFormatProcessOptions, + ProofFormatAcceptProposalOptions, + ProofFormatAcceptRequestOptions, + ProofFormatProcessPresentationOptions, + ProofFormatGetCredentialsForRequestOptions, + ProofFormatGetCredentialsForRequestReturn, + ProofFormatSelectCredentialsForRequestOptions, + ProofFormatSelectCredentialsForRequestReturn, + ProofFormatAutoRespondProposalOptions, + ProofFormatAutoRespondRequestOptions, + ProofFormatAutoRespondPresentationOptions, +} from '@credo-ts/core' + +import { CredoError, Attachment, AttachmentData, JsonEncoder, ProofFormatSpec, JsonTransformer } from '@credo-ts/core' + +import { AnonCredsProofRequest as AnonCredsProofRequestClass } from '../models/AnonCredsProofRequest' +import { AnonCredsVerifierServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' +import { + sortRequestedCredentialsMatches, + createRequestFromPreview, + areAnonCredsProofRequestsEqual, + assertBestPracticeRevocationInterval, + checkValidCredentialValueEncoding, + assertNoDuplicateGroupsNamesInProofRequest, + getRevocationRegistriesForRequest, + getRevocationRegistriesForProof, + fetchSchema, + fetchCredentialDefinition, + fetchRevocationStatusList, +} from '../utils' +import { encodeCredentialValue } from '../utils/credential' +import { + getUnQualifiedDidIndyDid, + isUnqualifiedCredentialDefinitionId, + isUnqualifiedSchemaId, + getUnqualifiedDidIndySchema, + getUnqualifiedDidIndyCredentialDefinition, +} from '../utils/indyIdentifiers' +import { dateToTimestamp } from '../utils/timestamp' + +const V2_INDY_PRESENTATION_PROPOSAL = 'hlindy/proof-req@v2.0' +const V2_INDY_PRESENTATION_REQUEST = 'hlindy/proof-req@v2.0' +const V2_INDY_PRESENTATION = 'hlindy/proof@v2.0' + +export class LegacyIndyProofFormatService implements ProofFormatService { + public readonly formatKey = 'indy' as const + + public async createProposal( + agentContext: AgentContext, + { attachmentId, proofFormats }: ProofFormatCreateProposalOptions + ): Promise { + const format = new ProofFormatSpec({ + format: V2_INDY_PRESENTATION_PROPOSAL, + attachmentId, + }) + + const indyFormat = proofFormats.indy + if (!indyFormat) { + throw Error('Missing indy format to create proposal attachment format') + } + + const proofRequest = createRequestFromPreview({ + attributes: indyFormat.attributes ?? [], + predicates: indyFormat.predicates ?? [], + name: indyFormat.name ?? 'Proof request', + version: indyFormat.version ?? '1.0', + nonce: await agentContext.wallet.generateNonce(), + }) + const attachment = this.getFormatData(proofRequest, format.attachmentId) + + return { attachment, format } + } + + public async processProposal(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const proposalJson = attachment.getDataAsJson() + + // fromJson also validates + JsonTransformer.fromJSON(proposalJson, AnonCredsProofRequestClass) + + // Assert attribute and predicate (group) names do not match + assertNoDuplicateGroupsNamesInProofRequest(proposalJson) + } + + public async acceptProposal( + agentContext: AgentContext, + { proposalAttachment, attachmentId }: ProofFormatAcceptProposalOptions + ): Promise { + const format = new ProofFormatSpec({ + format: V2_INDY_PRESENTATION_REQUEST, + attachmentId, + }) + + const proposalJson = proposalAttachment.getDataAsJson() + + const request = { + ...proposalJson, + // We never want to reuse the nonce from the proposal, as this will allow replay attacks + nonce: await agentContext.wallet.generateNonce(), + } + + const attachment = this.getFormatData(request, format.attachmentId) + + return { attachment, format } + } + + public async createRequest( + agentContext: AgentContext, + { attachmentId, proofFormats }: FormatCreateRequestOptions + ): Promise { + const format = new ProofFormatSpec({ + format: V2_INDY_PRESENTATION_REQUEST, + attachmentId, + }) + + const indyFormat = proofFormats.indy + if (!indyFormat) { + throw Error('Missing indy format in create request attachment format') + } + + const request = { + name: indyFormat.name, + version: indyFormat.version, + nonce: await agentContext.wallet.generateNonce(), + requested_attributes: indyFormat.requested_attributes ?? {}, + requested_predicates: indyFormat.requested_predicates ?? {}, + non_revoked: indyFormat.non_revoked, + } satisfies AnonCredsProofRequest + + // Assert attribute and predicate (group) names do not match + assertNoDuplicateGroupsNamesInProofRequest(request) + + const attachment = this.getFormatData(request, format.attachmentId) + + return { attachment, format } + } + + public async processRequest(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const requestJson = attachment.getDataAsJson() + + // fromJson also validates + JsonTransformer.fromJSON(requestJson, AnonCredsProofRequestClass) + + // Assert attribute and predicate (group) names do not match + assertNoDuplicateGroupsNamesInProofRequest(requestJson) + } + + public async acceptRequest( + agentContext: AgentContext, + { proofFormats, requestAttachment, attachmentId }: ProofFormatAcceptRequestOptions + ): Promise { + const format = new ProofFormatSpec({ + format: V2_INDY_PRESENTATION, + attachmentId, + }) + const requestJson = requestAttachment.getDataAsJson() + + const indyFormat = proofFormats?.indy + + const selectedCredentials = + indyFormat ?? + (await this._selectCredentialsForRequest(agentContext, requestJson, { + filterByNonRevocationRequirements: true, + })) + + const proof = await this.createProof(agentContext, requestJson, selectedCredentials) + const attachment = this.getFormatData(proof, format.attachmentId) + + return { + attachment, + format, + } + } + + public async processPresentation( + agentContext: AgentContext, + { requestAttachment, attachment }: ProofFormatProcessPresentationOptions + ): Promise { + const verifierService = + agentContext.dependencyManager.resolve(AnonCredsVerifierServiceSymbol) + + const proofRequestJson = requestAttachment.getDataAsJson() + + // NOTE: we don't do validation here, as this is handled by the AnonCreds implementation, however + // this can lead to confusing error messages. We should consider doing validation here as well. + // Defining a class-transformer/class-validator class seems a bit overkill, and the usage of interfaces + // for the anoncreds package keeps things simple. Maybe we can try to use something like zod to validate + const proofJson = attachment.getDataAsJson() + + for (const [referent, attribute] of Object.entries(proofJson.requested_proof.revealed_attrs)) { + if (!checkValidCredentialValueEncoding(attribute.raw, attribute.encoded)) { + throw new CredoError( + `The encoded value for '${referent}' is invalid. ` + + `Expected '${encodeCredentialValue(attribute.raw)}'. ` + + `Actual '${attribute.encoded}'` + ) + } + } + + for (const [, attributeGroup] of Object.entries(proofJson.requested_proof.revealed_attr_groups ?? {})) { + for (const [attributeName, attribute] of Object.entries(attributeGroup.values)) { + if (!checkValidCredentialValueEncoding(attribute.raw, attribute.encoded)) { + throw new CredoError( + `The encoded value for '${attributeName}' is invalid. ` + + `Expected '${encodeCredentialValue(attribute.raw)}'. ` + + `Actual '${attribute.encoded}'` + ) + } + } + } + + // TODO: pre verify proof json + // I'm not 100% sure how much indy does. Also if it checks whether the proof requests matches the proof + // @see https://github.com/hyperledger/aries-cloudagent-python/blob/master/aries_cloudagent/indy/sdk/verifier.py#L79-L164 + + const schemas = await this.getSchemas(agentContext, new Set(proofJson.identifiers.map((i) => i.schema_id))) + const credentialDefinitions = await this.getCredentialDefinitions( + agentContext, + new Set(proofJson.identifiers.map((i) => i.cred_def_id)) + ) + + const revocationRegistries = await getRevocationRegistriesForProof(agentContext, proofJson) + + return await verifierService.verifyProof(agentContext, { + proofRequest: proofRequestJson, + proof: proofJson, + schemas, + credentialDefinitions, + revocationRegistries, + }) + } + + public async getCredentialsForRequest( + agentContext: AgentContext, + { requestAttachment, proofFormats }: ProofFormatGetCredentialsForRequestOptions + ): Promise> { + const proofRequestJson = requestAttachment.getDataAsJson() + + // Set default values + const { filterByNonRevocationRequirements = true } = proofFormats?.indy ?? {} + + const credentialsForRequest = await this._getCredentialsForRequest(agentContext, proofRequestJson, { + filterByNonRevocationRequirements, + }) + + return credentialsForRequest + } + + public async selectCredentialsForRequest( + agentContext: AgentContext, + { requestAttachment, proofFormats }: ProofFormatSelectCredentialsForRequestOptions + ): Promise> { + const proofRequestJson = requestAttachment.getDataAsJson() + + // Set default values + const { filterByNonRevocationRequirements = true } = proofFormats?.indy ?? {} + + const selectedCredentials = this._selectCredentialsForRequest(agentContext, proofRequestJson, { + filterByNonRevocationRequirements, + }) + + return selectedCredentials + } + + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + { proposalAttachment, requestAttachment }: ProofFormatAutoRespondProposalOptions + ): Promise { + const proposalJson = proposalAttachment.getDataAsJson() + const requestJson = requestAttachment.getDataAsJson() + + const areRequestsEqual = areAnonCredsProofRequestsEqual(proposalJson, requestJson) + agentContext.config.logger.debug(`AnonCreds request and proposal are are equal: ${areRequestsEqual}`, { + proposalJson, + requestJson, + }) + + return areRequestsEqual + } + + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + { proposalAttachment, requestAttachment }: ProofFormatAutoRespondRequestOptions + ): Promise { + const proposalJson = proposalAttachment.getDataAsJson() + const requestJson = requestAttachment.getDataAsJson() + + return areAnonCredsProofRequestsEqual(proposalJson, requestJson) + } + + public async shouldAutoRespondToPresentation( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _options: ProofFormatAutoRespondPresentationOptions + ): Promise { + // The presentation is already verified in processPresentation, so we can just return true here. + // It's only an ack, so it's just that we received the presentation. + return true + } + + public supportsFormat(formatIdentifier: string): boolean { + const supportedFormats = [V2_INDY_PRESENTATION_PROPOSAL, V2_INDY_PRESENTATION_REQUEST, V2_INDY_PRESENTATION] + return supportedFormats.includes(formatIdentifier) + } + + private async _getCredentialsForRequest( + agentContext: AgentContext, + proofRequest: AnonCredsProofRequest, + options: AnonCredsGetCredentialsForProofRequestOptions + ): Promise { + const credentialsForProofRequest: AnonCredsCredentialsForProofRequest = { + attributes: {}, + predicates: {}, + } + + for (const [referent, requestedAttribute] of Object.entries(proofRequest.requested_attributes)) { + const credentials = await this.getCredentialsForProofRequestReferent(agentContext, proofRequest, referent) + + credentialsForProofRequest.attributes[referent] = sortRequestedCredentialsMatches( + await Promise.all( + credentials.map(async (credential) => { + const { isRevoked, timestamp } = await this.getRevocationStatus( + agentContext, + proofRequest, + requestedAttribute, + credential.credentialInfo + ) + + return { + credentialId: credential.credentialInfo.credentialId, + revealed: true, + credentialInfo: credential.credentialInfo, + timestamp, + revoked: isRevoked, + } satisfies AnonCredsRequestedAttributeMatch + }) + ) + ) + + // We only attach revoked state if non-revocation is requested. So if revoked is true it means + // the credential is not applicable to the proof request + if (options.filterByNonRevocationRequirements) { + credentialsForProofRequest.attributes[referent] = credentialsForProofRequest.attributes[referent].filter( + (r) => !r.revoked + ) + } + } + + for (const [referent, requestedPredicate] of Object.entries(proofRequest.requested_predicates)) { + const credentials = await this.getCredentialsForProofRequestReferent(agentContext, proofRequest, referent) + + credentialsForProofRequest.predicates[referent] = sortRequestedCredentialsMatches( + await Promise.all( + credentials.map(async (credential) => { + const { isRevoked, timestamp } = await this.getRevocationStatus( + agentContext, + proofRequest, + requestedPredicate, + credential.credentialInfo + ) + + return { + credentialId: credential.credentialInfo.credentialId, + credentialInfo: credential.credentialInfo, + timestamp, + revoked: isRevoked, + } satisfies AnonCredsRequestedPredicateMatch + }) + ) + ) + + // We only attach revoked state if non-revocation is requested. So if revoked is true it means + // the credential is not applicable to the proof request + if (options.filterByNonRevocationRequirements) { + credentialsForProofRequest.predicates[referent] = credentialsForProofRequest.predicates[referent].filter( + (r) => !r.revoked + ) + } + } + + return credentialsForProofRequest + } + + private async _selectCredentialsForRequest( + agentContext: AgentContext, + proofRequest: AnonCredsProofRequest, + options: AnonCredsGetCredentialsForProofRequestOptions + ): Promise { + const credentialsForRequest = await this._getCredentialsForRequest(agentContext, proofRequest, options) + + const selectedCredentials: AnonCredsSelectedCredentials = { + attributes: {}, + predicates: {}, + selfAttestedAttributes: {}, + } + + Object.keys(credentialsForRequest.attributes).forEach((attributeName) => { + const attributeArray = credentialsForRequest.attributes[attributeName] + + if (attributeArray.length === 0) { + throw new CredoError('Unable to automatically select requested attributes.') + } + + selectedCredentials.attributes[attributeName] = attributeArray[0] + }) + + Object.keys(credentialsForRequest.predicates).forEach((attributeName) => { + if (credentialsForRequest.predicates[attributeName].length === 0) { + throw new CredoError('Unable to automatically select requested predicates.') + } else { + selectedCredentials.predicates[attributeName] = credentialsForRequest.predicates[attributeName][0] + } + }) + + return selectedCredentials + } + + private async getCredentialsForProofRequestReferent( + agentContext: AgentContext, + proofRequest: AnonCredsProofRequest, + attributeReferent: string + ): Promise { + const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const credentials = await holderService.getCredentialsForProofRequest(agentContext, { + proofRequest, + attributeReferent, + }) + + return credentials + } + + /** + * Build schemas object needed to create and verify proof objects. + * + * Creates object with `{ schemaId: AnonCredsSchema }` mapping + * + * @param schemaIds List of schema ids + * @returns Object containing schemas for specified schema ids + * + */ + private async getSchemas(agentContext: AgentContext, schemaIds: Set) { + const schemas: { [key: string]: AnonCredsSchema } = {} + + for (const schemaId of schemaIds) { + const schemaResult = await fetchSchema(agentContext, schemaId) + if (isUnqualifiedSchemaId(schemaResult.schemaId)) { + schemas[schemaId] = schemaResult.schema + } else { + schemas[getUnQualifiedDidIndyDid(schemaId)] = getUnqualifiedDidIndySchema(schemaResult.schema) + } + } + + return schemas + } + + /** + * Build credential definitions object needed to create and verify proof objects. + * + * Creates object with `{ credentialDefinitionId: AnonCredsCredentialDefinition }` mapping + * + * @param credentialDefinitionIds List of credential definition ids + * @returns Object containing credential definitions for specified credential definition ids + * + */ + private async getCredentialDefinitions(agentContext: AgentContext, credentialDefinitionIds: Set) { + const credentialDefinitions: { [key: string]: AnonCredsCredentialDefinition } = {} + + for (const credentialDefinitionId of credentialDefinitionIds) { + const credentialDefinitionResult = await fetchCredentialDefinition(agentContext, credentialDefinitionId) + if (isUnqualifiedCredentialDefinitionId(credentialDefinitionResult.credentialDefinitionId)) { + credentialDefinitions[credentialDefinitionId] = credentialDefinitionResult.credentialDefinition + } else { + credentialDefinitions[getUnQualifiedDidIndyDid(credentialDefinitionId)] = + getUnqualifiedDidIndyCredentialDefinition(credentialDefinitionResult.credentialDefinition) + } + } + + return credentialDefinitions + } + + private async getRevocationStatus( + agentContext: AgentContext, + proofRequest: AnonCredsProofRequest, + requestedItem: AnonCredsRequestedAttribute | AnonCredsRequestedPredicate, + credentialInfo: AnonCredsCredentialInfo + ) { + const requestNonRevoked = requestedItem.non_revoked ?? proofRequest.non_revoked + const credentialRevocationId = credentialInfo.credentialRevocationId + const revocationRegistryId = credentialInfo.revocationRegistryId + + // If revocation interval is not present or the credential is not revocable then we + // don't need to fetch the revocation status + if (!requestNonRevoked || credentialRevocationId === null || !revocationRegistryId) { + return { isRevoked: undefined, timestamp: undefined } + } + + agentContext.config.logger.trace( + `Fetching credential revocation status for credential revocation id '${credentialRevocationId}' with revocation interval with from '${requestNonRevoked.from}' and to '${requestNonRevoked.to}'` + ) + + // Make sure the revocation interval follows best practices from Aries RFC 0441 + assertBestPracticeRevocationInterval(requestNonRevoked) + + const { revocationStatusList } = await fetchRevocationStatusList( + agentContext, + revocationRegistryId, + requestNonRevoked.to ?? dateToTimestamp(new Date()) + ) + + // Item is revoked when the value at the index is 1 + const isRevoked = revocationStatusList.revocationList[parseInt(credentialRevocationId)] === 1 + + agentContext.config.logger.trace( + `Credential with credential revocation index '${credentialRevocationId}' is ${ + isRevoked ? '' : 'not ' + }revoked with revocation interval with to '${requestNonRevoked.to}' & from '${requestNonRevoked.from}'` + ) + + return { + isRevoked, + timestamp: revocationStatusList.timestamp, + } + } + + /** + * Create indy proof from a given proof request and requested credential object. + * + * @param proofRequest The proof request to create the proof for + * @param requestedCredentials The requested credentials object specifying which credentials to use for the proof + * @returns indy proof object + */ + private async createProof( + agentContext: AgentContext, + proofRequest: AnonCredsProofRequest, + selectedCredentials: AnonCredsSelectedCredentials + ): Promise { + const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const credentialObjects = await Promise.all( + [...Object.values(selectedCredentials.attributes), ...Object.values(selectedCredentials.predicates)].map( + async (c) => c.credentialInfo ?? holderService.getCredential(agentContext, { id: c.credentialId }) + ) + ) + + const schemas = await this.getSchemas(agentContext, new Set(credentialObjects.map((c) => c.schemaId))) + const credentialDefinitions = await this.getCredentialDefinitions( + agentContext, + new Set(credentialObjects.map((c) => c.credentialDefinitionId)) + ) + + // selectedCredentials are overridden with specified timestamps of the revocation status list that + // should be used for the selected credentials. + const { revocationRegistries, updatedSelectedCredentials } = await getRevocationRegistriesForRequest( + agentContext, + proofRequest, + selectedCredentials + ) + + return await holderService.createProof(agentContext, { + proofRequest, + selectedCredentials: updatedSelectedCredentials, + schemas, + credentialDefinitions, + revocationRegistries, + }) + } + + /** + * Returns an object of type {@link Attachment} for use in credential exchange messages. + * It looks up the correct format identifier and encodes the data as a base64 attachment. + * + * @param data The data to include in the attach object + * @param id the attach id from the formats component of the message + */ + private getFormatData(data: unknown, id: string): Attachment { + const attachment = new Attachment({ + id, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(data), + }), + }) + + return attachment + } +} diff --git a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts new file mode 100644 index 0000000000..6c2187f106 --- /dev/null +++ b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts @@ -0,0 +1,409 @@ +import type { AnonCredsCredentialRequest } from '../../models' +import type { DidRepository } from '@credo-ts/core' + +import { + CredentialState, + CredentialExchangeRecord, + KeyType, + CredentialPreviewAttribute, + ProofExchangeRecord, + ProofState, + EventEmitter, + InjectionSymbols, + SignatureSuiteToken, + W3cCredentialsModuleConfig, + DidResolverService, + DidsModuleConfig, + ProofRole, + CredentialRole, +} from '@credo-ts/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' +import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' +import { anoncreds } from '../../../../anoncreds/tests/helpers' +import { indyDidFromPublicKeyBase58 } from '../../../../core/src/utils/did' +import { testLogger } from '../../../../core/tests' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' +import { InMemoryAnonCredsRegistry } from '../../../tests/InMemoryAnonCredsRegistry' +import { AnonCredsModuleConfig } from '../../AnonCredsModuleConfig' +import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from '../../anoncreds-rs' +import { + AnonCredsCredentialDefinitionPrivateRecord, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsCredentialDefinitionRecord, + AnonCredsCredentialDefinitionRepository, + AnonCredsCredentialRepository, + AnonCredsKeyCorrectnessProofRecord, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsLinkSecretRecord, + AnonCredsLinkSecretRepository, +} from '../../repository' +import { + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsVerifierServiceSymbol, +} from '../../services' +import { AnonCredsRegistryService } from '../../services/registry/AnonCredsRegistryService' +import { + getUnqualifiedCredentialDefinitionId, + getUnqualifiedSchemaId, + parseIndyCredentialDefinitionId, + parseIndySchemaId, +} from '../../utils/indyIdentifiers' +import { LegacyIndyCredentialFormatService } from '../LegacyIndyCredentialFormatService' +import { LegacyIndyProofFormatService } from '../LegacyIndyProofFormatService' + +const registry = new InMemoryAnonCredsRegistry() +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + registries: [registry], + anoncreds, + autoCreateLinkSecret: false, +}) + +const agentConfig = getAgentConfig('LegacyIndyFormatServicesTest') +const anonCredsVerifierService = new AnonCredsRsVerifierService() +const anonCredsHolderService = new AnonCredsRsHolderService() +const anonCredsIssuerService = new AnonCredsRsIssuerService() +const wallet = new InMemoryWallet() +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const storageService = new InMemoryStorageService() +const eventEmitter = new EventEmitter(agentDependencies, new Subject()) +const anonCredsLinkSecretRepository = new AnonCredsLinkSecretRepository(storageService, eventEmitter) +const anonCredsCredentialDefinitionRepository = new AnonCredsCredentialDefinitionRepository( + storageService, + eventEmitter +) +const anonCredsCredentialDefinitionPrivateRepository = new AnonCredsCredentialDefinitionPrivateRepository( + storageService, + eventEmitter +) + +const inMemoryStorageService = new InMemoryStorageService() +const anonCredsCredentialRepository = new AnonCredsCredentialRepository(storageService, eventEmitter) +const anonCredsKeyCorrectnessProofRepository = new AnonCredsKeyCorrectnessProofRepository(storageService, eventEmitter) +const agentContext = getAgentContext({ + registerInstances: [ + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, new agentDependencies.FileSystem()], + [InjectionSymbols.StorageService, inMemoryStorageService], + [InjectionSymbols.Logger, testLogger], + [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [DidResolverService, new DidResolverService(testLogger, new DidsModuleConfig(), {} as unknown as DidRepository)], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + [AnonCredsLinkSecretRepository, anonCredsLinkSecretRepository], + [AnonCredsCredentialDefinitionRepository, anonCredsCredentialDefinitionRepository], + [AnonCredsCredentialDefinitionPrivateRepository, anonCredsCredentialDefinitionPrivateRepository], + [AnonCredsCredentialRepository, anonCredsCredentialRepository], + [AnonCredsKeyCorrectnessProofRepository, anonCredsKeyCorrectnessProofRepository], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [InjectionSymbols.StorageService, storageService], + [SignatureSuiteToken, 'default'], + ], + agentConfig, + wallet, +}) + +const indyCredentialFormatService = new LegacyIndyCredentialFormatService() +const indyProofFormatService = new LegacyIndyProofFormatService() + +// We can split up these tests when we can use AnonCredsRS as a backend, but currently +// we need to have the link secrets etc in the wallet which is not so easy to do with Indy +describe('Legacy indy format services', () => { + beforeEach(async () => { + await wallet.createAndOpen(agentConfig.walletConfig) + }) + + afterEach(async () => { + await wallet.delete() + }) + + test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => { + // This is just so we don't have to register an actual indy did (as we don't have the indy did registrar configured) + const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const unqualifiedIndyDid = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const indyDid = `did:indy:pool1:${unqualifiedIndyDid}` + + // Create link secret + const { linkSecretValue } = await anonCredsHolderService.createLinkSecret(agentContext, { + linkSecretId: 'link-secret-id', + }) + const anonCredsLinkSecret = new AnonCredsLinkSecretRecord({ + linkSecretId: 'link-secret-id', + value: linkSecretValue, + }) + anonCredsLinkSecret.setTag('isDefault', true) + await anonCredsLinkSecretRepository.save(agentContext, anonCredsLinkSecret) + + const schema = await anonCredsIssuerService.createSchema(agentContext, { + attrNames: ['name', 'age'], + issuerId: indyDid, + name: 'Employee Credential', + version: '1.0.0', + }) + + const { schemaState } = await registry.registerSchema(agentContext, { + schema, + options: {}, + }) + + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await anonCredsIssuerService.createCredentialDefinition(agentContext, { + issuerId: indyDid, + schemaId: schemaState.schemaId as string, + schema, + tag: 'Employee Credential', + supportRevocation: false, + }) + + const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition, + options: {}, + }) + + if ( + !credentialDefinitionState.credentialDefinition || + !credentialDefinitionState.credentialDefinitionId || + !schemaState.schema || + !schemaState.schemaId + ) { + throw new Error('Failed to create schema or credential definition') + } + + await anonCredsCredentialDefinitionRepository.save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + methodName: 'indy', + }) + ) + + if (!keyCorrectnessProof || !credentialDefinitionPrivate) { + throw new Error('Failed to create credential definition private or key correctness proof') + } + + await anonCredsKeyCorrectnessProofRepository.save( + agentContext, + new AnonCredsKeyCorrectnessProofRecord({ + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + value: keyCorrectnessProof, + }) + ) + + await anonCredsCredentialDefinitionPrivateRepository.save( + agentContext, + new AnonCredsCredentialDefinitionPrivateRecord({ + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + value: credentialDefinitionPrivate, + }) + ) + + const holderCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + role: CredentialRole.Holder, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const issuerCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalReceived, + role: CredentialRole.Issuer, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const credentialAttributes = [ + new CredentialPreviewAttribute({ + name: 'name', + value: 'John', + }), + new CredentialPreviewAttribute({ + name: 'age', + value: '25', + }), + ] + + const cd = parseIndyCredentialDefinitionId(credentialDefinitionState.credentialDefinitionId) + const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId( + cd.namespaceIdentifier, + cd.schemaSeqNo, + cd.tag + ) + + const s = parseIndySchemaId(schemaState.schemaId) + const legacySchemaId = getUnqualifiedSchemaId(s.namespaceIdentifier, s.schemaName, s.schemaVersion) + + // Holder creates proposal + holderCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: proposalAttachment } = await indyCredentialFormatService.createProposal(agentContext, { + credentialRecord: holderCredentialRecord, + credentialFormats: { + indy: { + attributes: credentialAttributes, + credentialDefinitionId: legacyCredentialDefinitionId, + }, + }, + }) + + // Issuer processes and accepts proposal + await indyCredentialFormatService.processProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: proposalAttachment, + }) + // Set attributes on the credential record, this is normally done by the protocol service + issuerCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: offerAttachment } = await indyCredentialFormatService.acceptProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + proposalAttachment: proposalAttachment, + }) + + // Holder processes and accepts offer + await indyCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment } = await indyCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + }) + + // Make sure the request contains a prover_did field + expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeDefined() + + // Issuer processes and accepts request + await indyCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await indyCredentialFormatService.acceptRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + }) + + // Holder processes and accepts credential + await indyCredentialFormatService.processCredential(agentContext, { + offerAttachment, + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) + + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, + ]) + + const credentialId = holderCredentialRecord.credentials[0].credentialRecordId + const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { + id: credentialId, + }) + + expect(anonCredsCredential).toEqual({ + credentialId, + attributes: { + age: 25, + name: 'John', + }, + schemaId: schemaState.schemaId, + linkSecretId: 'link-secret-id', + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: null, + credentialRevocationId: null, + methodName: 'inMemory', + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + expect(holderCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': { + schemaId: legacySchemaId, + credentialDefinitionId: legacyCredentialDefinitionId, + }, + '_anoncreds/credentialRequest': { + link_secret_blinding_data: expect.any(Object), + link_secret_name: expect.any(String), + nonce: expect.any(String), + }, + }) + + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': { + schemaId: legacySchemaId, + credentialDefinitionId: legacyCredentialDefinitionId, + }, + }) + + const holderProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalSent, + role: ProofRole.Prover, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + const verifierProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalReceived, + role: ProofRole.Verifier, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + + const { attachment: proofProposalAttachment } = await indyProofFormatService.createProposal(agentContext, { + proofFormats: { + indy: { + attributes: [ + { + name: 'name', + credentialDefinitionId: legacyCredentialDefinitionId, + value: 'John', + referent: '1', + }, + ], + predicates: [ + { + credentialDefinitionId: legacyCredentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 18, + }, + ], + name: 'Proof Request', + version: '1.0', + }, + }, + proofRecord: holderProofRecord, + }) + + await indyProofFormatService.processProposal(agentContext, { + attachment: proofProposalAttachment, + proofRecord: verifierProofRecord, + }) + + const { attachment: proofRequestAttachment } = await indyProofFormatService.acceptProposal(agentContext, { + proofRecord: verifierProofRecord, + proposalAttachment: proofProposalAttachment, + }) + + await indyProofFormatService.processRequest(agentContext, { + attachment: proofRequestAttachment, + proofRecord: holderProofRecord, + }) + + const { attachment: proofAttachment } = await indyProofFormatService.acceptRequest(agentContext, { + proofRecord: holderProofRecord, + requestAttachment: proofRequestAttachment, + proposalAttachment: proofProposalAttachment, + }) + + const isValid = await indyProofFormatService.processPresentation(agentContext, { + attachment: proofAttachment, + proofRecord: verifierProofRecord, + requestAttachment: proofRequestAttachment, + }) + + expect(isValid).toBe(true) + }) +}) diff --git a/packages/anoncreds/src/formats/index.ts b/packages/anoncreds/src/formats/index.ts new file mode 100644 index 0000000000..2036d28145 --- /dev/null +++ b/packages/anoncreds/src/formats/index.ts @@ -0,0 +1,10 @@ +export * from './AnonCredsCredentialFormat' +export * from './LegacyIndyCredentialFormat' +export { AnonCredsCredentialFormatService } from './AnonCredsCredentialFormatService' +export { DataIntegrityCredentialFormatService } from './DataIntegrityCredentialFormatService' +export { LegacyIndyCredentialFormatService } from './LegacyIndyCredentialFormatService' + +export * from './AnonCredsProofFormat' +export * from './LegacyIndyProofFormat' +export { AnonCredsProofFormatService } from './AnonCredsProofFormatService' +export { LegacyIndyProofFormatService } from './LegacyIndyProofFormatService' diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts new file mode 100644 index 0000000000..62294bc542 --- /dev/null +++ b/packages/anoncreds/src/index.ts @@ -0,0 +1,30 @@ +import 'reflect-metadata' + +export * from './models' +export * from './services' +export * from './error' +export * from './repository' +export * from './formats' +export * from './protocols' + +export { AnonCredsModule } from './AnonCredsModule' +export { AnonCredsModuleConfig, AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig' +export { AnonCredsApi } from './AnonCredsApi' +export * from './AnonCredsApiOptions' +export { generateLegacyProverDidLikeString } from './utils/proverDid' +export * from './utils/indyIdentifiers' +export { assertBestPracticeRevocationInterval } from './utils/revocationInterval' +export { storeLinkSecret } from './utils/linkSecret' + +export { dateToTimestamp, AnonCredsCredentialValue, AnonCredsCredentialMetadata } from './utils' +export { + fetchCredentialDefinition, + fetchRevocationRegistryDefinition, + fetchSchema, + fetchRevocationStatusList, +} from './utils/anonCredsObjects' + +export { AnonCredsCredentialMetadataKey } from './utils/metadata' +export { getAnonCredsTagsFromRecord, AnonCredsCredentialTags } from './utils/w3cAnonCredsUtils' +export { W3cAnonCredsCredentialMetadataKey } from './utils/metadata' +export { getCredentialsForAnonCredsProofRequest } from './utils/getCredentialsForAnonCredsRequest' diff --git a/packages/anoncreds/src/models/AnonCredsCredentialProposal.ts b/packages/anoncreds/src/models/AnonCredsCredentialProposal.ts new file mode 100644 index 0000000000..928c26b5d5 --- /dev/null +++ b/packages/anoncreds/src/models/AnonCredsCredentialProposal.ts @@ -0,0 +1,111 @@ +import { Expose } from 'class-transformer' +import { IsOptional, IsString } from 'class-validator' + +export interface AnonCredsCredentialProposalOptions { + /** + * @deprecated Use `schemaIssuerId` instead. Only valid for legacy indy identifiers. + */ + schemaIssuerDid?: string + schemaIssuerId?: string + + schemaId?: string + schemaName?: string + schemaVersion?: string + credentialDefinitionId?: string + + /** + * @deprecated Use `issuerId` instead. Only valid for legacy indy identifiers. + */ + issuerDid?: string + issuerId?: string +} + +/** + * Class representing an AnonCreds credential proposal as defined in Aries RFC 0592 (and soon the new AnonCreds RFC) + */ +export class AnonCredsCredentialProposal { + public constructor(options: AnonCredsCredentialProposalOptions) { + if (options) { + this.schemaIssuerDid = options.schemaIssuerDid + this.schemaIssuerId = options.schemaIssuerId + this.schemaId = options.schemaId + this.schemaName = options.schemaName + this.schemaVersion = options.schemaVersion + this.credentialDefinitionId = options.credentialDefinitionId + this.issuerDid = options.issuerDid + this.issuerId = options.issuerId + } + } + + /** + * Filter to request credential based on a particular Schema issuer DID. + * + * May only be used with legacy indy identifiers + * + * @deprecated Use schemaIssuerId instead + */ + @Expose({ name: 'schema_issuer_did' }) + @IsString() + @IsOptional() + public schemaIssuerDid?: string + + /** + * Filter to request credential based on a particular Schema issuer DID. + */ + @Expose({ name: 'schema_issuer_id' }) + @IsString() + @IsOptional() + public schemaIssuerId?: string + + /** + * Filter to request credential based on a particular Schema. + */ + @Expose({ name: 'schema_id' }) + @IsString() + @IsOptional() + public schemaId?: string + + /** + * Filter to request credential based on a schema name. + */ + @Expose({ name: 'schema_name' }) + @IsString() + @IsOptional() + public schemaName?: string + + /** + * Filter to request credential based on a schema version. + */ + @Expose({ name: 'schema_version' }) + @IsString() + @IsOptional() + public schemaVersion?: string + + /** + * Filter to request credential based on a particular Credential Definition. + */ + @Expose({ name: 'cred_def_id' }) + @IsString() + @IsOptional() + public credentialDefinitionId?: string + + /** + * Filter to request a credential issued by the owner of a particular DID. + * + * May only be used with legacy indy identifiers + * + * @deprecated Use issuerId instead + */ + @Expose({ name: 'issuer_did' }) + @IsString() + @IsOptional() + public issuerDid?: string + + /** + * Filter to request a credential issued by the owner of a particular DID. + */ + @Expose({ name: 'issuer_id' }) + @IsString() + @IsOptional() + public issuerId?: string +} diff --git a/packages/anoncreds/src/models/AnonCredsProofRequest.ts b/packages/anoncreds/src/models/AnonCredsProofRequest.ts new file mode 100644 index 0000000000..3448b71570 --- /dev/null +++ b/packages/anoncreds/src/models/AnonCredsProofRequest.ts @@ -0,0 +1,83 @@ +import type { AnonCredsRequestedAttributeOptions } from './AnonCredsRequestedAttribute' +import type { AnonCredsRequestedPredicateOptions } from './AnonCredsRequestedPredicate' + +import { Expose, Type } from 'class-transformer' +import { IsIn, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { IsMap } from '../utils' + +import { AnonCredsRequestedAttribute } from './AnonCredsRequestedAttribute' +import { AnonCredsRequestedPredicate } from './AnonCredsRequestedPredicate' +import { AnonCredsRevocationInterval } from './AnonCredsRevocationInterval' + +export interface AnonCredsProofRequestOptions { + name: string + version: string + nonce: string + nonRevoked?: AnonCredsRevocationInterval + ver?: '1.0' | '2.0' + requestedAttributes?: Record + requestedPredicates?: Record +} + +/** + * Proof Request for AnonCreds based proof format + */ +export class AnonCredsProofRequest { + public constructor(options: AnonCredsProofRequestOptions) { + if (options) { + this.name = options.name + this.version = options.version + this.nonce = options.nonce + + this.requestedAttributes = new Map( + Object.entries(options.requestedAttributes ?? {}).map(([key, attribute]) => [ + key, + new AnonCredsRequestedAttribute(attribute), + ]) + ) + + this.requestedPredicates = new Map( + Object.entries(options.requestedPredicates ?? {}).map(([key, predicate]) => [ + key, + new AnonCredsRequestedPredicate(predicate), + ]) + ) + + this.nonRevoked = options.nonRevoked ? new AnonCredsRevocationInterval(options.nonRevoked) : undefined + this.ver = options.ver + } + } + + @IsString() + public name!: string + + @IsString() + public version!: string + + @IsString() + public nonce!: string + + @Expose({ name: 'requested_attributes' }) + @IsMap() + @ValidateNested({ each: true }) + @Type(() => AnonCredsRequestedAttribute) + public requestedAttributes!: Map + + @Expose({ name: 'requested_predicates' }) + @IsMap() + @ValidateNested({ each: true }) + @Type(() => AnonCredsRequestedPredicate) + public requestedPredicates!: Map + + @Expose({ name: 'non_revoked' }) + @ValidateNested() + @Type(() => AnonCredsRevocationInterval) + @IsOptional() + @IsInstance(AnonCredsRevocationInterval) + public nonRevoked?: AnonCredsRevocationInterval + + @IsIn(['1.0', '2.0']) + @IsOptional() + public ver?: '1.0' | '2.0' +} diff --git a/packages/anoncreds/src/models/AnonCredsRequestedAttribute.ts b/packages/anoncreds/src/models/AnonCredsRequestedAttribute.ts new file mode 100644 index 0000000000..7e2df55c8f --- /dev/null +++ b/packages/anoncreds/src/models/AnonCredsRequestedAttribute.ts @@ -0,0 +1,48 @@ +import type { AnonCredsRestrictionOptions } from './AnonCredsRestriction' + +import { Expose, Type } from 'class-transformer' +import { ArrayNotEmpty, IsArray, IsInstance, IsOptional, IsString, ValidateIf, ValidateNested } from 'class-validator' + +import { AnonCredsRestriction, AnonCredsRestrictionTransformer } from './AnonCredsRestriction' +import { AnonCredsRevocationInterval } from './AnonCredsRevocationInterval' + +export interface AnonCredsRequestedAttributeOptions { + name?: string + names?: string[] + nonRevoked?: AnonCredsRevocationInterval + restrictions?: AnonCredsRestrictionOptions[] +} + +export class AnonCredsRequestedAttribute { + public constructor(options: AnonCredsRequestedAttributeOptions) { + if (options) { + this.name = options.name + this.names = options.names + this.nonRevoked = options.nonRevoked ? new AnonCredsRevocationInterval(options.nonRevoked) : undefined + this.restrictions = options.restrictions?.map((r) => new AnonCredsRestriction(r)) + } + } + + @IsString() + @ValidateIf((o: AnonCredsRequestedAttribute) => o.names === undefined) + public name?: string + + @IsArray() + @IsString({ each: true }) + @ValidateIf((o: AnonCredsRequestedAttribute) => o.name === undefined) + @ArrayNotEmpty() + public names?: string[] + + @Expose({ name: 'non_revoked' }) + @ValidateNested() + @IsInstance(AnonCredsRevocationInterval) + @Type(() => AnonCredsRevocationInterval) + @IsOptional() + public nonRevoked?: AnonCredsRevocationInterval + + @ValidateNested({ each: true }) + @Type(() => AnonCredsRestriction) + @IsOptional() + @AnonCredsRestrictionTransformer() + public restrictions?: AnonCredsRestriction[] +} diff --git a/packages/anoncreds/src/models/AnonCredsRequestedPredicate.ts b/packages/anoncreds/src/models/AnonCredsRequestedPredicate.ts new file mode 100644 index 0000000000..9df0bcd698 --- /dev/null +++ b/packages/anoncreds/src/models/AnonCredsRequestedPredicate.ts @@ -0,0 +1,55 @@ +import type { AnonCredsRestrictionOptions } from './AnonCredsRestriction' + +import { Expose, Type } from 'class-transformer' +import { IsArray, IsIn, IsInstance, IsInt, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AnonCredsPredicateType, anonCredsPredicateType } from '../models' + +import { AnonCredsRestriction, AnonCredsRestrictionTransformer } from './AnonCredsRestriction' +import { AnonCredsRevocationInterval } from './AnonCredsRevocationInterval' + +export interface AnonCredsRequestedPredicateOptions { + name: string + // Also allow string value of the enum as input, to make it easier to use in the API + predicateType: AnonCredsPredicateType + predicateValue: number + nonRevoked?: AnonCredsRevocationInterval + restrictions?: AnonCredsRestrictionOptions[] +} + +export class AnonCredsRequestedPredicate { + public constructor(options: AnonCredsRequestedPredicateOptions) { + if (options) { + this.name = options.name + this.nonRevoked = options.nonRevoked ? new AnonCredsRevocationInterval(options.nonRevoked) : undefined + this.restrictions = options.restrictions?.map((r) => new AnonCredsRestriction(r)) + this.predicateType = options.predicateType as AnonCredsPredicateType + this.predicateValue = options.predicateValue + } + } + + @IsString() + public name!: string + + @Expose({ name: 'p_type' }) + @IsIn(anonCredsPredicateType) + public predicateType!: AnonCredsPredicateType + + @Expose({ name: 'p_value' }) + @IsInt() + public predicateValue!: number + + @Expose({ name: 'non_revoked' }) + @ValidateNested() + @Type(() => AnonCredsRevocationInterval) + @IsOptional() + @IsInstance(AnonCredsRevocationInterval) + public nonRevoked?: AnonCredsRevocationInterval + + @ValidateNested({ each: true }) + @Type(() => AnonCredsRestriction) + @IsOptional() + @IsArray() + @AnonCredsRestrictionTransformer() + public restrictions?: AnonCredsRestriction[] +} diff --git a/packages/anoncreds/src/models/AnonCredsRestriction.ts b/packages/anoncreds/src/models/AnonCredsRestriction.ts new file mode 100644 index 0000000000..c3f8ce843c --- /dev/null +++ b/packages/anoncreds/src/models/AnonCredsRestriction.ts @@ -0,0 +1,152 @@ +import { Exclude, Expose, Transform, TransformationType } from 'class-transformer' +import { IsOptional, IsString } from 'class-validator' + +export interface AnonCredsRestrictionOptions { + schemaId?: string + schemaIssuerDid?: string + schemaIssuerId?: string + schemaName?: string + schemaVersion?: string + issuerDid?: string + issuerId?: string + credentialDefinitionId?: string + attributeMarkers?: Record + attributeValues?: Record +} + +export class AnonCredsRestriction { + public constructor(options: AnonCredsRestrictionOptions) { + if (options) { + this.schemaId = options.schemaId + this.schemaIssuerDid = options.schemaIssuerDid + this.schemaIssuerId = options.schemaIssuerId + this.schemaName = options.schemaName + this.schemaVersion = options.schemaVersion + this.issuerDid = options.issuerDid + this.issuerId = options.issuerId + this.credentialDefinitionId = options.credentialDefinitionId + this.attributeMarkers = options.attributeMarkers ?? {} + this.attributeValues = options.attributeValues ?? {} + } + } + + @Expose({ name: 'schema_id' }) + @IsOptional() + @IsString() + public schemaId?: string + + @Expose({ name: 'schema_issuer_did' }) + @IsOptional() + @IsString() + public schemaIssuerDid?: string + + @Expose({ name: 'schema_issuer_id' }) + @IsOptional() + @IsString() + public schemaIssuerId?: string + + @Expose({ name: 'schema_name' }) + @IsOptional() + @IsString() + public schemaName?: string + + @Expose({ name: 'schema_version' }) + @IsOptional() + @IsString() + public schemaVersion?: string + + @Expose({ name: 'issuer_did' }) + @IsOptional() + @IsString() + public issuerDid?: string + + @Expose({ name: 'issuer_id' }) + @IsOptional() + @IsString() + public issuerId?: string + + @Expose({ name: 'cred_def_id' }) + @IsOptional() + @IsString() + public credentialDefinitionId?: string + + @Exclude() + public attributeMarkers: Record = {} + + @Exclude() + public attributeValues: Record = {} +} + +/** + * Decorator that transforms attribute values and attribute markers. + * + * It will transform between the following JSON structure: + * ```json + * { + * "attr::test_prop::value": "test_value" + * "attr::test_prop::marker": "1 + * } + * ``` + * + * And the following AnonCredsRestriction: + * ```json + * { + * "attributeValues": { + * "test_prop": "test_value" + * }, + * "attributeMarkers": { + * "test_prop": true + * } + * } + * ``` + * + * @example + * class Example { + * AttributeFilterTransformer() + * public restrictions!: AnonCredsRestriction[] + * } + */ +export function AnonCredsRestrictionTransformer() { + return Transform(({ value: restrictions, type }) => { + switch (type) { + case TransformationType.CLASS_TO_PLAIN: + if (restrictions && Array.isArray(restrictions)) { + for (const restriction of restrictions) { + const r = restriction as AnonCredsRestriction + + for (const [attributeName, attributeValue] of Object.entries(r.attributeValues)) { + restriction[`attr::${attributeName}::value`] = attributeValue + } + + for (const [attributeName] of Object.entries(r.attributeMarkers)) { + restriction[`attr::${attributeName}::marker`] = '1' + } + } + } + + return restrictions + + case TransformationType.PLAIN_TO_CLASS: + if (restrictions && Array.isArray(restrictions)) { + for (const restriction of restrictions) { + const r = restriction as AnonCredsRestriction + + for (const [attributeName, attributeValue] of Object.entries(r)) { + const match = new RegExp('^attr::([^:]+)::(value|marker)$').exec(attributeName) + + if (match && match[2] === 'marker' && attributeValue === '1') { + r.attributeMarkers[match[1]] = true + delete restriction[attributeName] + } else if (match && match[2] === 'value') { + r.attributeValues[match[1]] = attributeValue + delete restriction[attributeName] + } + } + } + } + return restrictions + default: + return restrictions + } + }) +} diff --git a/packages/anoncreds/src/models/AnonCredsRestrictionWrapper.ts b/packages/anoncreds/src/models/AnonCredsRestrictionWrapper.ts new file mode 100644 index 0000000000..a701c9e6ec --- /dev/null +++ b/packages/anoncreds/src/models/AnonCredsRestrictionWrapper.ts @@ -0,0 +1,11 @@ +import { Type } from 'class-transformer' +import { ValidateNested } from 'class-validator' + +import { AnonCredsRestrictionTransformer, AnonCredsRestriction } from './AnonCredsRestriction' + +export class AnonCredsRestrictionWrapper { + @ValidateNested({ each: true }) + @Type(() => AnonCredsRestriction) + @AnonCredsRestrictionTransformer() + public restrictions!: AnonCredsRestriction[] +} diff --git a/packages/anoncreds/src/models/AnonCredsRevocationInterval.ts b/packages/anoncreds/src/models/AnonCredsRevocationInterval.ts new file mode 100644 index 0000000000..0ae0160616 --- /dev/null +++ b/packages/anoncreds/src/models/AnonCredsRevocationInterval.ts @@ -0,0 +1,18 @@ +import { IsInt, IsOptional } from 'class-validator' + +export class AnonCredsRevocationInterval { + public constructor(options: AnonCredsRevocationInterval) { + if (options) { + this.from = options.from + this.to = options.to + } + } + + @IsInt() + @IsOptional() + public from?: number + + @IsInt() + @IsOptional() + public to?: number +} diff --git a/packages/anoncreds/src/models/__tests__/AnonCredsRestriction.test.ts b/packages/anoncreds/src/models/__tests__/AnonCredsRestriction.test.ts new file mode 100644 index 0000000000..0df0d943ae --- /dev/null +++ b/packages/anoncreds/src/models/__tests__/AnonCredsRestriction.test.ts @@ -0,0 +1,145 @@ +import { JsonTransformer } from '@credo-ts/core' +import { Type } from 'class-transformer' +import { IsArray } from 'class-validator' + +import { AnonCredsRestriction, AnonCredsRestrictionTransformer } from '../AnonCredsRestriction' + +// We need to add the transformer class to the wrapper +class Wrapper { + public constructor(options: Wrapper) { + if (options) { + this.restrictions = options.restrictions + } + } + + @Type(() => AnonCredsRestriction) + @IsArray() + @AnonCredsRestrictionTransformer() + public restrictions!: AnonCredsRestriction[] +} + +describe('AnonCredsRestriction', () => { + test('parses attribute values and markers', () => { + const anonCredsRestrictions = JsonTransformer.fromJSON( + { + restrictions: [ + { + 'attr::test_prop::value': 'test_value', + 'attr::test_prop2::value': 'test_value2', + 'attr::test_prop::marker': '1', + 'attr::test_prop2::marker': '1', + }, + ], + }, + Wrapper + ) + + expect(anonCredsRestrictions).toEqual({ + restrictions: [ + { + attributeValues: { + test_prop: 'test_value', + test_prop2: 'test_value2', + }, + attributeMarkers: { + test_prop: true, + test_prop2: true, + }, + }, + ], + }) + }) + + test('transforms attributeValues and attributeMarkers to json', () => { + const restrictions = new Wrapper({ + restrictions: [ + new AnonCredsRestriction({ + attributeMarkers: { + test_prop: true, + test_prop2: true, + }, + attributeValues: { + test_prop: 'test_value', + test_prop2: 'test_value2', + }, + }), + ], + }) + + expect(JsonTransformer.toJSON(restrictions)).toMatchObject({ + restrictions: [ + { + 'attr::test_prop::value': 'test_value', + 'attr::test_prop2::value': 'test_value2', + 'attr::test_prop::marker': '1', + 'attr::test_prop2::marker': '1', + }, + ], + }) + }) + + test('transforms properties from and to json with correct casing', () => { + const restrictions = new Wrapper({ + restrictions: [ + new AnonCredsRestriction({ + credentialDefinitionId: 'credentialDefinitionId', + issuerDid: 'issuerDid', + issuerId: 'issuerId', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + schemaId: 'schemaId', + schemaIssuerDid: 'schemaIssuerDid', + schemaIssuerId: 'schemaIssuerId', + }), + ], + }) + + expect(JsonTransformer.toJSON(restrictions)).toMatchObject({ + restrictions: [ + { + cred_def_id: 'credentialDefinitionId', + issuer_did: 'issuerDid', + issuer_id: 'issuerId', + schema_name: 'schemaName', + schema_version: 'schemaVersion', + schema_id: 'schemaId', + schema_issuer_did: 'schemaIssuerDid', + schema_issuer_id: 'schemaIssuerId', + }, + ], + }) + + expect( + JsonTransformer.fromJSON( + { + restrictions: [ + { + cred_def_id: 'credentialDefinitionId', + issuer_did: 'issuerDid', + issuer_id: 'issuerId', + schema_name: 'schemaName', + schema_version: 'schemaVersion', + schema_id: 'schemaId', + schema_issuer_did: 'schemaIssuerDid', + schema_issuer_id: 'schemaIssuerId', + }, + ], + }, + Wrapper + ) + ).toMatchObject({ + restrictions: [ + { + credentialDefinitionId: 'credentialDefinitionId', + issuerDid: 'issuerDid', + issuerId: 'issuerId', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + schemaId: 'schemaId', + schemaIssuerDid: 'schemaIssuerDid', + schemaIssuerId: 'schemaIssuerId', + }, + ], + }) + }) +}) diff --git a/packages/anoncreds/src/models/exchange.ts b/packages/anoncreds/src/models/exchange.ts new file mode 100644 index 0000000000..075e950220 --- /dev/null +++ b/packages/anoncreds/src/models/exchange.ts @@ -0,0 +1,126 @@ +import type { AnonCredsCredentialValue } from '../utils/credential' + +export const anonCredsPredicateType = ['>=', '>', '<=', '<'] as const +export type AnonCredsPredicateType = (typeof anonCredsPredicateType)[number] + +export interface AnonCredsProofRequestRestriction { + schema_id?: string + schema_issuer_id?: string + schema_name?: string + schema_version?: string + issuer_id?: string + cred_def_id?: string + rev_reg_id?: string + + // Deprecated, but kept for backwards compatibility with legacy indy anoncreds implementations + schema_issuer_did?: string + issuer_did?: string + + // the following keys can be used for every `attribute name` in credential. + [key: `attr::${string}::marker`]: '1' | '0' + [key: `attr::${string}::value`]: string +} + +export interface AnonCredsNonRevokedInterval { + from?: number + to?: number +} + +export interface AnonCredsCredentialOffer { + schema_id: string + cred_def_id: string + nonce: string + key_correctness_proof: Record +} + +export interface AnonCredsCredentialRequest { + // prover_did is deprecated, however it is kept for backwards compatibility with legacy anoncreds implementations + prover_did?: string + entropy?: string + cred_def_id: string + blinded_ms: Record + blinded_ms_correctness_proof: Record + nonce: string +} + +export type AnonCredsCredentialValues = Record + +export interface AnonCredsCredential { + schema_id: string + cred_def_id: string + rev_reg_id?: string + values: Record + signature: unknown + signature_correctness_proof: unknown + rev_reg?: unknown + witness?: unknown +} + +export interface AnonCredsProof { + requested_proof: { + revealed_attrs: Record< + string, + { + sub_proof_index: number + raw: string + encoded: string + } + > + // revealed_attr_groups is only defined if there's a requested attribute using `names` + revealed_attr_groups?: Record< + string, + { + sub_proof_index: number + values: { + [key: string]: { + raw: string + encoded: string + } + } + } + > + unrevealed_attrs: Record< + string, + { + sub_proof_index: number + } + > + self_attested_attrs: Record + + predicates: Record + } + // TODO: extend types for proof property + // eslint-disable-next-line @typescript-eslint/no-explicit-any + proof: any + identifiers: Array<{ + schema_id: string + cred_def_id: string + rev_reg_id?: string + timestamp?: number + }> +} + +export interface AnonCredsRequestedAttribute { + name?: string + names?: string[] + restrictions?: AnonCredsProofRequestRestriction[] + non_revoked?: AnonCredsNonRevokedInterval +} + +export interface AnonCredsRequestedPredicate { + name: string + p_type: AnonCredsPredicateType + p_value: number + restrictions?: AnonCredsProofRequestRestriction[] + non_revoked?: AnonCredsNonRevokedInterval +} + +export interface AnonCredsProofRequest { + name: string + version: string + nonce: string + requested_attributes: Record + requested_predicates: Record + non_revoked?: AnonCredsNonRevokedInterval + ver?: '1.0' | '2.0' +} diff --git a/packages/anoncreds/src/models/index.ts b/packages/anoncreds/src/models/index.ts new file mode 100644 index 0000000000..3ad7724723 --- /dev/null +++ b/packages/anoncreds/src/models/index.ts @@ -0,0 +1,4 @@ +export * from './internal' +export * from './exchange' +export * from './registry' +export * from './AnonCredsRestrictionWrapper' diff --git a/packages/anoncreds/src/models/internal.ts b/packages/anoncreds/src/models/internal.ts new file mode 100644 index 0000000000..d626bd84b1 --- /dev/null +++ b/packages/anoncreds/src/models/internal.ts @@ -0,0 +1,40 @@ +import type { AnonCredsClaimRecord } from '../utils/credential' + +export interface AnonCredsCredentialInfo { + credentialId: string + attributes: AnonCredsClaimRecord + schemaId: string + credentialDefinitionId: string + revocationRegistryId: string | null + credentialRevocationId: string | null + methodName: string + createdAt: Date + updatedAt: Date + linkSecretId: string +} + +export interface AnonCredsRequestedAttributeMatch { + credentialId: string + timestamp?: number + revealed: boolean + credentialInfo: AnonCredsCredentialInfo + revoked?: boolean +} + +export interface AnonCredsRequestedPredicateMatch { + credentialId: string + timestamp?: number + credentialInfo: AnonCredsCredentialInfo + revoked?: boolean +} + +export interface AnonCredsSelectedCredentials { + attributes: Record + predicates: Record + selfAttestedAttributes: Record +} + +export interface AnonCredsLinkSecretBlindingData { + v_prime: string + vr_prime: string | null +} diff --git a/packages/anoncreds/src/models/registry.ts b/packages/anoncreds/src/models/registry.ts new file mode 100644 index 0000000000..883fd859fe --- /dev/null +++ b/packages/anoncreds/src/models/registry.ts @@ -0,0 +1,43 @@ +export interface AnonCredsSchema { + issuerId: string + name: string + version: string + attrNames: string[] +} + +export interface AnonCredsCredentialDefinition { + issuerId: string + schemaId: string + type: 'CL' + tag: string + // TODO: work out in more detail + value: { + primary: Record + revocation?: unknown + } +} + +export interface AnonCredsRevocationRegistryDefinition { + issuerId: string + revocDefType: 'CL_ACCUM' + credDefId: string + tag: string + value: { + publicKeys: { + accumKey: { + z: string + } + } + maxCredNum: number + tailsLocation: string + tailsHash: string + } +} + +export interface AnonCredsRevocationStatusList { + issuerId: string + revRegDefId: string + revocationList: Array + currentAccumulator: string + timestamp: number +} diff --git a/packages/anoncreds/src/models/utils.ts b/packages/anoncreds/src/models/utils.ts new file mode 100644 index 0000000000..beb1c97451 --- /dev/null +++ b/packages/anoncreds/src/models/utils.ts @@ -0,0 +1,32 @@ +import type { AnonCredsNonRevokedInterval } from './exchange' +import type { + AnonCredsSchema, + AnonCredsCredentialDefinition, + AnonCredsRevocationRegistryDefinition, + AnonCredsRevocationStatusList, +} from './registry' +import type { W3cJsonLdVerifiableCredential } from '@credo-ts/core' + +export interface AnonCredsSchemas { + [schemaId: string]: AnonCredsSchema +} + +export interface AnonCredsCredentialDefinitions { + [credentialDefinitionId: string]: AnonCredsCredentialDefinition +} + +export interface AnonCredsRevocationRegistries { + [revocationRegistryDefinitionId: string]: { + // tails file MUST already be downloaded on a higher level and stored + tailsFilePath: string + definition: AnonCredsRevocationRegistryDefinition + revocationStatusLists: { + [timestamp: number]: AnonCredsRevocationStatusList + } + } +} + +export interface CredentialWithRevocationMetadata { + credential: W3cJsonLdVerifiableCredential + nonRevoked?: AnonCredsNonRevokedInterval +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts new file mode 100644 index 0000000000..1937a7c20c --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts @@ -0,0 +1,1329 @@ +import type { LegacyIndyCredentialFormatService } from '../../../formats' +import type { + AgentContext, + AgentMessage, + DependencyManager, + FeatureRegistry, + CredentialProtocolOptions, + InboundMessageContext, + ProblemReportMessage, + ExtractCredentialFormats, + CredentialProtocol, +} from '@credo-ts/core' + +import { + CredentialRole, + Protocol, + CredentialRepository, + CredoError, + CredentialExchangeRecord, + CredentialState, + JsonTransformer, + ConnectionService, + Attachment, + AttachmentData, + AckStatus, + CredentialProblemReportReason, + CredentialsModuleConfig, + AutoAcceptCredential, + utils, + DidCommMessageRepository, + DidCommMessageRole, + BaseCredentialProtocol, + isLinkedAttachment, +} from '@credo-ts/core' + +import { AnonCredsCredentialProposal } from '../../../models/AnonCredsCredentialProposal' +import { composeCredentialAutoAccept, areCredentialPreviewAttributesEqual } from '../../../utils' + +import { + V1ProposeCredentialHandler, + V1OfferCredentialHandler, + V1RequestCredentialHandler, + V1IssueCredentialHandler, + V1CredentialAckHandler, + V1CredentialProblemReportHandler, +} from './handlers' +import { + V1CredentialPreview, + V1ProposeCredentialMessage, + V1OfferCredentialMessage, + INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + V1RequestCredentialMessage, + INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + V1IssueCredentialMessage, + INDY_CREDENTIAL_ATTACHMENT_ID, + V1CredentialAckMessage, + V1CredentialProblemReportMessage, +} from './messages' + +export interface V1CredentialProtocolConfig { + indyCredentialFormat: LegacyIndyCredentialFormatService +} + +export class V1CredentialProtocol + extends BaseCredentialProtocol<[LegacyIndyCredentialFormatService]> + implements CredentialProtocol<[LegacyIndyCredentialFormatService]> +{ + private indyCredentialFormat: LegacyIndyCredentialFormatService + + public constructor({ indyCredentialFormat }: V1CredentialProtocolConfig) { + super() + + // TODO: just create a new instance of LegacyIndyCredentialFormatService here so it makes the setup easier + this.indyCredentialFormat = indyCredentialFormat + } + + /** + * The version of the issue credential protocol this protocol supports + */ + public readonly version = 'v1' + + /** + * Registers the protocol implementation (handlers, feature registry) on the agent. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Register message handlers for the Issue Credential V1 Protocol + dependencyManager.registerMessageHandlers([ + new V1ProposeCredentialHandler(this), + new V1OfferCredentialHandler(this), + new V1RequestCredentialHandler(this), + new V1IssueCredentialHandler(this), + new V1CredentialAckHandler(this), + new V1CredentialProblemReportHandler(this), + ]) + + // Register Issue Credential V1 in feature registry, with supported roles + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/issue-credential/1.0', + roles: ['holder', 'issuer'], + }) + ) + } + + /** + * Create a {@link ProposeCredentialMessage} not bound to an existing credential exchange. + * To create a proposal as response to an existing credential exchange, use {@link createProposalAsResponse}. + * + * @param options The object containing config options + * @returns Object containing proposal message and associated credential record + * + */ + public async createProposal( + agentContext: AgentContext, + { + connectionRecord, + credentialFormats, + comment, + autoAcceptCredential, + }: CredentialProtocolOptions.CreateCredentialProposalOptions<[LegacyIndyCredentialFormatService]> + ): Promise> { + this.assertOnlyIndyFormat(credentialFormats) + + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + if (!credentialFormats.indy) { + throw new CredoError('Missing indy credential format in v1 create proposal call.') + } + + // TODO: linked attachments are broken currently. We never include them in the messages. + // The linking with previews does work, so it shouldn't be too much work to re-enable this. + const { linkedAttachments } = credentialFormats.indy + + // Create record + const credentialRecord = new CredentialExchangeRecord({ + connectionId: connectionRecord.id, + threadId: utils.uuid(), + state: CredentialState.ProposalSent, + role: CredentialRole.Holder, + linkedAttachments: linkedAttachments?.map((linkedAttachment) => linkedAttachment.attachment), + autoAcceptCredential, + protocolVersion: 'v1', + }) + + // call create proposal for validation of the proposal and addition of linked attachments + const { previewAttributes, attachment } = await this.indyCredentialFormat.createProposal(agentContext, { + credentialFormats, + credentialRecord, + }) + + // Transform the attachment into the attachment payload and use that to construct the v1 message + const indyCredentialProposal = JsonTransformer.fromJSON(attachment.getDataAsJson(), AnonCredsCredentialProposal) + + const credentialProposal = previewAttributes + ? new V1CredentialPreview({ + attributes: previewAttributes, + }) + : undefined + + // Create message + const message = new V1ProposeCredentialMessage({ + ...indyCredentialProposal, + id: credentialRecord.threadId, + credentialPreview: credentialProposal, + comment, + }) + + await didCommMessageRepository.saveAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + credentialRecord.credentialAttributes = credentialProposal?.attributes + await credentialRepository.save(agentContext, credentialRecord) + this.emitStateChangedEvent(agentContext, credentialRecord, null) + + return { credentialRecord, message } + } + + /** + * Process a received {@link ProposeCredentialMessage}. This will not accept the credential proposal + * or send a credential offer. It will only create a new, or update the existing credential record with + * the information from the credential proposal message. Use {@link createOfferAsResponse} + * after calling this method to create a credential offer. + * + * @param messageContext The message context containing a credential proposal message + * @returns credential record associated with the credential proposal message + * + */ + public async processProposal( + messageContext: InboundMessageContext + ): Promise { + const { message: proposalMessage, connection, agentContext } = messageContext + + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // TODO: with this method, we should update the credential protocol to use the ConnectionApi, so it + // only depends on the public api, rather than the internal API (this helps with breaking changes) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing credential proposal with message id ${proposalMessage.id}`) + + let credentialRecord = await this.findByProperties(messageContext.agentContext, { + threadId: proposalMessage.threadId, + role: CredentialRole.Issuer, + connectionId: connection?.id, + }) + + // Credential record already exists, this is a response to an earlier message sent by us + if (credentialRecord) { + agentContext.config.logger.debug('Credential record already exists for incoming proposal') + + // Assert + credentialRecord.assertProtocolVersion('v1') + credentialRecord.assertState(CredentialState.OfferSent) + + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1ProposeCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + const lastSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1OfferCredentialMessage, + role: DidCommMessageRole.Sender, + }) + + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, + expectedConnectionId: credentialRecord.connectionId, + }) + + await this.indyCredentialFormat.processProposal(messageContext.agentContext, { + credentialRecord, + attachment: new Attachment({ + data: new AttachmentData({ + json: JsonTransformer.toJSON(this.rfc0592ProposalFromV1ProposeMessage(proposalMessage)), + }), + }), + }) + + // Update record + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.ProposalReceived) + await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { + agentMessage: proposalMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } else { + agentContext.config.logger.debug('Credential record does not exists yet for incoming proposal') + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + + // No credential record exists with thread id + credentialRecord = new CredentialExchangeRecord({ + connectionId: connection?.id, + threadId: proposalMessage.threadId, + state: CredentialState.ProposalReceived, + role: CredentialRole.Issuer, + protocolVersion: 'v1', + }) + + // Save record + await credentialRepository.save(messageContext.agentContext, credentialRecord) + this.emitStateChangedEvent(messageContext.agentContext, credentialRecord, null) + + await didCommMessageRepository.saveAgentMessage(messageContext.agentContext, { + agentMessage: proposalMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } + return credentialRecord + } + + /** + * Processing an incoming credential message and create a credential offer as a response + * @param options The object containing config options + * @returns Object containing proposal message and associated credential record + */ + public async acceptProposal( + agentContext: AgentContext, + { + credentialRecord, + credentialFormats, + comment, + autoAcceptCredential, + }: CredentialProtocolOptions.AcceptCredentialProposalOptions<[LegacyIndyCredentialFormatService]> + ): Promise> { + // Assert + credentialRecord.assertProtocolVersion('v1') + credentialRecord.assertState(CredentialState.ProposalReceived) + if (credentialFormats) this.assertOnlyIndyFormat(credentialFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const proposalMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1ProposeCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + // NOTE: We set the credential attributes from the proposal on the record as we've 'accepted' them + // and can now use them to create the offer in the format services. It may be overwritten later on + // if the user provided other attributes in the credentialFormats array. + credentialRecord.credentialAttributes = proposalMessage.credentialPreview?.attributes + + const { attachment, previewAttributes } = await this.indyCredentialFormat.acceptProposal(agentContext, { + attachmentId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + credentialFormats, + credentialRecord, + proposalAttachment: new Attachment({ + data: new AttachmentData({ + json: JsonTransformer.toJSON(this.rfc0592ProposalFromV1ProposeMessage(proposalMessage)), + }), + }), + }) + + if (!previewAttributes) { + throw new CredoError('Missing required credential preview attributes from indy format service') + } + + const message = new V1OfferCredentialMessage({ + comment, + offerAttachments: [attachment], + credentialPreview: new V1CredentialPreview({ + attributes: previewAttributes, + }), + attachments: credentialRecord.linkedAttachments, + }) + + message.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + + credentialRecord.credentialAttributes = message.credentialPreview.attributes + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.OfferSent) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + return { credentialRecord, message } + } + + /** + * Negotiate a credential proposal as issuer (by sending a credential offer message) to the connection + * associated with the credential record. + * + * @param options configuration for the offer see {@link NegotiateCredentialProposalOptions} + * @returns Credential record associated with the credential offer and the corresponding new offer message + * + */ + public async negotiateProposal( + agentContext: AgentContext, + { + credentialFormats, + credentialRecord, + comment, + autoAcceptCredential, + }: CredentialProtocolOptions.NegotiateCredentialProposalOptions<[LegacyIndyCredentialFormatService]> + ): Promise> { + // Assert + credentialRecord.assertProtocolVersion('v1') + credentialRecord.assertState(CredentialState.ProposalReceived) + this.assertOnlyIndyFormat(credentialFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const { attachment, previewAttributes } = await this.indyCredentialFormat.createOffer(agentContext, { + attachmentId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + credentialFormats, + credentialRecord, + }) + + if (!previewAttributes) { + throw new CredoError('Missing required credential preview attributes from indy format service') + } + + const message = new V1OfferCredentialMessage({ + comment, + offerAttachments: [attachment], + credentialPreview: new V1CredentialPreview({ + attributes: previewAttributes, + }), + attachments: credentialRecord.linkedAttachments, + }) + message.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + + credentialRecord.credentialAttributes = message.credentialPreview.attributes + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.OfferSent) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + return { credentialRecord, message } + } + + /** + * Create a {@link OfferCredentialMessage} not bound to an existing credential exchange. + * To create an offer as response to an existing credential exchange, use {@link V1CredentialProtocol#createOfferAsResponse}. + * + * @param options The options containing config params for creating the credential offer + * @returns Object containing offer message and associated credential record + * + */ + public async createOffer( + agentContext: AgentContext, + { + credentialFormats, + autoAcceptCredential, + comment, + connectionRecord, + }: CredentialProtocolOptions.CreateCredentialOfferOptions<[LegacyIndyCredentialFormatService]> + ): Promise> { + // Assert + this.assertOnlyIndyFormat(credentialFormats) + + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + if (!credentialFormats.indy) { + throw new CredoError('Missing indy credential format data for v1 create offer') + } + + // Create record + const credentialRecord = new CredentialExchangeRecord({ + connectionId: connectionRecord?.id, + threadId: utils.uuid(), + linkedAttachments: credentialFormats.indy.linkedAttachments?.map( + (linkedAttachments) => linkedAttachments.attachment + ), + state: CredentialState.OfferSent, + role: CredentialRole.Issuer, + autoAcceptCredential, + protocolVersion: 'v1', + }) + + const { attachment, previewAttributes } = await this.indyCredentialFormat.createOffer(agentContext, { + attachmentId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + credentialFormats, + credentialRecord, + }) + + if (!previewAttributes) { + throw new CredoError('Missing required credential preview from indy format service') + } + + // Construct offer message + const message = new V1OfferCredentialMessage({ + id: credentialRecord.threadId, + credentialPreview: new V1CredentialPreview({ + attributes: previewAttributes, + }), + comment, + offerAttachments: [attachment], + attachments: credentialFormats.indy.linkedAttachments?.map((linkedAttachments) => linkedAttachments.attachment), + }) + + await didCommMessageRepository.saveAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + agentMessage: message, + role: DidCommMessageRole.Sender, + }) + + credentialRecord.credentialAttributes = message.credentialPreview.attributes + await credentialRepository.save(agentContext, credentialRecord) + this.emitStateChangedEvent(agentContext, credentialRecord, null) + + return { message, credentialRecord } + } + + /** + * Process a received {@link OfferCredentialMessage}. This will not accept the credential offer + * or send a credential request. It will only create a new credential record with + * the information from the credential offer message. Use {@link createRequest} + * after calling this method to create a credential request. + * + * @param messageContext The message context containing a credential request message + * @returns credential record associated with the credential offer message + * + */ + public async processOffer( + messageContext: InboundMessageContext + ): Promise { + const { message: offerMessage, connection, agentContext } = messageContext + + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // TODO: with this method, we should update the credential protocol to use the ConnectionApi, so it + // only depends on the public api, rather than the internal API (this helps with breaking changes) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing credential offer with id ${offerMessage.id}`) + + let credentialRecord = await this.findByProperties(agentContext, { + threadId: offerMessage.threadId, + role: CredentialRole.Holder, + connectionId: connection?.id, + }) + + const offerAttachment = offerMessage.getOfferAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + if (!offerAttachment) { + throw new CredoError(`Indy attachment with id ${INDY_CREDENTIAL_OFFER_ATTACHMENT_ID} not found in offer message`) + } + + if (credentialRecord) { + const lastSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1ProposeCredentialMessage, + role: DidCommMessageRole.Sender, + }) + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1OfferCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + // Assert + credentialRecord.assertProtocolVersion('v1') + credentialRecord.assertState(CredentialState.ProposalSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, + expectedConnectionId: credentialRecord.connectionId, + }) + + await this.indyCredentialFormat.processOffer(messageContext.agentContext, { + credentialRecord, + attachment: offerAttachment, + }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { + agentMessage: offerMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.OfferReceived) + + return credentialRecord + } else { + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + + // No credential record exists with thread id + credentialRecord = new CredentialExchangeRecord({ + connectionId: connection?.id, + threadId: offerMessage.threadId, + parentThreadId: offerMessage.thread?.parentThreadId, + state: CredentialState.OfferReceived, + role: CredentialRole.Holder, + protocolVersion: 'v1', + }) + + await this.indyCredentialFormat.processOffer(messageContext.agentContext, { + credentialRecord, + attachment: offerAttachment, + }) + + // Save in repository + await didCommMessageRepository.saveAgentMessage(messageContext.agentContext, { + agentMessage: offerMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + await credentialRepository.save(messageContext.agentContext, credentialRecord) + this.emitStateChangedEvent(messageContext.agentContext, credentialRecord, null) + + return credentialRecord + } + } + + /** + * Create a {@link RequestCredentialMessage} as response to a received credential offer. + * + * @param options configuration to use for the credential request + * @returns Object containing request message and associated credential record + * + */ + public async acceptOffer( + agentContext: AgentContext, + { + credentialRecord, + credentialFormats, + comment, + autoAcceptCredential, + }: CredentialProtocolOptions.AcceptCredentialOfferOptions<[LegacyIndyCredentialFormatService]> + ): Promise> { + // Assert credential + credentialRecord.assertProtocolVersion('v1') + credentialRecord.assertState(CredentialState.OfferReceived) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const offerMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1OfferCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + const offerAttachment = offerMessage.getOfferAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + if (!offerAttachment) { + throw new CredoError(`Indy attachment with id ${INDY_CREDENTIAL_OFFER_ATTACHMENT_ID} not found in offer message`) + } + + const { attachment } = await this.indyCredentialFormat.acceptOffer(agentContext, { + credentialRecord, + credentialFormats, + attachmentId: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + offerAttachment, + }) + + const requestMessage = new V1RequestCredentialMessage({ + comment, + requestAttachments: [attachment], + attachments: offerMessage.appendedAttachments?.filter((attachment) => isLinkedAttachment(attachment)), + }) + requestMessage.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + + credentialRecord.credentialAttributes = offerMessage.credentialPreview.attributes + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + credentialRecord.linkedAttachments = offerMessage.appendedAttachments?.filter((attachment) => + isLinkedAttachment(attachment) + ) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: requestMessage, + associatedRecordId: credentialRecord.id, + role: DidCommMessageRole.Sender, + }) + await this.updateState(agentContext, credentialRecord, CredentialState.RequestSent) + + return { message: requestMessage, credentialRecord } + } + + /** + * Process a received {@link RequestCredentialMessage}. This will not accept the credential request + * or send a credential. It will only update the existing credential record with + * the information from the credential request message. Use {@link createCredential} + * after calling this method to create a credential. + * + * @param messageContext The message context containing a credential request message + * @returns credential record associated with the credential request message + * + */ + public async negotiateOffer( + agentContext: AgentContext, + { + credentialFormats, + credentialRecord, + autoAcceptCredential, + comment, + }: CredentialProtocolOptions.NegotiateCredentialOfferOptions<[LegacyIndyCredentialFormatService]> + ): Promise> { + // Assert + credentialRecord.assertProtocolVersion('v1') + credentialRecord.assertState(CredentialState.OfferReceived) + this.assertOnlyIndyFormat(credentialFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + if (!credentialRecord.connectionId) { + throw new CredoError( + `No connectionId found for credential record '${credentialRecord.id}'. Connection-less issuance does not support negotiation.` + ) + } + + if (!credentialFormats.indy) { + throw new CredoError('Missing indy credential format in v1 negotiate proposal call.') + } + + const { linkedAttachments } = credentialFormats.indy + + // call create proposal for validation of the proposal and addition of linked attachments + // As the format is different for v1 of the issue credential protocol we won't be using the attachment + const { previewAttributes, attachment } = await this.indyCredentialFormat.createProposal(agentContext, { + credentialFormats, + credentialRecord, + }) + + // Transform the attachment into the attachment payload and use that to construct the v1 message + const indyCredentialProposal = JsonTransformer.fromJSON(attachment.getDataAsJson(), AnonCredsCredentialProposal) + + const credentialProposal = previewAttributes + ? new V1CredentialPreview({ + attributes: previewAttributes, + }) + : undefined + + // Create message + const message = new V1ProposeCredentialMessage({ + ...indyCredentialProposal, + credentialPreview: credentialProposal, + comment, + }) + + message.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + // Update record + credentialRecord.credentialAttributes = message.credentialPreview?.attributes + credentialRecord.linkedAttachments = linkedAttachments?.map((attachment) => attachment.attachment) + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.ProposalSent) + + return { credentialRecord, message } + } + + /** + * Starting from a request is not supported in v1 of the issue credential protocol + * because indy doesn't allow to start from a request + */ + public async createRequest(): Promise< + CredentialProtocolOptions.CredentialProtocolMsgReturnType + > { + throw new CredoError('Starting from a request is not supported for v1 issue credential protocol') + } + + public async processRequest( + messageContext: InboundMessageContext + ): Promise { + const { message: requestMessage, connection, agentContext } = messageContext + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // TODO: with this method, we should update the credential protocol to use the ConnectionApi, so it + // only depends on the public api, rather than the internal API (this helps with breaking changes) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing credential request with id ${requestMessage.id}`) + + const credentialRecord = await this.getByProperties(messageContext.agentContext, { + threadId: requestMessage.threadId, + role: CredentialRole.Issuer, + }) + + agentContext.config.logger.trace('Credential record found when processing credential request', credentialRecord) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1ProposeCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + const offerMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1OfferCredentialMessage, + role: DidCommMessageRole.Sender, + }) + + // Assert + credentialRecord.assertProtocolVersion('v1') + credentialRecord.assertState(CredentialState.OfferSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: proposalMessage ?? undefined, + lastSentMessage: offerMessage ?? undefined, + expectedConnectionId: credentialRecord.connectionId, + }) + + // This makes sure that the sender of the incoming message is authorized to do so. + if (!credentialRecord.connectionId) { + await connectionService.matchIncomingMessageToRequestMessageInOutOfBandExchange(messageContext, { + expectedConnectionId: credentialRecord.connectionId, + }) + credentialRecord.connectionId = connection?.id + } + + const requestAttachment = requestMessage.getRequestAttachmentById(INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID) + + if (!requestAttachment) { + throw new CredoError( + `Indy attachment with id ${INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID} not found in request message` + ) + } + + await this.indyCredentialFormat.processRequest(messageContext.agentContext, { + credentialRecord, + attachment: requestAttachment, + }) + + await didCommMessageRepository.saveAgentMessage(messageContext.agentContext, { + agentMessage: requestMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.RequestReceived) + + return credentialRecord + } + + /** + * Create a {@link V1IssueCredentialMessage} as response to a received credential request. + * + * @returns Object containing issue credential message and associated credential record + * + */ + public async acceptRequest( + agentContext: AgentContext, + { + credentialRecord, + credentialFormats, + comment, + autoAcceptCredential, + }: CredentialProtocolOptions.AcceptCredentialRequestOptions<[LegacyIndyCredentialFormatService]> + ): Promise> { + // Assert + credentialRecord.assertProtocolVersion('v1') + credentialRecord.assertState(CredentialState.RequestReceived) + if (credentialFormats) this.assertOnlyIndyFormat(credentialFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const offerMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1OfferCredentialMessage, + role: DidCommMessageRole.Sender, + }) + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1RequestCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + const offerAttachment = offerMessage.getOfferAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + const requestAttachment = requestMessage.getRequestAttachmentById(INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID) + + if (!offerAttachment || !requestAttachment) { + throw new CredoError( + `Missing data payload in offer or request attachment in credential Record ${credentialRecord.id}` + ) + } + + const { attachment } = await this.indyCredentialFormat.acceptRequest(agentContext, { + credentialRecord, + requestAttachment, + offerAttachment, + attachmentId: INDY_CREDENTIAL_ATTACHMENT_ID, + credentialFormats, + }) + + const issueMessage = new V1IssueCredentialMessage({ + comment, + credentialAttachments: [attachment], + attachments: credentialRecord.linkedAttachments, + }) + + issueMessage.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + issueMessage.setPleaseAck() + + await didCommMessageRepository.saveAgentMessage(agentContext, { + agentMessage: issueMessage, + associatedRecordId: credentialRecord.id, + role: DidCommMessageRole.Sender, + }) + + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.CredentialIssued) + + return { message: issueMessage, credentialRecord } + } + + /** + * Process an incoming {@link V1IssueCredentialMessage} + * + * @param messageContext The message context containing a credential acknowledgement message + * @returns credential record associated with the credential acknowledgement message + * + */ + public async processCredential( + messageContext: InboundMessageContext + ): Promise { + const { message: issueMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing credential with id ${issueMessage.id}`) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // TODO: with this method, we should update the credential protocol to use the ConnectionApi, so it + // only depends on the public api, rather than the internal API (this helps with breaking changes) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + const credentialRecord = await this.getByProperties(messageContext.agentContext, { + threadId: issueMessage.threadId, + role: CredentialRole.Holder, + connectionId: connection?.id, + }) + + const requestCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1RequestCredentialMessage, + role: DidCommMessageRole.Sender, + }) + const offerCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1OfferCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + // Assert + credentialRecord.assertProtocolVersion('v1') + credentialRecord.assertState(CredentialState.RequestSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: offerCredentialMessage, + lastSentMessage: requestCredentialMessage, + expectedConnectionId: credentialRecord.connectionId, + }) + + const issueAttachment = issueMessage.getCredentialAttachmentById(INDY_CREDENTIAL_ATTACHMENT_ID) + if (!issueAttachment) { + throw new CredoError('Missing indy credential attachment in processCredential') + } + + const requestAttachment = requestCredentialMessage?.getRequestAttachmentById(INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID) + if (!requestAttachment) { + throw new CredoError('Missing indy credential request attachment in processCredential') + } + + const offerAttachment = offerCredentialMessage?.getOfferAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + if (!offerAttachment) { + throw new CredoError('Missing indy credential request attachment in processCredential') + } + + await this.indyCredentialFormat.processCredential(messageContext.agentContext, { + offerAttachment, + attachment: issueAttachment, + credentialRecord, + requestAttachment, + }) + + await didCommMessageRepository.saveAgentMessage(messageContext.agentContext, { + agentMessage: issueMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.CredentialReceived) + + return credentialRecord + } + + /** + * Create a {@link CredentialAckMessage} as response to a received credential. + * + * @param credentialRecord The credential record for which to create the credential acknowledgement + * @returns Object containing credential acknowledgement message and associated credential record + * + */ + public async acceptCredential( + agentContext: AgentContext, + { credentialRecord }: CredentialProtocolOptions.AcceptCredentialOptions + ): Promise> { + credentialRecord.assertProtocolVersion('v1') + credentialRecord.assertState(CredentialState.CredentialReceived) + + // Create message + const ackMessage = new V1CredentialAckMessage({ + status: AckStatus.OK, + threadId: credentialRecord.threadId, + }) + + ackMessage.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + + await this.updateState(agentContext, credentialRecord, CredentialState.Done) + + return { message: ackMessage, credentialRecord } + } + + /** + * Process a received {@link CredentialAckMessage}. + * + * @param messageContext The message context containing a credential acknowledgement message + * @returns credential record associated with the credential acknowledgement message + * + */ + public async processAck( + messageContext: InboundMessageContext + ): Promise { + const { message: ackMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing credential ack with id ${ackMessage.id}`) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // TODO: with this method, we should update the credential protocol to use the ConnectionApi, so it + // only depends on the public api, rather than the internal API (this helps with breaking changes) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + const credentialRecord = await this.getByProperties(messageContext.agentContext, { + threadId: ackMessage.threadId, + + role: CredentialRole.Issuer, + connectionId: connection?.id, + }) + + const requestCredentialMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1RequestCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + const issueCredentialMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V1IssueCredentialMessage, + role: DidCommMessageRole.Sender, + }) + + // Assert + credentialRecord.assertProtocolVersion('v1') + credentialRecord.assertState(CredentialState.CredentialIssued) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: requestCredentialMessage, + lastSentMessage: issueCredentialMessage, + expectedConnectionId: credentialRecord.connectionId, + }) + + // Update record + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.Done) + + return credentialRecord + } + + /** + * Create a {@link V1CredentialProblemReportMessage} to be sent. + * + * @param message message to send + * @returns a {@link V1CredentialProblemReportMessage} + * + */ + public async createProblemReport( + _agentContext: AgentContext, + { credentialRecord, description }: CredentialProtocolOptions.CreateCredentialProblemReportOptions + ): Promise> { + const message = new V1CredentialProblemReportMessage({ + description: { + en: description, + code: CredentialProblemReportReason.IssuanceAbandoned, + }, + }) + + return { message, credentialRecord } + } + + // AUTO RESPOND METHODS + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + options: { + credentialRecord: CredentialExchangeRecord + proposalMessage: V1ProposeCredentialMessage + } + ) { + const { credentialRecord, proposalMessage } = options + + const credentialsModuleConfig = agentContext.dependencyManager.resolve(CredentialsModuleConfig) + + const autoAccept = composeCredentialAutoAccept( + credentialRecord.autoAcceptCredential, + credentialsModuleConfig.autoAcceptCredentials + ) + + // Handle always / never cases + if (autoAccept === AutoAcceptCredential.Always) return true + if (autoAccept === AutoAcceptCredential.Never) return false + + const offerMessage = await this.findOfferMessage(agentContext, credentialRecord.id) + + // Do not auto accept if missing properties + if (!offerMessage || !offerMessage.credentialPreview) return false + if (!proposalMessage.credentialPreview || !proposalMessage.credentialDefinitionId) return false + + const credentialOfferJson = offerMessage.indyCredentialOffer + + // Check if credential definition id matches + if (!credentialOfferJson) return false + if (credentialOfferJson.cred_def_id !== proposalMessage.credentialDefinitionId) return false + + // Check if preview values match + return areCredentialPreviewAttributesEqual( + proposalMessage.credentialPreview.attributes, + offerMessage.credentialPreview.attributes + ) + } + + public async shouldAutoRespondToOffer( + agentContext: AgentContext, + options: { + credentialRecord: CredentialExchangeRecord + offerMessage: V1OfferCredentialMessage + } + ) { + const { credentialRecord, offerMessage } = options + + const credentialsModuleConfig = agentContext.dependencyManager.resolve(CredentialsModuleConfig) + + const autoAccept = composeCredentialAutoAccept( + credentialRecord.autoAcceptCredential, + credentialsModuleConfig.autoAcceptCredentials + ) + + // Handle always / never cases + if (autoAccept === AutoAcceptCredential.Always) return true + if (autoAccept === AutoAcceptCredential.Never) return false + + const proposalMessage = await this.findProposalMessage(agentContext, credentialRecord.id) + + // Do not auto accept if missing properties + if (!offerMessage.credentialPreview) return false + if (!proposalMessage || !proposalMessage.credentialPreview || !proposalMessage.credentialDefinitionId) return false + + const credentialOfferJson = offerMessage.indyCredentialOffer + + // Check if credential definition id matches + if (!credentialOfferJson) return false + if (credentialOfferJson.cred_def_id !== proposalMessage.credentialDefinitionId) return false + + // Check if preview values match + return areCredentialPreviewAttributesEqual( + proposalMessage.credentialPreview.attributes, + offerMessage.credentialPreview.attributes + ) + } + + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + options: { + credentialRecord: CredentialExchangeRecord + requestMessage: V1RequestCredentialMessage + } + ) { + const { credentialRecord, requestMessage } = options + + const credentialsModuleConfig = agentContext.dependencyManager.resolve(CredentialsModuleConfig) + + const autoAccept = composeCredentialAutoAccept( + credentialRecord.autoAcceptCredential, + credentialsModuleConfig.autoAcceptCredentials + ) + + // Handle always / never cases + if (autoAccept === AutoAcceptCredential.Always) return true + if (autoAccept === AutoAcceptCredential.Never) return false + + const offerMessage = await this.findOfferMessage(agentContext, credentialRecord.id) + if (!offerMessage) return false + + const offerAttachment = offerMessage.getOfferAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + const requestAttachment = requestMessage.getRequestAttachmentById(INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID) + + if (!offerAttachment || !requestAttachment) return false + + return this.indyCredentialFormat.shouldAutoRespondToRequest(agentContext, { + credentialRecord, + offerAttachment, + requestAttachment, + }) + } + + public async shouldAutoRespondToCredential( + agentContext: AgentContext, + options: { + credentialRecord: CredentialExchangeRecord + credentialMessage: V1IssueCredentialMessage + } + ) { + const { credentialRecord, credentialMessage } = options + + const credentialsModuleConfig = agentContext.dependencyManager.resolve(CredentialsModuleConfig) + + const autoAccept = composeCredentialAutoAccept( + credentialRecord.autoAcceptCredential, + credentialsModuleConfig.autoAcceptCredentials + ) + + // Handle always / never cases + if (autoAccept === AutoAcceptCredential.Always) return true + if (autoAccept === AutoAcceptCredential.Never) return false + + const requestMessage = await this.findRequestMessage(agentContext, credentialRecord.id) + const offerMessage = await this.findOfferMessage(agentContext, credentialRecord.id) + + const credentialAttachment = credentialMessage.getCredentialAttachmentById(INDY_CREDENTIAL_ATTACHMENT_ID) + if (!credentialAttachment) return false + + const requestAttachment = requestMessage?.getRequestAttachmentById(INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID) + if (!requestAttachment) return false + + const offerAttachment = offerMessage?.getOfferAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + + return this.indyCredentialFormat.shouldAutoRespondToCredential(agentContext, { + credentialRecord, + credentialAttachment, + requestAttachment, + offerAttachment, + }) + } + + public async findProposalMessage(agentContext: AgentContext, credentialExchangeId: string) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialExchangeId, + messageClass: V1ProposeCredentialMessage, + }) + } + + public async findOfferMessage(agentContext: AgentContext, credentialExchangeId: string) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialExchangeId, + messageClass: V1OfferCredentialMessage, + }) + } + + public async findRequestMessage(agentContext: AgentContext, credentialExchangeId: string) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialExchangeId, + messageClass: V1RequestCredentialMessage, + }) + } + + public async findCredentialMessage(agentContext: AgentContext, credentialExchangeId: string) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialExchangeId, + messageClass: V1IssueCredentialMessage, + }) + } + + public async getFormatData( + agentContext: AgentContext, + credentialExchangeId: string + ): Promise< + CredentialProtocolOptions.GetCredentialFormatDataReturn< + ExtractCredentialFormats<[LegacyIndyCredentialFormatService]> + > + > { + // TODO: we could looking at fetching all record using a single query and then filtering based on the type of the message. + const [proposalMessage, offerMessage, requestMessage, credentialMessage] = await Promise.all([ + this.findProposalMessage(agentContext, credentialExchangeId), + this.findOfferMessage(agentContext, credentialExchangeId), + this.findRequestMessage(agentContext, credentialExchangeId), + this.findCredentialMessage(agentContext, credentialExchangeId), + ]) + + const indyProposal = proposalMessage + ? JsonTransformer.toJSON(this.rfc0592ProposalFromV1ProposeMessage(proposalMessage)) + : undefined + + const indyOffer = offerMessage?.indyCredentialOffer ?? undefined + const indyRequest = requestMessage?.indyCredentialRequest ?? undefined + const indyCredential = credentialMessage?.indyCredential ?? undefined + + return { + proposalAttributes: proposalMessage?.credentialPreview?.attributes, + proposal: proposalMessage + ? { + indy: indyProposal, + } + : undefined, + offerAttributes: offerMessage?.credentialPreview?.attributes, + offer: offerMessage + ? { + indy: indyOffer, + } + : undefined, + request: requestMessage + ? { + indy: indyRequest, + } + : undefined, + credential: credentialMessage + ? { + indy: indyCredential, + } + : undefined, + } + } + + private rfc0592ProposalFromV1ProposeMessage(proposalMessage: V1ProposeCredentialMessage) { + const indyCredentialProposal = new AnonCredsCredentialProposal({ + credentialDefinitionId: proposalMessage.credentialDefinitionId, + schemaId: proposalMessage.schemaId, + issuerDid: proposalMessage.issuerDid, + schemaIssuerDid: proposalMessage.schemaIssuerDid, + schemaName: proposalMessage.schemaName, + schemaVersion: proposalMessage.schemaVersion, + }) + + return indyCredentialProposal + } + + private assertOnlyIndyFormat(credentialFormats: Record) { + const formatKeys = Object.keys(credentialFormats) + + // It's fine to not have any formats in some cases, if indy is required the method that calls this should check for this + if (formatKeys.length === 0) return + + if (formatKeys.length !== 1 || !formatKeys.includes('indy')) { + throw new CredoError('Only indy credential format is supported for issue credential v1 protocol') + } + } + + public getFormatServiceForRecordType(credentialRecordType: string) { + if (credentialRecordType !== this.indyCredentialFormat.credentialRecordType) { + throw new CredoError( + `Unsupported credential record type ${credentialRecordType} for v1 issue credential protocol (need ${this.indyCredentialFormat.credentialRecordType})` + ) + } + + return this.indyCredentialFormat + } +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts new file mode 100644 index 0000000000..a138d43163 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts @@ -0,0 +1,882 @@ +import type { + AgentContext, + CustomCredentialTags, + CredentialPreviewAttribute, + AgentConfig, + CredentialStateChangedEvent, +} from '@credo-ts/core' + +import { + EventEmitter, + DidExchangeState, + Attachment, + AttachmentData, + JsonEncoder, + DidCommMessageRecord, + DidCommMessageRole, + CredoError, + CredentialState, + CredentialExchangeRecord, + CredentialFormatSpec, + AutoAcceptCredential, + JsonTransformer, + InboundMessageContext, + CredentialEventTypes, + AckStatus, + CredentialProblemReportReason, + CredentialRole, +} from '@credo-ts/core' +import { Subject } from 'rxjs' + +import { ConnectionService } from '../../../../../../core/src/modules/connections/services/ConnectionService' +import { CredentialRepository } from '../../../../../../core/src/modules/credentials/repository/CredentialRepository' +import { DidCommMessageRepository } from '../../../../../../core/src/storage/didcomm/DidCommMessageRepository' +import { getMockConnection, getAgentConfig, getAgentContext, mockFunction } from '../../../../../../core/tests/helpers' +import { LegacyIndyCredentialFormatService } from '../../../../formats/LegacyIndyCredentialFormatService' +import { convertAttributesToCredentialValues } from '../../../../utils/credential' +import { V1CredentialProtocol } from '../V1CredentialProtocol' +import { + INDY_CREDENTIAL_ATTACHMENT_ID, + INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + V1CredentialAckMessage, + V1CredentialProblemReportMessage, + V1IssueCredentialMessage, + V1OfferCredentialMessage, + V1ProposeCredentialMessage, + V1RequestCredentialMessage, + V1CredentialPreview, +} from '../messages' + +// Mock classes +jest.mock('../../../../../../core/src/modules/credentials/repository/CredentialRepository') +jest.mock('../../../../formats/LegacyIndyCredentialFormatService') +jest.mock('../../../../../../core/src/storage/didcomm/DidCommMessageRepository') +jest.mock('../../../../../../core/src/modules/connections/services/ConnectionService') + +// Mock typed object +const CredentialRepositoryMock = CredentialRepository as jest.Mock +const LegacyIndyCredentialFormatServiceMock = + LegacyIndyCredentialFormatService as jest.Mock +const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock +const ConnectionServiceMock = ConnectionService as jest.Mock + +const credentialRepository = new CredentialRepositoryMock() +const didCommMessageRepository = new DidCommMessageRepositoryMock() +const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatServiceMock() +const connectionService = new ConnectionServiceMock() + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +legacyIndyCredentialFormatService.credentialRecordType = 'w3c' + +const connection = getMockConnection({ + id: '123', + state: DidExchangeState.Completed, +}) + +const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', +}) + +const offerAttachment = new Attachment({ + id: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + mimeType: 'application/json', + data: new AttachmentData({ + base64: + 'eyJzY2hlbWFfaWQiOiJhYWEiLCJjcmVkX2RlZl9pZCI6IlRoN01wVGFSWlZSWW5QaWFiZHM4MVk6MzpDTDoxNzpUQUciLCJub25jZSI6Im5vbmNlIiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7fX0', + }), +}) + +const requestAttachment = new Attachment({ + id: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64({}), + }), +}) + +const credentialAttachment = new Attachment({ + id: INDY_CREDENTIAL_ATTACHMENT_ID, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64({ + values: convertAttributesToCredentialValues(credentialPreview.attributes), + }), + }), +}) + +const credentialProposalMessage = new V1ProposeCredentialMessage({ + comment: 'comment', + credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', +}) +const credentialRequestMessage = new V1RequestCredentialMessage({ + comment: 'abcd', + requestAttachments: [requestAttachment], +}) +const credentialOfferMessage = new V1OfferCredentialMessage({ + comment: 'some comment', + credentialPreview: credentialPreview, + offerAttachments: [offerAttachment], +}) +const credentialIssueMessage = new V1IssueCredentialMessage({ + comment: 'some comment', + credentialAttachments: [offerAttachment], +}) + +const didCommMessageRecord = new DidCommMessageRecord({ + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + message: { '@id': '123', '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential' }, + role: DidCommMessageRole.Receiver, +}) + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getAgentMessageMock = async (_agentContext: AgentContext, options: { messageClass: any }) => { + if (options.messageClass === V1ProposeCredentialMessage) { + return credentialProposalMessage + } + if (options.messageClass === V1OfferCredentialMessage) { + return credentialOfferMessage + } + if (options.messageClass === V1RequestCredentialMessage) { + return credentialRequestMessage + } + if (options.messageClass === V1IssueCredentialMessage) { + return credentialIssueMessage + } + + throw new CredoError('Could not find message') +} + +// A record is deserialized to JSON when it's stored into the storage. We want to simulate this behaviour for `offer` +// object to test our service would behave correctly. We use type assertion for `offer` attribute to `any`. +const mockCredentialRecord = ({ + state, + role, + threadId, + connectionId, + tags, + id, + credentialAttributes, +}: { + state?: CredentialState + role?: CredentialRole + tags?: CustomCredentialTags + threadId?: string + connectionId?: string + credentialId?: string + id?: string + credentialAttributes?: CredentialPreviewAttribute[] +} = {}) => { + const credentialRecord = new CredentialExchangeRecord({ + id, + credentialAttributes: credentialAttributes || credentialPreview.attributes, + state: state || CredentialState.OfferSent, + role: role || CredentialRole.Issuer, + threadId: threadId ?? '809dd7ec-f0e7-4b97-9231-7a3615af6139', + connectionId: connectionId ?? '123', + credentials: [ + { + credentialRecordType: 'w3c', + credentialRecordId: '123456', + }, + ], + tags, + protocolVersion: 'v1', + }) + + return credentialRecord +} + +describe('V1CredentialProtocol', () => { + let eventEmitter: EventEmitter + let agentConfig: AgentConfig + let agentContext: AgentContext + let credentialProtocol: V1CredentialProtocol + + beforeEach(async () => { + // real objects + agentConfig = getAgentConfig('V1CredentialProtocolCredTest') + eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) + + agentContext = getAgentContext({ + registerInstances: [ + [CredentialRepository, credentialRepository], + [DidCommMessageRepository, didCommMessageRepository], + [EventEmitter, eventEmitter], + [ConnectionService, connectionService], + ], + agentConfig, + }) + + // mock function implementations + mockFunction(connectionService.getById).mockResolvedValue(connection) + mockFunction(didCommMessageRepository.findAgentMessage).mockImplementation(getAgentMessageMock) + mockFunction(didCommMessageRepository.getAgentMessage).mockImplementation(getAgentMessageMock) + mockFunction(didCommMessageRepository.findByQuery).mockResolvedValue([ + didCommMessageRecord, + didCommMessageRecord, + didCommMessageRecord, + ]) + + credentialProtocol = new V1CredentialProtocol({ indyCredentialFormat: legacyIndyCredentialFormatService }) + }) + + afterEach(() => { + jest.resetAllMocks() + }) + + describe('acceptOffer', () => { + test(`calls the format service and updates state to ${CredentialState.RequestSent}`, async () => { + const credentialRecord = mockCredentialRecord({ + id: '84353745-8bd9-42e1-8d81-238ca77c29d2', + state: CredentialState.OfferReceived, + threadId: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + + // mock resolved format call + mockFunction(legacyIndyCredentialFormatService.acceptOffer).mockResolvedValue({ + attachment: requestAttachment, + format: new CredentialFormatSpec({ + format: 'indy', + attachmentId: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + }), + }) + + // when + const { message } = await credentialProtocol.acceptOffer(agentContext, { + comment: 'hello', + autoAcceptCredential: AutoAcceptCredential.Never, + credentialRecord, + }) + + // then + expect(credentialRecord).toMatchObject({ + state: CredentialState.RequestSent, + autoAcceptCredential: AutoAcceptCredential.Never, + }) + expect(message).toBeInstanceOf(V1RequestCredentialMessage) + expect(message.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/request-credential', + comment: 'hello', + '~thread': { + thid: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + }, + 'requests~attach': [JsonTransformer.toJSON(requestAttachment)], + }) + expect(credentialRepository.update).toHaveBeenCalledTimes(1) + expect(legacyIndyCredentialFormatService.acceptOffer).toHaveBeenCalledWith(agentContext, { + credentialRecord, + attachmentId: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + offerAttachment, + }) + expect(didCommMessageRepository.saveOrUpdateAgentMessage).toHaveBeenCalledWith(agentContext, { + agentMessage: message, + associatedRecordId: '84353745-8bd9-42e1-8d81-238ca77c29d2', + role: DidCommMessageRole.Sender, + }) + }) + + test(`calls updateState to update the state to ${CredentialState.RequestSent}`, async () => { + const credentialRecord = mockCredentialRecord({ + state: CredentialState.OfferReceived, + }) + + const updateStateSpy = jest.spyOn(credentialProtocol, 'updateState') + + // mock resolved format call + mockFunction(legacyIndyCredentialFormatService.acceptOffer).mockResolvedValue({ + attachment: requestAttachment, + format: new CredentialFormatSpec({ + format: 'indy', + attachmentId: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + }), + }) + + // when + await credentialProtocol.acceptOffer(agentContext, { + credentialRecord, + }) + + // then + expect(updateStateSpy).toHaveBeenCalledWith(agentContext, credentialRecord, CredentialState.RequestSent) + }) + + const validState = CredentialState.OfferReceived + const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) + test(`throws an error when state transition is invalid`, async () => { + await Promise.all( + invalidCredentialStates.map(async (state) => { + await expect( + credentialProtocol.acceptOffer(agentContext, { credentialRecord: mockCredentialRecord({ state }) }) + ).rejects.toThrow(`Credential record is in invalid state ${state}. Valid states are: ${validState}.`) + }) + ) + }) + }) + + describe('processRequest', () => { + let credential: CredentialExchangeRecord + let messageContext: InboundMessageContext + beforeEach(() => { + credential = mockCredentialRecord({ state: CredentialState.OfferSent }) + + const credentialRequest = new V1RequestCredentialMessage({ + comment: 'abcd', + requestAttachments: [requestAttachment], + }) + credentialRequest.setThread({ threadId: 'somethreadid' }) + messageContext = new InboundMessageContext(credentialRequest, { + agentContext, + connection, + }) + }) + + test(`updates state to ${CredentialState.RequestReceived}, set request and returns credential record`, async () => { + const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') + + // given + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) + + // when + const returnedCredentialRecord = await credentialProtocol.processRequest(messageContext) + + // then + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { + threadId: 'somethreadid', + role: CredentialRole.Issuer, + }) + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + expect(returnedCredentialRecord.state).toEqual(CredentialState.RequestReceived) + }) + + test(`emits stateChange event from ${CredentialState.OfferSent} to ${CredentialState.RequestReceived}`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) + + // mock offer so that the request works + const returnedCredentialRecord = await credentialProtocol.processRequest(messageContext) + + // then + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { + threadId: 'somethreadid', + role: CredentialRole.Issuer, + }) + expect(returnedCredentialRecord.state).toEqual(CredentialState.RequestReceived) + }) + + const validState = CredentialState.OfferSent + const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) + test(`throws an error when state transition is invalid`, async () => { + await Promise.all( + invalidCredentialStates.map(async (state) => { + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue( + Promise.resolve(mockCredentialRecord({ state })) + ) + await expect(credentialProtocol.processRequest(messageContext)).rejects.toThrow( + `Credential record is in invalid state ${state}. Valid states are: ${validState}.` + ) + }) + ) + }) + }) + + describe('acceptRequest', () => { + test(`updates state to ${CredentialState.CredentialIssued}`, async () => { + // given + const credentialRecord = mockCredentialRecord({ + state: CredentialState.RequestReceived, + threadId: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + + mockFunction(legacyIndyCredentialFormatService.acceptRequest).mockResolvedValue({ + attachment: credentialAttachment, + format: new CredentialFormatSpec({ + format: 'the-format', + attachmentId: 'the-attach-id', + }), + }) + + // when + await credentialProtocol.acceptRequest(agentContext, { credentialRecord }) + + // then + expect(credentialRepository.update).toHaveBeenCalledWith( + agentContext, + expect.objectContaining({ + state: CredentialState.CredentialIssued, + }) + ) + }) + + test(`emits stateChange event from ${CredentialState.RequestReceived} to ${CredentialState.CredentialIssued}`, async () => { + // given + const credentialRecord = mockCredentialRecord({ + state: CredentialState.RequestReceived, + threadId: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + + mockFunction(legacyIndyCredentialFormatService.acceptRequest).mockResolvedValue({ + attachment: credentialAttachment, + format: new CredentialFormatSpec({ + format: 'the-format', + attachmentId: 'the-attach-id', + }), + }) + + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + // when + await credentialProtocol.acceptRequest(agentContext, { credentialRecord }) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: CredentialState.RequestReceived, + credentialRecord: expect.objectContaining({ + state: CredentialState.CredentialIssued, + }), + }, + }) + }) + + test('returns credential response message based on credential request message', async () => { + // given + const credentialRecord = mockCredentialRecord({ + state: CredentialState.RequestReceived, + threadId: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + const comment = 'credential response comment' + + mockFunction(legacyIndyCredentialFormatService.acceptRequest).mockResolvedValue({ + attachment: credentialAttachment, + format: new CredentialFormatSpec({ + format: 'the-format', + attachmentId: 'the-attach-id', + }), + }) + + // when + const { message } = await credentialProtocol.acceptRequest(agentContext, { credentialRecord, comment }) + + // then + expect(message.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/issue-credential', + '~thread': { + thid: credentialRecord.threadId, + }, + comment, + 'credentials~attach': [JsonTransformer.toJSON(credentialAttachment)], + '~please_ack': expect.any(Object), + }) + + expect(legacyIndyCredentialFormatService.acceptRequest).toHaveBeenCalledWith(agentContext, { + credentialRecord, + requestAttachment, + offerAttachment, + attachmentId: INDY_CREDENTIAL_ATTACHMENT_ID, + }) + }) + }) + + describe('processCredential', () => { + test('finds credential record by thread id and calls processCredential on indyCredentialFormatService', async () => { + // given + const credentialRecord = mockCredentialRecord({ + state: CredentialState.RequestSent, + }) + const credentialResponse = new V1IssueCredentialMessage({ + comment: 'abcd', + credentialAttachments: [credentialAttachment], + }) + credentialResponse.setThread({ threadId: 'somethreadid' }) + const messageContext = new InboundMessageContext(credentialResponse, { agentContext, connection }) + + mockFunction(credentialRepository.getSingleByQuery).mockResolvedValue(credentialRecord) + + // when + await credentialProtocol.processCredential(messageContext) + + // then + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { + threadId: 'somethreadid', + connectionId: connection.id, + role: CredentialRole.Holder, + }) + + expect(didCommMessageRepository.saveAgentMessage).toHaveBeenCalledWith(agentContext, { + agentMessage: credentialResponse, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + + expect(legacyIndyCredentialFormatService.processCredential).toHaveBeenNthCalledWith(1, agentContext, { + attachment: credentialAttachment, + credentialRecord, + requestAttachment: expect.any(Attachment), + offerAttachment: expect.any(Attachment), + }) + }) + }) + + describe('acceptCredential', () => { + const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' + let credential: CredentialExchangeRecord + + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.CredentialReceived, + threadId, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + }) + + test(`updates state to ${CredentialState.Done}`, async () => { + // given + const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') + + // when + await credentialProtocol.acceptCredential(agentContext, { credentialRecord: credential }) + + // then + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + const [[, updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls + expect(updatedCredentialRecord).toMatchObject({ + state: CredentialState.Done, + }) + }) + + test(`emits stateChange event from ${CredentialState.CredentialReceived} to ${CredentialState.Done}`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + // when + await credentialProtocol.acceptCredential(agentContext, { credentialRecord: credential }) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: CredentialState.CredentialReceived, + credentialRecord: expect.objectContaining({ + state: CredentialState.Done, + }), + }, + }) + }) + + test('returns credential response message base on credential request message', async () => { + // given + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credential)) + + // when + const { message: ackMessage } = await credentialProtocol.acceptCredential(agentContext, { + credentialRecord: credential, + }) + + // then + expect(ackMessage.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/ack', + '~thread': { + thid: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + }, + }) + }) + + const validState = CredentialState.CredentialReceived + const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) + test(`throws an error when state transition is invalid`, async () => { + await Promise.all( + invalidCredentialStates.map(async (state) => { + await expect( + credentialProtocol.acceptCredential(agentContext, { + credentialRecord: mockCredentialRecord({ + state, + threadId, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }), + }) + ).rejects.toThrow(`Credential record is in invalid state ${state}. Valid states are: ${validState}.`) + }) + ) + }) + }) + + describe('processAck', () => { + let credential: CredentialExchangeRecord + let messageContext: InboundMessageContext + + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.CredentialIssued, + }) + + const credentialRequest = new V1CredentialAckMessage({ + status: AckStatus.OK, + threadId: 'somethreadid', + }) + messageContext = new InboundMessageContext(credentialRequest, { agentContext, connection }) + }) + + test(`updates state to ${CredentialState.Done} and returns credential record`, async () => { + const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') + + // given + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) + + // when + const returnedCredentialRecord = await credentialProtocol.processAck(messageContext) + + // then + const expectedCredentialRecord = { + state: CredentialState.Done, + } + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { + threadId: 'somethreadid', + connectionId: connection.id, + role: CredentialRole.Issuer, + }) + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + const [[, updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls + expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord) + expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) + }) + }) + + describe('createProblemReport', () => { + const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' + let credential: CredentialExchangeRecord + + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.OfferReceived, + threadId, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + }) + + test('returns problem report message base once get error', async () => { + // given + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credential)) + + // when + const { message } = await credentialProtocol.createProblemReport(agentContext, { + description: 'Indy error', + credentialRecord: credential, + }) + + message.setThread({ threadId }) + // then + expect(message.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/problem-report', + '~thread': { + thid: threadId, + }, + description: { + code: CredentialProblemReportReason.IssuanceAbandoned, + en: 'Indy error', + }, + }) + }) + }) + + describe('processProblemReport', () => { + let credential: CredentialExchangeRecord + let messageContext: InboundMessageContext + + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.OfferReceived, + }) + + const credentialProblemReportMessage = new V1CredentialProblemReportMessage({ + description: { + en: 'Indy error', + code: CredentialProblemReportReason.IssuanceAbandoned, + }, + }) + credentialProblemReportMessage.setThread({ threadId: 'somethreadid' }) + messageContext = new InboundMessageContext(credentialProblemReportMessage, { agentContext, connection }) + }) + + test(`updates problem report error message and returns credential record`, async () => { + const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') + + // given + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) + + // when + const returnedCredentialRecord = await credentialProtocol.processProblemReport(messageContext) + + // then + const expectedCredentialRecord = { + errorMessage: 'issuance-abandoned: Indy error', + } + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { + threadId: 'somethreadid', + }) + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + const [[, updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls + expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord) + expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) + }) + }) + + describe('repository methods', () => { + it('getById should return value from credentialRepository.getById', async () => { + const expected = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(expected)) + const result = await credentialProtocol.getById(agentContext, expected.id) + expect(credentialRepository.getById).toHaveBeenCalledWith(agentContext, expected.id) + + expect(result).toBe(expected) + }) + + it('getById should return value from credentialRepository.getSingleByQuery', async () => { + const expected = mockCredentialRecord() + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(expected)) + + const result = await credentialProtocol.getByProperties(agentContext, { + threadId: 'threadId', + role: CredentialRole.Issuer, + connectionId: 'connectionId', + }) + + expect(credentialRepository.getSingleByQuery).toHaveBeenCalledWith(agentContext, { + threadId: 'threadId', + connectionId: 'connectionId', + role: CredentialRole.Issuer, + }) + + expect(result).toBe(expected) + }) + + it('findById should return value from credentialRepository.findById', async () => { + const expected = mockCredentialRecord() + mockFunction(credentialRepository.findById).mockReturnValue(Promise.resolve(expected)) + const result = await credentialProtocol.findById(agentContext, expected.id) + expect(credentialRepository.findById).toHaveBeenCalledWith(agentContext, expected.id) + + expect(result).toBe(expected) + }) + + it('getAll should return value from credentialRepository.getAll', async () => { + const expected = [mockCredentialRecord(), mockCredentialRecord()] + + mockFunction(credentialRepository.getAll).mockReturnValue(Promise.resolve(expected)) + const result = await credentialProtocol.getAll(agentContext) + expect(credentialRepository.getAll).toHaveBeenCalledWith(agentContext) + + expect(result).toEqual(expect.arrayContaining(expected)) + }) + + it('findAllByQuery should return value from credentialRepository.findByQuery', async () => { + const expected = [mockCredentialRecord(), mockCredentialRecord()] + + mockFunction(credentialRepository.findByQuery).mockReturnValue(Promise.resolve(expected)) + const result = await credentialProtocol.findAllByQuery(agentContext, { state: CredentialState.OfferSent }, {}) + expect(credentialRepository.findByQuery).toHaveBeenCalledWith( + agentContext, + { state: CredentialState.OfferSent }, + {} + ) + + expect(result).toEqual(expect.arrayContaining(expected)) + }) + }) + + describe('deleteCredential', () => { + it('should call delete from repository', async () => { + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credentialRecord)) + + const repositoryDeleteSpy = jest.spyOn(credentialRepository, 'delete') + await credentialProtocol.delete(agentContext, credentialRecord) + expect(repositoryDeleteSpy).toHaveBeenNthCalledWith(1, agentContext, credentialRecord) + }) + + it('should call deleteCredentialById in indyCredentialFormatService if deleteAssociatedCredential is true', async () => { + const deleteCredentialMock = mockFunction(legacyIndyCredentialFormatService.deleteCredentialById) + + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + + await credentialProtocol.delete(agentContext, credentialRecord, { + deleteAssociatedCredentials: true, + deleteAssociatedDidCommMessages: false, + }) + + expect(deleteCredentialMock).toHaveBeenNthCalledWith( + 1, + agentContext, + credentialRecord.credentials[0].credentialRecordId + ) + }) + + it('should not call deleteCredentialById in indyCredentialFormatService if deleteAssociatedCredential is false', async () => { + const deleteCredentialMock = mockFunction(legacyIndyCredentialFormatService.deleteCredentialById) + + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + + await credentialProtocol.delete(agentContext, credentialRecord, { + deleteAssociatedCredentials: false, + deleteAssociatedDidCommMessages: false, + }) + + expect(deleteCredentialMock).not.toHaveBeenCalled() + }) + + it('deleteAssociatedCredentials should default to true', async () => { + const deleteCredentialMock = mockFunction(legacyIndyCredentialFormatService.deleteCredentialById) + + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + + await credentialProtocol.delete(agentContext, credentialRecord) + + expect(deleteCredentialMock).toHaveBeenNthCalledWith( + 1, + agentContext, + credentialRecord.credentials[0].credentialRecordId + ) + }) + it('deleteAssociatedDidCommMessages should default to true', async () => { + const deleteCredentialMock = mockFunction(legacyIndyCredentialFormatService.deleteCredentialById) + + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + + await credentialProtocol.delete(agentContext, credentialRecord) + + expect(deleteCredentialMock).toHaveBeenNthCalledWith( + 1, + agentContext, + credentialRecord.credentials[0].credentialRecordId + ) + expect(didCommMessageRepository.delete).toHaveBeenCalledTimes(3) + }) + }) +}) diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolProposeOffer.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolProposeOffer.test.ts new file mode 100644 index 0000000000..fb5f973652 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolProposeOffer.test.ts @@ -0,0 +1,394 @@ +import type { CredentialProtocolOptions, CredentialStateChangedEvent } from '@credo-ts/core' + +import { + EventEmitter, + DidExchangeState, + Attachment, + AttachmentData, + CredentialState, + CredentialFormatSpec, + CredentialExchangeRecord, + CredentialEventTypes, + JsonTransformer, + InboundMessageContext, +} from '@credo-ts/core' +import { Subject } from 'rxjs' + +import { ConnectionService } from '../../../../../../core/src/modules/connections/services/ConnectionService' +import { CredentialRepository } from '../../../../../../core/src/modules/credentials/repository/CredentialRepository' +import { DidCommMessageRepository } from '../../../../../../core/src/storage/didcomm/DidCommMessageRepository' +import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../../core/tests/helpers' +import { LegacyIndyCredentialFormatService } from '../../../../formats/LegacyIndyCredentialFormatService' +import { V1CredentialProtocol } from '../V1CredentialProtocol' +import { V1CredentialPreview, INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, V1OfferCredentialMessage } from '../messages' + +// Mock classes +jest.mock('../../../../../../core/src/modules/credentials/repository/CredentialRepository') +jest.mock('../../../../formats/LegacyIndyCredentialFormatService') +jest.mock('../../../../../../core/src/storage/didcomm/DidCommMessageRepository') +jest.mock('../../../../../../core/src/modules/connections/services/ConnectionService') + +// Mock typed object +const CredentialRepositoryMock = CredentialRepository as jest.Mock +const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock +const ConnectionServiceMock = ConnectionService as jest.Mock +const LegacyIndyCredentialFormatServiceMock = + LegacyIndyCredentialFormatService as jest.Mock + +const credentialRepository = new CredentialRepositoryMock() +const didCommMessageRepository = new DidCommMessageRepositoryMock() +const connectionService = new ConnectionServiceMock() +const indyCredentialFormatService = new LegacyIndyCredentialFormatServiceMock() + +const agentConfig = getAgentConfig('V1CredentialProtocolProposeOfferTest') +const eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) + +const agentContext = getAgentContext({ + registerInstances: [ + [CredentialRepository, credentialRepository], + [DidCommMessageRepository, didCommMessageRepository], + [ConnectionService, connectionService], + [EventEmitter, eventEmitter], + ], + agentConfig, +}) + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +indyCredentialFormatService.credentialRecordType = 'w3c' + +const connectionRecord = getMockConnection({ + id: '123', + state: DidExchangeState.Completed, +}) + +const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', +}) + +const offerAttachment = new Attachment({ + id: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + mimeType: 'application/json', + data: new AttachmentData({ + base64: + 'eyJzY2hlbWFfaWQiOiJhYWEiLCJjcmVkX2RlZl9pZCI6IlRoN01wVGFSWlZSWW5QaWFiZHM4MVk6MzpDTDoxNzpUQUciLCJub25jZSI6Im5vbmNlIiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7fX0', + }), +}) + +const proposalAttachment = new Attachment({ + data: new AttachmentData({ + json: { + cred_def_id: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', + schema_issuer_did: 'GMm4vMw8LLrLJjp81kRRLp', + schema_name: 'ahoy', + schema_version: '1.0', + schema_id: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuer_did: 'GMm4vMw8LLrLJjp81kRRLp', + }, + }), +}) + +describe('V1CredentialProtocolProposeOffer', () => { + let credentialProtocol: V1CredentialProtocol + + beforeEach(async () => { + // mock function implementations + mockFunction(connectionService.getById).mockResolvedValue(connectionRecord) + + credentialProtocol = new V1CredentialProtocol({ + indyCredentialFormat: indyCredentialFormatService, + }) + }) + + afterEach(() => { + jest.resetAllMocks() + }) + + describe('createProposal', () => { + const proposeOptions: CredentialProtocolOptions.CreateCredentialProposalOptions< + [LegacyIndyCredentialFormatService] + > = { + connectionRecord: connectionRecord, + credentialFormats: { + indy: { + credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + attributes: credentialPreview.attributes, + }, + }, + comment: 'v1 propose credential test', + } + + test(`creates credential record in ${CredentialState.OfferSent} state with offer, thread id`, async () => { + const repositorySaveSpy = jest.spyOn(credentialRepository, 'save') + + mockFunction(indyCredentialFormatService.createProposal).mockResolvedValue({ + attachment: proposalAttachment, + format: new CredentialFormatSpec({ + format: 'indy', + attachmentId: 'indy-proposal', + }), + }) + + await credentialProtocol.createProposal(agentContext, proposeOptions) + + // then + expect(repositorySaveSpy).toHaveBeenNthCalledWith( + 1, + agentContext, + expect.objectContaining({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + connectionId: connectionRecord.id, + state: CredentialState.ProposalSent, + }) + ) + }) + + test(`emits stateChange event with a new credential in ${CredentialState.ProposalSent} state`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + mockFunction(indyCredentialFormatService.createProposal).mockResolvedValue({ + attachment: proposalAttachment, + format: new CredentialFormatSpec({ + format: 'indy', + attachmentId: 'indy-proposal', + }), + }) + + await credentialProtocol.createProposal(agentContext, proposeOptions) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: null, + credentialRecord: expect.objectContaining({ + state: CredentialState.ProposalSent, + }), + }, + }) + }) + + test('returns credential proposal message', async () => { + mockFunction(indyCredentialFormatService.createProposal).mockResolvedValue({ + attachment: proposalAttachment, + format: new CredentialFormatSpec({ + format: 'indy', + attachmentId: 'indy-proposal', + }), + previewAttributes: credentialPreview.attributes, + }) + + const { message } = await credentialProtocol.createProposal(agentContext, proposeOptions) + + expect(message.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/propose-credential', + comment: 'v1 propose credential test', + cred_def_id: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', + schema_issuer_did: 'GMm4vMw8LLrLJjp81kRRLp', + schema_name: 'ahoy', + schema_version: '1.0', + schema_id: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuer_did: 'GMm4vMw8LLrLJjp81kRRLp', + credential_proposal: { + '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + ], + }, + }) + }) + }) + + describe('createOffer', () => { + const offerOptions: CredentialProtocolOptions.CreateCredentialOfferOptions<[LegacyIndyCredentialFormatService]> = { + comment: 'some comment', + connectionRecord, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', + }, + }, + } + + test(`creates credential record in ${CredentialState.OfferSent} state with offer, thread id`, async () => { + mockFunction(indyCredentialFormatService.createOffer).mockResolvedValue({ + attachment: offerAttachment, + format: new CredentialFormatSpec({ + format: 'indy', + attachmentId: 'indy-offer', + }), + previewAttributes: credentialPreview.attributes, + }) + + const repositorySaveSpy = jest.spyOn(credentialRepository, 'save') + + await credentialProtocol.createOffer(agentContext, offerOptions) + + // then + expect(repositorySaveSpy).toHaveBeenCalledTimes(1) + + const [[, createdCredentialRecord]] = repositorySaveSpy.mock.calls + expect(createdCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: createdCredentialRecord.threadId, + connectionId: connectionRecord.id, + state: CredentialState.OfferSent, + }) + }) + + test(`emits stateChange event with a new credential in ${CredentialState.OfferSent} state`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + mockFunction(indyCredentialFormatService.createOffer).mockResolvedValue({ + attachment: offerAttachment, + format: new CredentialFormatSpec({ + format: 'indy', + attachmentId: 'indy-offer', + }), + previewAttributes: credentialPreview.attributes, + }) + + await credentialProtocol.createOffer(agentContext, offerOptions) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: null, + credentialRecord: expect.objectContaining({ + state: CredentialState.OfferSent, + }), + }, + }) + }) + + test('throws error if preview is not returned from createProposal in indyCredentialFormatService', async () => { + mockFunction(indyCredentialFormatService.createOffer).mockResolvedValue({ + attachment: offerAttachment, + format: new CredentialFormatSpec({ + format: 'indy', + attachmentId: 'indy-offer', + }), + }) + + await expect(credentialProtocol.createOffer(agentContext, offerOptions)).rejects.toThrowError( + 'Missing required credential preview from indy format service' + ) + }) + + test('returns credential offer message', async () => { + mockFunction(indyCredentialFormatService.createOffer).mockResolvedValue({ + attachment: offerAttachment, + format: new CredentialFormatSpec({ + format: 'indy', + attachmentId: 'indy-offer', + }), + previewAttributes: credentialPreview.attributes, + }) + + const { message: credentialOffer } = await credentialProtocol.createOffer(agentContext, offerOptions) + expect(credentialOffer.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', + comment: 'some comment', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + ], + }, + 'offers~attach': [JsonTransformer.toJSON(offerAttachment)], + }) + }) + }) + + describe('processOffer', () => { + const credentialOfferMessage = new V1OfferCredentialMessage({ + comment: 'some comment', + credentialPreview: credentialPreview, + offerAttachments: [offerAttachment], + }) + const messageContext = new InboundMessageContext(credentialOfferMessage, { + agentContext, + connection: connectionRecord, + }) + + test(`creates and return credential record in ${CredentialState.OfferReceived} state with offer, thread ID`, async () => { + // when + await credentialProtocol.processOffer(messageContext) + + // then + expect(credentialRepository.save).toHaveBeenNthCalledWith( + 1, + agentContext, + expect.objectContaining({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: credentialOfferMessage.id, + connectionId: connectionRecord.id, + state: CredentialState.OfferReceived, + credentialAttributes: undefined, + }) + ) + }) + + test(`emits stateChange event with ${CredentialState.OfferReceived}`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + // when + await credentialProtocol.processOffer(messageContext) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: null, + credentialRecord: expect.objectContaining({ + state: CredentialState.OfferReceived, + }), + }, + }) + }) + }) +}) diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts new file mode 100644 index 0000000000..3b4a66d16f --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts @@ -0,0 +1,223 @@ +import type { EventReplaySubject } from '../../../../../../core/tests' +import type { AnonCredsTestsAgent } from '../../../../../tests/legacyAnonCredsSetup' +import type { AcceptCredentialOfferOptions, AcceptCredentialRequestOptions } from '@credo-ts/core' + +import { AutoAcceptCredential, CredentialExchangeRecord, CredentialState } from '@credo-ts/core' + +import { waitForCredentialRecordSubject, testLogger } from '../../../../../../core/tests' +import { setupAnonCredsTests } from '../../../../../tests/legacyAnonCredsSetup' +import { V1CredentialPreview } from '../messages' + +const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', +}) + +describe('V1 Connectionless Credentials', () => { + let faberAgent: AnonCredsTestsAgent + let aliceAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceReplay: EventReplaySubject + let credentialDefinitionId: string + let schemaId: string + + beforeEach(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + schemaId, + } = await setupAnonCredsTests({ + issuerName: 'Faber connection-less Credentials V1', + holderName: 'Alice connection-less Credentials V1', + attributeNames: ['name', 'age'], + createConnections: false, + })) + }) + + afterEach(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Faber starts with connection-less credential offer to Alice', async () => { + testLogger.test('Faber sends credential offer to Alice') + + // eslint-disable-next-line prefer-const + let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOffer({ + comment: 'V1 Out of Band offer', + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId, + }, + }, + protocolVersion: 'v1', + }) + + const { invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberCredentialRecord.id, + message, + domain: 'https://a-domain.com', + }) + + await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) + + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + testLogger.test('Alice sends credential request to Faber') + const acceptOfferOptions: AcceptCredentialOfferOptions = { + credentialRecordId: aliceCredentialRecord.id, + } + const credentialRecord = await aliceAgent.credentials.acceptOffer(acceptOfferOptions) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + const options: AcceptCredentialRequestOptions = { + credentialRecordId: faberCredentialRecord.id, + comment: 'V1 Indy Credential', + } + faberCredentialRecord = await faberAgent.credentials.acceptRequest(options) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Alice sends credential ack to Faber') + aliceCredentialRecord = await aliceAgent.credentials.acceptCredential({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + schemaId, + credentialDefinitionId, + }, + }, + }, + credentials: [ + { + credentialRecordType: 'w3c', + credentialRecordId: expect.any(String), + }, + ], + state: CredentialState.Done, + threadId: expect.any(String), + }) + + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + schemaId, + credentialDefinitionId, + }, + }, + }, + state: CredentialState.Done, + threadId: expect.any(String), + }) + }) + + test('Faber starts with connection-less credential offer to Alice with auto-accept enabled', async () => { + // eslint-disable-next-line prefer-const + let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOffer({ + comment: 'V1 Out of Band offer', + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId, + }, + }, + protocolVersion: 'v1', + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + }) + + const { message: offerMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + message, + domain: 'https://a-domain.com', + }) + + // Receive Message + await aliceAgent.receiveMessage(offerMessage.toJSON()) + + // Wait for it to be processed + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + }) + + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + schemaId, + credentialDefinitionId: credentialDefinitionId, + }, + }, + }, + credentials: [ + { + credentialRecordType: 'w3c', + credentialRecordId: expect.any(String), + }, + ], + state: CredentialState.Done, + threadId: expect.any(String), + }) + + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + threadId: expect.any(String), + }) + }) +}) diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials-auto-accept.e2e.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials-auto-accept.e2e.test.ts new file mode 100644 index 0000000000..274595a299 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials-auto-accept.e2e.test.ts @@ -0,0 +1,466 @@ +import type { EventReplaySubject } from '../../../../../../core/tests' +import type { AnonCredsTestsAgent } from '../../../../../tests/legacyAnonCredsSetup' + +import { + AutoAcceptCredential, + CredentialState, + CredentialExchangeRecord, + JsonTransformer, + CredentialRole, +} from '@credo-ts/core' + +import { waitForCredentialRecord, waitForCredentialRecordSubject, testLogger } from '../../../../../../core/tests' +import { setupAnonCredsTests } from '../../../../../tests/legacyAnonCredsSetup' +import { V1CredentialPreview } from '../messages' + +const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', +}) +const newCredentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'another x-ray value', + profile_picture: 'another profile picture', +}) + +describe('V1 Credentials Auto Accept', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let credentialDefinitionId: string + let schemaId: string + let faberConnectionId: string + let aliceConnectionId: string + + describe("Auto accept on 'always'", () => { + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + schemaId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber Credentials Auto Accept V1', + holderName: 'Alice Credentials Auto Accept V1', + attributeNames: ['name', 'age', 'x-ray', 'profile_picture'], + autoAcceptCredentials: AutoAcceptCredential.Always, + })) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test("Alice starts with V1 credential proposal to Faber, both with autoAcceptCredential on 'always'", async () => { + testLogger.test('Alice sends credential proposal to Faber') + + const aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + comment: 'v1 propose credential test', + }) + + testLogger.test('Alice waits for credential from Faber') + let aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber waits for credential ack from Alice') + aliceCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + schemaId: schemaId, + credentialDefinitionId: credentialDefinitionId, + }, + }, + }, + state: CredentialState.Done, + }) + }) + + test("Faber starts with V1 credential offer to Alice, both with autoAcceptCredential on 'always'", async () => { + testLogger.test('Faber sends credential offer to Alice') + const faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v1', + }) + testLogger.test('Alice waits for credential from Faber') + const aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + testLogger.test('Faber waits for credential ack from Alice') + const faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credentialRequest': expect.any(Object), + '_anoncreds/credential': { + schemaId, + credentialDefinitionId, + }, + }, + }, + credentials: [ + { + credentialRecordType: 'w3c', + credentialRecordId: expect.any(String), + }, + ], + state: CredentialState.CredentialReceived, + }) + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + }) + }) + }) + + describe("Auto accept on 'contentApproved'", () => { + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + schemaId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'faber agent: contentApproved v1', + holderName: 'alice agent: contentApproved v1', + attributeNames: ['name', 'age', 'x-ray', 'profile_picture'], + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + })) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + // ============================== + // TESTS v1 BEGIN + // ========================== + test("Alice starts with V1 credential proposal to Faber, both with autoAcceptCredential on 'contentApproved'", async () => { + testLogger.test('Alice sends credential proposal to Faber') + let aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + }) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialExchangeRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + faberCredentialExchangeRecord = await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialExchangeRecord.id, + comment: 'V1 Indy Offer', + credentialFormats: { + indy: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialExchangeRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialExchangeRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialExchangeRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credentialRequest': expect.any(Object), + '_anoncreds/credential': { + schemaId, + credentialDefinitionId: credentialDefinitionId, + }, + }, + }, + credentials: [ + { + credentialRecordType: 'w3c', + credentialRecordId: expect.any(String), + }, + ], + state: CredentialState.CredentialReceived, + }) + + expect(faberCredentialExchangeRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + schemaId, + credentialDefinitionId: credentialDefinitionId, + }, + }, + }, + state: CredentialState.Done, + }) + }) + + test("Faber starts with V1 credential offer to Alice, both with autoAcceptCredential on 'contentApproved'", async () => { + testLogger.test('Faber sends credential offer to Alice') + let faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v1', + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialExchangeRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + expect(JsonTransformer.toJSON(aliceCredentialExchangeRecord)).toMatchObject({ + state: CredentialState.OfferReceived, + }) + + // below values are not in json object + expect(aliceCredentialExchangeRecord.id).not.toBeNull() + expect(aliceCredentialExchangeRecord.getTags()).toEqual({ + role: CredentialRole.Holder, + parentThreadId: undefined, + threadId: aliceCredentialExchangeRecord.threadId, + state: aliceCredentialExchangeRecord.state, + connectionId: aliceConnectionId, + credentialIds: [], + }) + + testLogger.test('alice sends credential request to faber') + faberCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialExchangeRecord.id, + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialExchangeRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialExchangeRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialExchangeRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credentialRequest': expect.any(Object), + '_anoncreds/credential': { + schemaId, + credentialDefinitionId: credentialDefinitionId, + }, + }, + }, + credentials: [ + { + credentialRecordType: 'w3c', + credentialRecordId: expect.any(String), + }, + ], + state: CredentialState.CredentialReceived, + }) + + expect(faberCredentialExchangeRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + }) + }) + + test("Faber starts with V1 credential offer to Alice, both have autoAcceptCredential on 'contentApproved' and attributes did change", async () => { + testLogger.test('Faber sends credential offer to Alice') + let faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v1', + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialExchangeRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + // below values are not in json object + expect(aliceCredentialExchangeRecord.id).not.toBeNull() + expect(aliceCredentialExchangeRecord.getTags()).toEqual({ + role: CredentialRole.Holder, + parentThreadId: undefined, + threadId: aliceCredentialExchangeRecord.threadId, + state: aliceCredentialExchangeRecord.state, + connectionId: aliceConnectionId, + credentialIds: [], + }) + + testLogger.test('Alice sends credential request to Faber') + const aliceExchangeCredentialRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialExchangeRecord.id, + credentialFormats: { + indy: { + attributes: newCredentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + comment: 'v1 propose credential test', + }) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialExchangeRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceExchangeCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + // Check if the state of fabers credential record did not change + const faberRecord = await faberAgent.credentials.getById(faberCredentialExchangeRecord.id) + faberRecord.assertState(CredentialState.ProposalReceived) + + aliceCredentialExchangeRecord = await aliceAgent.credentials.getById(aliceCredentialExchangeRecord.id) + aliceCredentialExchangeRecord.assertState(CredentialState.ProposalSent) + }) + + test("Alice starts with V1 credential proposal to Faber, both have autoAcceptCredential on 'contentApproved' and attributes did change", async () => { + testLogger.test('Alice sends credential proposal to Faber') + const aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + comment: 'v1 propose credential test', + }) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialExchangeRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialExchangeRecord.id, + credentialFormats: { + indy: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + + const record = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + // below values are not in json object + expect(record.id).not.toBeNull() + expect(record.getTags()).toEqual({ + role: CredentialRole.Holder, + parentThreadId: undefined, + threadId: record.threadId, + state: record.state, + connectionId: aliceConnectionId, + credentialIds: [], + }) + + // Check if the state of the credential records did not change + faberCredentialExchangeRecord = await faberAgent.credentials.getById(faberCredentialExchangeRecord.id) + faberCredentialExchangeRecord.assertState(CredentialState.OfferSent) + + const aliceRecord = await aliceAgent.credentials.getById(record.id) + aliceRecord.assertState(CredentialState.OfferReceived) + }) + }) +}) diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials.e2e.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials.e2e.test.ts new file mode 100644 index 0000000000..62de0baa28 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials.e2e.test.ts @@ -0,0 +1,310 @@ +import type { AnonCredsTestsAgent } from '../../../../../tests/legacyAnonCredsSetup' + +import { + CredentialExchangeRecord, + CredentialRole, + CredentialState, + DidCommMessageRepository, + JsonTransformer, +} from '@credo-ts/core' + +import { waitForCredentialRecord } from '../../../../../../core/tests/helpers' +import testLogger from '../../../../../../core/tests/logger' +import { setupAnonCredsTests } from '../../../../../tests/legacyAnonCredsSetup' +import { + V1ProposeCredentialMessage, + V1RequestCredentialMessage, + V1IssueCredentialMessage, + V1OfferCredentialMessage, + V1CredentialPreview, +} from '../messages' + +describe('V1 Credentials', () => { + let faberAgent: AnonCredsTestsAgent + let aliceAgent: AnonCredsTestsAgent + let credentialDefinitionId: string + let aliceConnectionId: string + + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + credentialDefinitionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber Agent Credentials V1', + holderName: 'Alice Agent Credentials V1', + attributeNames: ['name', 'age', 'x-ray', 'profile_picture'], + })) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with V1 credential proposal to Faber', async () => { + const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', + }) + + testLogger.test('Alice sends (v1) credential proposal to Faber') + + const credentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', + }, + }, + comment: 'v1 propose credential test', + }) + + expect(credentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: credentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V1 Indy Proposal', + credentialFormats: { + indy: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) + const offerMessageRecord = await didCommMessageRepository.findAgentMessage(faberAgent.context, { + associatedRecordId: faberCredentialRecord.id, + messageClass: V1OfferCredentialMessage, + }) + + expect(JsonTransformer.toJSON(offerMessageRecord)).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', + comment: 'V1 Indy Proposal', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + { + name: 'x-ray', + 'mime-type': 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + 'mime-type': 'text/plain', + value: 'profile picture', + }, + ], + }, + 'offers~attach': expect.any(Array), + }) + + expect(aliceCredentialRecord).toMatchObject({ + id: expect.any(String), + connectionId: expect.any(String), + type: CredentialExchangeRecord.type, + }) + + // below values are not in json object + expect(aliceCredentialRecord.getTags()).toEqual({ + role: CredentialRole.Holder, + parentThreadId: undefined, + threadId: faberCredentialRecord.threadId, + connectionId: aliceCredentialRecord.connectionId, + state: aliceCredentialRecord.state, + credentialIds: [], + }) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + state: CredentialState.RequestSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V1 Indy Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + await aliceAgent.credentials.acceptCredential({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Faber waits for state done') + await waitForCredentialRecord(faberAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + const proposalMessage = await aliceAgent.credentials.findProposalMessage(aliceCredentialRecord.id) + const offerMessage = await aliceAgent.credentials.findOfferMessage(aliceCredentialRecord.id) + const requestMessage = await aliceAgent.credentials.findRequestMessage(aliceCredentialRecord.id) + const credentialMessage = await aliceAgent.credentials.findCredentialMessage(aliceCredentialRecord.id) + + expect(proposalMessage).toBeInstanceOf(V1ProposeCredentialMessage) + expect(offerMessage).toBeInstanceOf(V1OfferCredentialMessage) + expect(requestMessage).toBeInstanceOf(V1RequestCredentialMessage) + expect(credentialMessage).toBeInstanceOf(V1IssueCredentialMessage) + + const formatData = await aliceAgent.credentials.getFormatData(aliceCredentialRecord.id) + expect(formatData).toMatchObject({ + proposalAttributes: [ + { + name: 'name', + mimeType: 'text/plain', + value: 'John', + }, + { + name: 'age', + mimeType: 'text/plain', + value: '99', + }, + { + name: 'x-ray', + mimeType: 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + mimeType: 'text/plain', + value: 'profile picture', + }, + ], + proposal: { + indy: { + schema_issuer_did: expect.any(String), + schema_id: expect.any(String), + schema_name: expect.any(String), + schema_version: expect.any(String), + cred_def_id: expect.any(String), + issuer_did: expect.any(String), + }, + }, + offer: { + indy: { + schema_id: expect.any(String), + cred_def_id: expect.any(String), + key_correctness_proof: expect.any(Object), + nonce: expect.any(String), + }, + }, + offerAttributes: [ + { + name: 'name', + mimeType: 'text/plain', + value: 'John', + }, + { + name: 'age', + mimeType: 'text/plain', + value: '99', + }, + { + name: 'x-ray', + mimeType: 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + mimeType: 'text/plain', + value: 'profile picture', + }, + ], + request: { + indy: { + prover_did: expect.any(String), + cred_def_id: expect.any(String), + blinded_ms: expect.any(Object), + blinded_ms_correctness_proof: expect.any(Object), + nonce: expect.any(String), + }, + }, + credential: { + indy: { + schema_id: expect.any(String), + cred_def_id: expect.any(String), + rev_reg_id: null, + values: { + age: { raw: '99', encoded: '99' }, + profile_picture: { + raw: 'profile picture', + encoded: '28661874965215723474150257281172102867522547934697168414362313592277831163345', + }, + name: { + raw: 'John', + encoded: '76355713903561865866741292988746191972523015098789458240077478826513114743258', + }, + 'x-ray': { + raw: 'some x-ray', + encoded: '43715611391396952879378357808399363551139229809726238083934532929974486114650', + }, + }, + signature: expect.any(Object), + signature_correctness_proof: expect.any(Object), + rev_reg: null, + witness: null, + }, + }, + }) + }) +}) diff --git a/packages/anoncreds/src/protocols/credentials/v1/errors/V1CredentialProblemReportError.ts b/packages/anoncreds/src/protocols/credentials/v1/errors/V1CredentialProblemReportError.ts new file mode 100644 index 0000000000..2870a2d46b --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/errors/V1CredentialProblemReportError.ts @@ -0,0 +1,23 @@ +import type { ProblemReportErrorOptions, CredentialProblemReportReason } from '@credo-ts/core' + +import { ProblemReportError } from '@credo-ts/core' + +import { V1CredentialProblemReportMessage } from '../messages' + +export interface V1CredentialProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: CredentialProblemReportReason +} + +export class V1CredentialProblemReportError extends ProblemReportError { + public problemReport: V1CredentialProblemReportMessage + + public constructor(message: string, { problemCode }: V1CredentialProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new V1CredentialProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/errors/index.ts b/packages/anoncreds/src/protocols/credentials/v1/errors/index.ts new file mode 100644 index 0000000000..5d2b6fc15e --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/errors/index.ts @@ -0,0 +1 @@ +export { V1CredentialProblemReportError, V1CredentialProblemReportErrorOptions } from './V1CredentialProblemReportError' diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1CredentialAckHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1CredentialAckHandler.ts new file mode 100644 index 0000000000..157c838c55 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1CredentialAckHandler.ts @@ -0,0 +1,17 @@ +import type { V1CredentialProtocol } from '../V1CredentialProtocol' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { V1CredentialAckMessage } from '../messages' + +export class V1CredentialAckHandler implements MessageHandler { + private credentialProtocol: V1CredentialProtocol + public supportedMessages = [V1CredentialAckMessage] + + public constructor(credentialProtocol: V1CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.credentialProtocol.processAck(messageContext) + } +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1CredentialProblemReportHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1CredentialProblemReportHandler.ts new file mode 100644 index 0000000000..d715fe5e0d --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1CredentialProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { V1CredentialProtocol } from '../V1CredentialProtocol' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { V1CredentialProblemReportMessage } from '../messages' + +export class V1CredentialProblemReportHandler implements MessageHandler { + private credentialProtocol: V1CredentialProtocol + public supportedMessages = [V1CredentialProblemReportMessage] + + public constructor(credentialProtocol: V1CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.credentialProtocol.processProblemReport(messageContext) + } +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1IssueCredentialHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1IssueCredentialHandler.ts new file mode 100644 index 0000000000..9f674b6841 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1IssueCredentialHandler.ts @@ -0,0 +1,55 @@ +import type { V1CredentialProtocol } from '../V1CredentialProtocol' +import type { MessageHandler, MessageHandlerInboundMessage, CredentialExchangeRecord } from '@credo-ts/core' + +import { CredoError, getOutboundMessageContext } from '@credo-ts/core' + +import { V1IssueCredentialMessage } from '../messages' + +export class V1IssueCredentialHandler implements MessageHandler { + private credentialProtocol: V1CredentialProtocol + + public supportedMessages = [V1IssueCredentialMessage] + + public constructor(credentialProtocol: V1CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const credentialRecord = await this.credentialProtocol.processCredential(messageContext) + + const shouldAutoRespond = await this.credentialProtocol.shouldAutoRespondToCredential(messageContext.agentContext, { + credentialRecord, + credentialMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return await this.acceptCredential(credentialRecord, messageContext) + } + } + + private async acceptCredential( + credentialRecord: CredentialExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending acknowledgement with autoAccept`) + const { message } = await this.credentialProtocol.acceptCredential(messageContext.agentContext, { + credentialRecord, + }) + + const requestMessage = await this.credentialProtocol.findRequestMessage( + messageContext.agentContext, + credentialRecord.id + ) + if (!requestMessage) { + throw new CredoError(`No request message found for credential record with id '${credentialRecord.id}'`) + } + + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: requestMessage, + }) + } +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1OfferCredentialHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1OfferCredentialHandler.ts new file mode 100644 index 0000000000..25ba57e784 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1OfferCredentialHandler.ts @@ -0,0 +1,43 @@ +import type { V1CredentialProtocol } from '../V1CredentialProtocol' +import type { MessageHandler, MessageHandlerInboundMessage, CredentialExchangeRecord } from '@credo-ts/core' + +import { getOutboundMessageContext } from '@credo-ts/core' + +import { V1OfferCredentialMessage } from '../messages' + +export class V1OfferCredentialHandler implements MessageHandler { + private credentialProtocol: V1CredentialProtocol + public supportedMessages = [V1OfferCredentialMessage] + + public constructor(credentialProtocol: V1CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const credentialRecord = await this.credentialProtocol.processOffer(messageContext) + + const shouldAutoRespond = await this.credentialProtocol.shouldAutoRespondToOffer(messageContext.agentContext, { + credentialRecord, + offerMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return await this.acceptOffer(credentialRecord, messageContext) + } + } + + private async acceptOffer( + credentialRecord: CredentialExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) + const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { credentialRecord }) + + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + }) + } +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1ProposeCredentialHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1ProposeCredentialHandler.ts new file mode 100644 index 0000000000..e57e4445e2 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1ProposeCredentialHandler.ts @@ -0,0 +1,53 @@ +import type { V1CredentialProtocol } from '../V1CredentialProtocol' +import type { CredentialExchangeRecord, MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { getOutboundMessageContext } from '@credo-ts/core' + +import { V1ProposeCredentialMessage } from '../messages' + +export class V1ProposeCredentialHandler implements MessageHandler { + private credentialProtocol: V1CredentialProtocol + public supportedMessages = [V1ProposeCredentialMessage] + + public constructor(credentialProtocol: V1CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const credentialRecord = await this.credentialProtocol.processProposal(messageContext) + + const shouldAutoAcceptProposal = await this.credentialProtocol.shouldAutoRespondToProposal( + messageContext.agentContext, + { + credentialRecord, + proposalMessage: messageContext.message, + } + ) + + if (shouldAutoAcceptProposal) { + return await this.acceptProposal(credentialRecord, messageContext) + } + } + + private async acceptProposal( + credentialRecord: CredentialExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending offer with autoAccept`) + + if (!messageContext.connection) { + messageContext.agentContext.config.logger.error('No connection on the messageContext, aborting auto accept') + return + } + + const { message } = await this.credentialProtocol.acceptProposal(messageContext.agentContext, { + credentialRecord, + }) + + return getOutboundMessageContext(messageContext.agentContext, { + message, + connectionRecord: messageContext.connection, + associatedRecord: credentialRecord, + }) + } +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1RequestCredentialHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1RequestCredentialHandler.ts new file mode 100644 index 0000000000..807438438a --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1RequestCredentialHandler.ts @@ -0,0 +1,55 @@ +import type { V1CredentialProtocol } from '../V1CredentialProtocol' +import type { CredentialExchangeRecord, MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { CredoError, getOutboundMessageContext } from '@credo-ts/core' + +import { V1RequestCredentialMessage } from '../messages' + +export class V1RequestCredentialHandler implements MessageHandler { + private credentialProtocol: V1CredentialProtocol + public supportedMessages = [V1RequestCredentialMessage] + + public constructor(credentialProtocol: V1CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const credentialRecord = await this.credentialProtocol.processRequest(messageContext) + + const shouldAutoRespond = await this.credentialProtocol.shouldAutoRespondToRequest(messageContext.agentContext, { + credentialRecord, + requestMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return await this.acceptRequest(credentialRecord, messageContext) + } + } + + private async acceptRequest( + credentialRecord: CredentialExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending credential with autoAccept`) + + const offerMessage = await this.credentialProtocol.findOfferMessage( + messageContext.agentContext, + credentialRecord.id + ) + if (!offerMessage) { + throw new CredoError(`Could not find offer message for credential record with id ${credentialRecord.id}`) + } + + const { message } = await this.credentialProtocol.acceptRequest(messageContext.agentContext, { + credentialRecord, + }) + + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: offerMessage, + }) + } +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/index.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/index.ts new file mode 100644 index 0000000000..8566870084 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/index.ts @@ -0,0 +1,6 @@ +export * from './V1CredentialAckHandler' +export * from './V1IssueCredentialHandler' +export * from './V1OfferCredentialHandler' +export * from './V1ProposeCredentialHandler' +export * from './V1RequestCredentialHandler' +export * from './V1CredentialProblemReportHandler' diff --git a/packages/anoncreds/src/protocols/credentials/v1/index.ts b/packages/anoncreds/src/protocols/credentials/v1/index.ts new file mode 100644 index 0000000000..4529b2ab71 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/index.ts @@ -0,0 +1,2 @@ +export * from './V1CredentialProtocol' +export * from './messages' diff --git a/packages/anoncreds/src/protocols/credentials/v1/messages/V1CredentialAckMessage.ts b/packages/anoncreds/src/protocols/credentials/v1/messages/V1CredentialAckMessage.ts new file mode 100644 index 0000000000..107962e531 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/messages/V1CredentialAckMessage.ts @@ -0,0 +1,24 @@ +import type { AckMessageOptions } from '@credo-ts/core' + +import { AckMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' + +export type V1CredentialAckMessageOptions = AckMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0015-acks/README.md#explicit-acks + */ +export class V1CredentialAckMessage extends AckMessage { + public readonly allowDidSovPrefix = true + + /** + * Create new CredentialAckMessage instance. + * @param options + */ + public constructor(options: V1CredentialAckMessageOptions) { + super(options) + } + + @IsValidMessageType(V1CredentialAckMessage.type) + public readonly type = V1CredentialAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/ack') +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/messages/V1CredentialPreview.ts b/packages/anoncreds/src/protocols/credentials/v1/messages/V1CredentialPreview.ts new file mode 100644 index 0000000000..ed1fcfc4d6 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/messages/V1CredentialPreview.ts @@ -0,0 +1,67 @@ +import type { CredentialPreviewOptions } from '@credo-ts/core' + +import { + CredentialPreviewAttribute, + IsValidMessageType, + parseMessageType, + JsonTransformer, + replaceLegacyDidSovPrefix, +} from '@credo-ts/core' +import { Expose, Transform, Type } from 'class-transformer' +import { ValidateNested, IsInstance } from 'class-validator' + +/** + * Credential preview inner message class. + * + * This is not a message but an inner object for other messages in this protocol. It is used construct a preview of the data for the credential. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0036-issue-credential/README.md#preview-credential + */ +export class V1CredentialPreview { + public constructor(options: CredentialPreviewOptions) { + if (options) { + this.attributes = options.attributes.map((a) => new CredentialPreviewAttribute(a)) + } + } + + @Expose({ name: '@type' }) + @IsValidMessageType(V1CredentialPreview.type) + @Transform(({ value }) => replaceLegacyDidSovPrefix(value), { + toClassOnly: true, + }) + public readonly type = V1CredentialPreview.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/credential-preview') + + @Type(() => CredentialPreviewAttribute) + @ValidateNested({ each: true }) + @IsInstance(CredentialPreviewAttribute, { each: true }) + public attributes!: CredentialPreviewAttribute[] + + public toJSON(): Record { + return JsonTransformer.toJSON(this) + } + + /** + * Create a credential preview from a record with name and value entries. + * + * @example + * const preview = CredentialPreview.fromRecord({ + * name: "Bob", + * age: "20" + * }) + */ + public static fromRecord(record: Record) { + const attributes = Object.entries(record).map( + ([name, value]) => + new CredentialPreviewAttribute({ + name, + mimeType: 'text/plain', + value, + }) + ) + + return new V1CredentialPreview({ + attributes, + }) + } +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/messages/V1CredentialProblemReportMessage.ts b/packages/anoncreds/src/protocols/credentials/v1/messages/V1CredentialProblemReportMessage.ts new file mode 100644 index 0000000000..520fb474c6 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/messages/V1CredentialProblemReportMessage.ts @@ -0,0 +1,24 @@ +import type { ProblemReportMessageOptions } from '@credo-ts/core' + +import { ProblemReportMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' + +export type V1CredentialProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class V1CredentialProblemReportMessage extends ProblemReportMessage { + public readonly allowDidSovPrefix = true + + /** + * Create new CredentialProblemReportMessage instance. + * @param options + */ + public constructor(options: V1CredentialProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(V1CredentialProblemReportMessage.type) + public readonly type = V1CredentialProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/problem-report') +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/messages/V1IssueCredentialMessage.ts b/packages/anoncreds/src/protocols/credentials/v1/messages/V1IssueCredentialMessage.ts new file mode 100644 index 0000000000..5985f51b11 --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/messages/V1IssueCredentialMessage.ts @@ -0,0 +1,59 @@ +import type { AnonCredsCredential } from '../../../../models' + +import { Attachment, AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' +import { Expose, Type } from 'class-transformer' +import { IsString, IsOptional, IsArray, ValidateNested, IsInstance } from 'class-validator' + +export const INDY_CREDENTIAL_ATTACHMENT_ID = 'libindy-cred-0' + +export interface V1IssueCredentialMessageOptions { + id?: string + comment?: string + credentialAttachments: Attachment[] + attachments?: Attachment[] +} + +export class V1IssueCredentialMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + + public constructor(options: V1IssueCredentialMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.credentialAttachments = options.credentialAttachments + this.appendedAttachments = options.attachments + } + } + + @IsValidMessageType(V1IssueCredentialMessage.type) + public readonly type = V1IssueCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/issue-credential') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'credentials~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public credentialAttachments!: Attachment[] + + public get indyCredential(): AnonCredsCredential | null { + const attachment = this.credentialAttachments.find((attachment) => attachment.id === INDY_CREDENTIAL_ATTACHMENT_ID) + + // Extract credential from attachment + const credentialJson = attachment?.getDataAsJson() ?? null + + return credentialJson + } + + public getCredentialAttachmentById(id: string): Attachment | undefined { + return this.credentialAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/messages/V1OfferCredentialMessage.ts b/packages/anoncreds/src/protocols/credentials/v1/messages/V1OfferCredentialMessage.ts new file mode 100644 index 0000000000..3697d011ec --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/messages/V1OfferCredentialMessage.ts @@ -0,0 +1,74 @@ +import type { AnonCredsCredentialOffer } from '../../../../models' + +import { Attachment, AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' +import { Expose, Type } from 'class-transformer' +import { IsString, IsOptional, ValidateNested, IsInstance, IsArray } from 'class-validator' + +import { V1CredentialPreview } from './V1CredentialPreview' + +export const INDY_CREDENTIAL_OFFER_ATTACHMENT_ID = 'libindy-cred-offer-0' + +export interface V1OfferCredentialMessageOptions { + id?: string + comment?: string + offerAttachments: Attachment[] + credentialPreview: V1CredentialPreview + attachments?: Attachment[] +} + +/** + * Message part of Issue Credential Protocol used to continue or initiate credential exchange by issuer. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0036-issue-credential/README.md#offer-credential + */ +export class V1OfferCredentialMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + + public constructor(options: V1OfferCredentialMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.comment = options.comment + this.credentialPreview = options.credentialPreview + this.offerAttachments = options.offerAttachments + this.appendedAttachments = options.attachments + } + } + + @IsValidMessageType(V1OfferCredentialMessage.type) + public readonly type = V1OfferCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/offer-credential') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'credential_preview' }) + @Type(() => V1CredentialPreview) + @ValidateNested() + @IsInstance(V1CredentialPreview) + public credentialPreview!: V1CredentialPreview + + @Expose({ name: 'offers~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public offerAttachments!: Attachment[] + + public get indyCredentialOffer(): AnonCredsCredentialOffer | null { + const attachment = this.offerAttachments.find((attachment) => attachment.id === INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + + // Extract credential offer from attachment + const credentialOfferJson = attachment?.getDataAsJson() ?? null + + return credentialOfferJson + } + + public getOfferAttachmentById(id: string): Attachment | undefined { + return this.offerAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/messages/V1ProposeCredentialMessage.ts b/packages/anoncreds/src/protocols/credentials/v1/messages/V1ProposeCredentialMessage.ts new file mode 100644 index 0000000000..4c4f1c05fa --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/messages/V1ProposeCredentialMessage.ts @@ -0,0 +1,130 @@ +import type { Attachment } from '@credo-ts/core' + +import { AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' +import { Expose, Type } from 'class-transformer' +import { IsInstance, IsOptional, IsString, Matches, ValidateNested } from 'class-validator' + +import { + unqualifiedCredentialDefinitionIdRegex, + unqualifiedIndyDidRegex, + unqualifiedSchemaIdRegex, + unqualifiedSchemaVersionRegex, +} from '../../../../utils' + +import { V1CredentialPreview } from './V1CredentialPreview' + +export interface V1ProposeCredentialMessageOptions { + id?: string + comment?: string + credentialPreview?: V1CredentialPreview + schemaIssuerDid?: string + schemaId?: string + schemaName?: string + schemaVersion?: string + credentialDefinitionId?: string + issuerDid?: string + attachments?: Attachment[] +} + +/** + * Message part of Issue Credential Protocol used to initiate credential exchange by prover. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0036-issue-credential/README.md#propose-credential + */ +export class V1ProposeCredentialMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + + public constructor(options: V1ProposeCredentialMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.credentialPreview = options.credentialPreview + this.schemaIssuerDid = options.schemaIssuerDid + this.schemaId = options.schemaId + this.schemaName = options.schemaName + this.schemaVersion = options.schemaVersion + this.credentialDefinitionId = options.credentialDefinitionId + this.issuerDid = options.issuerDid + this.appendedAttachments = options.attachments + } + } + + @IsValidMessageType(V1ProposeCredentialMessage.type) + public readonly type = V1ProposeCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/propose-credential') + + /** + * Human readable information about this Credential Proposal, + * so the proposal can be evaluated by human judgment. + */ + @IsOptional() + @IsString() + public comment?: string + + /** + * Represents the credential data that Prover wants to receive. + */ + @Expose({ name: 'credential_proposal' }) + @Type(() => V1CredentialPreview) + @ValidateNested() + @IsOptional() + @IsInstance(V1CredentialPreview) + public credentialPreview?: V1CredentialPreview + + /** + * Filter to request credential based on a particular Schema issuer DID. + */ + @Expose({ name: 'schema_issuer_did' }) + @IsString() + @IsOptional() + @Matches(unqualifiedIndyDidRegex) + public schemaIssuerDid?: string + + /** + * Filter to request credential based on a particular Schema. + */ + @Expose({ name: 'schema_id' }) + @IsString() + @IsOptional() + @Matches(unqualifiedSchemaIdRegex) + public schemaId?: string + + /** + * Filter to request credential based on a schema name. + */ + @Expose({ name: 'schema_name' }) + @IsString() + @IsOptional() + public schemaName?: string + + /** + * Filter to request credential based on a schema version. + */ + @Expose({ name: 'schema_version' }) + @IsString() + @IsOptional() + @Matches(unqualifiedSchemaVersionRegex, { + message: 'Version must be X.X or X.X.X', + }) + public schemaVersion?: string + + /** + * Filter to request credential based on a particular Credential Definition. + */ + @Expose({ name: 'cred_def_id' }) + @IsString() + @IsOptional() + @Matches(unqualifiedCredentialDefinitionIdRegex) + public credentialDefinitionId?: string + + /** + * Filter to request a credential issued by the owner of a particular DID. + */ + @Expose({ name: 'issuer_did' }) + @IsString() + @IsOptional() + @Matches(unqualifiedIndyDidRegex) + public issuerDid?: string +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/messages/V1RequestCredentialMessage.ts b/packages/anoncreds/src/protocols/credentials/v1/messages/V1RequestCredentialMessage.ts new file mode 100644 index 0000000000..794b485bfd --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/messages/V1RequestCredentialMessage.ts @@ -0,0 +1,60 @@ +import type { LegacyIndyCredentialRequest } from '../../../../formats' + +import { Attachment, AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +export const INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID = 'libindy-cred-request-0' + +export interface V1RequestCredentialMessageOptions { + id?: string + comment?: string + requestAttachments: Attachment[] + attachments?: Attachment[] +} + +export class V1RequestCredentialMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + + public constructor(options: V1RequestCredentialMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.comment = options.comment + this.requestAttachments = options.requestAttachments + this.appendedAttachments = options.attachments + } + } + + @IsValidMessageType(V1RequestCredentialMessage.type) + public readonly type = V1RequestCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/request-credential') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'requests~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public requestAttachments!: Attachment[] + + public get indyCredentialRequest(): LegacyIndyCredentialRequest | null { + const attachment = this.requestAttachments.find( + (attachment) => attachment.id === INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID + ) + // Extract proof request from attachment + const credentialReqJson = attachment?.getDataAsJson() ?? null + + return credentialReqJson + } + + public getRequestAttachmentById(id: string): Attachment | undefined { + return this.requestAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/anoncreds/src/protocols/credentials/v1/messages/index.ts b/packages/anoncreds/src/protocols/credentials/v1/messages/index.ts new file mode 100644 index 0000000000..350988c67d --- /dev/null +++ b/packages/anoncreds/src/protocols/credentials/v1/messages/index.ts @@ -0,0 +1,7 @@ +export * from './V1CredentialAckMessage' +export * from './V1CredentialPreview' +export * from './V1RequestCredentialMessage' +export * from './V1IssueCredentialMessage' +export * from './V1OfferCredentialMessage' +export * from './V1ProposeCredentialMessage' +export * from './V1CredentialProblemReportMessage' diff --git a/packages/anoncreds/src/protocols/index.ts b/packages/anoncreds/src/protocols/index.ts new file mode 100644 index 0000000000..d5a3d13f6c --- /dev/null +++ b/packages/anoncreds/src/protocols/index.ts @@ -0,0 +1,2 @@ +export * from './credentials/v1' +export * from './proofs/v1' diff --git a/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts b/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts new file mode 100644 index 0000000000..df5c931712 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts @@ -0,0 +1,1191 @@ +import type { LegacyIndyProofFormatService } from '../../../formats' +import type { + ProofProtocol, + DependencyManager, + FeatureRegistry, + AgentContext, + ProofProtocolOptions, + InboundMessageContext, + AgentMessage, + ProblemReportMessage, + GetProofFormatDataReturn, + ProofFormat, +} from '@credo-ts/core' + +import { + ProofRole, + BaseProofProtocol, + Protocol, + ProofRepository, + DidCommMessageRepository, + CredoError, + MessageValidator, + ProofExchangeRecord, + ProofState, + DidCommMessageRole, + ConnectionService, + Attachment, + JsonTransformer, + PresentationProblemReportReason, + AckStatus, + ProofsModuleConfig, + AutoAcceptProof, + JsonEncoder, + utils, +} from '@credo-ts/core' + +import { composeProofAutoAccept, createRequestFromPreview } from '../../../utils' + +import { V1PresentationProblemReportError } from './errors' +import { + V1PresentationAckHandler, + V1PresentationHandler, + V1PresentationProblemReportHandler, + V1ProposePresentationHandler, + V1RequestPresentationHandler, +} from './handlers' +import { + INDY_PROOF_ATTACHMENT_ID, + INDY_PROOF_REQUEST_ATTACHMENT_ID, + V1PresentationAckMessage, + V1PresentationMessage, + V1ProposePresentationMessage, + V1RequestPresentationMessage, +} from './messages' +import { V1PresentationProblemReportMessage } from './messages/V1PresentationProblemReportMessage' +import { V1PresentationPreview } from './models/V1PresentationPreview' + +export interface V1ProofProtocolConfig { + indyProofFormat: LegacyIndyProofFormatService +} + +export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol<[LegacyIndyProofFormatService]> { + private indyProofFormat: LegacyIndyProofFormatService + + public constructor({ indyProofFormat }: V1ProofProtocolConfig) { + super() + + // TODO: just create a new instance of LegacyIndyProofFormatService here so it makes the setup easier + this.indyProofFormat = indyProofFormat + } + + /** + * The version of the present proof protocol this protocol supports + */ + public readonly version = 'v1' as const + + /** + * Registers the protocol implementation (handlers, feature registry) on the agent. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Register message handlers for the Issue Credential V1 Protocol + dependencyManager.registerMessageHandlers([ + new V1ProposePresentationHandler(this), + new V1RequestPresentationHandler(this), + new V1PresentationHandler(this), + new V1PresentationAckHandler(this), + new V1PresentationProblemReportHandler(this), + ]) + + // Register Present Proof V1 in feature registry, with supported roles + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/present-proof/1.0', + roles: ['prover', 'verifier'], + }) + ) + } + + public async createProposal( + agentContext: AgentContext, + { + proofFormats, + connectionRecord, + comment, + parentThreadId, + autoAcceptProof, + }: ProofProtocolOptions.CreateProofProposalOptions<[LegacyIndyProofFormatService]> + ): Promise> { + this.assertOnlyIndyFormat(proofFormats) + + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + if (!proofFormats.indy) { + throw new CredoError('Missing indy proof format in v1 create proposal call.') + } + + const presentationProposal = new V1PresentationPreview({ + attributes: proofFormats.indy?.attributes, + predicates: proofFormats.indy?.predicates, + }) + + // validate input data from user + MessageValidator.validateSync(presentationProposal) + + // Create message + const message = new V1ProposePresentationMessage({ + presentationProposal, + comment, + }) + + if (parentThreadId) + message.setThread({ + parentThreadId, + }) + + // Create record + const proofRecord = new ProofExchangeRecord({ + connectionId: connectionRecord.id, + threadId: message.threadId, + parentThreadId: message.thread?.parentThreadId, + state: ProofState.ProposalSent, + role: ProofRole.Prover, + autoAcceptProof, + protocolVersion: 'v1', + }) + + await didCommMessageRepository.saveAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + await proofRepository.save(agentContext, proofRecord) + this.emitStateChangedEvent(agentContext, proofRecord, null) + + return { proofRecord, message } + } + + public async processProposal( + messageContext: InboundMessageContext + ): Promise { + const { message: proposalMessage, connection, agentContext } = messageContext + + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // TODO: with this method, we should update the credential protocol to use the ConnectionApi, so it + // only depends on the public api, rather than the internal API (this helps with breaking changes) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing presentation proposal with message id ${proposalMessage.id}`) + + let proofRecord = await this.findByProperties(agentContext, { + threadId: proposalMessage.threadId, + role: ProofRole.Verifier, + connectionId: connection?.id, + }) + + // Proof record already exists, this is a response to an earlier message sent by us + if (proofRecord) { + agentContext.config.logger.debug('Proof record already exists for incoming proposal') + + // Assert + proofRecord.assertState(ProofState.RequestSent) + proofRecord.assertProtocolVersion('v1') + + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + role: DidCommMessageRole.Receiver, + }) + const lastSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + role: DidCommMessageRole.Sender, + }) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, + expectedConnectionId: proofRecord.connectionId, + }) + + // Update record + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: proposalMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + await this.updateState(agentContext, proofRecord, ProofState.ProposalReceived) + } else { + agentContext.config.logger.debug('Proof record does not exist yet for incoming proposal') + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + + // No proof record exists with thread id + proofRecord = new ProofExchangeRecord({ + connectionId: connection?.id, + threadId: proposalMessage.threadId, + parentThreadId: proposalMessage.thread?.parentThreadId, + state: ProofState.ProposalReceived, + role: ProofRole.Verifier, + protocolVersion: 'v1', + }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: proposalMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + // Save record + await proofRepository.save(agentContext, proofRecord) + this.emitStateChangedEvent(agentContext, proofRecord, null) + } + + return proofRecord + } + + public async acceptProposal( + agentContext: AgentContext, + { + proofRecord, + proofFormats, + comment, + autoAcceptProof, + }: ProofProtocolOptions.AcceptProofProposalOptions<[LegacyIndyProofFormatService]> + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.ProposalReceived) + if (proofFormats) this.assertOnlyIndyFormat(proofFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const proposalMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + const indyFormat = proofFormats?.indy + + // Create a proof request from the preview, so we can let the messages + // be handled using the indy proof format which supports RFC0592 + const requestFromPreview = createRequestFromPreview({ + attributes: proposalMessage.presentationProposal.attributes, + predicates: proposalMessage.presentationProposal.predicates, + name: indyFormat?.name ?? 'Proof Request', + version: indyFormat?.version ?? '1.0', + nonce: await agentContext.wallet.generateNonce(), + }) + + const proposalAttachment = new Attachment({ + data: { + json: JsonTransformer.toJSON(requestFromPreview), + }, + }) + + // Create message + const { attachment } = await this.indyProofFormat.acceptProposal(agentContext, { + attachmentId: INDY_PROOF_REQUEST_ATTACHMENT_ID, + proofRecord, + proposalAttachment, + }) + + const requestPresentationMessage = new V1RequestPresentationMessage({ + comment, + requestAttachments: [attachment], + }) + + requestPresentationMessage.setThread({ + threadId: proofRecord.threadId, + parentThreadId: proofRecord.parentThreadId, + }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: requestPresentationMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + // Update record + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.RequestSent) + + return { message: requestPresentationMessage, proofRecord } + } + + public async negotiateProposal( + agentContext: AgentContext, + { + proofFormats, + proofRecord, + comment, + autoAcceptProof, + }: ProofProtocolOptions.NegotiateProofProposalOptions<[LegacyIndyProofFormatService]> + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.ProposalReceived) + this.assertOnlyIndyFormat(proofFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Create message + const { attachment } = await this.indyProofFormat.createRequest(agentContext, { + attachmentId: INDY_PROOF_REQUEST_ATTACHMENT_ID, + proofFormats, + proofRecord, + }) + + const requestPresentationMessage = new V1RequestPresentationMessage({ + comment, + requestAttachments: [attachment], + }) + requestPresentationMessage.setThread({ + threadId: proofRecord.threadId, + parentThreadId: proofRecord.parentThreadId, + }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: requestPresentationMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.RequestSent) + + return { message: requestPresentationMessage, proofRecord } + } + + public async createRequest( + agentContext: AgentContext, + { + proofFormats, + connectionRecord, + comment, + parentThreadId, + autoAcceptProof, + }: ProofProtocolOptions.CreateProofRequestOptions<[LegacyIndyProofFormatService]> + ): Promise> { + this.assertOnlyIndyFormat(proofFormats) + + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + if (!proofFormats.indy) { + throw new CredoError('Missing indy proof request data for v1 create request') + } + + // Create record + const proofRecord = new ProofExchangeRecord({ + connectionId: connectionRecord?.id, + threadId: utils.uuid(), + parentThreadId, + state: ProofState.RequestSent, + role: ProofRole.Verifier, + autoAcceptProof, + protocolVersion: 'v1', + }) + + // Create message + const { attachment } = await this.indyProofFormat.createRequest(agentContext, { + attachmentId: INDY_PROOF_REQUEST_ATTACHMENT_ID, + proofFormats, + proofRecord, + }) + + // Construct request message + const message = new V1RequestPresentationMessage({ + id: proofRecord.threadId, + comment, + requestAttachments: [attachment], + }) + + message.setThread({ + threadId: proofRecord.threadId, + parentThreadId: proofRecord.parentThreadId, + }) + + await didCommMessageRepository.saveAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + await proofRepository.save(agentContext, proofRecord) + this.emitStateChangedEvent(agentContext, proofRecord, null) + + return { message, proofRecord } + } + + public async processRequest( + messageContext: InboundMessageContext + ): Promise { + const { message: proofRequestMessage, connection, agentContext } = messageContext + + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // TODO: with this method, we should update the credential protocol to use the ConnectionApi, so it + // only depends on the public api, rather than the internal API (this helps with breaking changes) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing presentation request with id ${proofRequestMessage.id}`) + + let proofRecord = await this.findByProperties(agentContext, { + threadId: proofRequestMessage.threadId, + role: ProofRole.Prover, + connectionId: connection?.id, + }) + + const requestAttachment = proofRequestMessage.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + if (!requestAttachment) { + throw new CredoError(`Indy attachment with id ${INDY_PROOF_REQUEST_ATTACHMENT_ID} not found in request message`) + } + + // proof record already exists, this means we are the message is sent as reply to a proposal we sent + if (proofRecord) { + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + role: DidCommMessageRole.Receiver, + }) + const lastSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + role: DidCommMessageRole.Sender, + }) + + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.ProposalSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, + expectedConnectionId: proofRecord.connectionId, + }) + + await this.indyProofFormat.processRequest(agentContext, { + attachment: requestAttachment, + proofRecord, + }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: proofRequestMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + await this.updateState(agentContext, proofRecord, ProofState.RequestReceived) + } else { + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + + // No proof record exists with thread id + proofRecord = new ProofExchangeRecord({ + connectionId: connection?.id, + threadId: proofRequestMessage.threadId, + parentThreadId: proofRequestMessage.thread?.parentThreadId, + state: ProofState.RequestReceived, + role: ProofRole.Prover, + protocolVersion: 'v1', + }) + + await this.indyProofFormat.processRequest(agentContext, { + attachment: requestAttachment, + proofRecord, + }) + + await didCommMessageRepository.saveAgentMessage(agentContext, { + agentMessage: proofRequestMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + // Save in repository + await proofRepository.save(agentContext, proofRecord) + this.emitStateChangedEvent(agentContext, proofRecord, null) + } + + return proofRecord + } + + public async negotiateRequest( + agentContext: AgentContext, + { + proofFormats, + proofRecord, + comment, + autoAcceptProof, + }: ProofProtocolOptions.NegotiateProofRequestOptions<[LegacyIndyProofFormatService]> + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.RequestReceived) + this.assertOnlyIndyFormat(proofFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + if (!proofRecord.connectionId) { + throw new CredoError( + `No connectionId found for proof record '${proofRecord.id}'. Connection-less verification does not support negotiation.` + ) + } + + if (!proofFormats.indy) { + throw new CredoError('Missing indy proof format in v1 negotiate request call.') + } + + const presentationProposal = new V1PresentationPreview({ + attributes: proofFormats.indy?.attributes, + predicates: proofFormats.indy?.predicates, + }) + + // validate input data from user + MessageValidator.validateSync(presentationProposal) + + const message = new V1ProposePresentationMessage({ + comment, + presentationProposal, + }) + message.setThread({ threadId: proofRecord.threadId, parentThreadId: proofRecord.parentThreadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + // Update record + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.ProposalSent) + + return { proofRecord, message: message } + } + + public async acceptRequest( + agentContext: AgentContext, + { + proofRecord, + proofFormats, + autoAcceptProof, + comment, + }: ProofProtocolOptions.AcceptProofRequestOptions<[LegacyIndyProofFormatService]> + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.RequestReceived) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + role: DidCommMessageRole.Receiver, + }) + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + role: DidCommMessageRole.Sender, + }) + + const requestAttachment = requestMessage.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + const indyProofRequest = requestMessage.indyProofRequest + + if (!requestAttachment || !indyProofRequest) { + throw new V1PresentationProblemReportError( + `Missing indy attachment in request message for presentation with thread id ${proofRecord.threadId}`, + { problemCode: PresentationProblemReportReason.Abandoned } + ) + } + + const proposalAttachment = proposalMessage + ? new Attachment({ + data: { + json: JsonTransformer.toJSON( + createRequestFromPreview({ + attributes: proposalMessage.presentationProposal?.attributes, + predicates: proposalMessage.presentationProposal?.predicates, + name: indyProofRequest.name, + nonce: indyProofRequest.nonce, + version: indyProofRequest.nonce, + }) + ), + }, + }) + : undefined + + const { attachment } = await this.indyProofFormat.acceptRequest(agentContext, { + attachmentId: INDY_PROOF_ATTACHMENT_ID, + requestAttachment, + proposalAttachment, + proofFormats, + proofRecord, + }) + + const message = new V1PresentationMessage({ + comment, + presentationAttachments: [attachment], + }) + message.setThread({ threadId: proofRecord.threadId, parentThreadId: proofRecord.parentThreadId }) + + await didCommMessageRepository.saveAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + // Update record + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.PresentationSent) + + return { message, proofRecord } + } + + public async getCredentialsForRequest( + agentContext: AgentContext, + { proofRecord, proofFormats }: ProofProtocolOptions.GetCredentialsForRequestOptions<[LegacyIndyProofFormatService]> + ): Promise> { + if (proofFormats) this.assertOnlyIndyFormat(proofFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + role: DidCommMessageRole.Sender, + }) + + const requestAttachment = requestMessage.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + const indyProofRequest = requestMessage.indyProofRequest + + if (!requestAttachment || !indyProofRequest) { + throw new CredoError( + `Missing indy attachment in request message for presentation with thread id ${proofRecord.threadId}` + ) + } + + const proposalAttachment = proposalMessage + ? new Attachment({ + data: { + json: JsonTransformer.toJSON( + createRequestFromPreview({ + attributes: proposalMessage.presentationProposal?.attributes, + predicates: proposalMessage.presentationProposal?.predicates, + name: indyProofRequest.name, + nonce: indyProofRequest.nonce, + version: indyProofRequest.nonce, + }) + ), + }, + }) + : undefined + + const credentialForRequest = await this.indyProofFormat.getCredentialsForRequest(agentContext, { + proofRecord, + requestAttachment, + proofFormats, + proposalAttachment, + }) + + return { + proofFormats: { + indy: credentialForRequest, + }, + } + } + + public async selectCredentialsForRequest( + agentContext: AgentContext, + { + proofRecord, + proofFormats, + }: ProofProtocolOptions.SelectCredentialsForRequestOptions<[LegacyIndyProofFormatService]> + ): Promise> { + if (proofFormats) this.assertOnlyIndyFormat(proofFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + role: DidCommMessageRole.Sender, + }) + + const requestAttachment = requestMessage.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + const indyProofRequest = requestMessage.indyProofRequest + + if (!requestAttachment || !indyProofRequest) { + throw new CredoError( + `Missing indy attachment in request message for presentation with thread id ${proofRecord.threadId}` + ) + } + + const proposalAttachment = proposalMessage + ? new Attachment({ + data: { + json: JsonTransformer.toJSON( + createRequestFromPreview({ + attributes: proposalMessage.presentationProposal?.attributes, + predicates: proposalMessage.presentationProposal?.predicates, + name: indyProofRequest.name, + nonce: indyProofRequest.nonce, + version: indyProofRequest.nonce, + }) + ), + }, + }) + : undefined + + const selectedCredentials = await this.indyProofFormat.selectCredentialsForRequest(agentContext, { + proofFormats, + proofRecord, + requestAttachment, + proposalAttachment, + }) + + return { + proofFormats: { + indy: selectedCredentials, + }, + } + } + + public async processPresentation( + messageContext: InboundMessageContext + ): Promise { + const { message: presentationMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing presentation with message id ${presentationMessage.id}`) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // TODO: with this method, we should update the credential protocol to use the ConnectionApi, so it + // only depends on the public api, rather than the internal API (this helps with breaking changes) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + const proofRecord = await this.getByProperties(agentContext, { + threadId: presentationMessage.threadId, + role: ProofRole.Verifier, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + role: DidCommMessageRole.Sender, + }) + + // Assert + proofRecord.assertState(ProofState.RequestSent) + proofRecord.assertProtocolVersion('v1') + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: proposalMessage, + lastSentMessage: requestMessage, + expectedConnectionId: proofRecord.connectionId, + }) + + // This makes sure that the sender of the incoming message is authorized to do so. + if (!proofRecord.connectionId) { + await connectionService.matchIncomingMessageToRequestMessageInOutOfBandExchange(messageContext, { + expectedConnectionId: proofRecord.connectionId, + }) + + proofRecord.connectionId = connection?.id + } + + const presentationAttachment = presentationMessage.getPresentationAttachmentById(INDY_PROOF_ATTACHMENT_ID) + if (!presentationAttachment) { + proofRecord.errorMessage = 'Missing indy proof attachment' + await this.updateState(agentContext, proofRecord, ProofState.Abandoned) + throw new V1PresentationProblemReportError(proofRecord.errorMessage, { + problemCode: PresentationProblemReportReason.Abandoned, + }) + } + + const requestAttachment = requestMessage.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + if (!requestAttachment) { + proofRecord.errorMessage = 'Missing indy proof request attachment' + await this.updateState(agentContext, proofRecord, ProofState.Abandoned) + throw new V1PresentationProblemReportError(proofRecord.errorMessage, { + problemCode: PresentationProblemReportReason.Abandoned, + }) + } + + await didCommMessageRepository.saveAgentMessage(agentContext, { + agentMessage: presentationMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + let isValid: boolean + try { + isValid = await this.indyProofFormat.processPresentation(agentContext, { + proofRecord, + attachment: presentationAttachment, + requestAttachment, + }) + } catch (error) { + proofRecord.errorMessage = error.message ?? 'Error verifying proof on presentation' + proofRecord.isVerified = false + await this.updateState(agentContext, proofRecord, ProofState.Abandoned) + throw new V1PresentationProblemReportError('Error verifying proof on presentation', { + problemCode: PresentationProblemReportReason.Abandoned, + }) + } + + if (!isValid) { + proofRecord.errorMessage = 'Invalid proof' + proofRecord.isVerified = false + await this.updateState(agentContext, proofRecord, ProofState.Abandoned) + throw new V1PresentationProblemReportError('Invalid proof', { + problemCode: PresentationProblemReportReason.Abandoned, + }) + } + + // Update record + proofRecord.isVerified = isValid + await this.updateState(agentContext, proofRecord, ProofState.PresentationReceived) + + return proofRecord + } + + public async acceptPresentation( + agentContext: AgentContext, + { proofRecord }: ProofProtocolOptions.AcceptPresentationOptions + ): Promise> { + agentContext.config.logger.debug(`Creating presentation ack for proof record with id ${proofRecord.id}`) + + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.PresentationReceived) + + // Create message + const ackMessage = new V1PresentationAckMessage({ + status: AckStatus.OK, + threadId: proofRecord.threadId, + }) + + ackMessage.setThread({ + threadId: proofRecord.threadId, + parentThreadId: proofRecord.parentThreadId, + }) + + // Update record + await this.updateState(agentContext, proofRecord, ProofState.Done) + + return { message: ackMessage, proofRecord } + } + + public async processAck( + messageContext: InboundMessageContext + ): Promise { + const { message: presentationAckMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing presentation ack with message id ${presentationAckMessage.id}`) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // TODO: with this method, we should update the credential protocol to use the ConnectionApi, so it + // only depends on the public api, rather than the internal API (this helps with breaking changes) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + const proofRecord = await this.getByProperties(agentContext, { + threadId: presentationAckMessage.threadId, + role: ProofRole.Prover, + connectionId: connection?.id, + }) + + const lastReceivedMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + const lastSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1PresentationMessage, + role: DidCommMessageRole.Sender, + }) + + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.PresentationSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, + expectedConnectionId: proofRecord.connectionId, + }) + + // Update record + await this.updateState(agentContext, proofRecord, ProofState.Done) + + return proofRecord + } + + public async createProblemReport( + _agentContext: AgentContext, + { proofRecord, description }: ProofProtocolOptions.CreateProofProblemReportOptions + ): Promise> { + const message = new V1PresentationProblemReportMessage({ + description: { + code: PresentationProblemReportReason.Abandoned, + en: description, + }, + }) + + message.setThread({ + threadId: proofRecord.threadId, + parentThreadId: proofRecord.parentThreadId, + }) + + return { + proofRecord, + message, + } + } + + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + options: { + proofRecord: ProofExchangeRecord + proposalMessage: V1ProposePresentationMessage + } + ): Promise { + const { proofRecord, proposalMessage } = options + + const proofsModuleConfig = agentContext.dependencyManager.resolve(ProofsModuleConfig) + + const autoAccept = composeProofAutoAccept(proofRecord.autoAcceptProof, proofsModuleConfig.autoAcceptProofs) + + // Handle always / never cases + if (autoAccept === AutoAcceptProof.Always) return true + if (autoAccept === AutoAcceptProof.Never) return false + + // We are in the ContentApproved case. We need to make sure we've sent a request, and it matches the proposal + const requestMessage = await this.findRequestMessage(agentContext, proofRecord.id) + const requestAttachment = requestMessage?.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + if (!requestAttachment) return false + + const rfc0592Proposal = JsonTransformer.toJSON( + createRequestFromPreview({ + name: 'Proof Request', + nonce: await agentContext.wallet.generateNonce(), + version: '1.0', + attributes: proposalMessage.presentationProposal.attributes, + predicates: proposalMessage.presentationProposal.predicates, + }) + ) + + return this.indyProofFormat.shouldAutoRespondToProposal(agentContext, { + proofRecord, + proposalAttachment: new Attachment({ + data: { + json: rfc0592Proposal, + }, + }), + requestAttachment, + }) + } + + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + options: { + proofRecord: ProofExchangeRecord + requestMessage: V1RequestPresentationMessage + } + ): Promise { + const { proofRecord, requestMessage } = options + + const proofsModuleConfig = agentContext.dependencyManager.resolve(ProofsModuleConfig) + + const autoAccept = composeProofAutoAccept(proofRecord.autoAcceptProof, proofsModuleConfig.autoAcceptProofs) + + // Handle always / never cases + if (autoAccept === AutoAcceptProof.Always) return true + if (autoAccept === AutoAcceptProof.Never) return false + + const requestAttachment = requestMessage.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + if (!requestAttachment) return false + + // We are in the ContentApproved case. We need to make sure we've sent a proposal, and it matches the request + const proposalMessage = await this.findProposalMessage(agentContext, proofRecord.id) + if (!proposalMessage) return false + + const rfc0592Proposal = createRequestFromPreview({ + name: 'Proof Request', + nonce: await agentContext.wallet.generateNonce(), + version: '1.0', + attributes: proposalMessage.presentationProposal.attributes, + predicates: proposalMessage.presentationProposal.predicates, + }) + + return this.indyProofFormat.shouldAutoRespondToRequest(agentContext, { + proofRecord, + proposalAttachment: new Attachment({ + data: { + base64: JsonEncoder.toBase64(rfc0592Proposal), + }, + }), + requestAttachment, + }) + } + + public async shouldAutoRespondToPresentation( + agentContext: AgentContext, + options: { + proofRecord: ProofExchangeRecord + presentationMessage: V1PresentationMessage + } + ): Promise { + const { proofRecord, presentationMessage } = options + + const proofsModuleConfig = agentContext.dependencyManager.resolve(ProofsModuleConfig) + + const autoAccept = composeProofAutoAccept(proofRecord.autoAcceptProof, proofsModuleConfig.autoAcceptProofs) + + // Handle always / never cases + if (autoAccept === AutoAcceptProof.Always) return true + if (autoAccept === AutoAcceptProof.Never) return false + + const presentationAttachment = presentationMessage.getPresentationAttachmentById(INDY_PROOF_ATTACHMENT_ID) + if (!presentationAttachment) return false + + // We are in the ContentApproved case. We need to make sure we've sent a request, and it matches the presentation + const requestMessage = await this.findRequestMessage(agentContext, proofRecord.id) + const requestAttachment = requestMessage?.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + if (!requestAttachment) return false + + // We are in the ContentApproved case. We need to make sure we've sent a proposal, and it matches the request + const proposalMessage = await this.findProposalMessage(agentContext, proofRecord.id) + + const rfc0592Proposal = proposalMessage + ? JsonTransformer.toJSON( + createRequestFromPreview({ + name: 'Proof Request', + nonce: await agentContext.wallet.generateNonce(), + version: '1.0', + attributes: proposalMessage.presentationProposal.attributes, + predicates: proposalMessage.presentationProposal.predicates, + }) + ) + : undefined + + return this.indyProofFormat.shouldAutoRespondToPresentation(agentContext, { + proofRecord, + requestAttachment, + presentationAttachment, + proposalAttachment: new Attachment({ + data: { + json: rfc0592Proposal, + }, + }), + }) + } + + public async findProposalMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V1ProposePresentationMessage, + }) + } + + public async findRequestMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V1RequestPresentationMessage, + }) + } + + public async findPresentationMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V1PresentationMessage, + }) + } + + public async getFormatData( + agentContext: AgentContext, + proofRecordId: string + ): Promise> { + // TODO: we could looking at fetching all record using a single query and then filtering based on the type of the message. + const [proposalMessage, requestMessage, presentationMessage] = await Promise.all([ + this.findProposalMessage(agentContext, proofRecordId), + this.findRequestMessage(agentContext, proofRecordId), + this.findPresentationMessage(agentContext, proofRecordId), + ]) + + let indyProposeProof = undefined + const indyRequestProof = requestMessage?.indyProofRequest ?? undefined + const indyPresentProof = presentationMessage?.indyProof ?? undefined + + if (proposalMessage && indyRequestProof) { + indyProposeProof = createRequestFromPreview({ + name: indyRequestProof.name, + version: indyRequestProof.version, + nonce: indyRequestProof.nonce, + attributes: proposalMessage.presentationProposal.attributes, + predicates: proposalMessage.presentationProposal.predicates, + }) + } else if (proposalMessage) { + indyProposeProof = createRequestFromPreview({ + name: 'Proof Request', + version: '1.0', + nonce: await agentContext.wallet.generateNonce(), + attributes: proposalMessage.presentationProposal.attributes, + predicates: proposalMessage.presentationProposal.predicates, + }) + } + + return { + proposal: proposalMessage + ? { + indy: indyProposeProof, + } + : undefined, + request: requestMessage + ? { + indy: indyRequestProof, + } + : undefined, + presentation: presentationMessage + ? { + indy: indyPresentProof, + } + : undefined, + } + } + + private assertOnlyIndyFormat(proofFormats: Record) { + const formatKeys = Object.keys(proofFormats) + + // It's fine to not have any formats in some cases, if indy is required the method that calls this should check for this + if (formatKeys.length === 0) return + + if (formatKeys.length !== 1 || !formatKeys.includes('indy')) { + throw new CredoError('Only indy proof format is supported for present proof v1 protocol') + } + } +} diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/V1ProofProtocol.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/V1ProofProtocol.test.ts new file mode 100644 index 0000000000..5d7a2cde62 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/V1ProofProtocol.test.ts @@ -0,0 +1,255 @@ +import type { CustomProofTags, AgentConfig, AgentContext, ProofStateChangedEvent } from '../../../../../../core/src' + +import { Subject } from 'rxjs' + +import { + ProofRole, + DidExchangeState, + Attachment, + AttachmentData, + ProofState, + ProofExchangeRecord, + InboundMessageContext, + ProofEventTypes, + PresentationProblemReportReason, + EventEmitter, +} from '../../../../../../core/src' +import { ConnectionService } from '../../../../../../core/src/modules/connections/services/ConnectionService' +import { ProofRepository } from '../../../../../../core/src/modules/proofs/repository/ProofRepository' +import { DidCommMessageRepository } from '../../../../../../core/src/storage/didcomm/DidCommMessageRepository' +import { getMockConnection, getAgentConfig, getAgentContext, mockFunction } from '../../../../../../core/tests' +import { LegacyIndyProofFormatService } from '../../../../formats/LegacyIndyProofFormatService' +import { V1ProofProtocol } from '../V1ProofProtocol' +import { INDY_PROOF_REQUEST_ATTACHMENT_ID, V1RequestPresentationMessage } from '../messages' +import { V1PresentationProblemReportMessage } from '../messages/V1PresentationProblemReportMessage' + +// Mock classes +jest.mock('../../../../../../core/src/modules/proofs/repository/ProofRepository') +jest.mock('../../../../formats/LegacyIndyProofFormatService') +jest.mock('../../../../../../core/src/storage/didcomm/DidCommMessageRepository') +jest.mock('../../../../../../core/src/modules/connections/services/ConnectionService') + +// Mock typed object +const ProofRepositoryMock = ProofRepository as jest.Mock +const connectionServiceMock = ConnectionService as jest.Mock +const didCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock +const indyProofFormatServiceMock = LegacyIndyProofFormatService as jest.Mock + +const proofRepository = new ProofRepositoryMock() +const connectionService = new connectionServiceMock() +const didCommMessageRepository = new didCommMessageRepositoryMock() +const indyProofFormatService = new indyProofFormatServiceMock() + +const connection = getMockConnection({ + id: '123', + state: DidExchangeState.Completed, +}) + +const requestAttachment = new Attachment({ + id: INDY_PROOF_REQUEST_ATTACHMENT_ID, + mimeType: 'application/json', + data: new AttachmentData({ + base64: + 'eyJuYW1lIjogIlByb29mIHJlcXVlc3QiLCAibm9uX3Jldm9rZWQiOiB7ImZyb20iOiAxNjQwOTk1MTk5LCAidG8iOiAxNjQwOTk1MTk5fSwgIm5vbmNlIjogIjEiLCAicmVxdWVzdGVkX2F0dHJpYnV0ZXMiOiB7ImFkZGl0aW9uYWxQcm9wMSI6IHsibmFtZSI6ICJmYXZvdXJpdGVEcmluayIsICJub25fcmV2b2tlZCI6IHsiZnJvbSI6IDE2NDA5OTUxOTksICJ0byI6IDE2NDA5OTUxOTl9LCAicmVzdHJpY3Rpb25zIjogW3siY3JlZF9kZWZfaWQiOiAiV2dXeHF6dHJOb29HOTJSWHZ4U1RXdjozOkNMOjIwOnRhZyJ9XX19LCAicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOiB7fSwgInZlcnNpb24iOiAiMS4wIn0=', + }), +}) + +// A record is deserialized to JSON when it's stored into the storage. We want to simulate this behaviour for `offer` +// object to test our service would behave correctly. We use type assertion for `offer` attribute to `any`. +const mockProofExchangeRecord = ({ + state, + role, + threadId, + connectionId, + tags, + id, +}: { + state?: ProofState + role?: ProofRole + requestMessage?: V1RequestPresentationMessage + tags?: CustomProofTags + threadId?: string + connectionId?: string + id?: string +} = {}) => { + const requestPresentationMessage = new V1RequestPresentationMessage({ + comment: 'some comment', + requestAttachments: [requestAttachment], + }) + + const proofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + id, + state: state || ProofState.RequestSent, + role: role || ProofRole.Verifier, + threadId: threadId ?? requestPresentationMessage.id, + connectionId: connectionId ?? '123', + tags, + }) + + return proofRecord +} + +describe('V1ProofProtocol', () => { + let eventEmitter: EventEmitter + let agentConfig: AgentConfig + let agentContext: AgentContext + let proofProtocol: V1ProofProtocol + + beforeEach(() => { + // real objects + agentConfig = getAgentConfig('V1ProofProtocolTest') + eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) + + agentContext = getAgentContext({ + registerInstances: [ + [ProofRepository, proofRepository], + [DidCommMessageRepository, didCommMessageRepository], + [EventEmitter, eventEmitter], + [ConnectionService, connectionService], + ], + agentConfig, + }) + proofProtocol = new V1ProofProtocol({ indyProofFormat: indyProofFormatService }) + }) + + describe('processRequest', () => { + let presentationRequest: V1RequestPresentationMessage + let messageContext: InboundMessageContext + + beforeEach(() => { + presentationRequest = new V1RequestPresentationMessage({ + comment: 'abcd', + requestAttachments: [requestAttachment], + }) + messageContext = new InboundMessageContext(presentationRequest, { + connection, + agentContext, + }) + }) + + test(`creates and return proof record in ${ProofState.PresentationReceived} state with offer, without thread ID`, async () => { + const repositorySaveSpy = jest.spyOn(proofRepository, 'save') + + // when + const returnedProofExchangeRecord = await proofProtocol.processRequest(messageContext) + + // then + const expectedProofExchangeRecord = { + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: ProofState.RequestReceived, + threadId: presentationRequest.id, + connectionId: connection.id, + } + expect(repositorySaveSpy).toHaveBeenCalledTimes(1) + const [[, createdProofExchangeRecord]] = repositorySaveSpy.mock.calls + expect(createdProofExchangeRecord).toMatchObject(expectedProofExchangeRecord) + expect(returnedProofExchangeRecord).toMatchObject(expectedProofExchangeRecord) + }) + + test(`emits stateChange event with ${ProofState.RequestReceived}`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(ProofEventTypes.ProofStateChanged, eventListenerMock) + + // when + await proofProtocol.processRequest(messageContext) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'ProofStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: null, + proofRecord: expect.objectContaining({ + state: ProofState.RequestReceived, + }), + }, + }) + }) + }) + + describe('createProblemReport', () => { + const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' + let proof: ProofExchangeRecord + + beforeEach(() => { + proof = mockProofExchangeRecord({ + state: ProofState.RequestReceived, + threadId, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + }) + + test('returns problem report message base once get error', async () => { + // given + mockFunction(proofRepository.getById).mockReturnValue(Promise.resolve(proof)) + + // when + const presentationProblemReportMessage = await new V1PresentationProblemReportMessage({ + description: { + en: 'Indy error', + code: PresentationProblemReportReason.Abandoned, + }, + }) + + presentationProblemReportMessage.setThread({ threadId }) + // then + expect(presentationProblemReportMessage.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/present-proof/1.0/problem-report', + '~thread': { + thid: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + }, + }) + }) + }) + + describe('processProblemReport', () => { + let proof: ProofExchangeRecord + let messageContext: InboundMessageContext + + beforeEach(() => { + proof = mockProofExchangeRecord({ + state: ProofState.RequestReceived, + }) + + const presentationProblemReportMessage = new V1PresentationProblemReportMessage({ + description: { + en: 'Indy error', + code: PresentationProblemReportReason.Abandoned, + }, + }) + presentationProblemReportMessage.setThread({ threadId: 'somethreadid' }) + messageContext = new InboundMessageContext(presentationProblemReportMessage, { + connection, + agentContext, + }) + }) + + test(`updates problem report error message and returns proof record`, async () => { + const repositoryUpdateSpy = jest.spyOn(proofRepository, 'update') + + // given + mockFunction(proofRepository.getSingleByQuery).mockReturnValue(Promise.resolve(proof)) + + // when + const returnedCredentialRecord = await proofProtocol.processProblemReport(messageContext) + + // then + const expectedCredentialRecord = { + errorMessage: 'abandoned: Indy error', + } + expect(proofRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { + threadId: 'somethreadid', + }) + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + const [[, updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls + expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord) + expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) + }) + }) +}) diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts new file mode 100644 index 0000000000..9b7c86c770 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts @@ -0,0 +1,544 @@ +import type { SubjectMessage } from '../../../../../../../tests/transport/SubjectInboundTransport' +import type { AnonCredsTestsAgent } from '../../../../../tests/legacyAnonCredsSetup' + +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../../../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../../../../../tests/transport/SubjectOutboundTransport' +import { + CredentialEventTypes, + Agent, + AutoAcceptProof, + ProofState, + HandshakeProtocol, + MediatorPickupStrategy, + LinkedAttachment, + Attachment, + AttachmentData, + ProofEventTypes, + MediatorModule, + MediationRecipientModule, +} from '../../../../../../core/src' +import { uuid } from '../../../../../../core/src/utils/uuid' +import { + testLogger, + waitForProofExchangeRecordSubject, + makeConnection, + setupEventReplaySubjects, + getInMemoryAgentOptions, +} from '../../../../../../core/tests' +import { + getAnonCredsIndyModules, + issueLegacyAnonCredsCredential, + prepareForAnonCredsIssuance, + setupAnonCredsTests, +} from '../../../../../tests/legacyAnonCredsSetup' +import { V1CredentialPreview } from '../../../credentials/v1' + +describe('V1 Proofs - Connectionless - Indy', () => { + let agents: Agent[] + + afterEach(async () => { + for (const agent of agents) { + await agent.shutdown() + await agent.wallet.delete() + } + }) + + // new method to test the return route and mediator together + const connectionlessTest = async (returnRoute?: boolean) => { + const { + holderAgent: aliceAgent, + issuerAgent: faberAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber v1 connection-less Proofs - Never', + holderName: 'Alice v1 connection-less Proofs - Never', + autoAcceptProofs: AutoAcceptProof.Never, + attributeNames: ['name', 'age'], + }) + + // FIXME: We should reuse anoncreds crypto object as it will speed up tests significantly + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + issuerReplay: faberReplay, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'John', + }, + { + name: 'age', + value: '99', + }, + ], + }, + }) + + agents = [aliceAgent, faberAgent] + testLogger.test('Faber sends presentation request to Alice') + + // eslint-disable-next-line prefer-const + let { proofRecord: faberProofExchangeRecord, message } = await faberAgent.proofs.createRequest({ + protocolVersion: 'v1', + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + const outOfBandRecord = await faberAgent.oob.createInvitation({ + messages: [message], + handshake: false, + }) + await aliceAgent.oob.receiveInvitation(outOfBandRecord.outOfBandInvitation) + + testLogger.test('Alice waits for presentation request from Faber') + let aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, { + state: ProofState.RequestReceived, + }) + + testLogger.test('Alice accepts presentation request from Faber') + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + useReturnRoute: returnRoute, + proofFormats: { indy: requestedCredentials.proofFormats.indy }, + }) + + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await waitForProofExchangeRecordSubject(faberReplay, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + const sentPresentationMessage = aliceAgent.proofs.findPresentationMessage(aliceProofExchangeRecord.id) + // assert presentation is valid + expect(faberProofExchangeRecord.isVerified).toBe(true) + + // Faber accepts presentation + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits till it receives presentation ack + aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + return sentPresentationMessage + } + + test('Faber starts with connection-less proof requests to Alice', async () => { + await connectionlessTest() + }) + + test('Faber starts with connection-less proof requests to Alice with auto-accept enabled', async () => { + const { + holderAgent: aliceAgent, + issuerAgent: faberAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber v1 connection-less Proofs - Always', + holderName: 'Alice v1 connection-less Proofs - Always', + autoAcceptProofs: AutoAcceptProof.Always, + attributeNames: ['name', 'age'], + }) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + issuerReplay: faberReplay, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'John', + }, + { + name: 'age', + value: '99', + }, + ], + }, + }) + + agents = [aliceAgent, faberAgent] + + const { message, proofRecord: faberProofExchangeRecord } = await faberAgent.proofs.createRequest({ + protocolVersion: 'v1', + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + autoAcceptProof: AutoAcceptProof.ContentApproved, + }) + + const { message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberProofExchangeRecord.id, + message, + domain: 'https://a-domain.com', + }) + + await aliceAgent.receiveMessage(requestMessage.toJSON()) + + await waitForProofExchangeRecordSubject(aliceReplay, { + state: ProofState.Done, + threadId: message.threadId, + }) + + await waitForProofExchangeRecordSubject(faberReplay, { + state: ProofState.Done, + threadId: message.threadId, + }) + }) + + test('Faber starts with connection-less proof requests to Alice with auto-accept enabled and without an outbound transport', async () => { + const { + holderAgent: aliceAgent, + issuerAgent: faberAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber v1 connection-less Proofs - Always', + holderName: 'Alice v1 connection-less Proofs - Always', + autoAcceptProofs: AutoAcceptProof.Always, + attributeNames: ['name', 'age'], + }) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + issuerReplay: faberReplay, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'John', + }, + { + name: 'age', + value: '99', + }, + ], + }, + }) + + agents = [aliceAgent, faberAgent] + + const { message } = await faberAgent.proofs.createRequest({ + protocolVersion: 'v1', + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + autoAcceptProof: AutoAcceptProof.ContentApproved, + }) + + const { invitationUrl, message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + message, + domain: 'https://a-domain.com', + }) + + for (const transport of faberAgent.outboundTransports) { + await faberAgent.unregisterOutboundTransport(transport) + } + + await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) + + await waitForProofExchangeRecordSubject(aliceReplay, { + state: ProofState.Done, + threadId: requestMessage.threadId, + }) + + await waitForProofExchangeRecordSubject(faberReplay, { + state: ProofState.Done, + threadId: requestMessage.threadId, + }) + }) + + test('Faber starts with connection-less proof requests to Alice with auto-accept enabled and both agents having a mediator', async () => { + testLogger.test('Faber sends presentation request to Alice') + + const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', + }) + + const unique = uuid().substring(0, 4) + + const mediatorAgentOptions = getInMemoryAgentOptions( + `Connectionless proofs with mediator Mediator-${unique}`, + { + endpoints: ['rxjs:mediator'], + }, + { + mediator: new MediatorModule({ + autoAcceptMediationRequests: true, + }), + } + ) + + const mediatorMessages = new Subject() + const subjectMap = { 'rxjs:mediator': mediatorMessages } + + // Initialize mediator + const mediatorAgent = new Agent(mediatorAgentOptions) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + await mediatorAgent.initialize() + + const faberMediationOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'faber invitation', + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + const aliceMediationOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'alice invitation', + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + const faberAgentOptions = getInMemoryAgentOptions( + `Connectionless proofs with mediator Faber-${unique}`, + {}, + { + ...getAnonCredsIndyModules({ + autoAcceptProofs: AutoAcceptProof.Always, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorInvitationUrl: faberMediationOutOfBandRecord.outOfBandInvitation.toUrl({ + domain: 'https://example.com', + }), + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + } + ) + + const aliceAgentOptions = getInMemoryAgentOptions( + `Connectionless proofs with mediator Alice-${unique}`, + {}, + { + ...getAnonCredsIndyModules({ + autoAcceptProofs: AutoAcceptProof.Always, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorInvitationUrl: aliceMediationOutOfBandRecord.outOfBandInvitation.toUrl({ + domain: 'https://example.com', + }), + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + } + ) + + const faberAgent = new Agent(faberAgentOptions) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + + const aliceAgent = new Agent(aliceAgentOptions) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + const [faberReplay, aliceReplay] = setupEventReplaySubjects( + [faberAgent, aliceAgent], + [CredentialEventTypes.CredentialStateChanged, ProofEventTypes.ProofStateChanged] + ) + + agents = [aliceAgent, faberAgent, mediatorAgent] + + const { credentialDefinition } = await prepareForAnonCredsIssuance(faberAgent, { + attributeNames: ['name', 'age', 'image_0', 'image_1'], + }) + + const [faberConnection, aliceConnection] = await makeConnection(faberAgent, aliceAgent) + expect(faberConnection.isReady).toBe(true) + expect(aliceConnection.isReady).toBe(true) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent as AnonCredsTestsAgent, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnection.id, + holderAgent: aliceAgent as AnonCredsTestsAgent, + holderReplay: aliceReplay, + offer: { + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + attributes: credentialPreview.attributes, + linkedAttachments: [ + new LinkedAttachment({ + name: 'image_0', + attachment: new Attachment({ + filename: 'picture-of-a-cat.png', + data: new AttachmentData({ base64: 'cGljdHVyZSBvZiBhIGNhdA==' }), + }), + }), + new LinkedAttachment({ + name: 'image_1', + attachment: new Attachment({ + filename: 'picture-of-a-dog.png', + data: new AttachmentData({ base64: 'UGljdHVyZSBvZiBhIGRvZw==' }), + }), + }), + ], + }, + }) + + // eslint-disable-next-line prefer-const + let { message, proofRecord: faberProofExchangeRecord } = await faberAgent.proofs.createRequest({ + protocolVersion: 'v1', + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinition.credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinition.credentialDefinitionId, + }, + ], + }, + }, + }, + }, + autoAcceptProof: AutoAcceptProof.ContentApproved, + }) + + const { message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberProofExchangeRecord.id, + message, + domain: 'https://a-domain.com', + }) + + const mediationRecord = await faberAgent.mediationRecipient.findDefaultMediator() + if (!mediationRecord) { + throw new Error('Faber agent has no default mediator') + } + + expect(requestMessage).toMatchObject({ + service: { + recipientKeys: [expect.any(String)], + routingKeys: mediationRecord.routingKeys, + serviceEndpoint: mediationRecord.endpoint, + }, + }) + + await aliceAgent.receiveMessage(requestMessage.toJSON()) + + await waitForProofExchangeRecordSubject(aliceReplay, { + state: ProofState.Done, + threadId: requestMessage.threadId, + }) + + await waitForProofExchangeRecordSubject(faberReplay, { + state: ProofState.Done, + threadId: requestMessage.threadId, + }) + + await aliceAgent.mediationRecipient.stopMessagePickup() + await faberAgent.mediationRecipient.stopMessagePickup() + }) +}) diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-negotiation.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-negotiation.e2e.test.ts new file mode 100644 index 0000000000..917f5c805d --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-negotiation.e2e.test.ts @@ -0,0 +1,293 @@ +import type { AcceptProofProposalOptions } from '../../../../../../core/src' +import type { AnonCredsTestsAgent } from '../../../../../tests/legacyAnonCredsSetup' +import type { V1RequestPresentationMessage } from '../messages' + +import { ProofState } from '../../../../../../core/src' +import { testLogger, waitForProofExchangeRecord } from '../../../../../../core/tests' +import { setupAnonCredsTests } from '../../../../../tests/legacyAnonCredsSetup' + +describe('Present Proof', () => { + let faberAgent: AnonCredsTestsAgent + let aliceAgent: AnonCredsTestsAgent + let aliceConnectionId: string + let credentialDefinitionId: string + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + credentialDefinitionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber - V1 Indy Proof Negotiation', + holderName: 'Alice - V1 Indy Proof Negotiation', + attributeNames: ['name', 'age'], + })) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test(`Proof negotiation between Alice and Faber`, async () => { + testLogger.test('Alice sends proof proposal to Faber') + + let faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + state: ProofState.ProposalReceived, + }) + + let aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + attributes: [], + predicates: [ + { + credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 18, + }, + ], + }, + }, + comment: 'V1 propose proof test 1', + }) + + testLogger.test('Faber waits for presentation from Alice') + let faberProofExchangeRecord = await faberProofExchangeRecordPromise + + let proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/propose-presentation', + id: expect.any(String), + comment: 'V1 propose proof test 1', + presentationProposal: { + type: 'https://didcomm.org/present-proof/1.0/presentation-preview', + attributes: [], + predicates: [ + { + name: 'age', + credentialDefinitionId, + predicate: '>=', + threshold: 18, + }, + ], + }, + }) + expect(faberProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v1', + }) + + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Faber sends new proof request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.negotiateProposal({ + proofRecordId: faberProofExchangeRecord.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + something: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + somethingElse: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + testLogger.test('Alice waits for proof request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + let request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/request-presentation', + id: expect.any(String), + requestAttachments: [ + { + id: 'libindy-request-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(aliceProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v1', + }) + + testLogger.test('Alice sends proof proposal to Faber') + + faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + state: ProofState.ProposalReceived, + }) + + aliceProofExchangeRecord = await aliceAgent.proofs.negotiateRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + attributes: [], + predicates: [ + { + credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 18, + }, + ], + }, + }, + comment: 'V1 propose proof test 2', + }) + + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/propose-presentation', + id: expect.any(String), + comment: 'V1 propose proof test 2', + presentationProposal: { + type: 'https://didcomm.org/present-proof/1.0/presentation-preview', + attributes: [], + predicates: [ + { + name: 'age', + credentialDefinitionId, + predicate: '>=', + threshold: 18, + }, + ], + }, + }) + expect(faberProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v1', + }) + + // Accept Proposal + const acceptProposalOptions: AcceptProofProposalOptions = { + proofRecordId: faberProofExchangeRecord.id, + } + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal(acceptProposalOptions) + + testLogger.test('Alice waits for proof request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/request-presentation', + id: expect.any(String), + requestAttachments: [ + { + id: 'libindy-request-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(aliceProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v1', + }) + + const proposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id) + expect(proposalMessage).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/propose-presentation', + id: expect.any(String), + comment: 'V1 propose proof test 2', + presentationProposal: { + type: 'https://didcomm.org/present-proof/1.0/presentation-preview', + attributes: [], + predicates: [ + { + name: 'age', + credentialDefinitionId, + predicate: '>=', + threshold: 18, + }, + ], + }, + }) + + const proofRequestMessage = (await aliceAgent.proofs.findRequestMessage( + aliceProofExchangeRecord.id + )) as V1RequestPresentationMessage + + const predicateKey = Object.keys(proofRequestMessage.indyProofRequest?.requested_predicates ?? {})[0] + expect(proofRequestMessage.indyProofRequest).toMatchObject({ + name: 'Proof Request', + version: '1.0', + requested_attributes: {}, + requested_predicates: { + [predicateKey]: { + p_type: '>=', + p_value: 18, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }) + }) +}) diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-presentation.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-presentation.e2e.test.ts new file mode 100644 index 0000000000..5b4c358a2f --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-presentation.e2e.test.ts @@ -0,0 +1,246 @@ +import type { EventReplaySubject } from '../../../../../../core/tests' +import type { AnonCredsTestsAgent } from '../../../../../tests/legacyAnonCredsSetup' + +import { ProofState, ProofExchangeRecord } from '../../../../../../core/src' +import { testLogger, waitForProofExchangeRecord } from '../../../../../../core/tests' +import { issueLegacyAnonCredsCredential, setupAnonCredsTests } from '../../../../../tests/legacyAnonCredsSetup' + +describe('Present Proof', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let aliceConnectionId: string + let faberConnectionId: string + let credentialDefinitionId: string + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + holderIssuerConnectionId: aliceConnectionId, + issuerHolderConnectionId: faberConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber - V1 Indy Proof', + holderName: 'Alice - V1 Indy Proof', + attributeNames: ['name', 'age'], + })) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'John', + }, + { + name: 'age', + value: '55', + }, + ], + }, + }) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test(`Alice Creates and sends Proof Proposal to Faber`, async () => { + testLogger.test('Alice sends proof proposal to Faber') + + let faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + state: ProofState.ProposalReceived, + }) + + let aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + proofFormats: { + indy: { + name: 'ProofRequest', + version: '1.0', + attributes: [ + { + name: 'name', + value: 'John', + credentialDefinitionId, + referent: '0', + }, + ], + predicates: [ + { + name: 'age', + predicate: '>=', + threshold: 50, + credentialDefinitionId, + }, + ], + }, + }, + comment: 'V1 propose proof test', + }) + + testLogger.test('Faber waits for presentation from Alice') + + let faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/propose-presentation', + id: expect.any(String), + comment: 'V1 propose proof test', + presentationProposal: { + type: 'https://didcomm.org/present-proof/1.0/presentation-preview', + attributes: [ + { + name: 'name', + credentialDefinitionId, + value: 'John', + referent: '0', + }, + ], + predicates: [ + { + name: 'age', + credentialDefinitionId, + predicate: '>=', + threshold: 50, + }, + ], + }, + }) + expect(faberProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v1', + }) + + // Accept Proposal + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) + + testLogger.test('Alice waits for proof request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/request-presentation', + id: expect.any(String), + requestAttachments: [ + { + id: 'libindy-request-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(aliceProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v1', + }) + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { indy: requestedCredentials.proofFormats.indy }, + }) + + // Faber waits for the presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/presentation', + id: expect.any(String), + presentationAttachments: [ + { + id: 'libindy-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: expect.any(String), + }, + }) + + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v1', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation provided by Alice + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) +}) diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-proposal.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-proposal.e2e.test.ts new file mode 100644 index 0000000000..14e9e72145 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-proposal.e2e.test.ts @@ -0,0 +1,106 @@ +import type { AnonCredsTestsAgent } from '../../../../../tests/legacyAnonCredsSetup' + +import { ProofState } from '../../../../../../core/src' +import { testLogger, waitForProofExchangeRecord } from '../../../../../../core/tests' +import { setupAnonCredsTests } from '../../../../../tests/legacyAnonCredsSetup' + +describe('Present Proof', () => { + let faberAgent: AnonCredsTestsAgent + let aliceAgent: AnonCredsTestsAgent + let aliceConnectionId: string + let credentialDefinitionId: string + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + credentialDefinitionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber - V1 Indy Proof Request', + holderName: 'Alice - V1 Indy Proof Request', + attributeNames: ['name', 'age'], + })) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test(`Alice Creates and sends Proof Proposal to Faber`, async () => { + testLogger.test('Alice sends proof proposal to Faber') + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + state: ProofState.ProposalReceived, + }) + + await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + proofFormats: { + indy: { + name: 'ProofRequest', + version: '1.0', + attributes: [ + { + name: 'name', + value: 'John', + credentialDefinitionId, + referent: '0', + }, + ], + predicates: [ + { + name: 'age', + predicate: '>=', + threshold: 50, + credentialDefinitionId, + }, + ], + }, + }, + comment: 'V1 propose proof test', + }) + + testLogger.test('Faber waits for presentation from Alice') + const faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/propose-presentation', + id: expect.any(String), + comment: 'V1 propose proof test', + presentationProposal: { + type: 'https://didcomm.org/present-proof/1.0/presentation-preview', + attributes: [ + { + name: 'name', + credentialDefinitionId, + value: 'John', + referent: '0', + }, + ], + predicates: [ + { + name: 'age', + credentialDefinitionId, + predicate: '>=', + threshold: 50, + }, + ], + }, + }) + + expect(faberProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v1', + }) + }) +}) diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-request.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-request.e2e.test.ts new file mode 100644 index 0000000000..36e9203b0d --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-request.e2e.test.ts @@ -0,0 +1,144 @@ +import type { AnonCredsTestsAgent } from '../../../../../tests/legacyAnonCredsSetup' + +import { ProofState } from '../../../../../../core/src' +import { testLogger, waitForProofExchangeRecord } from '../../../../../../core/tests' +import { setupAnonCredsTests } from '../../../../../tests/legacyAnonCredsSetup' + +describe('Present Proof | V1ProofProtocol', () => { + let faberAgent: AnonCredsTestsAgent + let aliceAgent: AnonCredsTestsAgent + let aliceConnectionId: string + let credentialDefinitionId: string + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + credentialDefinitionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber - V1 Indy Proof Request', + holderName: 'Alice - V1 Indy Proof Request', + attributeNames: ['name', 'age'], + })) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test(`Alice Creates and sends Proof Proposal to Faber and Faber accepts the proposal`, async () => { + testLogger.test('Alice sends proof proposal to Faber') + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + state: ProofState.ProposalReceived, + }) + + let aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + proofFormats: { + indy: { + name: 'Proof Request', + version: '1.0', + attributes: [ + { + name: 'name', + value: 'John', + credentialDefinitionId, + referent: '0', + }, + ], + predicates: [ + { + name: 'age', + predicate: '>=', + threshold: 50, + credentialDefinitionId, + }, + ], + }, + }, + comment: 'V1 propose proof test', + }) + + testLogger.test('Faber waits for presentation from Alice') + let faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal?.toJSON()).toMatchObject({ + '@type': 'https://didcomm.org/present-proof/1.0/propose-presentation', + '@id': expect.any(String), + comment: 'V1 propose proof test', + presentation_proposal: { + '@type': 'https://didcomm.org/present-proof/1.0/presentation-preview', + attributes: [ + { + name: 'name', + cred_def_id: credentialDefinitionId, + value: 'John', + referent: '0', + }, + ], + predicates: [ + { + name: 'age', + cred_def_id: credentialDefinitionId, + predicate: '>=', + threshold: 50, + }, + ], + }, + }) + expect(faberProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v1', + }) + + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + // Accept Proposal + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) + + testLogger.test('Alice waits for proof request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/request-presentation', + id: expect.any(String), + requestAttachments: [ + { + id: 'libindy-request-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v1', + }) + }) +}) diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proofs.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proofs.e2e.test.ts new file mode 100644 index 0000000000..ff71996463 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proofs.e2e.test.ts @@ -0,0 +1,574 @@ +import type { EventReplaySubject } from '../../../../../../core/tests' +import type { AnonCredsTestsAgent } from '../../../../../tests/legacyAnonCredsSetup' + +import { ProofState, ProofExchangeRecord } from '../../../../../../core/src' +import { testLogger, waitForProofExchangeRecord } from '../../../../../../core/tests' +import { issueLegacyAnonCredsCredential, setupAnonCredsTests } from '../../../../../tests/legacyAnonCredsSetup' +import { V1ProposePresentationMessage, V1RequestPresentationMessage, V1PresentationMessage } from '../messages' + +describe('Present Proof', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let faberConnectionId: string + let aliceConnectionId: string + let credentialDefinitionId: string + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber Proofs V1 - Full', + holderName: 'Alice Proofs V1 - Full', + attributeNames: ['name', 'age'], + })) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { name: 'name', value: 'John' }, + { name: 'age', value: '99' }, + ], + }, + }) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with proof proposal to Faber', async () => { + // Alice sends a presentation proposal to Faber + testLogger.test('Alice sends a presentation proposal to Faber') + + let faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + state: ProofState.ProposalReceived, + }) + + let aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + proofFormats: { + indy: { + attributes: [ + { + name: 'name', + value: 'John', + credentialDefinitionId, + referent: '0', + }, + ], + predicates: [ + { + name: 'age', + predicate: '>=', + threshold: 50, + credentialDefinitionId, + }, + ], + }, + }, + }) + + // Faber waits for a presentation proposal from Alice + testLogger.test('Faber waits for a presentation proposal from Alice') + let faberProofExchangeRecord = await faberProofExchangeRecordPromise + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/propose-presentation', + id: expect.any(String), + presentationProposal: { + type: 'https://didcomm.org/present-proof/1.0/presentation-preview', + attributes: [ + { + name: 'name', + credentialDefinitionId, + value: 'John', + referent: '0', + }, + ], + predicates: [ + { + name: 'age', + credentialDefinitionId, + predicate: '>=', + threshold: 50, + }, + ], + }, + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v1', + }) + + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + // Faber accepts the presentation proposal from Alice + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/request-presentation', + id: expect.any(String), + requestAttachments: [ + { + id: 'libindy-request-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { indy: requestedCredentials.proofFormats.indy }, + }) + + // Faber waits for the presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/presentation', + id: expect.any(String), + presentationAttachments: [ + { + id: 'libindy-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: expect.any(String), + }, + }) + + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v1', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation provided by Alice + testLogger.test('Faber accepts the presentation provided by Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + + const proposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id) + const requestMessage = await aliceAgent.proofs.findRequestMessage(aliceProofExchangeRecord.id) + const presentationMessage = await aliceAgent.proofs.findPresentationMessage(aliceProofExchangeRecord.id) + + expect(proposalMessage).toBeInstanceOf(V1ProposePresentationMessage) + expect(requestMessage).toBeInstanceOf(V1RequestPresentationMessage) + expect(presentationMessage).toBeInstanceOf(V1PresentationMessage) + + const formatData = await aliceAgent.proofs.getFormatData(aliceProofExchangeRecord.id) + + const proposalPredicateKey = Object.keys(formatData.proposal?.indy?.requested_predicates || {})[0] + const requestPredicateKey = Object.keys(formatData.request?.indy?.requested_predicates || {})[0] + + expect(formatData).toMatchObject({ + proposal: { + indy: { + name: 'Proof Request', + version: '1.0', + nonce: expect.any(String), + requested_attributes: { + 0: { + name: 'name', + }, + }, + requested_predicates: { + [proposalPredicateKey]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + request: { + indy: { + name: 'Proof Request', + version: '1.0', + nonce: expect.any(String), + requested_attributes: { + 0: { + name: 'name', + }, + }, + requested_predicates: { + [requestPredicateKey]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + presentation: { + indy: { + proof: expect.any(Object), + requested_proof: expect.any(Object), + identifiers: expect.any(Array), + }, + }, + }) + }) + + test('Faber starts with proof request to Alice', async () => { + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + let faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v1', + connectionId: faberConnectionId, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + let aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/request-presentation', + id: expect.any(String), + requestAttachments: [ + { + id: 'libindy-request-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + }) + + expect(aliceProofExchangeRecord.id).not.toBeNull() + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v1', + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { indy: requestedCredentials.proofFormats.indy }, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/presentation', + id: expect.any(String), + presentationAttachments: [ + { + id: 'libindy-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: expect.any(String), + }, + }) + + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v1', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation + testLogger.test('Faber accept the presentation from Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she receives a presentation acknowledgement + testLogger.test('Alice waits for acceptance by Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) + + test('an attribute group name matches with a predicate group name so an error is thrown', async () => { + await expect( + faberAgent.proofs.requestProof({ + protocolVersion: 'v1', + connectionId: faberConnectionId, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + age: { + name: 'age', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + ).rejects.toThrowError(`The proof request contains duplicate predicates and attributes: age`) + }) + + test('Faber starts with proof request to Alice but gets Problem Reported', async () => { + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + let faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v1', + connectionId: faberConnectionId, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + let aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/request-presentation', + id: expect.any(String), + requestAttachments: [ + { + id: 'libindy-request-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + }) + + expect(aliceProofExchangeRecord.id).not.toBeNull() + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v1', + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + }) + + aliceProofExchangeRecord = await aliceAgent.proofs.sendProblemReport({ + proofRecordId: aliceProofExchangeRecord.id, + description: 'Problem inside proof request', + }) + + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + protocolVersion: 'v1', + }) + }) +}) diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-proofs-auto-accept.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-proofs-auto-accept.e2e.test.ts new file mode 100644 index 0000000000..407e975271 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-proofs-auto-accept.e2e.test.ts @@ -0,0 +1,272 @@ +import type { EventReplaySubject } from '../../../../../../core/tests' +import type { AnonCredsTestsAgent } from '../../../../../tests/legacyAnonCredsSetup' + +import { AutoAcceptProof, ProofState } from '../../../../../../core/src' +import { testLogger, waitForProofExchangeRecord } from '../../../../../../core/tests' +import { issueLegacyAnonCredsCredential, setupAnonCredsTests } from '../../../../../tests/legacyAnonCredsSetup' + +describe('Auto accept present proof', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let faberConnectionId: string + let aliceConnectionId: string + let credentialDefinitionId: string + + describe("Auto accept on 'always'", () => { + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber Auto Accept Always Proofs', + holderName: 'Alice Auto Accept Always Proofs', + autoAcceptProofs: AutoAcceptProof.Always, + attributeNames: ['name', 'age'], + })) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { name: 'name', value: 'John' }, + { name: 'age', value: '99' }, + ], + }, + }) + }) + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test("Alice starts with proof proposal to Faber, both with autoAcceptProof on 'always'", async () => { + testLogger.test('Alice sends presentation proposal to Faber') + + await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + proofFormats: { + indy: { + name: 'abc', + version: '1.0', + attributes: [ + { + name: 'name', + value: 'John', + credentialDefinitionId, + }, + ], + predicates: [ + { + name: 'age', + predicate: '>=', + threshold: 50, + credentialDefinitionId, + }, + ], + }, + }, + }) + + testLogger.test('Faber waits for presentation from Alice') + testLogger.test('Alice waits till it receives presentation ack') + await Promise.all([ + waitForProofExchangeRecord(faberAgent, { state: ProofState.Done }), + waitForProofExchangeRecord(aliceAgent, { state: ProofState.Done }), + ]) + }) + + test("Faber starts with proof requests to Alice, both with autoAcceptProof on 'always'", async () => { + testLogger.test('Faber sends presentation request to Alice') + + await faberAgent.proofs.requestProof({ + protocolVersion: 'v1', + connectionId: faberConnectionId, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + testLogger.test('Faber waits for presentation from Alice') + await Promise.all([ + waitForProofExchangeRecord(faberAgent, { state: ProofState.Done }), + waitForProofExchangeRecord(aliceAgent, { state: ProofState.Done }), + ]) + }) + }) + + describe("Auto accept on 'contentApproved'", () => { + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber Auto Accept ContentApproved Proofs', + holderName: 'Alice Auto Accept ContentApproved Proofs', + autoAcceptProofs: AutoAcceptProof.ContentApproved, + attributeNames: ['name', 'age'], + })) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { name: 'name', value: 'John' }, + { name: 'age', value: '99' }, + ], + }, + }) + }) + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test("Alice starts with proof proposal to Faber, both with autoAcceptProof on 'contentApproved'", async () => { + testLogger.test('Alice sends presentation proposal to Faber') + + const aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + proofFormats: { + indy: { + name: 'abc', + version: '1.0', + attributes: [ + { + name: 'name', + value: 'John', + credentialDefinitionId, + }, + ], + predicates: [ + { + name: 'age', + predicate: '>=', + threshold: 50, + credentialDefinitionId, + }, + ], + }, + }, + }) + + testLogger.test('Faber waits for presentation proposal from Alice') + const faberProofExchangeRecord = await waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + }) + + testLogger.test('Faber accepts presentation proposal from Alice') + await faberAgent.proofs.acceptProposal({ proofRecordId: faberProofExchangeRecord.id }) + + await Promise.all([ + waitForProofExchangeRecord(aliceAgent, { state: ProofState.Done }), + waitForProofExchangeRecord(faberAgent, { state: ProofState.Done }), + ]) + }) + + test("Faber starts with proof requests to Alice, both with autoAcceptProof on 'contentApproved'", async () => { + testLogger.test('Faber sends presentation request to Alice') + + await faberAgent.proofs.requestProof({ + protocolVersion: 'v1', + connectionId: faberConnectionId, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + testLogger.test('Alice waits for request from Faber') + const { id: proofRecordId } = await waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + const { proofFormats } = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId }) + await aliceAgent.proofs.acceptRequest({ proofRecordId, proofFormats }) + + await Promise.all([ + waitForProofExchangeRecord(aliceAgent, { state: ProofState.Done }), + waitForProofExchangeRecord(faberAgent, { state: ProofState.Done }), + ]) + }) + }) +}) diff --git a/packages/anoncreds/src/protocols/proofs/v1/errors/V1PresentationProblemReportError.ts b/packages/anoncreds/src/protocols/proofs/v1/errors/V1PresentationProblemReportError.ts new file mode 100644 index 0000000000..67558537bd --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/errors/V1PresentationProblemReportError.ts @@ -0,0 +1,23 @@ +import type { ProblemReportErrorOptions, PresentationProblemReportReason } from '@credo-ts/core' + +import { ProblemReportError } from '@credo-ts/core' + +import { V1PresentationProblemReportMessage } from '../messages' + +interface V1PresentationProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: PresentationProblemReportReason +} + +export class V1PresentationProblemReportError extends ProblemReportError { + public problemReport: V1PresentationProblemReportMessage + + public constructor(public message: string, { problemCode }: V1PresentationProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new V1PresentationProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/anoncreds/src/protocols/proofs/v1/errors/index.ts b/packages/anoncreds/src/protocols/proofs/v1/errors/index.ts new file mode 100644 index 0000000000..75d23e13a1 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/errors/index.ts @@ -0,0 +1 @@ +export * from './V1PresentationProblemReportError' diff --git a/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationAckHandler.ts b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationAckHandler.ts new file mode 100644 index 0000000000..dca810be6e --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationAckHandler.ts @@ -0,0 +1,17 @@ +import type { V1ProofProtocol } from '../V1ProofProtocol' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { V1PresentationAckMessage } from '../messages' + +export class V1PresentationAckHandler implements MessageHandler { + private proofProtocol: V1ProofProtocol + public supportedMessages = [V1PresentationAckMessage] + + public constructor(proofProtocol: V1ProofProtocol) { + this.proofProtocol = proofProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.proofProtocol.processAck(messageContext) + } +} diff --git a/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationHandler.ts b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationHandler.ts new file mode 100644 index 0000000000..33c270f76c --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationHandler.ts @@ -0,0 +1,52 @@ +import type { V1ProofProtocol } from '../V1ProofProtocol' +import type { MessageHandler, MessageHandlerInboundMessage, ProofExchangeRecord } from '@credo-ts/core' + +import { CredoError, getOutboundMessageContext } from '@credo-ts/core' + +import { V1PresentationMessage } from '../messages' + +export class V1PresentationHandler implements MessageHandler { + private proofProtocol: V1ProofProtocol + public supportedMessages = [V1PresentationMessage] + + public constructor(proofProtocol: V1ProofProtocol) { + this.proofProtocol = proofProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const proofRecord = await this.proofProtocol.processPresentation(messageContext) + + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToPresentation(messageContext.agentContext, { + presentationMessage: messageContext.message, + proofRecord, + }) + + if (shouldAutoRespond) { + return await this.acceptPresentation(proofRecord, messageContext) + } + } + + private async acceptPresentation( + proofRecord: ProofExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending acknowledgement with autoAccept`) + + const requestMessage = await this.proofProtocol.findRequestMessage(messageContext.agentContext, proofRecord.id) + if (!requestMessage) { + throw new CredoError(`No request message found for proof record with id '${proofRecord.id}'`) + } + + const { message } = await this.proofProtocol.acceptPresentation(messageContext.agentContext, { + proofRecord, + }) + + return getOutboundMessageContext(messageContext.agentContext, { + message, + lastReceivedMessage: messageContext.message, + lastSentMessage: requestMessage, + associatedRecord: proofRecord, + connectionRecord: messageContext.connection, + }) + } +} diff --git a/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationProblemReportHandler.ts b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationProblemReportHandler.ts new file mode 100644 index 0000000000..f0239f4088 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { V1ProofProtocol } from '../V1ProofProtocol' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { V1PresentationProblemReportMessage } from '../messages/V1PresentationProblemReportMessage' + +export class V1PresentationProblemReportHandler implements MessageHandler { + private proofProtocol: V1ProofProtocol + public supportedMessages = [V1PresentationProblemReportMessage] + + public constructor(proofProtocol: V1ProofProtocol) { + this.proofProtocol = proofProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.proofProtocol.processProblemReport(messageContext) + } +} diff --git a/packages/anoncreds/src/protocols/proofs/v1/handlers/V1ProposePresentationHandler.ts b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1ProposePresentationHandler.ts new file mode 100644 index 0000000000..79237132df --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1ProposePresentationHandler.ts @@ -0,0 +1,50 @@ +import type { V1ProofProtocol } from '../V1ProofProtocol' +import type { MessageHandler, MessageHandlerInboundMessage, ProofExchangeRecord } from '@credo-ts/core' + +import { OutboundMessageContext } from '@credo-ts/core' + +import { V1ProposePresentationMessage } from '../messages' + +export class V1ProposePresentationHandler implements MessageHandler { + private proofProtocol: V1ProofProtocol + public supportedMessages = [V1ProposePresentationMessage] + + public constructor(proofProtocol: V1ProofProtocol) { + this.proofProtocol = proofProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const proofRecord = await this.proofProtocol.processProposal(messageContext) + + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToProposal(messageContext.agentContext, { + proofRecord, + proposalMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return await this.acceptProposal(proofRecord, messageContext) + } + } + + private async acceptProposal( + proofRecord: ProofExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) + + if (!messageContext.connection) { + messageContext.agentContext.config.logger.error('No connection on the messageContext, aborting auto accept') + return + } + + const { message } = await this.proofProtocol.acceptProposal(messageContext.agentContext, { + proofRecord, + }) + + return new OutboundMessageContext(message, { + agentContext: messageContext.agentContext, + connection: messageContext.connection, + associatedRecord: proofRecord, + }) + } +} diff --git a/packages/anoncreds/src/protocols/proofs/v1/handlers/V1RequestPresentationHandler.ts b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1RequestPresentationHandler.ts new file mode 100644 index 0000000000..4bef0b7682 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1RequestPresentationHandler.ts @@ -0,0 +1,46 @@ +import type { V1ProofProtocol } from '../V1ProofProtocol' +import type { MessageHandler, MessageHandlerInboundMessage, ProofExchangeRecord } from '@credo-ts/core' + +import { getOutboundMessageContext } from '@credo-ts/core' + +import { V1RequestPresentationMessage } from '../messages' + +export class V1RequestPresentationHandler implements MessageHandler { + private proofProtocol: V1ProofProtocol + public supportedMessages = [V1RequestPresentationMessage] + + public constructor(proofProtocol: V1ProofProtocol) { + this.proofProtocol = proofProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const proofRecord = await this.proofProtocol.processRequest(messageContext) + + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToRequest(messageContext.agentContext, { + proofRecord, + requestMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return await this.acceptRequest(proofRecord, messageContext) + } + } + + private async acceptRequest( + proofRecord: ProofExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending presentation with autoAccept on`) + + const { message } = await this.proofProtocol.acceptRequest(messageContext.agentContext, { + proofRecord, + }) + + return getOutboundMessageContext(messageContext.agentContext, { + message, + lastReceivedMessage: messageContext.message, + associatedRecord: proofRecord, + connectionRecord: messageContext.connection, + }) + } +} diff --git a/packages/anoncreds/src/protocols/proofs/v1/handlers/index.ts b/packages/anoncreds/src/protocols/proofs/v1/handlers/index.ts new file mode 100644 index 0000000000..c202042b9b --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/handlers/index.ts @@ -0,0 +1,5 @@ +export * from './V1PresentationAckHandler' +export * from './V1PresentationHandler' +export * from './V1ProposePresentationHandler' +export * from './V1RequestPresentationHandler' +export * from './V1PresentationProblemReportHandler' diff --git a/packages/anoncreds/src/protocols/proofs/v1/index.ts b/packages/anoncreds/src/protocols/proofs/v1/index.ts new file mode 100644 index 0000000000..e698bc7140 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/index.ts @@ -0,0 +1,4 @@ +export * from './errors' +export * from './messages' +export * from './models' +export * from './V1ProofProtocol' diff --git a/packages/anoncreds/src/protocols/proofs/v1/messages/V1PresentationAckMessage.ts b/packages/anoncreds/src/protocols/proofs/v1/messages/V1PresentationAckMessage.ts new file mode 100644 index 0000000000..1fb511065f --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/messages/V1PresentationAckMessage.ts @@ -0,0 +1,15 @@ +import type { AckMessageOptions } from '@credo-ts/core' + +import { AckMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' + +export class V1PresentationAckMessage extends AckMessage { + public readonly allowDidSovPrefix = true + + public constructor(options: AckMessageOptions) { + super(options) + } + + @IsValidMessageType(V1PresentationAckMessage.type) + public readonly type = V1PresentationAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/ack') +} diff --git a/packages/anoncreds/src/protocols/proofs/v1/messages/V1PresentationMessage.ts b/packages/anoncreds/src/protocols/proofs/v1/messages/V1PresentationMessage.ts new file mode 100644 index 0000000000..c862708eef --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/messages/V1PresentationMessage.ts @@ -0,0 +1,70 @@ +import type { AnonCredsProof } from '../../../../models' + +import { Attachment, AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' +import { Expose, Type } from 'class-transformer' +import { IsArray, IsString, ValidateNested, IsOptional, IsInstance } from 'class-validator' + +export const INDY_PROOF_ATTACHMENT_ID = 'libindy-presentation-0' + +export interface V1PresentationMessageOptions { + id?: string + comment?: string + presentationAttachments: Attachment[] + attachments?: Attachment[] +} + +/** + * Presentation Message part of Present Proof Protocol used as a response to a {@link PresentationRequestMessage | Presentation Request Message} from prover to verifier. + * Contains signed presentations. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#presentation + */ +export class V1PresentationMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + public constructor(options: V1PresentationMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.presentationAttachments = options.presentationAttachments + this.appendedAttachments = options.attachments + } + } + + @IsValidMessageType(V1PresentationMessage.type) + public readonly type = V1PresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/presentation') + + /** + * Provides some human readable information about this request for a presentation. + */ + @IsOptional() + @IsString() + public comment?: string + + /** + * An array of attachments containing the presentation in the requested format(s). + */ + @Expose({ name: 'presentations~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public presentationAttachments!: Attachment[] + + public get indyProof(): AnonCredsProof | null { + const attachment = + this.presentationAttachments.find((attachment) => attachment.id === INDY_PROOF_ATTACHMENT_ID) ?? null + + const proofJson = attachment?.getDataAsJson() ?? null + + return proofJson + } + + public getPresentationAttachmentById(id: string): Attachment | undefined { + return this.presentationAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/anoncreds/src/protocols/proofs/v1/messages/V1PresentationProblemReportMessage.ts b/packages/anoncreds/src/protocols/proofs/v1/messages/V1PresentationProblemReportMessage.ts new file mode 100644 index 0000000000..ef603cc082 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/messages/V1PresentationProblemReportMessage.ts @@ -0,0 +1,24 @@ +import type { ProblemReportMessageOptions } from '@credo-ts/core' + +import { ProblemReportMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' + +export type V1PresentationProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class V1PresentationProblemReportMessage extends ProblemReportMessage { + public readonly allowDidSovPrefix = true + + /** + * Create new PresentationProblemReportMessage instance. + * @param options description of error and multiple optional fields for reporting problem + */ + public constructor(options: V1PresentationProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(V1PresentationProblemReportMessage.type) + public readonly type = V1PresentationProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/problem-report') +} diff --git a/packages/anoncreds/src/protocols/proofs/v1/messages/V1ProposePresentationMessage.ts b/packages/anoncreds/src/protocols/proofs/v1/messages/V1ProposePresentationMessage.ts new file mode 100644 index 0000000000..107e2f5ed4 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/messages/V1ProposePresentationMessage.ts @@ -0,0 +1,49 @@ +import { AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' +import { Expose, Type } from 'class-transformer' +import { IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { V1PresentationPreview } from '../models/V1PresentationPreview' + +export interface V1ProposePresentationMessageOptions { + id?: string + comment?: string + presentationProposal: V1PresentationPreview +} + +/** + * Propose Presentation Message part of Present Proof Protocol used to initiate presentation exchange by holder. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#propose-presentation + */ +export class V1ProposePresentationMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + public constructor(options: V1ProposePresentationMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.presentationProposal = options.presentationProposal + } + } + + @IsValidMessageType(V1ProposePresentationMessage.type) + public readonly type = V1ProposePresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/propose-presentation') + + /** + * Provides some human readable information about the proposed presentation. + */ + @IsString() + @IsOptional() + public comment?: string + + /** + * Represents the presentation example that prover wants to provide. + */ + @Expose({ name: 'presentation_proposal' }) + @Type(() => V1PresentationPreview) + @ValidateNested() + @IsInstance(V1PresentationPreview) + public presentationProposal!: V1PresentationPreview +} diff --git a/packages/anoncreds/src/protocols/proofs/v1/messages/V1RequestPresentationMessage.ts b/packages/anoncreds/src/protocols/proofs/v1/messages/V1RequestPresentationMessage.ts new file mode 100644 index 0000000000..b5caecd945 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/messages/V1RequestPresentationMessage.ts @@ -0,0 +1,65 @@ +import type { LegacyIndyProofRequest } from '../../../../formats' + +import { Attachment, AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' +import { Expose, Type } from 'class-transformer' +import { IsArray, IsString, ValidateNested, IsOptional, IsInstance } from 'class-validator' + +export interface V1RequestPresentationMessageOptions { + id?: string + comment?: string + requestAttachments: Attachment[] +} + +export const INDY_PROOF_REQUEST_ATTACHMENT_ID = 'libindy-request-presentation-0' + +/** + * Request Presentation Message part of Present Proof Protocol used to initiate request from verifier to prover. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#request-presentation + */ +export class V1RequestPresentationMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + + public constructor(options: V1RequestPresentationMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.requestAttachments = options.requestAttachments + } + } + + @IsValidMessageType(V1RequestPresentationMessage.type) + public readonly type = V1RequestPresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/request-presentation') + + /** + * Provides some human readable information about this request for a presentation. + */ + @IsOptional() + @IsString() + public comment?: string + + /** + * An array of attachments defining the acceptable formats for the presentation. + */ + @Expose({ name: 'request_presentations~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public requestAttachments!: Attachment[] + + public get indyProofRequest(): LegacyIndyProofRequest | null { + const attachment = this.requestAttachments.find((attachment) => attachment.id === INDY_PROOF_REQUEST_ATTACHMENT_ID) + // Extract proof request from attachment + return attachment?.getDataAsJson() ?? null + } + + public getRequestAttachmentById(id: string): Attachment | undefined { + return this.requestAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/anoncreds/src/protocols/proofs/v1/messages/index.ts b/packages/anoncreds/src/protocols/proofs/v1/messages/index.ts new file mode 100644 index 0000000000..5aef9dbd79 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/messages/index.ts @@ -0,0 +1,5 @@ +export * from './V1ProposePresentationMessage' +export * from './V1RequestPresentationMessage' +export * from './V1PresentationProblemReportMessage' +export * from './V1PresentationMessage' +export * from './V1PresentationAckMessage' diff --git a/packages/anoncreds/src/protocols/proofs/v1/models/V1PresentationPreview.ts b/packages/anoncreds/src/protocols/proofs/v1/models/V1PresentationPreview.ts new file mode 100644 index 0000000000..3d23e55bc8 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/models/V1PresentationPreview.ts @@ -0,0 +1,140 @@ +import { JsonTransformer, IsValidMessageType, replaceLegacyDidSovPrefix, parseMessageType } from '@credo-ts/core' +import { Expose, Transform, Type } from 'class-transformer' +import { + IsIn, + IsInstance, + IsInt, + IsMimeType, + IsOptional, + IsString, + Matches, + ValidateIf, + ValidateNested, +} from 'class-validator' + +import { anonCredsPredicateType, AnonCredsPredicateType } from '../../../../models' +import { unqualifiedCredentialDefinitionIdRegex } from '../../../../utils' + +export interface V1PresentationPreviewAttributeOptions { + name: string + credentialDefinitionId?: string + mimeType?: string + value?: string + referent?: string +} + +export class V1PresentationPreviewAttribute { + public constructor(options: V1PresentationPreviewAttributeOptions) { + if (options) { + this.name = options.name + this.credentialDefinitionId = options.credentialDefinitionId + this.mimeType = options.mimeType + this.value = options.value + this.referent = options.referent + } + } + + public name!: string + + @Expose({ name: 'cred_def_id' }) + @IsString() + @ValidateIf((o: V1PresentationPreviewAttribute) => o.referent !== undefined) + @Matches(unqualifiedCredentialDefinitionIdRegex) + public credentialDefinitionId?: string + + @Expose({ name: 'mime-type' }) + @IsOptional() + @IsMimeType() + public mimeType?: string + + @IsString() + @IsOptional() + public value?: string + + @IsString() + @IsOptional() + public referent?: string + + public toJSON(): Record { + return JsonTransformer.toJSON(this) + } +} + +export interface V1PresentationPreviewPredicateOptions { + name: string + credentialDefinitionId: string + predicate: AnonCredsPredicateType + threshold: number +} + +export class V1PresentationPreviewPredicate { + public constructor(options: V1PresentationPreviewPredicateOptions) { + if (options) { + this.name = options.name + this.credentialDefinitionId = options.credentialDefinitionId + this.predicate = options.predicate + this.threshold = options.threshold + } + } + + @IsString() + public name!: string + + @Expose({ name: 'cred_def_id' }) + @IsString() + @Matches(unqualifiedCredentialDefinitionIdRegex) + public credentialDefinitionId!: string + + @IsIn(anonCredsPredicateType) + public predicate!: AnonCredsPredicateType + + @IsInt() + public threshold!: number + + public toJSON(): Record { + return JsonTransformer.toJSON(this) + } +} + +export interface V1PresentationPreviewOptions { + attributes?: V1PresentationPreviewAttributeOptions[] + predicates?: V1PresentationPreviewPredicateOptions[] +} + +/** + * Presentation preview inner message class. + * + * This is not a message but an inner object for other messages in this protocol. It is used to construct a preview of the data for the presentation. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#presentation-preview + */ +export class V1PresentationPreview { + public constructor(options: V1PresentationPreviewOptions) { + if (options) { + this.attributes = options.attributes?.map((a) => new V1PresentationPreviewAttribute(a)) ?? [] + this.predicates = options.predicates?.map((p) => new V1PresentationPreviewPredicate(p)) ?? [] + } + } + + @Expose({ name: '@type' }) + @IsValidMessageType(V1PresentationPreview.type) + @Transform(({ value }) => replaceLegacyDidSovPrefix(value), { + toClassOnly: true, + }) + public readonly type = V1PresentationPreview.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/presentation-preview') + + @Type(() => V1PresentationPreviewAttribute) + @ValidateNested({ each: true }) + @IsInstance(V1PresentationPreviewAttribute, { each: true }) + public attributes!: V1PresentationPreviewAttribute[] + + @Type(() => V1PresentationPreviewPredicate) + @ValidateNested({ each: true }) + @IsInstance(V1PresentationPreviewPredicate, { each: true }) + public predicates!: V1PresentationPreviewPredicate[] + + public toJSON(): Record { + return JsonTransformer.toJSON(this) + } +} diff --git a/packages/anoncreds/src/protocols/proofs/v1/models/index.ts b/packages/anoncreds/src/protocols/proofs/v1/models/index.ts new file mode 100644 index 0000000000..56c1a4fde0 --- /dev/null +++ b/packages/anoncreds/src/protocols/proofs/v1/models/index.ts @@ -0,0 +1 @@ +export * from './V1PresentationPreview' diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRecord.ts b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRecord.ts new file mode 100644 index 0000000000..f6c67078c9 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRecord.ts @@ -0,0 +1,43 @@ +import type { TagsBase } from '@credo-ts/core' + +import { BaseRecord, utils } from '@credo-ts/core' + +export interface AnonCredsCredentialDefinitionPrivateRecordProps { + id?: string + credentialDefinitionId: string + value: Record + createdAt?: Date +} + +export type DefaultAnonCredsCredentialDefinitionPrivateTags = { + credentialDefinitionId: string +} + +export class AnonCredsCredentialDefinitionPrivateRecord extends BaseRecord< + DefaultAnonCredsCredentialDefinitionPrivateTags, + TagsBase +> { + public static readonly type = 'AnonCredsCredentialDefinitionPrivateRecord' + public readonly type = AnonCredsCredentialDefinitionPrivateRecord.type + + public readonly credentialDefinitionId!: string + public readonly value!: Record // TODO: Define structure + + public constructor(props: AnonCredsCredentialDefinitionPrivateRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.credentialDefinitionId = props.credentialDefinitionId + this.value = props.value + this.createdAt = props.createdAt ?? new Date() + } + } + + public getTags() { + return { + ...this._tags, + credentialDefinitionId: this.credentialDefinitionId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRepository.ts b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRepository.ts new file mode 100644 index 0000000000..dd141e02e3 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@credo-ts/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@credo-ts/core' + +import { AnonCredsCredentialDefinitionPrivateRecord } from './AnonCredsCredentialDefinitionPrivateRecord' + +@injectable() +export class AnonCredsCredentialDefinitionPrivateRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsCredentialDefinitionPrivateRecord, storageService, eventEmitter) + } + + public async getByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.getSingleByQuery(agentContext, { credentialDefinitionId }) + } + + public async findByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findSingleByQuery(agentContext, { credentialDefinitionId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts new file mode 100644 index 0000000000..1969d0f10f --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts @@ -0,0 +1,81 @@ +import type { AnonCredsCredentialDefinitionRecordMetadata } from './anonCredsCredentialDefinitionRecordMetadataTypes' +import type { AnonCredsCredentialDefinition } from '../models' +import type { TagsBase } from '@credo-ts/core' + +import { BaseRecord, utils } from '@credo-ts/core' + +import { + getUnqualifiedCredentialDefinitionId, + isDidIndyCredentialDefinitionId, + parseIndyCredentialDefinitionId, +} from '../utils/indyIdentifiers' + +export interface AnonCredsCredentialDefinitionRecordProps { + id?: string + credentialDefinitionId: string + credentialDefinition: AnonCredsCredentialDefinition + methodName: string + createdAt?: Date +} + +export type DefaultAnonCredsCredentialDefinitionTags = { + schemaId: string + credentialDefinitionId: string + issuerId: string + tag: string + methodName: string + + // Stores the unqualified variant of the credential definition id, which allows issuing credentials using the legacy + // credential definition id, even though the credential definition id is stored in the wallet as a qualified id. + // This is only added when the credential definition id is an did:indy identifier. + unqualifiedCredentialDefinitionId?: string +} + +export class AnonCredsCredentialDefinitionRecord extends BaseRecord< + DefaultAnonCredsCredentialDefinitionTags, + TagsBase, + AnonCredsCredentialDefinitionRecordMetadata +> { + public static readonly type = 'AnonCredsCredentialDefinitionRecord' + public readonly type = AnonCredsCredentialDefinitionRecord.type + + public credentialDefinitionId!: string + public credentialDefinition!: AnonCredsCredentialDefinition + + /** + * AnonCreds method name. We don't use names explicitly from the registry (there's no identifier for a registry) + * @see https://hyperledger.github.io/anoncreds-methods-registry/ + */ + public methodName!: string + + public constructor(props: AnonCredsCredentialDefinitionRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this.credentialDefinitionId = props.credentialDefinitionId + this.credentialDefinition = props.credentialDefinition + this.methodName = props.methodName + } + } + + public getTags() { + let unqualifiedCredentialDefinitionId: string | undefined = undefined + if (isDidIndyCredentialDefinitionId(this.credentialDefinitionId)) { + const { namespaceIdentifier, schemaSeqNo, tag } = parseIndyCredentialDefinitionId(this.credentialDefinitionId) + + unqualifiedCredentialDefinitionId = getUnqualifiedCredentialDefinitionId(namespaceIdentifier, schemaSeqNo, tag) + } + + return { + ...this._tags, + credentialDefinitionId: this.credentialDefinitionId, + schemaId: this.credentialDefinition.schemaId, + issuerId: this.credentialDefinition.issuerId, + tag: this.credentialDefinition.tag, + methodName: this.methodName, + unqualifiedCredentialDefinitionId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRepository.ts b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRepository.ts new file mode 100644 index 0000000000..8d4619930b --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRepository.ts @@ -0,0 +1,41 @@ +import type { AgentContext } from '@credo-ts/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@credo-ts/core' + +import { AnonCredsCredentialDefinitionRecord } from './AnonCredsCredentialDefinitionRecord' + +@injectable() +export class AnonCredsCredentialDefinitionRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsCredentialDefinitionRecord, storageService, eventEmitter) + } + + public async getByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.getSingleByQuery(agentContext, { + $or: [ + { + credentialDefinitionId, + }, + { + unqualifiedCredentialDefinitionId: credentialDefinitionId, + }, + ], + }) + } + + public async findByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findSingleByQuery(agentContext, { + $or: [ + { + credentialDefinitionId, + }, + { + unqualifiedCredentialDefinitionId: credentialDefinitionId, + }, + ], + }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialRecord.ts b/packages/anoncreds/src/repository/AnonCredsCredentialRecord.ts new file mode 100644 index 0000000000..05fb9953df --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialRecord.ts @@ -0,0 +1,98 @@ +import type { AnonCredsCredential } from '../models' +import type { Tags } from '@credo-ts/core' + +import { BaseRecord, utils } from '@credo-ts/core' + +export interface AnonCredsCredentialRecordProps { + id?: string + createdAt?: Date + credential: AnonCredsCredential + credentialId: string + credentialRevocationId?: string + linkSecretId: string + schemaName: string + schemaVersion: string + schemaIssuerId: string + issuerId: string + methodName: string +} + +export type DefaultAnonCredsCredentialTags = { + credentialId: string + linkSecretId: string + credentialDefinitionId: string + credentialRevocationId?: string + revocationRegistryId?: string + schemaId: string + methodName: string + + // the following keys can be used for every `attribute name` in credential. + [key: `attr::${string}::marker`]: true | undefined + [key: `attr::${string}::value`]: string | undefined +} + +export type CustomAnonCredsCredentialTags = { + schemaName: string + schemaVersion: string + schemaIssuerId: string + issuerId: string +} + +export class AnonCredsCredentialRecord extends BaseRecord< + DefaultAnonCredsCredentialTags, + CustomAnonCredsCredentialTags +> { + public static readonly type = 'AnonCredsCredentialRecord' + public readonly type = AnonCredsCredentialRecord.type + + public readonly credentialId!: string + public readonly credentialRevocationId?: string + public readonly linkSecretId!: string + public readonly credential!: AnonCredsCredential + + /** + * AnonCreds method name. We don't use names explicitly from the registry (there's no identifier for a registry) + * @see https://hyperledger.github.io/anoncreds-methods-registry/ + */ + public readonly methodName!: string + + public constructor(props: AnonCredsCredentialRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this.credentialId = props.credentialId + this.credential = props.credential + this.credentialRevocationId = props.credentialRevocationId + this.linkSecretId = props.linkSecretId + this.methodName = props.methodName + this.setTags({ + issuerId: props.issuerId, + schemaIssuerId: props.schemaIssuerId, + schemaName: props.schemaName, + schemaVersion: props.schemaVersion, + }) + } + } + + public getTags() { + const tags: Tags = { + ...this._tags, + credentialDefinitionId: this.credential.cred_def_id, + schemaId: this.credential.schema_id, + credentialId: this.credentialId, + credentialRevocationId: this.credentialRevocationId, + revocationRegistryId: this.credential.rev_reg_id, + linkSecretId: this.linkSecretId, + methodName: this.methodName, + } + + for (const [key, value] of Object.entries(this.credential.values)) { + tags[`attr::${key}::value`] = value.raw + tags[`attr::${key}::marker`] = true + } + + return tags + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialRepository.ts b/packages/anoncreds/src/repository/AnonCredsCredentialRepository.ts new file mode 100644 index 0000000000..56cf1cfaa5 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialRepository.ts @@ -0,0 +1,31 @@ +import type { AgentContext } from '@credo-ts/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@credo-ts/core' + +import { AnonCredsCredentialRecord } from './AnonCredsCredentialRecord' + +@injectable() +export class AnonCredsCredentialRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsCredentialRecord, storageService, eventEmitter) + } + + public async getByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.getSingleByQuery(agentContext, { credentialDefinitionId }) + } + + public async findByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findSingleByQuery(agentContext, { credentialDefinitionId }) + } + + public async getByCredentialId(agentContext: AgentContext, credentialId: string) { + return this.getSingleByQuery(agentContext, { credentialId }) + } + + public async findByCredentialId(agentContext: AgentContext, credentialId: string) { + return this.findSingleByQuery(agentContext, { credentialId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRecord.ts b/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRecord.ts new file mode 100644 index 0000000000..fd79faf1f0 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRecord.ts @@ -0,0 +1,43 @@ +import type { TagsBase } from '@credo-ts/core' + +import { BaseRecord, utils } from '@credo-ts/core' + +export interface AnonCredsKeyCorrectnessProofRecordProps { + id?: string + credentialDefinitionId: string + value: Record + createdAt?: Date +} + +export type DefaultAnonCredsKeyCorrectnessProofPrivateTags = { + credentialDefinitionId: string +} + +export class AnonCredsKeyCorrectnessProofRecord extends BaseRecord< + DefaultAnonCredsKeyCorrectnessProofPrivateTags, + TagsBase +> { + public static readonly type = 'AnonCredsKeyCorrectnessProofRecord' + public readonly type = AnonCredsKeyCorrectnessProofRecord.type + + public readonly credentialDefinitionId!: string + public readonly value!: Record // TODO: Define structure + + public constructor(props: AnonCredsKeyCorrectnessProofRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.credentialDefinitionId = props.credentialDefinitionId + this.value = props.value + this.createdAt = props.createdAt ?? new Date() + } + } + + public getTags() { + return { + ...this._tags, + credentialDefinitionId: this.credentialDefinitionId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRepository.ts b/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRepository.ts new file mode 100644 index 0000000000..2e4548e6a1 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@credo-ts/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@credo-ts/core' + +import { AnonCredsKeyCorrectnessProofRecord } from './AnonCredsKeyCorrectnessProofRecord' + +@injectable() +export class AnonCredsKeyCorrectnessProofRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsKeyCorrectnessProofRecord, storageService, eventEmitter) + } + + public async getByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.getSingleByQuery(agentContext, { credentialDefinitionId }) + } + + public async findByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findSingleByQuery(agentContext, { credentialDefinitionId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsLinkSecretRecord.ts b/packages/anoncreds/src/repository/AnonCredsLinkSecretRecord.ts new file mode 100644 index 0000000000..3c3acf38f1 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsLinkSecretRecord.ts @@ -0,0 +1,42 @@ +import type { TagsBase } from '@credo-ts/core' + +import { BaseRecord, utils } from '@credo-ts/core' + +export interface AnonCredsLinkSecretRecordProps { + id?: string + linkSecretId: string + value?: string // If value is not provided, only reference to link secret is stored in regular storage +} + +export type DefaultAnonCredsLinkSecretTags = { + linkSecretId: string +} + +export type CustomAnonCredsLinkSecretTags = TagsBase & { + isDefault?: boolean +} + +export class AnonCredsLinkSecretRecord extends BaseRecord { + public static readonly type = 'AnonCredsLinkSecretRecord' + public readonly type = AnonCredsLinkSecretRecord.type + + public readonly linkSecretId!: string + public readonly value?: string + + public constructor(props: AnonCredsLinkSecretRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.linkSecretId = props.linkSecretId + this.value = props.value + } + } + + public getTags() { + return { + ...this._tags, + linkSecretId: this.linkSecretId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsLinkSecretRepository.ts b/packages/anoncreds/src/repository/AnonCredsLinkSecretRepository.ts new file mode 100644 index 0000000000..ad804aa880 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsLinkSecretRepository.ts @@ -0,0 +1,31 @@ +import type { AgentContext } from '@credo-ts/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@credo-ts/core' + +import { AnonCredsLinkSecretRecord } from './AnonCredsLinkSecretRecord' + +@injectable() +export class AnonCredsLinkSecretRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsLinkSecretRecord, storageService, eventEmitter) + } + + public async getDefault(agentContext: AgentContext) { + return this.getSingleByQuery(agentContext, { isDefault: true }) + } + + public async findDefault(agentContext: AgentContext) { + return this.findSingleByQuery(agentContext, { isDefault: true }) + } + + public async getByLinkSecretId(agentContext: AgentContext, linkSecretId: string) { + return this.getSingleByQuery(agentContext, { linkSecretId }) + } + + public async findByLinkSecretId(agentContext: AgentContext, linkSecretId: string) { + return this.findSingleByQuery(agentContext, { linkSecretId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts new file mode 100644 index 0000000000..dbc4d537d0 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts @@ -0,0 +1,59 @@ +import type { TagsBase } from '@credo-ts/core' + +import { BaseRecord, utils } from '@credo-ts/core' + +export enum AnonCredsRevocationRegistryState { + Created = 'created', + Active = 'active', + Full = 'full', +} + +export interface AnonCredsRevocationRegistryDefinitionPrivateRecordProps { + id?: string + revocationRegistryDefinitionId: string + credentialDefinitionId: string + value: Record + index?: number + state?: AnonCredsRevocationRegistryState +} + +export type DefaultAnonCredsRevocationRegistryPrivateTags = { + revocationRegistryDefinitionId: string + credentialDefinitionId: string + state: AnonCredsRevocationRegistryState +} + +export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseRecord< + DefaultAnonCredsRevocationRegistryPrivateTags, + TagsBase +> { + public static readonly type = 'AnonCredsRevocationRegistryDefinitionPrivateRecord' + public readonly type = AnonCredsRevocationRegistryDefinitionPrivateRecord.type + + public readonly revocationRegistryDefinitionId!: string + public readonly credentialDefinitionId!: string + public readonly value!: Record // TODO: Define structure + + public state!: AnonCredsRevocationRegistryState + + public constructor(props: AnonCredsRevocationRegistryDefinitionPrivateRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.revocationRegistryDefinitionId = props.revocationRegistryDefinitionId + this.credentialDefinitionId = props.credentialDefinitionId + this.value = props.value + this.state = props.state ?? AnonCredsRevocationRegistryState.Created + } + } + + public getTags() { + return { + ...this._tags, + revocationRegistryDefinitionId: this.revocationRegistryDefinitionId, + credentialDefinitionId: this.credentialDefinitionId, + state: this.state, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts new file mode 100644 index 0000000000..4410a382a2 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts @@ -0,0 +1,36 @@ +import type { AnonCredsRevocationRegistryState } from './AnonCredsRevocationRegistryDefinitionPrivateRecord' +import type { AgentContext } from '@credo-ts/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@credo-ts/core' + +import { AnonCredsRevocationRegistryDefinitionPrivateRecord } from './AnonCredsRevocationRegistryDefinitionPrivateRecord' + +@injectable() +export class AnonCredsRevocationRegistryDefinitionPrivateRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) + storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsRevocationRegistryDefinitionPrivateRecord, storageService, eventEmitter) + } + + public async getByRevocationRegistryDefinitionId(agentContext: AgentContext, revocationRegistryDefinitionId: string) { + return this.getSingleByQuery(agentContext, { revocationRegistryDefinitionId }) + } + + public async findByRevocationRegistryDefinitionId( + agentContext: AgentContext, + revocationRegistryDefinitionId: string + ) { + return this.findSingleByQuery(agentContext, { revocationRegistryDefinitionId }) + } + + public async findAllByCredentialDefinitionIdAndState( + agentContext: AgentContext, + credentialDefinitionId: string, + state?: AnonCredsRevocationRegistryState + ) { + return this.findByQuery(agentContext, { credentialDefinitionId, state }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts new file mode 100644 index 0000000000..0c986d2bcb --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts @@ -0,0 +1,48 @@ +import type { AnonCredsRevocationRegistryDefinitionRecordMetadata } from './anonCredsRevocationRegistryDefinitionRecordMetadataTypes' +import type { AnonCredsRevocationRegistryDefinition } from '../models' +import type { TagsBase } from '@credo-ts/core' + +import { BaseRecord, utils } from '@credo-ts/core' + +export interface AnonCredsRevocationRegistryDefinitionRecordProps { + id?: string + revocationRegistryDefinitionId: string + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + createdAt?: Date +} + +export type DefaultAnonCredsRevocationRegistryDefinitionTags = { + revocationRegistryDefinitionId: string + credentialDefinitionId: string +} + +export class AnonCredsRevocationRegistryDefinitionRecord extends BaseRecord< + DefaultAnonCredsRevocationRegistryDefinitionTags, + TagsBase, + AnonCredsRevocationRegistryDefinitionRecordMetadata +> { + public static readonly type = 'AnonCredsRevocationRegistryDefinitionRecord' + public readonly type = AnonCredsRevocationRegistryDefinitionRecord.type + + public readonly revocationRegistryDefinitionId!: string + public readonly revocationRegistryDefinition!: AnonCredsRevocationRegistryDefinition + + public constructor(props: AnonCredsRevocationRegistryDefinitionRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.revocationRegistryDefinitionId = props.revocationRegistryDefinitionId + this.revocationRegistryDefinition = props.revocationRegistryDefinition + this.createdAt = props.createdAt ?? new Date() + } + } + + public getTags() { + return { + ...this._tags, + revocationRegistryDefinitionId: this.revocationRegistryDefinitionId, + credentialDefinitionId: this.revocationRegistryDefinition.credDefId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts new file mode 100644 index 0000000000..30c1cf15c1 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts @@ -0,0 +1,31 @@ +import type { AgentContext } from '@credo-ts/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@credo-ts/core' + +import { AnonCredsRevocationRegistryDefinitionRecord } from './AnonCredsRevocationRegistryDefinitionRecord' + +@injectable() +export class AnonCredsRevocationRegistryDefinitionRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) + storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsRevocationRegistryDefinitionRecord, storageService, eventEmitter) + } + + public async getByRevocationRegistryDefinitionId(agentContext: AgentContext, revocationRegistryDefinitionId: string) { + return this.getSingleByQuery(agentContext, { revocationRegistryDefinitionId }) + } + + public async findByRevocationRegistryDefinitionId( + agentContext: AgentContext, + revocationRegistryDefinitionId: string + ) { + return this.findSingleByQuery(agentContext, { revocationRegistryDefinitionId }) + } + + public async findAllByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findByQuery(agentContext, { credentialDefinitionId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts b/packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts new file mode 100644 index 0000000000..bbb725b2ff --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts @@ -0,0 +1,76 @@ +import type { AnonCredsSchemaRecordMetadata } from './anonCredsSchemaRecordMetadataTypes' +import type { AnonCredsSchema } from '../models' +import type { TagsBase } from '@credo-ts/core' + +import { BaseRecord, utils } from '@credo-ts/core' + +import { getUnqualifiedSchemaId, isDidIndySchemaId, parseIndySchemaId } from '../utils/indyIdentifiers' + +export interface AnonCredsSchemaRecordProps { + id?: string + schemaId: string + schema: AnonCredsSchema + methodName: string + createdAt?: Date +} + +export type DefaultAnonCredsSchemaTags = { + schemaId: string + issuerId: string + schemaName: string + schemaVersion: string + methodName: string + + // Stores the unqualified variant of the schema id, which allows issuing credentials using the legacy + // schema id, even though the schema id is stored in the wallet as a qualified id. + // This is only added when the schema id is an did:indy identifier. + unqualifiedSchemaId?: string +} + +export class AnonCredsSchemaRecord extends BaseRecord< + DefaultAnonCredsSchemaTags, + TagsBase, + AnonCredsSchemaRecordMetadata +> { + public static readonly type = 'AnonCredsSchemaRecord' + public readonly type = AnonCredsSchemaRecord.type + + public schemaId!: string + public schema!: AnonCredsSchema + + /** + * AnonCreds method name. We don't use names explicitly from the registry (there's no identifier for a registry) + * @see https://hyperledger.github.io/anoncreds-methods-registry/ + */ + public methodName!: string + + public constructor(props: AnonCredsSchemaRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this.schema = props.schema + this.schemaId = props.schemaId + this.methodName = props.methodName + } + } + + public getTags() { + let unqualifiedSchemaId: string | undefined = undefined + if (isDidIndySchemaId(this.schemaId)) { + const { namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(this.schemaId) + unqualifiedSchemaId = getUnqualifiedSchemaId(namespaceIdentifier, schemaName, schemaVersion) + } + + return { + ...this._tags, + schemaId: this.schemaId, + issuerId: this.schema.issuerId, + schemaName: this.schema.name, + schemaVersion: this.schema.version, + methodName: this.methodName, + unqualifiedSchemaId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsSchemaRepository.ts b/packages/anoncreds/src/repository/AnonCredsSchemaRepository.ts new file mode 100644 index 0000000000..56a426163a --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsSchemaRepository.ts @@ -0,0 +1,41 @@ +import type { AgentContext } from '@credo-ts/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, inject, injectable } from '@credo-ts/core' + +import { AnonCredsSchemaRecord } from './AnonCredsSchemaRecord' + +@injectable() +export class AnonCredsSchemaRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsSchemaRecord, storageService, eventEmitter) + } + + public async getBySchemaId(agentContext: AgentContext, schemaId: string) { + return this.getSingleByQuery(agentContext, { + $or: [ + { + schemaId, + }, + { + unqualifiedSchemaId: schemaId, + }, + ], + }) + } + + public async findBySchemaId(agentContext: AgentContext, schemaId: string) { + return await this.findSingleByQuery(agentContext, { + $or: [ + { + schemaId, + }, + { + unqualifiedSchemaId: schemaId, + }, + ], + }) + } +} diff --git a/packages/anoncreds/src/repository/__tests__/AnonCredsCredentialRecord.test.ts b/packages/anoncreds/src/repository/__tests__/AnonCredsCredentialRecord.test.ts new file mode 100644 index 0000000000..34d79a2f8a --- /dev/null +++ b/packages/anoncreds/src/repository/__tests__/AnonCredsCredentialRecord.test.ts @@ -0,0 +1,45 @@ +import type { AnonCredsCredential } from '@credo-ts/anoncreds' + +import { AnonCredsCredentialRecord } from '../AnonCredsCredentialRecord' + +describe('AnoncredsCredentialRecords', () => { + test('Returns the correct tags from the getTags methods based on the credential record values', () => { + const anoncredsCredentialRecords = new AnonCredsCredentialRecord({ + credential: { + cred_def_id: 'credDefId', + schema_id: 'schemaId', + signature: 'signature', + signature_correctness_proof: 'signatureCorrectnessProof', + values: { attr1: { raw: 'value1', encoded: 'encvalue1' }, attr2: { raw: 'value2', encoded: 'encvalue2' } }, + rev_reg_id: 'revRegId', + } as AnonCredsCredential, + credentialId: 'myCredentialId', + credentialRevocationId: 'credentialRevocationId', + linkSecretId: 'linkSecretId', + issuerId: 'issuerDid', + schemaIssuerId: 'schemaIssuerDid', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + methodName: 'methodName', + }) + + const tags = anoncredsCredentialRecords.getTags() + + expect(tags).toMatchObject({ + issuerId: 'issuerDid', + schemaIssuerId: 'schemaIssuerDid', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + credentialDefinitionId: 'credDefId', + schemaId: 'schemaId', + credentialId: 'myCredentialId', + credentialRevocationId: 'credentialRevocationId', + linkSecretId: 'linkSecretId', + 'attr::attr1::value': 'value1', + 'attr::attr1::marker': true, + 'attr::attr2::value': 'value2', + 'attr::attr2::marker': true, + methodName: 'methodName', + }) + }) +}) diff --git a/packages/anoncreds/src/repository/anonCredsCredentialDefinitionRecordMetadataTypes.ts b/packages/anoncreds/src/repository/anonCredsCredentialDefinitionRecordMetadataTypes.ts new file mode 100644 index 0000000000..05806802e4 --- /dev/null +++ b/packages/anoncreds/src/repository/anonCredsCredentialDefinitionRecordMetadataTypes.ts @@ -0,0 +1,11 @@ +import type { Extensible } from '../services/registry/base' + +export enum AnonCredsCredentialDefinitionRecordMetadataKeys { + CredentialDefinitionRegistrationMetadata = '_internal/anonCredsCredentialDefinitionRegistrationMetadata', + CredentialDefinitionMetadata = '_internal/anonCredsCredentialDefinitionMetadata', +} + +export type AnonCredsCredentialDefinitionRecordMetadata = { + [AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionRegistrationMetadata]: Extensible + [AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionMetadata]: Extensible +} diff --git a/packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes.ts b/packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes.ts new file mode 100644 index 0000000000..7a960c2af1 --- /dev/null +++ b/packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes.ts @@ -0,0 +1,11 @@ +import type { Extensible } from '../services/registry/base' + +export enum AnonCredsRevocationRegistryDefinitionRecordMetadataKeys { + RevocationRegistryDefinitionRegistrationMetadata = '_internal/anonCredsRevocationRegistryDefinitionRegistrationMetadata', + RevocationRegistryDefinitionMetadata = '_internal/anonCredsRevocationRegistryDefinitionMetadata', +} + +export type AnonCredsRevocationRegistryDefinitionRecordMetadata = { + [AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.RevocationRegistryDefinitionRegistrationMetadata]: Extensible + [AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.RevocationRegistryDefinitionMetadata]: Extensible +} diff --git a/packages/anoncreds/src/repository/anonCredsSchemaRecordMetadataTypes.ts b/packages/anoncreds/src/repository/anonCredsSchemaRecordMetadataTypes.ts new file mode 100644 index 0000000000..9880a50625 --- /dev/null +++ b/packages/anoncreds/src/repository/anonCredsSchemaRecordMetadataTypes.ts @@ -0,0 +1,11 @@ +import type { Extensible } from '../services/registry/base' + +export enum AnonCredsSchemaRecordMetadataKeys { + SchemaRegistrationMetadata = '_internal/anonCredsSchemaRegistrationMetadata', + SchemaMetadata = '_internal/anonCredsSchemaMetadata', +} + +export type AnonCredsSchemaRecordMetadata = { + [AnonCredsSchemaRecordMetadataKeys.SchemaRegistrationMetadata]: Extensible + [AnonCredsSchemaRecordMetadataKeys.SchemaMetadata]: Extensible +} diff --git a/packages/anoncreds/src/repository/index.ts b/packages/anoncreds/src/repository/index.ts new file mode 100644 index 0000000000..8772b528e7 --- /dev/null +++ b/packages/anoncreds/src/repository/index.ts @@ -0,0 +1,16 @@ +export * from './AnonCredsCredentialRecord' +export * from './AnonCredsCredentialRepository' +export * from './AnonCredsCredentialDefinitionRecord' +export * from './AnonCredsCredentialDefinitionRepository' +export * from './AnonCredsCredentialDefinitionPrivateRecord' +export * from './AnonCredsCredentialDefinitionPrivateRepository' +export * from './AnonCredsKeyCorrectnessProofRecord' +export * from './AnonCredsKeyCorrectnessProofRepository' +export * from './AnonCredsLinkSecretRecord' +export * from './AnonCredsLinkSecretRepository' +export * from './AnonCredsRevocationRegistryDefinitionRecord' +export * from './AnonCredsRevocationRegistryDefinitionRepository' +export * from './AnonCredsRevocationRegistryDefinitionPrivateRecord' +export * from './AnonCredsRevocationRegistryDefinitionPrivateRepository' +export * from './AnonCredsSchemaRecord' +export * from './AnonCredsSchemaRepository' diff --git a/packages/anoncreds/src/services/AnonCredsHolderService.ts b/packages/anoncreds/src/services/AnonCredsHolderService.ts new file mode 100644 index 0000000000..a40291274f --- /dev/null +++ b/packages/anoncreds/src/services/AnonCredsHolderService.ts @@ -0,0 +1,60 @@ +import type { + CreateCredentialRequestOptions, + CreateCredentialRequestReturn, + CreateProofOptions, + GetCredentialOptions, + StoreCredentialOptions, + GetCredentialsForProofRequestOptions, + GetCredentialsForProofRequestReturn, + CreateLinkSecretReturn, + CreateLinkSecretOptions, + GetCredentialsOptions, + CreateW3cPresentationOptions, + LegacyToW3cCredentialOptions, + W3cToLegacyCredentialOptions, +} from './AnonCredsHolderServiceOptions' +import type { AnonCredsCredentialInfo } from '../models' +import type { AnonCredsCredential, AnonCredsProof } from '../models/exchange' +import type { AgentContext, W3cJsonLdVerifiableCredential, W3cJsonLdVerifiablePresentation } from '@credo-ts/core' + +export const AnonCredsHolderServiceSymbol = Symbol('AnonCredsHolderService') + +export interface AnonCredsHolderService { + createLinkSecret(agentContext: AgentContext, options: CreateLinkSecretOptions): Promise + + createProof(agentContext: AgentContext, options: CreateProofOptions): Promise + storeCredential( + agentContext: AgentContext, + options: StoreCredentialOptions, + metadata?: Record + ): Promise + + // TODO: this doesn't actually return the credential, as the indy-sdk doesn't support that + // We could come up with a hack (as we've received the credential at one point), but for + // now I think it's not that much of an issue + getCredential(agentContext: AgentContext, options: GetCredentialOptions): Promise + getCredentials(agentContext: AgentContext, options: GetCredentialsOptions): Promise + + createCredentialRequest( + agentContext: AgentContext, + options: CreateCredentialRequestOptions + ): Promise + + deleteCredential(agentContext: AgentContext, credentialId: string): Promise + getCredentialsForProofRequest( + agentContext: AgentContext, + options: GetCredentialsForProofRequestOptions + ): Promise + + createW3cPresentation( + agentContext: AgentContext, + options: CreateW3cPresentationOptions + ): Promise + + w3cToLegacyCredential(agentContext: AgentContext, options: W3cToLegacyCredentialOptions): Promise + + legacyToW3cCredential( + agentContext: AgentContext, + options: LegacyToW3cCredentialOptions + ): Promise +} diff --git a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts new file mode 100644 index 0000000000..f6775f9f54 --- /dev/null +++ b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts @@ -0,0 +1,133 @@ +import type { AnonCredsCredentialInfo, AnonCredsSelectedCredentials } from '../models' +import type { + AnonCredsCredential, + AnonCredsCredentialOffer, + AnonCredsCredentialRequest, + AnonCredsNonRevokedInterval, + AnonCredsProofRequest, +} from '../models/exchange' +import type { + AnonCredsCredentialDefinition, + AnonCredsRevocationRegistryDefinition, + AnonCredsSchema, +} from '../models/registry' +import type { + AnonCredsSchemas, + AnonCredsCredentialDefinitions, + AnonCredsRevocationRegistries, + CredentialWithRevocationMetadata, +} from '../models/utils' +import type { AnonCredsCredentialRequestMetadata } from '../utils/metadata' +import type { W3cJsonLdVerifiableCredential } from '@credo-ts/core' + +export interface AnonCredsAttributeInfo { + name?: string + names?: string[] +} + +export interface CreateProofOptions { + proofRequest: AnonCredsProofRequest + selectedCredentials: AnonCredsSelectedCredentials + schemas: AnonCredsSchemas + credentialDefinitions: AnonCredsCredentialDefinitions + revocationRegistries: AnonCredsRevocationRegistries + useUnqualifiedIdentifiers?: boolean +} + +export interface StoreCredentialOptions { + credential: W3cJsonLdVerifiableCredential | AnonCredsCredential + credentialRequestMetadata: AnonCredsCredentialRequestMetadata + credentialDefinition: AnonCredsCredentialDefinition + schema: AnonCredsSchema + credentialDefinitionId: string + credentialId?: string + revocationRegistry?: { + id: string + definition: AnonCredsRevocationRegistryDefinition + } +} + +export interface GetCredentialOptions { + id: string + useUnqualifiedIdentifiersIfPresent?: boolean +} + +export interface GetCredentialsOptions { + credentialDefinitionId?: string + schemaId?: string + schemaIssuerId?: string + schemaName?: string + schemaVersion?: string + issuerId?: string + methodName?: string +} + +// TODO: Maybe we can make this a bit more specific? +export type WalletQuery = Record +export interface ReferentWalletQuery { + [referent: string]: WalletQuery +} + +export interface GetCredentialsForProofRequestOptions { + proofRequest: AnonCredsProofRequest + attributeReferent: string + start?: number + limit?: number + extraQuery?: ReferentWalletQuery +} + +export type GetCredentialsForProofRequestReturn = Array<{ + credentialInfo: AnonCredsCredentialInfo + interval?: AnonCredsNonRevokedInterval +}> + +export interface CreateCredentialRequestOptions { + credentialOffer: AnonCredsCredentialOffer + credentialDefinition: AnonCredsCredentialDefinition + linkSecretId?: string + useLegacyProverDid?: boolean +} + +export interface CreateCredentialRequestReturn { + credentialRequest: AnonCredsCredentialRequest + credentialRequestMetadata: AnonCredsCredentialRequestMetadata +} + +export interface CreateLinkSecretOptions { + linkSecretId?: string +} + +export interface CreateLinkSecretReturn { + linkSecretId: string + linkSecretValue?: string +} + +export interface AnonCredsCredentialProve { + entryIndex: number + referent: string + isPredicate: boolean + reveal: boolean +} + +export interface CreateW3cPresentationOptions { + proofRequest: AnonCredsProofRequest + linkSecretId: string + schemas: AnonCredsSchemas + credentialDefinitions: AnonCredsCredentialDefinitions + credentialsProve: AnonCredsCredentialProve[] + credentialsWithRevocationMetadata: CredentialWithRevocationMetadata[] +} + +export interface LegacyToW3cCredentialOptions { + credential: AnonCredsCredential + issuerId: string + processOptions?: { + credentialDefinition: AnonCredsCredentialDefinition + credentialRequestMetadata: AnonCredsCredentialRequestMetadata + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition | undefined + } +} + +export interface W3cToLegacyCredentialOptions { + credential: W3cJsonLdVerifiableCredential +} diff --git a/packages/anoncreds/src/services/AnonCredsIssuerService.ts b/packages/anoncreds/src/services/AnonCredsIssuerService.ts new file mode 100644 index 0000000000..d299d85f95 --- /dev/null +++ b/packages/anoncreds/src/services/AnonCredsIssuerService.ts @@ -0,0 +1,51 @@ +import type { + CreateSchemaOptions, + CreateCredentialDefinitionOptions, + CreateCredentialOfferOptions, + CreateCredentialReturn, + CreateCredentialOptions, + CreateCredentialDefinitionReturn, + CreateRevocationRegistryDefinitionOptions, + CreateRevocationRegistryDefinitionReturn, + CreateRevocationStatusListOptions, + UpdateRevocationStatusListOptions, +} from './AnonCredsIssuerServiceOptions' +import type { AnonCredsCredentialOffer } from '../models/exchange' +import type { AnonCredsRevocationStatusList, AnonCredsSchema } from '../models/registry' +import type { AgentContext } from '@credo-ts/core' + +export const AnonCredsIssuerServiceSymbol = Symbol('AnonCredsIssuerService') + +export interface AnonCredsIssuerService { + createSchema(agentContext: AgentContext, options: CreateSchemaOptions): Promise + + // This should store the private part of the credential definition as in the indy-sdk + // we don't have access to the private part of the credential definition + createCredentialDefinition( + agentContext: AgentContext, + options: CreateCredentialDefinitionOptions, + metadata?: Record + ): Promise + + createRevocationRegistryDefinition( + agentContext: AgentContext, + options: CreateRevocationRegistryDefinitionOptions + ): Promise + + createRevocationStatusList( + agentContext: AgentContext, + options: CreateRevocationStatusListOptions + ): Promise + + updateRevocationStatusList( + agentContext: AgentContext, + options: UpdateRevocationStatusListOptions + ): Promise + + createCredentialOffer( + agentContext: AgentContext, + options: CreateCredentialOfferOptions + ): Promise + + createCredential(agentContext: AgentContext, options: CreateCredentialOptions): Promise +} diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts new file mode 100644 index 0000000000..936ea91af1 --- /dev/null +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -0,0 +1,81 @@ +import type { + AnonCredsCredential, + AnonCredsCredentialOffer, + AnonCredsCredentialRequest, + AnonCredsCredentialValues, +} from '../models/exchange' +import type { + AnonCredsCredentialDefinition, + AnonCredsRevocationRegistryDefinition, + AnonCredsRevocationStatusList, + AnonCredsSchema, +} from '../models/registry' + +export interface CreateSchemaOptions { + issuerId: string + name: string + version: string + attrNames: string[] +} + +export interface CreateCredentialDefinitionOptions { + issuerId: string + tag: string + supportRevocation: boolean + schemaId: string + schema: AnonCredsSchema +} + +export interface CreateRevocationRegistryDefinitionOptions { + issuerId: string + tag: string + credentialDefinitionId: string + credentialDefinition: AnonCredsCredentialDefinition + maximumCredentialNumber: number + tailsDirectoryPath: string +} + +export interface CreateRevocationStatusListOptions { + issuerId: string + revocationRegistryDefinitionId: string + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + tailsFilePath: string +} + +export interface UpdateRevocationStatusListOptions { + revocationStatusList: AnonCredsRevocationStatusList + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revoked?: number[] + issued?: number[] + timestamp?: number + tailsFilePath: string +} + +export interface CreateCredentialOfferOptions { + credentialDefinitionId: string +} + +export interface CreateCredentialOptions { + credentialOffer: AnonCredsCredentialOffer + credentialRequest: AnonCredsCredentialRequest + credentialValues: AnonCredsCredentialValues + revocationRegistryDefinitionId?: string + revocationStatusList?: AnonCredsRevocationStatusList + revocationRegistryIndex?: number +} + +export interface CreateCredentialReturn { + credential: AnonCredsCredential + credentialRevocationId?: string +} + +export interface CreateCredentialDefinitionReturn { + credentialDefinition: AnonCredsCredentialDefinition + credentialDefinitionPrivate?: Record + keyCorrectnessProof?: Record +} + +export interface CreateRevocationRegistryDefinitionReturn { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionPrivate?: Record +} diff --git a/packages/anoncreds/src/services/AnonCredsVerifierService.ts b/packages/anoncreds/src/services/AnonCredsVerifierService.ts new file mode 100644 index 0000000000..cec7b0b237 --- /dev/null +++ b/packages/anoncreds/src/services/AnonCredsVerifierService.ts @@ -0,0 +1,12 @@ +import type { VerifyProofOptions, VerifyW3cPresentationOptions } from './AnonCredsVerifierServiceOptions' +import type { AgentContext } from '@credo-ts/core' + +export const AnonCredsVerifierServiceSymbol = Symbol('AnonCredsVerifierService') + +export interface AnonCredsVerifierService { + // TODO: do we want to extend the return type with more info besides a boolean. + // If the value is false it would be nice to have some extra contexts about why it failed + verifyProof(agentContext: AgentContext, options: VerifyProofOptions): Promise + + verifyW3cPresentation(agentContext: AgentContext, options: VerifyW3cPresentationOptions): Promise +} diff --git a/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts new file mode 100644 index 0000000000..6b13e93a5e --- /dev/null +++ b/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts @@ -0,0 +1,36 @@ +import type { AnonCredsProof, AnonCredsProofRequest } from '../models/exchange' +import type { AnonCredsRevocationStatusList, AnonCredsRevocationRegistryDefinition } from '../models/registry' +import type { + AnonCredsSchemas, + AnonCredsCredentialDefinitions, + CredentialWithRevocationMetadata, +} from '../models/utils' +import type { W3cJsonLdVerifiablePresentation } from '@credo-ts/core' + +export interface VerifyProofOptions { + proofRequest: AnonCredsProofRequest + proof: AnonCredsProof + schemas: AnonCredsSchemas + credentialDefinitions: AnonCredsCredentialDefinitions + revocationRegistries: { + [revocationRegistryDefinitionId: string]: { + definition: AnonCredsRevocationRegistryDefinition + // NOTE: the verifier only needs the accumulator, not the whole state of the revocation registry + // Requiring this to be the full state means we need to retrieve the full state from the ledger + // as a verifier. This is just following the data models from the AnonCreds spec, but for e.g. indy + // this means we need to retrieve _ALL_ deltas from the ledger to verify a proof. While currently we + // only need to fetch the registry. + revocationStatusLists: { + [timestamp: number]: AnonCredsRevocationStatusList + } + } + } +} + +export interface VerifyW3cPresentationOptions { + proofRequest: AnonCredsProofRequest + presentation: W3cJsonLdVerifiablePresentation + schemas: AnonCredsSchemas + credentialDefinitions: AnonCredsCredentialDefinitions + credentialsWithRevocationMetadata: CredentialWithRevocationMetadata[] +} diff --git a/packages/anoncreds/src/services/index.ts b/packages/anoncreds/src/services/index.ts new file mode 100644 index 0000000000..419436fde9 --- /dev/null +++ b/packages/anoncreds/src/services/index.ts @@ -0,0 +1,8 @@ +export * from './AnonCredsHolderService' +export * from './AnonCredsHolderServiceOptions' +export * from './AnonCredsIssuerService' +export * from './AnonCredsIssuerServiceOptions' +export * from './registry' +export { TailsFileService, BasicTailsFileService } from './tails' +export * from './AnonCredsVerifierService' +export * from './AnonCredsVerifierServiceOptions' diff --git a/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts new file mode 100644 index 0000000000..f0374127a2 --- /dev/null +++ b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts @@ -0,0 +1,64 @@ +import type { + GetCredentialDefinitionReturn, + RegisterCredentialDefinitionOptions, + RegisterCredentialDefinitionReturn, +} from './CredentialDefinitionOptions' +import type { + GetRevocationRegistryDefinitionReturn, + RegisterRevocationRegistryDefinitionOptions, + RegisterRevocationRegistryDefinitionReturn, +} from './RevocationRegistryDefinitionOptions' +import type { + GetRevocationStatusListReturn, + RegisterRevocationStatusListOptions, + RegisterRevocationStatusListReturn, +} from './RevocationStatusListOptions' +import type { GetSchemaReturn, RegisterSchemaOptions, RegisterSchemaReturn } from './SchemaOptions' +import type { AgentContext } from '@credo-ts/core' + +/** + * @public + */ +export interface AnonCredsRegistry { + /** + * A name to identify the registry. This will be stored as part of the reigstered anoncreds objects to allow querying + * for created objects using a specific registry. Multilpe implementations can use the same name, but they should in that + * case also reference objects on the same networks. + */ + methodName: string + + supportedIdentifier: RegExp + + getSchema(agentContext: AgentContext, schemaId: string): Promise + registerSchema(agentContext: AgentContext, options: RegisterSchemaOptions): Promise + + getCredentialDefinition( + agentContext: AgentContext, + credentialDefinitionId: string + ): Promise + registerCredentialDefinition( + agentContext: AgentContext, + options: RegisterCredentialDefinitionOptions + ): Promise + + getRevocationRegistryDefinition( + agentContext: AgentContext, + revocationRegistryDefinitionId: string + ): Promise + + registerRevocationRegistryDefinition( + agentContext: AgentContext, + options: RegisterRevocationRegistryDefinitionOptions + ): Promise + + getRevocationStatusList( + agentContext: AgentContext, + revocationRegistryId: string, + timestamp: number + ): Promise + + registerRevocationStatusList( + agentContext: AgentContext, + options: RegisterRevocationStatusListOptions + ): Promise +} diff --git a/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts b/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts new file mode 100644 index 0000000000..ef2668bd93 --- /dev/null +++ b/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts @@ -0,0 +1,28 @@ +import type { AnonCredsRegistry } from '.' +import type { AgentContext } from '@credo-ts/core' + +import { injectable } from '@credo-ts/core' + +import { AnonCredsModuleConfig } from '../../AnonCredsModuleConfig' +import { AnonCredsError } from '../../error' + +/** + * @internal + * The AnonCreds registry service manages multiple {@link AnonCredsRegistry} instances + * and returns the correct registry based on a given identifier + */ +@injectable() +export class AnonCredsRegistryService { + public getRegistryForIdentifier(agentContext: AgentContext, identifier: string): AnonCredsRegistry { + const registries = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).registries + + // TODO: should we check if multiple are registered? + const registry = registries.find((registry) => registry.supportedIdentifier.test(identifier)) + + if (!registry) { + throw new AnonCredsError(`No AnonCredsRegistry registered for identifier '${identifier}'`) + } + + return registry + } +} diff --git a/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts b/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts new file mode 100644 index 0000000000..be835da0d6 --- /dev/null +++ b/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts @@ -0,0 +1,52 @@ +import type { + AnonCredsOperationStateAction, + AnonCredsOperationStateFailed, + AnonCredsOperationStateFinished, + AnonCredsOperationStateWait, + AnonCredsResolutionMetadata, + Extensible, +} from './base' +import type { AnonCredsCredentialDefinition } from '../../models/registry' + +export interface GetCredentialDefinitionReturn { + credentialDefinition?: AnonCredsCredentialDefinition + credentialDefinitionId: string + resolutionMetadata: AnonCredsResolutionMetadata + credentialDefinitionMetadata: Extensible +} + +export interface RegisterCredentialDefinitionOptions { + credentialDefinition: AnonCredsCredentialDefinition + options: Extensible +} + +export interface RegisterCredentialDefinitionReturnStateFailed extends AnonCredsOperationStateFailed { + credentialDefinition?: AnonCredsCredentialDefinition + credentialDefinitionId?: string +} + +export interface RegisterCredentialDefinitionReturnStateFinished extends AnonCredsOperationStateFinished { + credentialDefinition: AnonCredsCredentialDefinition + credentialDefinitionId: string +} + +export interface RegisterCredentialDefinitionReturnStateWait extends AnonCredsOperationStateWait { + credentialDefinition?: AnonCredsCredentialDefinition + credentialDefinitionId?: string +} + +export interface RegisterCredentialDefinitionReturnStateAction extends AnonCredsOperationStateAction { + credentialDefinitionId: string + credentialDefinition: AnonCredsCredentialDefinition +} + +export interface RegisterCredentialDefinitionReturn { + jobId?: string + credentialDefinitionState: + | RegisterCredentialDefinitionReturnStateWait + | RegisterCredentialDefinitionReturnStateAction + | RegisterCredentialDefinitionReturnStateFinished + | RegisterCredentialDefinitionReturnStateFailed + credentialDefinitionMetadata: Extensible + registrationMetadata: Extensible +} diff --git a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts new file mode 100644 index 0000000000..fee5653c1e --- /dev/null +++ b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts @@ -0,0 +1,52 @@ +import type { + AnonCredsOperationStateWait, + AnonCredsOperationStateFailed, + AnonCredsOperationStateFinished, + AnonCredsResolutionMetadata, + Extensible, + AnonCredsOperationStateAction, +} from './base' +import type { AnonCredsRevocationRegistryDefinition } from '../../models/registry' + +export interface GetRevocationRegistryDefinitionReturn { + revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId: string + resolutionMetadata: AnonCredsResolutionMetadata + revocationRegistryDefinitionMetadata: Extensible +} + +export interface RegisterRevocationRegistryDefinitionOptions { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + options: Extensible +} + +export interface RegisterRevocationRegistryDefinitionReturnStateAction extends AnonCredsOperationStateAction { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId: string +} + +export interface RegisterRevocationRegistryDefinitionReturnStateFailed extends AnonCredsOperationStateFailed { + revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string +} + +export interface RegisterRevocationRegistryDefinitionReturnStateWait extends AnonCredsOperationStateWait { + revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string +} + +export interface RegisterRevocationRegistryDefinitionReturnStateFinished extends AnonCredsOperationStateFinished { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId: string +} + +export interface RegisterRevocationRegistryDefinitionReturn { + jobId?: string + revocationRegistryDefinitionState: + | RegisterRevocationRegistryDefinitionReturnStateWait + | RegisterRevocationRegistryDefinitionReturnStateAction + | RegisterRevocationRegistryDefinitionReturnStateFailed + | RegisterRevocationRegistryDefinitionReturnStateFinished + revocationRegistryDefinitionMetadata: Extensible + registrationMetadata: Extensible +} diff --git a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts new file mode 100644 index 0000000000..1d7b11a5ef --- /dev/null +++ b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts @@ -0,0 +1,53 @@ +import type { + AnonCredsOperationStateWait, + AnonCredsOperationStateFailed, + AnonCredsOperationStateFinished, + AnonCredsResolutionMetadata, + Extensible, + AnonCredsOperationStateAction, +} from './base' +import type { AnonCredsRevocationStatusList } from '../../models/registry' +import type { Optional } from '@credo-ts/core' + +export interface GetRevocationStatusListReturn { + revocationStatusList?: AnonCredsRevocationStatusList + resolutionMetadata: AnonCredsResolutionMetadata + revocationStatusListMetadata: Extensible +} + +// Timestamp is often calculated by the ledger, otherwise method should just take current time +// Return type does include the timestamp. +export type AnonCredsRevocationStatusListWithoutTimestamp = Omit +export type AnonCredsRevocationStatusListWithOptionalTimestamp = Optional + +export interface RegisterRevocationStatusListOptions { + revocationStatusList: AnonCredsRevocationStatusListWithoutTimestamp + options: Extensible +} + +export interface RegisterRevocationStatusListReturnStateAction extends AnonCredsOperationStateAction { + revocationStatusList: AnonCredsRevocationStatusListWithOptionalTimestamp +} + +export interface RegisterRevocationStatusListReturnStateFailed extends AnonCredsOperationStateFailed { + revocationStatusList?: AnonCredsRevocationStatusListWithOptionalTimestamp +} + +export interface RegisterRevocationStatusListReturnStateWait extends AnonCredsOperationStateWait { + revocationStatusList?: AnonCredsRevocationStatusListWithOptionalTimestamp +} + +export interface RegisterRevocationStatusListReturnStateFinished extends AnonCredsOperationStateFinished { + revocationStatusList: AnonCredsRevocationStatusList +} + +export interface RegisterRevocationStatusListReturn { + jobId?: string + revocationStatusListState: + | RegisterRevocationStatusListReturnStateWait + | RegisterRevocationStatusListReturnStateAction + | RegisterRevocationStatusListReturnStateFailed + | RegisterRevocationStatusListReturnStateFinished + revocationStatusListMetadata: Extensible + registrationMetadata: Extensible +} diff --git a/packages/anoncreds/src/services/registry/SchemaOptions.ts b/packages/anoncreds/src/services/registry/SchemaOptions.ts new file mode 100644 index 0000000000..bd0541d676 --- /dev/null +++ b/packages/anoncreds/src/services/registry/SchemaOptions.ts @@ -0,0 +1,55 @@ +import type { + AnonCredsOperationStateAction, + AnonCredsOperationStateFailed, + AnonCredsOperationStateFinished, + AnonCredsOperationStateWait, + AnonCredsResolutionMetadata, + Extensible, +} from './base' +import type { AnonCredsSchema } from '../../models/registry' + +// Get Schema +export interface GetSchemaReturn { + schema?: AnonCredsSchema + schemaId: string + // Can contain e.g. the ledger transaction request/response + resolutionMetadata: AnonCredsResolutionMetadata + // Can contain additional fields + schemaMetadata: Extensible +} + +export interface RegisterSchemaOptions { + schema: AnonCredsSchema + options: Extensible +} + +export interface RegisterSchemaReturnStateFailed extends AnonCredsOperationStateFailed { + schema?: AnonCredsSchema + schemaId?: string +} + +export interface RegisterSchemaReturnStateFinished extends AnonCredsOperationStateFinished { + schema: AnonCredsSchema + schemaId: string +} + +export interface RegisterSchemaReturnStateAction extends AnonCredsOperationStateAction { + schema: AnonCredsSchema + schemaId: string +} + +export interface RegisterSchemaReturnStateWait extends AnonCredsOperationStateWait { + schema?: AnonCredsSchema + schemaId?: string +} + +export interface RegisterSchemaReturn { + jobId?: string + schemaState: + | RegisterSchemaReturnStateWait + | RegisterSchemaReturnStateAction + | RegisterSchemaReturnStateFinished + | RegisterSchemaReturnStateFailed + schemaMetadata: Extensible + registrationMetadata: Extensible +} diff --git a/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts b/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts new file mode 100644 index 0000000000..24832166f5 --- /dev/null +++ b/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts @@ -0,0 +1,40 @@ +import type { AnonCredsRegistry } from '../AnonCredsRegistry' + +import { getAgentContext } from '../../../../../core/tests/helpers' +import { anoncreds } from '../../../../tests/helpers' +import { AnonCredsModuleConfig } from '../../../AnonCredsModuleConfig' +import { AnonCredsError } from '../../../error' +import { AnonCredsRegistryService } from '../AnonCredsRegistryService' + +const registryOne = { + supportedIdentifier: /a/, +} as AnonCredsRegistry + +const registryTwo = { + supportedIdentifier: /b/, +} as AnonCredsRegistry + +const agentContext = getAgentContext({ + registerInstances: [ + [ + AnonCredsModuleConfig, + new AnonCredsModuleConfig({ + registries: [registryOne, registryTwo], + anoncreds, + }), + ], + ], +}) + +const anonCredsRegistryService = new AnonCredsRegistryService() + +describe('AnonCredsRegistryService', () => { + test('returns the registry for an identifier based on the supportedMethods regex', async () => { + expect(anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'a')).toEqual(registryOne) + expect(anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'b')).toEqual(registryTwo) + }) + + test('throws AnonCredsError if no registry is found for the given identifier', async () => { + expect(() => anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'c')).toThrow(AnonCredsError) + }) +}) diff --git a/packages/anoncreds/src/services/registry/base.ts b/packages/anoncreds/src/services/registry/base.ts new file mode 100644 index 0000000000..48ae451ea0 --- /dev/null +++ b/packages/anoncreds/src/services/registry/base.ts @@ -0,0 +1,24 @@ +export type Extensible = Record + +export interface AnonCredsOperationStateWait { + state: 'wait' +} + +export interface AnonCredsOperationStateAction { + state: 'action' + action: string +} + +export interface AnonCredsOperationStateFinished { + state: 'finished' +} + +export interface AnonCredsOperationStateFailed { + state: 'failed' + reason: string +} + +export interface AnonCredsResolutionMetadata extends Extensible { + error?: 'invalid' | 'notFound' | 'unsupportedAnonCredsMethod' | string + message?: string +} diff --git a/packages/anoncreds/src/services/registry/index.ts b/packages/anoncreds/src/services/registry/index.ts new file mode 100644 index 0000000000..6577018992 --- /dev/null +++ b/packages/anoncreds/src/services/registry/index.ts @@ -0,0 +1,7 @@ +export * from './AnonCredsRegistry' +export * from './CredentialDefinitionOptions' +export * from './SchemaOptions' +export * from './RevocationRegistryDefinitionOptions' +export * from './RevocationStatusListOptions' +export { AnonCredsResolutionMetadata } from './base' +export * from './AnonCredsRegistryService' diff --git a/packages/anoncreds/src/services/tails/BasicTailsFileService.ts b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts new file mode 100644 index 0000000000..a184c5827c --- /dev/null +++ b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts @@ -0,0 +1,88 @@ +import type { TailsFileService } from './TailsFileService' +import type { AnonCredsRevocationRegistryDefinition } from '../../models' +import type { AgentContext, FileSystem } from '@credo-ts/core' + +import { CredoError, InjectionSymbols, TypedArrayEncoder } from '@credo-ts/core' + +export class BasicTailsFileService implements TailsFileService { + private tailsDirectoryPath?: string + + public constructor(options?: { tailsDirectoryPath?: string; tailsServerBaseUrl?: string }) { + this.tailsDirectoryPath = options?.tailsDirectoryPath + } + + public async getTailsBasePath(agentContext: AgentContext) { + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + const basePath = `${this.tailsDirectoryPath ?? fileSystem.cachePath}/anoncreds/tails` + if (!(await fileSystem.exists(basePath))) { + await fileSystem.createDirectory(`${basePath}/file`) + } + return basePath + } + + public async uploadTailsFile( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ): Promise<{ tailsFileUrl: string }> { + throw new CredoError('BasicTailsFileService only supports tails file downloading') + } + + public async getTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ) { + const { revocationRegistryDefinition } = options + const { tailsLocation, tailsHash } = revocationRegistryDefinition.value + + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + + try { + agentContext.config.logger.debug( + `Checking to see if tails file for URL ${revocationRegistryDefinition.value.tailsLocation} has been stored in the FileSystem` + ) + + // hash is used as file identifier + const tailsExists = await this.tailsFileExists(agentContext, tailsHash) + const tailsFilePath = await this.getTailsFilePath(agentContext, tailsHash) + agentContext.config.logger.debug( + `Tails file for ${tailsLocation} ${tailsExists ? 'is stored' : 'is not stored'} at ${tailsFilePath}` + ) + + if (!tailsExists) { + agentContext.config.logger.debug(`Retrieving tails file from URL ${tailsLocation}`) + + // download file and verify hash + await fileSystem.downloadToFile(tailsLocation, tailsFilePath, { + verifyHash: { + algorithm: 'sha256', + hash: TypedArrayEncoder.fromBase58(tailsHash), + }, + }) + agentContext.config.logger.debug(`Saved tails file to FileSystem at path ${tailsFilePath}`) + } + + return { tailsFilePath } + } catch (error) { + agentContext.config.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { + error, + }) + throw error + } + } + + protected async getTailsFilePath(agentContext: AgentContext, tailsHash: string) { + return `${await this.getTailsBasePath(agentContext)}/${tailsHash}` + } + + protected async tailsFileExists(agentContext: AgentContext, tailsHash: string): Promise { + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + const tailsFilePath = await this.getTailsFilePath(agentContext, tailsHash) + return await fileSystem.exists(tailsFilePath) + } +} diff --git a/packages/anoncreds/src/services/tails/TailsFileService.ts b/packages/anoncreds/src/services/tails/TailsFileService.ts new file mode 100644 index 0000000000..b3cb68b241 --- /dev/null +++ b/packages/anoncreds/src/services/tails/TailsFileService.ts @@ -0,0 +1,47 @@ +import type { AnonCredsRevocationRegistryDefinition } from '../../models' +import type { AgentContext } from '@credo-ts/core' + +export interface TailsFileService { + /** + * Retrieve base directory for tail file storage + * + * @param agentContext + */ + getTailsBasePath(agentContext: AgentContext): string | Promise + + /** + * Upload the tails file for a given revocation registry definition. + * + * Optionally, receives revocationRegistryDefinitionId in case the ID is + * known beforehand. + * + * Returns the published tail file URL + * @param agentContext + * @param options + */ + uploadTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string + } + ): Promise<{ tailsFileUrl: string }> + + /** + * Retrieve the tails file for a given revocation registry, downloading it + * from the tailsLocation URL if not present in internal cache + * + * Classes implementing this interface should verify integrity of the downloaded + * file. + * + * @param agentContext + * @param options + */ + getTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string + } + ): Promise<{ tailsFilePath: string }> +} diff --git a/packages/anoncreds/src/services/tails/index.ts b/packages/anoncreds/src/services/tails/index.ts new file mode 100644 index 0000000000..104617b380 --- /dev/null +++ b/packages/anoncreds/src/services/tails/index.ts @@ -0,0 +1,2 @@ +export * from './BasicTailsFileService' +export * from './TailsFileService' diff --git a/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/credentialDefinition.test.ts b/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/credentialDefinition.test.ts new file mode 100644 index 0000000000..65b40bcddb --- /dev/null +++ b/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/credentialDefinition.test.ts @@ -0,0 +1,154 @@ +import type { AnonCredsCredentialDefinition } from '../../../models' + +import { JsonTransformer } from '../../../../../core/src' +import { Agent } from '../../../../../core/src/agent/Agent' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../core/tests' +import { InMemoryAnonCredsRegistry } from '../../../../tests/InMemoryAnonCredsRegistry' +import { AnonCredsCredentialDefinitionRecord } from '../../../repository' +import { AnonCredsCredentialDefinitionRepository } from '../../../repository/AnonCredsCredentialDefinitionRepository' +import * as testModule from '../credentialDefinition' + +const agentConfig = getAgentConfig('AnonCreds Migration - Credential Exchange Record - 0.3.1-0.4.0') +const agentContext = getAgentContext() + +jest.mock('../../../repository/AnonCredsCredentialDefinitionRepository') +const AnonCredsCredentialDefinitionRepositoryMock = + AnonCredsCredentialDefinitionRepository as jest.Mock +const credentialDefinitionRepository = new AnonCredsCredentialDefinitionRepositoryMock() + +const inMemoryAnonCredsRegistry = new InMemoryAnonCredsRegistry({ + existingCredentialDefinitions: { + 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/CLAIM_DEF/104/default': { + schemaId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/SCHEMA/credentialDefinition-name/1.0', + tag: 'default', + type: 'CL', + value: { + primary: { + master_secret: '119999 00192381', + }, + }, + issuerId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH', + }, + }, +}) + +const registryService = { + getRegistryForIdentifier: () => inMemoryAnonCredsRegistry, +} +jest.mock('../../../../../core/src/agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn((injectionSymbol) => + injectionSymbol === AnonCredsCredentialDefinitionRepository ? credentialDefinitionRepository : registryService + ), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.3.1-0.4.0 | AnonCreds Migration | Credential Definition Record', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('migrateAnonCredsCredentialDefinitionRecordToV0_4()', () => { + it('should fetch all records and apply the needed updates', async () => { + const records: AnonCredsCredentialDefinitionRecord[] = [ + getCredentialDefinitionRecord({ + credentialDefinition: { + id: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/CLAIM_DEF/104/default', + schemaId: '104', + tag: 'default', + type: 'CL', + value: { + primary: { + master_secret: '119999 00192381', + }, + }, + ver: '1.0', + }, + }), + ] + + mockFunction(credentialDefinitionRepository.getAll).mockResolvedValue(records) + + await testModule.migrateAnonCredsCredentialDefinitionRecordToV0_4(agent) + + expect(credentialDefinitionRepository.getAll).toHaveBeenCalledTimes(1) + expect(credentialDefinitionRepository.update).toHaveBeenCalledTimes(1) + + const [, credentialDefinitionRecord] = mockFunction(credentialDefinitionRepository.update).mock.calls[0] + expect(credentialDefinitionRecord.toJSON()).toMatchObject({ + credentialDefinitionId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/CLAIM_DEF/104/default', + credentialDefinition: { + schemaId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/SCHEMA/credentialDefinition-name/1.0', + tag: 'default', + type: 'CL', + value: { + primary: { + master_secret: '119999 00192381', + }, + }, + issuerId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH', + }, + }) + }) + + it('should skip records that are already migrated to the 0.4.0 format', async () => { + const records: AnonCredsCredentialDefinitionRecord[] = [ + getCredentialDefinitionRecord({ + credentialDefinitionId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/CLAIM_DEF/104/default', + credentialDefinition: { + schemaId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/SCHEMA/credentialDefinition-name/1.0', + tag: 'default', + type: 'CL', + value: { + primary: { + master_secret: '119999 00192381', + }, + }, + issuerId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH', + }, + }), + ] + + mockFunction(credentialDefinitionRepository.getAll).mockResolvedValue(records) + + await testModule.migrateAnonCredsCredentialDefinitionRecordToV0_4(agent) + + expect(credentialDefinitionRepository.getAll).toHaveBeenCalledTimes(1) + expect(credentialDefinitionRepository.update).toHaveBeenCalledTimes(0) + }) + }) +}) + +function getCredentialDefinitionRecord({ + id, + credentialDefinition, + credentialDefinitionId, +}: { + id?: string + credentialDefinition: testModule.OldCredentialDefinition | AnonCredsCredentialDefinition + credentialDefinitionId?: string +}) { + return JsonTransformer.fromJSON( + { + id: id ?? 'credentialDefinition-record-id', + credentialDefinition, + credentialDefinitionId, + }, + AnonCredsCredentialDefinitionRecord + ) +} diff --git a/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/credentialExchangeRecord.test.ts b/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/credentialExchangeRecord.test.ts new file mode 100644 index 0000000000..58f116efdb --- /dev/null +++ b/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/credentialExchangeRecord.test.ts @@ -0,0 +1,163 @@ +import type { CredentialRecordBinding, CredentialState } from '../../../../../core/src' + +import { CredentialExchangeRecord, JsonTransformer } from '../../../../../core/src' +import { Agent } from '../../../../../core/src/agent/Agent' +import { CredentialRepository } from '../../../../../core/src/modules/credentials/repository/CredentialRepository' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../core/tests' +import { + migrateIndyCredentialMetadataToAnonCredsMetadata, + migrateIndyCredentialTypeToAnonCredsCredential, +} from '../credentialExchangeRecord' +import * as testModule from '../credentialExchangeRecord' + +const agentConfig = getAgentConfig('AnonCreds Migration - Credential Exchange Record - 0.3.1-0.4.0') +const agentContext = getAgentContext() + +jest.mock('../../../../../core/src/modules/credentials/repository/CredentialRepository') +const CredentialRepositoryMock = CredentialRepository as jest.Mock +const credentialRepository = new CredentialRepositoryMock() + +jest.mock('../../../../../core/src/agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn(() => credentialRepository), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.3.1-0.4.0 | AnonCreds Migration | Credential Exchange Record', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + describe('migrateCredentialExchangeRecordToV0_4()', () => { + it('should fetch all records and apply the needed updates ', async () => { + const records: CredentialExchangeRecord[] = [ + getCredentialRecord({ + metadata: { + '_internal/indyCredential': { some: 'value' }, + '_internal/indyRequest': { nonce: 'nonce', master_secret_name: 'ms', master_secret_blinding_data: 'msbd' }, + }, + credentials: [ + { + credentialRecordId: 'credential-id', + credentialRecordType: 'indy', + }, + { + credentialRecordId: 'credential-id2', + credentialRecordType: 'jsonld', + }, + ], + }), + ] + + mockFunction(credentialRepository.getAll).mockResolvedValue(records) + + await testModule.migrateCredentialExchangeRecordToV0_4(agent) + + expect(credentialRepository.getAll).toHaveBeenCalledTimes(1) + expect(credentialRepository.update).toHaveBeenCalledTimes(1) + + const [, credentialRecord] = mockFunction(credentialRepository.update).mock.calls[0] + expect(credentialRecord.toJSON()).toMatchObject({ + metadata: { + '_anoncreds/credential': { some: 'value' }, + '_anoncreds/credentialRequest': { nonce: 'nonce', link_secret_name: 'ms', link_secret_blinding_data: 'msbd' }, + }, + credentials: [ + { + credentialRecordId: 'credential-id', + credentialRecordType: 'anoncreds', + }, + { + credentialRecordId: 'credential-id2', + credentialRecordType: 'jsonld', + }, + ], + }) + }) + }) + + describe('migrateIndyCredentialMetadataToAnonCredsMetadata()', () => { + test('updates indy metadata to anoncreds metadata', () => { + const record = getCredentialRecord({ + metadata: { + '_internal/indyCredential': { some: 'value' }, + '_internal/indyRequest': { nonce: 'nonce', master_secret_name: 'ms', master_secret_blinding_data: 'msbd' }, + }, + }) + + migrateIndyCredentialMetadataToAnonCredsMetadata(agent, record) + + expect(record.toJSON()).toMatchObject({ + metadata: { + '_anoncreds/credential': { some: 'value' }, + '_anoncreds/credentialRequest': { nonce: 'nonce', link_secret_name: 'ms', link_secret_blinding_data: 'msbd' }, + }, + }) + }) + }) + + describe('migrateIndyCredentialTypeToAnonCredsCredential()', () => { + test('updates indy credential record binding to anoncreds binding', () => { + const record = getCredentialRecord({ + credentials: [ + { + credentialRecordId: 'credential-id', + credentialRecordType: 'indy', + }, + { + credentialRecordId: 'credential-id2', + credentialRecordType: 'jsonld', + }, + ], + }) + + migrateIndyCredentialTypeToAnonCredsCredential(agent, record) + + expect(record.toJSON()).toMatchObject({ + credentials: [ + { + credentialRecordId: 'credential-id', + credentialRecordType: 'anoncreds', + }, + { + credentialRecordId: 'credential-id2', + credentialRecordType: 'jsonld', + }, + ], + }) + }) + }) +}) + +function getCredentialRecord({ + id, + metadata, + credentials, + state, +}: { + id?: string + metadata?: Record + credentials?: CredentialRecordBinding[] + state?: CredentialState +}) { + return JsonTransformer.fromJSON( + { + id: id ?? 'credential-id', + metadata, + credentials, + state, + }, + CredentialExchangeRecord + ) +} diff --git a/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/linkSecret.test.ts b/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/linkSecret.test.ts new file mode 100644 index 0000000000..200e81914b --- /dev/null +++ b/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/linkSecret.test.ts @@ -0,0 +1,76 @@ +import { Agent } from '../../../../../core/src/agent/Agent' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../core/tests' +import { AnonCredsLinkSecretRecord } from '../../../repository' +import { AnonCredsLinkSecretRepository } from '../../../repository/AnonCredsLinkSecretRepository' +import * as testModule from '../linkSecret' + +const agentConfig = getAgentConfig('AnonCreds Migration - Link Secret - 0.3.1-0.4.0') +const agentContext = getAgentContext() + +jest.mock('../../../repository/AnonCredsLinkSecretRepository') +const AnonCredsLinkSecretRepositoryMock = AnonCredsLinkSecretRepository as jest.Mock +const linkSecretRepository = new AnonCredsLinkSecretRepositoryMock() + +jest.mock('../../../../../core/src/agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + wallet: { + walletConfig: { + id: 'wallet-id', + }, + }, + dependencyManager: { + resolve: jest.fn(() => linkSecretRepository), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.3.1-0.4.0 | AnonCreds Migration | Link Secret', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('migrateLinkSecretToV0_4()', () => { + test('creates default link secret record based on wallet id if no default link secret exists', async () => { + mockFunction(linkSecretRepository.findDefault).mockResolvedValue(null) + + await testModule.migrateLinkSecretToV0_4(agent) + + expect(linkSecretRepository.findDefault).toHaveBeenCalledTimes(1) + expect(linkSecretRepository.save).toHaveBeenCalledTimes(1) + + const [, linkSecretRecord] = mockFunction(linkSecretRepository.save).mock.calls[0] + expect(linkSecretRecord.toJSON()).toMatchObject({ + linkSecretId: 'wallet-id', + }) + expect(linkSecretRecord.getTags()).toMatchObject({ + isDefault: true, + }) + }) + + test('does not create default link secret record if default link secret record already exists', async () => { + mockFunction(linkSecretRepository.findDefault).mockResolvedValue( + new AnonCredsLinkSecretRecord({ + linkSecretId: 'some-link-secret-id', + }) + ) + + await testModule.migrateLinkSecretToV0_4(agent) + + expect(linkSecretRepository.findDefault).toHaveBeenCalledTimes(1) + expect(linkSecretRepository.update).toHaveBeenCalledTimes(0) + }) + }) +}) diff --git a/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/schema.test.ts b/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/schema.test.ts new file mode 100644 index 0000000000..0c2f2fd46e --- /dev/null +++ b/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/schema.test.ts @@ -0,0 +1,117 @@ +import type { AnonCredsSchema } from '../../../models' + +import { JsonTransformer } from '../../../../../core/src' +import { Agent } from '../../../../../core/src/agent/Agent' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../core/tests' +import { AnonCredsSchemaRecord } from '../../../repository' +import { AnonCredsSchemaRepository } from '../../../repository/AnonCredsSchemaRepository' +import * as testModule from '../schema' + +const agentConfig = getAgentConfig('AnonCreds Migration - Credential Exchange Record - 0.3.1-0.4.0') +const agentContext = getAgentContext() + +jest.mock('../../../repository/AnonCredsSchemaRepository') +const AnonCredsSchemaRepositoryMock = AnonCredsSchemaRepository as jest.Mock +const schemaRepository = new AnonCredsSchemaRepositoryMock() + +jest.mock('../../../../../core/src/agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn(() => schemaRepository), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.3.1-0.4.0 | AnonCreds Migration | Schema Record', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('migrateAnonCredsSchemaRecordToV0_4()', () => { + it('should fetch all records and apply the needed updates', async () => { + const records: AnonCredsSchemaRecord[] = [ + getSchemaRecord({ + schema: { + attrNames: ['name', 'age'], + id: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/SCHEMA/schema-name/1.0', + name: 'schema-name', + seqNo: 1, + version: '1.0', + ver: '1.0', + }, + }), + ] + + mockFunction(schemaRepository.getAll).mockResolvedValue(records) + + await testModule.migrateAnonCredsSchemaRecordToV0_4(agent) + + expect(schemaRepository.getAll).toHaveBeenCalledTimes(1) + expect(schemaRepository.update).toHaveBeenCalledTimes(1) + + const [, schemaRecord] = mockFunction(schemaRepository.update).mock.calls[0] + expect(schemaRecord.toJSON()).toMatchObject({ + schemaId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/SCHEMA/schema-name/1.0', + schema: { + attrNames: ['name', 'age'], + name: 'schema-name', + version: '1.0', + issuerId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH', + }, + }) + }) + + it('should skip records that are already migrated to the 0.4.0 format', async () => { + const records: AnonCredsSchemaRecord[] = [ + getSchemaRecord({ + schema: { + attrNames: ['name', 'age'], + name: 'schema-name', + version: '1.0', + issuerId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH', + }, + schemaId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/SCHEMA/schema-name/1.0', + }), + ] + + mockFunction(schemaRepository.getAll).mockResolvedValue(records) + + await testModule.migrateAnonCredsSchemaRecordToV0_4(agent) + + expect(schemaRepository.getAll).toHaveBeenCalledTimes(1) + expect(schemaRepository.update).toHaveBeenCalledTimes(0) + }) + }) +}) + +function getSchemaRecord({ + id, + schema, + schemaId, +}: { + id?: string + schema: testModule.OldSchema | AnonCredsSchema + schemaId?: string +}) { + return JsonTransformer.fromJSON( + { + id: id ?? 'schema-record-id', + schema, + schemaId, + }, + AnonCredsSchemaRecord + ) +} diff --git a/packages/anoncreds/src/updates/0.3.1-0.4/credentialDefinition.ts b/packages/anoncreds/src/updates/0.3.1-0.4/credentialDefinition.ts new file mode 100644 index 0000000000..1efaa05cae --- /dev/null +++ b/packages/anoncreds/src/updates/0.3.1-0.4/credentialDefinition.ts @@ -0,0 +1,94 @@ +import type { AnonCredsCredentialDefinition } from '../../models' +import type { BaseAgent } from '@credo-ts/core' + +import { CredoError } from '@credo-ts/core' + +import { AnonCredsCredentialDefinitionRepository } from '../../repository' +import { AnonCredsRegistryService } from '../../services/registry/AnonCredsRegistryService' + +/** + * Migrates the {@link AnonCredsCredentialDefinitionRecord} to 0.4 compatible format. It fetches all credential definition records from + * storage and updates the format based on the new ledger agnostic anoncreds models. After a record has been transformed, + * it is updated in storage and the next record will be transformed. + */ +export async function migrateAnonCredsCredentialDefinitionRecordToV0_4(agent: Agent) { + agent.config.logger.info('Migrating AnonCredsCredentialDefinitionRecord records to storage version 0.4') + const credentialDefinitionRepository = agent.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository) + + agent.config.logger.debug(`Fetching all credential definition records from storage`) + const credentialDefinitionRecords = await credentialDefinitionRepository.getAll(agent.context) + + agent.config.logger.debug( + `Found a total of ${credentialDefinitionRecords.length} credential definition records to update.` + ) + + for (const credentialDefinitionRecord of credentialDefinitionRecords) { + const oldCredentialDefinition = + credentialDefinitionRecord.credentialDefinition as unknown as OldCredentialDefinition + + // If askar migration script is ran, it could be that the credential definition record is already in 0.4 format + if (oldCredentialDefinition.id === undefined) { + agent.config.logger.info( + `Credential definition record with id ${credentialDefinitionRecord.id} and credential definition id ${credentialDefinitionRecord.credentialDefinitionId} is already in storage version 0.4 format. Probably due to Indy SDK to Askar migration. Skipping...` + ) + continue + } + + agent.config.logger.debug( + `Migrating anoncreds credential definition record with id ${credentialDefinitionRecord.id} and credential definition id ${oldCredentialDefinition.id} to storage version 0.4` + ) + + // the schemaId is actually the ledger seqNo. We'll have to fetch the schema from the ledger to get the schemaId + // However, we can't just fetch the schema by it's seqNo, so we'll actually fetch the credential definition, + // which will contain the valid schemaId + const registryService = agent.dependencyManager.resolve(AnonCredsRegistryService) + const registry = registryService.getRegistryForIdentifier(agent.context, oldCredentialDefinition.id) + agent.config.logger.debug( + `Using registry with supportedIdentifier ${registry.supportedIdentifier} to resolve credential definition` + ) + + const { credentialDefinition } = await registry.getCredentialDefinition(agent.context, oldCredentialDefinition.id) + if (!credentialDefinition) { + agent.config.logger.error( + `Could not resolve credential definition with id ${oldCredentialDefinition.id} from ledger` + ) + throw new CredoError(`Unable to resolve credential definition ${oldCredentialDefinition.id}`) + } + + agent.config.logger.debug(`Resolved credential definition with id ${oldCredentialDefinition.id} from ledger`, { + credentialDefinition, + }) + + const newCredentialDefinition = { + // Use the schemaId from the resolved credential definition so we get the qualified identifier + schemaId: credentialDefinition.schemaId, + tag: oldCredentialDefinition.tag, + type: oldCredentialDefinition.type, + value: oldCredentialDefinition.value, + issuerId: oldCredentialDefinition.id.split('/')[0], + } satisfies AnonCredsCredentialDefinition + + credentialDefinitionRecord.credentialDefinition = newCredentialDefinition + credentialDefinitionRecord.credentialDefinitionId = oldCredentialDefinition.id + credentialDefinitionRecord.methodName = 'indy' + + // Save updated credentialDefinition record + await credentialDefinitionRepository.update(agent.context, credentialDefinitionRecord) + + agent.config.logger.debug( + `Successfully migrated credential definition record with id ${credentialDefinitionRecord.id} to storage version 0.4` + ) + } +} + +export interface OldCredentialDefinition { + id: string + schemaId: string + type: 'CL' + tag: string + value: { + primary: Record + revocation?: unknown | undefined + } + ver: string +} diff --git a/packages/anoncreds/src/updates/0.3.1-0.4/credentialExchangeRecord.ts b/packages/anoncreds/src/updates/0.3.1-0.4/credentialExchangeRecord.ts new file mode 100644 index 0000000000..224a5d5ea3 --- /dev/null +++ b/packages/anoncreds/src/updates/0.3.1-0.4/credentialExchangeRecord.ts @@ -0,0 +1,151 @@ +import type { BaseAgent, CredentialExchangeRecord } from '@credo-ts/core' + +import { CredentialRepository } from '@credo-ts/core' + +/** + * Migrates the {@link CredentialExchangeRecord} to 0.4 compatible format. It fetches all credential exchange records from + * storage and applies the needed updates to the records. After a record has been transformed, it is updated + * in storage and the next record will be transformed. + * + * The following transformations are applied: + * - {@link migrateIndyCredentialMetadataToAnonCredsMetadata} + * - {@link migrateIndyCredentialTypeToAnonCredsCredential} + */ +export async function migrateCredentialExchangeRecordToV0_4(agent: Agent) { + agent.config.logger.info('Migrating credential exchange records to storage version 0.4') + const credentialRepository = agent.dependencyManager.resolve(CredentialRepository) + + agent.config.logger.debug(`Fetching all credential records from storage`) + const credentialRecords = await credentialRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${credentialRecords.length} credential exchange records to update.`) + for (const credentialRecord of credentialRecords) { + agent.config.logger.debug( + `Migrating credential exchange record with id ${credentialRecord.id} to storage version 0.4` + ) + + migrateIndyCredentialTypeToAnonCredsCredential(agent, credentialRecord) + migrateIndyCredentialMetadataToAnonCredsMetadata(agent, credentialRecord) + + // Save updated record + await credentialRepository.update(agent.context, credentialRecord) + + agent.config.logger.debug( + `Successfully migrated credential exchange record with id ${credentialRecord.id} to storage version 0.4` + ) + } +} + +/** + * Migrates the indy credential record binding to anoncreds credential record binding. + * + * The following 0.3.1 credential record structure (unrelated keys omitted): + * + * ```json + * { + * "credentials": [ + * { + * "credentialRecordId": "credential-id", + * "credentialRecordType": "indy" + * }, + * { + * "credentialRecordId": "credential-id2", + * "credentialRecordType": "jsonld" + * } + * ] + * } + * ``` + * + * Wil be tranformed into the following 0.4 credential record structure (unrelated keys omitted): + * ```json + * { + * "credentials": [ + * { + * "credentialRecordId": "credential-id", + * "credentialRecordType": "anoncreds" + * }, + * { + * "credentialRecordId": "credential-id2", + * "credentialRecordType": "jsonld" + * } + * ] + * } + * ``` + */ +export function migrateIndyCredentialTypeToAnonCredsCredential( + agent: Agent, + credentialRecord: CredentialExchangeRecord +) { + agent.config.logger.debug( + `Migrating credential record with id ${credentialRecord.id} to anoncreds credential binding for version 0.4` + ) + + const INDY_CREDENTIAL_RECORD_TYPE = 'indy' + const ANONCREDS_CREDENTIAL_RECORD_TYPE = 'anoncreds' + + for (const credential of credentialRecord.credentials) { + if (credential.credentialRecordType === INDY_CREDENTIAL_RECORD_TYPE) { + agent.config.logger.debug(`Updating credential binding ${credential.credentialRecordId} to anoncreds type`) + credential.credentialRecordType = ANONCREDS_CREDENTIAL_RECORD_TYPE + } + } + + agent.config.logger.debug( + `Successfully migrated credential record with id ${credentialRecord.id} to anoncreds credential binding for version 0.4` + ) +} + +/** + * Migrates the indy credential metadata type to anoncreds credential metadata type. + * + * The following 0.3.1 credential metadata structure (unrelated keys omitted): + * + * ```json + * { + * "_internal/indyRequest": {} + * "_internal/indyCredential": {} + * } + * ``` + * + * Wil be tranformed into the following 0.4 credential metadata structure (unrelated keys omitted): + * ```json + * { + * "_anoncreds/credentialRequest": {} + * "_anoncreds/credential": {} + * } + * ``` + */ +export function migrateIndyCredentialMetadataToAnonCredsMetadata( + agent: Agent, + credentialRecord: CredentialExchangeRecord +) { + agent.config.logger.debug( + `Migrating credential record with id ${credentialRecord.id} to anoncreds metadata for version 0.4` + ) + + const indyCredentialRequestMetadataKey = '_internal/indyRequest' + const indyCredentialMetadataKey = '_internal/indyCredential' + + const ANONCREDS_CREDENTIAL_REQUEST_METADATA = '_anoncreds/credentialRequest' + const ANONCREDS_CREDENTIAL_METADATA = '_anoncreds/credential' + + const indyCredentialRequestMetadata = credentialRecord.metadata.get(indyCredentialRequestMetadataKey) + if (indyCredentialRequestMetadata) { + credentialRecord.metadata.set(ANONCREDS_CREDENTIAL_REQUEST_METADATA, { + link_secret_blinding_data: indyCredentialRequestMetadata.master_secret_blinding_data, + link_secret_name: indyCredentialRequestMetadata.master_secret_name, + nonce: indyCredentialRequestMetadata.nonce, + }) + credentialRecord.metadata.delete(indyCredentialRequestMetadataKey) + } + + const indyCredentialMetadata = credentialRecord.metadata.get(indyCredentialMetadataKey) + if (indyCredentialMetadata) { + credentialRecord.metadata.set(ANONCREDS_CREDENTIAL_METADATA, indyCredentialMetadata) + credentialRecord.metadata.delete(indyCredentialMetadataKey) + } + + agent.config.logger.debug( + `Successfully migrated credential record with id ${credentialRecord.id} to anoncreds credential metadata for version 0.4` + ) +} diff --git a/packages/anoncreds/src/updates/0.3.1-0.4/index.ts b/packages/anoncreds/src/updates/0.3.1-0.4/index.ts new file mode 100644 index 0000000000..e7e466ef93 --- /dev/null +++ b/packages/anoncreds/src/updates/0.3.1-0.4/index.ts @@ -0,0 +1,13 @@ +import type { BaseAgent } from '@credo-ts/core' + +import { migrateAnonCredsCredentialDefinitionRecordToV0_4 } from './credentialDefinition' +import { migrateCredentialExchangeRecordToV0_4 } from './credentialExchangeRecord' +import { migrateLinkSecretToV0_4 } from './linkSecret' +import { migrateAnonCredsSchemaRecordToV0_4 } from './schema' + +export async function updateAnonCredsModuleV0_3_1ToV0_4(agent: Agent): Promise { + await migrateCredentialExchangeRecordToV0_4(agent) + await migrateLinkSecretToV0_4(agent) + await migrateAnonCredsCredentialDefinitionRecordToV0_4(agent) + await migrateAnonCredsSchemaRecordToV0_4(agent) +} diff --git a/packages/anoncreds/src/updates/0.3.1-0.4/linkSecret.ts b/packages/anoncreds/src/updates/0.3.1-0.4/linkSecret.ts new file mode 100644 index 0000000000..59b15e712e --- /dev/null +++ b/packages/anoncreds/src/updates/0.3.1-0.4/linkSecret.ts @@ -0,0 +1,43 @@ +import type { BaseAgent } from '@credo-ts/core' + +import { AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository } from '../../repository' + +/** + * Creates an {@link AnonCredsLinkSecretRecord} based on the wallet id. If an {@link AnonCredsLinkSecretRecord} + * already exists (which is the case when upgraded to Askar), no link secret record will be created. + */ +export async function migrateLinkSecretToV0_4(agent: Agent) { + agent.config.logger.info('Migrating link secret to storage version 0.4') + + const linkSecretRepository = agent.dependencyManager.resolve(AnonCredsLinkSecretRepository) + + agent.config.logger.debug(`Fetching default link secret record from storage`) + const defaultLinkSecret = await linkSecretRepository.findDefault(agent.context) + + if (!defaultLinkSecret) { + // If no default link secret record exists, we create one based on the wallet id and set is as default + agent.config.logger.debug(`No default link secret record found. Creating one based on wallet id.`) + + if (!agent.wallet.walletConfig?.id) { + agent.config.logger.error(`Wallet id not found. Cannot create default link secret record. Skipping...`) + return + } + + // We can't store the link secret value. This is not exposed by indy-sdk. + const linkSecret = new AnonCredsLinkSecretRecord({ + linkSecretId: agent.wallet.walletConfig?.id, + }) + linkSecret.setTag('isDefault', true) + + agent.config.logger.debug( + `Saving default link secret record with record id ${linkSecret.id} and link secret id ${linkSecret.linkSecretId} to storage` + ) + await linkSecretRepository.save(agent.context, linkSecret) + } else { + agent.config.logger.debug( + `Default link secret record with record id ${defaultLinkSecret.id} and link secret id ${defaultLinkSecret.linkSecretId} found. Skipping...` + ) + } + + agent.config.logger.debug(`Successfully migrated link secret to version 0.4`) +} diff --git a/packages/anoncreds/src/updates/0.3.1-0.4/schema.ts b/packages/anoncreds/src/updates/0.3.1-0.4/schema.ts new file mode 100644 index 0000000000..368649911d --- /dev/null +++ b/packages/anoncreds/src/updates/0.3.1-0.4/schema.ts @@ -0,0 +1,63 @@ +import type { AnonCredsSchema } from '../../models' +import type { BaseAgent } from '@credo-ts/core' + +import { AnonCredsSchemaRepository } from '../../repository' + +/** + * Migrates the {@link AnonCredsSchemaRecord} to 0.4 compatible format. It fetches all schema records from + * storage and updates the format based on the new ledger agnostic anoncreds models. After a record has been transformed, + * it is updated in storage and the next record will be transformed. + */ +export async function migrateAnonCredsSchemaRecordToV0_4(agent: Agent) { + agent.config.logger.info('Migrating AnonCredsSchemaRecord records to storage version 0.4') + const schemaRepository = agent.dependencyManager.resolve(AnonCredsSchemaRepository) + + agent.config.logger.debug(`Fetching all schema records from storage`) + const schemaRecords = await schemaRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${schemaRecords.length} schema records to update.`) + for (const schemaRecord of schemaRecords) { + const oldSchema = schemaRecord.schema as unknown as OldSchema + + // If askar migration script is ran, it could be that the credential definition record is already in 0.4 format + if (oldSchema.id === undefined) { + agent.config.logger.info( + `Schema record with id ${schemaRecord.id} and schema id ${schemaRecord.schemaId} is already in storage version 0.4 format. Probably due to Indy SDK to Askar migration. Skipping...` + ) + continue + } + + agent.config.logger.debug( + `Migrating anoncreds schema record with id ${schemaRecord.id} and schema id ${oldSchema.id} to storage version 0.4` + ) + + const newSchema = { + attrNames: oldSchema.attrNames, + name: oldSchema.name, + version: oldSchema.version, + issuerId: oldSchema.id.split('/')[0], + } satisfies AnonCredsSchema + + schemaRecord.schema = newSchema + schemaRecord.schemaId = oldSchema.id + schemaRecord.methodName = 'indy' + + // schemaIssuerDid was set as tag, but is now replaced by issuerId. It was also always set + // to the value `did` as it incorrectly parsed the schemaId. + schemaRecord.setTag('schemaIssuerDid', undefined) + + // Save updated schema record + await schemaRepository.update(agent.context, schemaRecord) + + agent.config.logger.debug(`Successfully migrated schema record with id ${schemaRecord.id} to storage version 0.4`) + } +} + +export interface OldSchema { + id: string + name: string + version: string + attrNames: string[] + seqNo: number + ver: string +} diff --git a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts new file mode 100644 index 0000000000..1a8afa526d --- /dev/null +++ b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts @@ -0,0 +1,433 @@ +import type { DidRepository, Wallet } from '@credo-ts/core' + +import { + CredentialState, + Agent, + CacheModuleConfig, + CredentialExchangeRecord, + CredentialRole, + CredoError, + DidResolverService, + DidsModuleConfig, + EventEmitter, + InjectionSymbols, + SignatureSuiteToken, + W3cCredentialRepository, + W3cCredentialsModuleConfig, + CredentialRepository, +} from '@credo-ts/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' +import { agentDependencies, getAgentConfig, getAgentContext, mockFunction, testLogger } from '../../../../../core/tests' +import { InMemoryAnonCredsRegistry } from '../../../../tests/InMemoryAnonCredsRegistry' +import { AnonCredsModuleConfig } from '../../../AnonCredsModuleConfig' +import { AnonCredsRsHolderService } from '../../../anoncreds-rs' +import { AnonCredsCredentialRecord } from '../../../repository' +import { AnonCredsHolderServiceSymbol, AnonCredsRegistryService } from '../../../services' +import { getUnQualifiedDidIndyDid, getQualifiedDidIndyDid, isUnqualifiedIndyDid } from '../../../utils/indyIdentifiers' +import * as testModule from '../anonCredsCredentialRecord' + +import { anoncreds } from './../../../../tests/helpers' + +const agentConfig = getAgentConfig('Migration AnonCreds Credential Records 0.4-0.5') +const registry = new InMemoryAnonCredsRegistry() +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + anoncreds, + registries: [registry], +}) + +const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123') } as Wallet + +const stop = new Subject() +const eventEmitter = new EventEmitter(agentDependencies, stop) + +const w3cRepo = { + save: jest.fn(), + update: jest.fn(), +} + +const credentialExchangeRepo = { + findByQuery: jest.fn(), + update: jest.fn(), +} + +const inMemoryLruCache = { + get: jest.fn(), + set: jest.fn(), + clear: jest.fn(), + remove: jest.fn(), +} + +const cacheModuleConfig = new CacheModuleConfig({ + cache: inMemoryLruCache, +}) + +const inMemoryStorageService = new InMemoryStorageService() + +const agentContext = getAgentContext({ + registerInstances: [ + [CacheModuleConfig, cacheModuleConfig], + [EventEmitter, eventEmitter], + [W3cCredentialRepository, w3cRepo], + [CredentialRepository, credentialExchangeRepo], + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, new agentDependencies.FileSystem()], + [InjectionSymbols.StorageService, inMemoryStorageService], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [DidResolverService, new DidResolverService(testLogger, new DidsModuleConfig(), {} as unknown as DidRepository)], + [InjectionSymbols.Logger, testLogger], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + [AnonCredsHolderServiceSymbol, new AnonCredsRsHolderService()], + [SignatureSuiteToken, 'default'], + ], + agentConfig, + wallet, +}) + +const anonCredsRepo = { + getAll: jest.fn(), + delete: jest.fn(), +} + +jest.mock('../../../../../core/src/agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolve: jest.fn((repo: any) => { + if (repo.prototype.constructor.name === 'AnonCredsCredentialRepository') { + return anonCredsRepo + } + throw new Error(`Couldn't resolve dependency`) + }), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.4-0.5 | AnonCredsRecord', () => { + let agent: Agent + + describe('migrateW3cCredentialRecordToV0_5()', () => { + beforeEach(() => { + anonCredsRepo.delete.mockClear() + anonCredsRepo.getAll.mockClear() + credentialExchangeRepo.findByQuery.mockClear() + credentialExchangeRepo.update.mockClear() + w3cRepo.save.mockClear() + w3cRepo.update.mockClear() + inMemoryLruCache.clear.mockClear() + inMemoryLruCache.get.mockClear() + inMemoryLruCache.set.mockClear() + inMemoryLruCache.remove.mockClear() + + agent = new AgentMock() + }) + + it('credential with cheqd identifier', async () => { + await testMigration(agent, { + issuerId: 'did:cheqd:mainnet:7BPMqYgYLQni258J8JPS8L', + schemaIssuerId: 'did:cheqd:mainnet:7BPMqYgYLQni258J8JPS8K', + schemaId: 'did:cheqd:mainnet:7BPMqYgYLQni258J8JPS8K/resources/6259d357-eeb1-4b98-8bee-12a8390d3497', + }) + }) + + it('credential with did:indy (sovrin) identifier', async () => { + await testMigration(agent, { + issuerId: 'did:indy:sovrin:7Tqg6BwSSWapxgUDm9KKgg', + schemaIssuerId: 'did:indy:sovrin:7Tqg6BwSSWapxgUDm9KKgf', + schemaId: 'did:indy:sovrin:LjgpST2rjsoxYegQDRm7EL/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', + indyNamespace: 'sovrin', + }) + }) + + it('revocable credential with did:indy (sovrin) identifier', async () => { + await testMigration(agent, { + issuerId: 'did:indy:sovrin:7Tqg6BwSSWapxgUDm9KKgg', + schemaIssuerId: 'did:indy:sovrin:7Tqg6BwSSWapxgUDm9KKgf', + schemaId: 'did:indy:sovrin:LjgpST2rjsoxYegQDRm7EL/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', + indyNamespace: 'sovrin', + revocable: true, + }) + }) + + it('credential with unqualified did:indy (bcovrin:test) identifiers', async () => { + await testMigration(agent, { + issuerId: getUnQualifiedDidIndyDid('did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH'), + schemaIssuerId: getUnQualifiedDidIndyDid('did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjG'), + schemaId: getUnQualifiedDidIndyDid( + 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjG/anoncreds/v0/SCHEMA/Employee Credential/1.0.0' + ), + indyNamespace: 'bcovrin:test', + }) + }) + + it('revocable credential with unqualified did:indy (bcovrin:test) identifiers', async () => { + await testMigration(agent, { + issuerId: getUnQualifiedDidIndyDid('did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH'), + schemaIssuerId: getUnQualifiedDidIndyDid('did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjG'), + schemaId: getUnQualifiedDidIndyDid( + 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjG/anoncreds/v0/SCHEMA/Employee Credential/1.0.0' + ), + indyNamespace: 'bcovrin:test', + revocable: true, + }) + }) + + it('credential with cached unqualified did:indy (bcovrin:test) identifiers', async () => { + inMemoryLruCache.get.mockReturnValueOnce({ indyNamespace: 'bcovrin:test' }) + + await testMigration(agent, { + issuerId: getUnQualifiedDidIndyDid('did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH'), + schemaIssuerId: getUnQualifiedDidIndyDid('did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjG'), + schemaId: getUnQualifiedDidIndyDid( + 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjG/anoncreds/v0/SCHEMA/Employee Credential/1.0.0' + ), + indyNamespace: 'bcovrin:test', + shouldBeInCache: 'indy', + }) + }) + + it('credential with cached unqualified did:sov identifiers', async () => { + inMemoryLruCache.get.mockReturnValueOnce(null).mockReturnValueOnce({ indyNamespace: 'sov' }) + + await testMigration(agent, { + issuerId: 'SDqTzbVuCowusqGBNbNDjH', + schemaIssuerId: 'SDqTzbVuCowusqGBNbNDjG', + schemaId: 'SDqTzbVuCowusqGBNbNDjG:2:Employee Credential:1.0.0', + indyNamespace: 'sov', + shouldBeInCache: 'sov', + }) + }) + }) +}) + +async function testMigration( + agent: Agent, + options: { + issuerId: string + schemaIssuerId: string + schemaId: string + indyNamespace?: string + shouldBeInCache?: 'indy' | 'sov' + revocable?: boolean + } +) { + const { issuerId, schemaIssuerId, schemaId, indyNamespace, revocable } = options + + const registry = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, issuerId) + + const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition: { + schemaId: indyNamespace ? getQualifiedDidIndyDid(schemaId, indyNamespace) : schemaId, + type: 'CL', + tag: 'Employee Credential', + value: { + primary: { + n: '96580316873365712442732878101936646890604119889300256012760004147648019614357085076364923021085826868139621573684543249964678348356482485140527957732786530916400278400000660594438781319168272211306232441102713960203075436899295821371799038925693667322779688360706410505540407867607819490853610928774850151039047069357657140257065718659230885391255982730600838743036039711140083284918623906117435892506848479452322000479436955298502839148769930281251929368562720371560260726440893569655811165804238971700685368149522154328822673070750192788830837447670660152195003043802510899143110060139772708073728514051890251226573', + s: '75501169085950126423249157998833414929129208062284812993616444532525695129548804062583842133218092574263501104948737639625833940700883624316320978432322582288936701621781896861131284952998380826417162040016550587340823832731945229065884469806723217100370126833740077464404509861175397581089717495779179489233739975691055780558708056569691296866880514640011052194662545371451908889892210433975411453987754134291774476185207289195701174795140189362641644917865101153841235103322243375241496141786303488408131721122704625842138002478498178520263715598899259097315781832554764315008915688899555385079843761690822607379111', + r: { + age: '77540670431411230038763922593314057361920691860149780021247345546110594816960144474334769978922558437548167814211078474008950463908860798685487527066465227411414311215109347438752200023045503271169383262727401013107872116564443896905324906462332380026785798806953280066387451803949226448584225962096665020244191229063723249351163778395354282347357165322007286709571349618598645876371030907856017571738360851407364231328550357981247798517795822722356010859380461592920151980368953491924564759581591539937752386114770938355831372517555540534219652937595339962248857890418611836415170566769174263185424389504546847791061', + name: '56742811203198572257254422595806148480437594543198516349563027967943211653217799525148065500107783030709376059668814822301811566517601408461597171188532787265942263962719966788682945248064629136273708677025304469521003291988851716171767936997105137959854045442533627185824896706311588434426708666794422548240008058413804660062414897767172901561637004230184962449104905433874433106461860673266368007446282814453132977549811373164579634926487398703746240854572636222768903661936542049761028833196194927339141225860442129881312421875004614067598828714629143133815560576383442835845338263420621113398541139833020926358483', + master_secret: + '47747144545528691003767568337472105276331891233385663931584274593369979405459771996932889017746007711684586508906823242148854224004122637231405489854166589517019033322603946444431305440324935310636815918200611202700765046091022859325187263050783813756813224792976045471735525150004048149843525973339369133943560241544453714388862237336971069786113757093274533177228170822141225802024684552058049687105759446916872700318309370449824235232087307054291066123530983268176971897233515383614938649406180978604188604030816485303101208443369021847829704196934707372786773595567687934642471997496883786836109942269282274646821', + }, + rctxt: + '56624145913410031711009467194049739028044689257231550726399481216874451927585543568732728200991667356553765568186221627220562697315384161695993324560029249334601709666000269987161110370944904361123034293076300325831500797294972192392858769494862446579930065658123775287266632055490150224877768031718759385137678458946705469525103921298013633970637295409365635673547258006414068589487568446936418629870049873056708576696589883095398217681918429160130132727488662842876963800048249179530353781028982129766362865351617486193454223628637074575525915653459208863652607756131262546529918749753409703149380392151341320092701', + z: '48207831484908089113913456529606728278875173243133137568203149862235480864817131176165695429997836542014395411854617371967345903846590322848315574430219622375108777832406077167357765312048126429295008846417923207098159790545077579480434122704652997388986707634157186643373176212809933891460515705299787583898608744041271224726626894030124816906292858431898018633343059228110335652476641836263281987023563730093708908265403781917908475102010080313484277539579578010231066258146934633395220956275733173978548481848026533424513200278825491847270318469226963088243667105115637069262564294713288882078385391140385504192475', + }, + }, + issuerId: indyNamespace ? getQualifiedDidIndyDid(issuerId, indyNamespace) : issuerId, + }, + options: {}, + }) + + if (!credentialDefinitionState.credentialDefinitionId) + throw new CredoError('Registering Credential Definition Failed') + + // We'll use unqualified form in case the inputs are unqualified as well + const credentialDefinitionId = + indyNamespace && isUnqualifiedIndyDid(issuerId) + ? getUnQualifiedDidIndyDid(credentialDefinitionState.credentialDefinitionId) + : credentialDefinitionState.credentialDefinitionId + let revocationRegistryDefinitionId: string | undefined + + if (revocable) { + const { revocationRegistryDefinitionState } = await registry.registerRevocationRegistryDefinition(agentContext, { + revocationRegistryDefinition: { + credDefId: credentialDefinitionState.credentialDefinitionId, + issuerId: indyNamespace ? getQualifiedDidIndyDid(issuerId, indyNamespace) : issuerId, + revocDefType: 'CL_ACCUM', + value: { + publicKeys: { + accumKey: { + z: 'ab81257c-be63-4051-9e21-c7d384412f64', + }, + }, + maxCredNum: 100, + tailsHash: 'ab81257c-be63-4051-9e21-c7d384412f64', + tailsLocation: 'http://localhost:7200/tails', + }, + tag: 'TAG', + }, + options: {}, + }) + if (!revocationRegistryDefinitionState.revocationRegistryDefinitionId) + throw new CredoError('Registering Revocation Registry Definition Failed') + + revocationRegistryDefinitionId = + indyNamespace && isUnqualifiedIndyDid(issuerId) + ? getUnQualifiedDidIndyDid(revocationRegistryDefinitionState.revocationRegistryDefinitionId) + : revocationRegistryDefinitionState.revocationRegistryDefinitionId + } + + const anonCredsRecord = new AnonCredsCredentialRecord({ + credential: { + schema_id: schemaId, + cred_def_id: credentialDefinitionId, + values: { + name: { + raw: 'John', + encoded: '76355713903561865866741292988746191972523015098789458240077478826513114743258', + }, + age: { raw: '25', encoded: '25' }, + }, + signature: { + p_credential: { + m_2: '96181142928573619139692730181044468294945970900261235940698944149443005219418', + a: '95552886901127172841432400616361951122825637102065915900211722444153579891548765880931308692457984326066263506661706967742637168349111737200116541217341739027256190535822337883555402874901690699603230292607481206740216276736875319709356355255797288879451730329296366840213920367976178079664448005608079197649139477441385127107355597906058676699377491628047651331689288017597714832563994968230904723400034478518535411493372596211553797813567090114739752408151368926090849149021350138796163980103411453098000223493524437564062789271302371287568506870484060911412715559140166845310368136412863128732929561146328431066870', + e: '259344723055062059907025491480697571938277889515152306249728583105665800713306759149981690559193987143012367913206299323899696942213235956742929837794489002147266183999965799605813', + v: '8070312275110314663750247899433202850238560575163878956819342967827136399370879736823043902982634515009588016797203155246614708232573921376646871743359587732590693401587607271972304303322060390310307460889523961550612965021232979808509508502354241838342542729225461467834597352210800168107201638861601487760961526713355932504366874557170337152964069325172574449356691055377568302458374147949937789910094307449082152173580675507028369533914480926873196435808261915052547630680304620062203647948590064800546491641963412948122135194369131128319694594446518925913583118382698018169919523769679141724867515604189334120099773703979769794325694804992635522127820413717601811493634024617930397944903746555691677663850240187799372670069559074549528342288602574968520156320273386872799429362106185458798531573424651644586691950218', + }, + r_credential: null, + }, + signature_correctness_proof: { + se: '22707379000451320101568757017184696744124237924783723059712360528872398590682272715197914336834321599243107036831239336605987281577690130807752876870302232265860540101807563741012022740942625464987934377354684266599492895835685698819662114798915664525092894122648542269399563759087759048742378622062870244156257780544523627249100818371255142174054148531811440128609220992508274170196108004985441276737673328642493312249112077836369109453214857237693701603680205115444482751700483514317558743227403858290707747986550689265796031162549838465391957776237071049436590886476581821857234951536091662216488995258175202055258', + c: '86499530658088050169174214946559930902913340880816576251403968391737698128027', + }, + rev_reg: revocable + ? { + accum: + '2 1ECC5AB3496DF286013468F9DC94FA57D2E0CB65809130F49493884DA849D88A 2 20F3F79A24E29B3DF958FA5471B68CAF2FBBAF8E3D3A1F8F17BC5E410242A1BE 2 071C3E27F50B72EB048E530E0A07AC87B5578A63678803D009A9D40E5D3E41B8 2 0E9330E77B1A56DE5C70C8D9B02658CF571F4465EA489A7CEA12CFDA1A311AF5 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + } + : undefined, + witness: revocable + ? { + omega: + '2 024D139F10D86B41FDFE98064B5794D0AFEE6183192A7CC2007803532F38CDB9 2 0AC11C34FDEDCA60FFD23E4FC37C9FAFB29737990D6B7E81190AA8C1BF654034 2 04CCBF871DA8BAB94769B08CBE777E83994F121F8BE1F64D3DE90EC6E2401EA9 2 1539F896A2C98798624E2AE12A0D2941EE898570BE3F0F40E59928008F95C969 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + } + : undefined, + + rev_reg_id: revocable ? revocationRegistryDefinitionId : undefined, + }, + credentialId: 'myCredentialId', + credentialRevocationId: revocable ? '1' : undefined, + linkSecretId: 'linkSecretId', + issuerId, + schemaIssuerId, + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + methodName: 'methodName', + }) + + anonCredsRecord.metadata.set('custom', { + key: 'value', + }) + const records = [anonCredsRecord] + + mockFunction(anonCredsRepo.getAll).mockResolvedValue(records) + + const initialCredentialExchangeRecord = new CredentialExchangeRecord({ + protocolVersion: 'v2', + role: CredentialRole.Holder, + state: CredentialState.Done, + threadId: 'threadId', + credentials: [ + { + credentialRecordId: anonCredsRecord.credentialId, + credentialRecordType: 'anoncreds', + }, + ], + tags: { + credentialDefinitionId, + issuerId, + revocationRegistryId: revocationRegistryDefinitionId, + schemaId, + schemaIssuerId, + }, + }) + + mockFunction(credentialExchangeRepo.findByQuery).mockResolvedValue([initialCredentialExchangeRecord]) + + await testModule.storeAnonCredsInW3cFormatV0_5(agent) + + const unqualifiedDidIndyDid = isUnqualifiedIndyDid(issuerId) + if (unqualifiedDidIndyDid) { + expect(inMemoryLruCache.get).toHaveBeenCalledTimes( + options.shouldBeInCache === 'sov' || !options.shouldBeInCache ? 2 : 1 + ) + expect(inMemoryLruCache.get).toHaveBeenCalledWith( + agent.context, + options.shouldBeInCache === 'sov' || !options.shouldBeInCache + ? 'IndySdkPoolService:' + issuerId + : 'IndyVdrPoolService:' + issuerId + ) + } else { + expect(inMemoryLruCache.get).toHaveBeenCalledTimes(0) + } + + expect(anonCredsRepo.getAll).toHaveBeenCalledTimes(1) + expect(anonCredsRepo.getAll).toHaveBeenCalledWith(agent.context) + expect(w3cRepo.save).toHaveBeenCalledTimes(1) + const [context, w3cCredentialRecord] = mockFunction(w3cRepo.save).mock.calls[0] + expect(context).toMatchObject(agent.context) + expect(w3cCredentialRecord).toMatchObject({ + metadata: expect.objectContaining({ + data: expect.objectContaining({ + custom: { key: 'value' }, + }), + }), + }) + + expect(w3cRepo.update).toHaveBeenCalledTimes(1) + expect(anonCredsRepo.delete).toHaveBeenCalledTimes(1) + expect(credentialExchangeRepo.findByQuery).toHaveBeenCalledTimes(1) + expect(credentialExchangeRepo.findByQuery).toHaveBeenCalledWith(agent.context, { + credentialIds: [anonCredsRecord.credentialId], + }) + expect(credentialExchangeRepo.update).toHaveBeenCalledTimes(1) + expect(credentialExchangeRepo.update).toHaveBeenCalledWith( + agent.context, + expect.objectContaining({ + credentials: [{ credentialRecordType: 'w3c', credentialRecordId: w3cCredentialRecord.id }], + }) + ) + + if (revocable) { + // TODO + expect(credentialExchangeRepo.update).toHaveBeenCalledWith( + agent.context, + expect.objectContaining({ + credentials: [{ credentialRecordType: 'w3c', credentialRecordId: w3cCredentialRecord.id }], + }) + ) + } + + if (unqualifiedDidIndyDid && options.shouldBeInCache) { + expect(inMemoryLruCache.get).toHaveReturnedWith({ indyNamespace }) + } else if (unqualifiedDidIndyDid && !options.shouldBeInCache) { + expect(inMemoryLruCache.get).toHaveBeenCalledTimes(2) + } else { + expect(inMemoryLruCache.get).toHaveBeenCalledTimes(0) + } +} diff --git a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts new file mode 100644 index 0000000000..53073483a7 --- /dev/null +++ b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts @@ -0,0 +1,212 @@ +import type { AnonCredsHolderService } from '../../services' +import type { W3cAnonCredsCredentialMetadata } from '../../utils/metadata' +import type { AgentContext, BaseAgent } from '@credo-ts/core' + +import { + CacheModuleConfig, + CredentialRepository, + CredoError, + W3cCredentialRepository, + W3cCredentialService, +} from '@credo-ts/core' + +import { AnonCredsCredentialRepository, type AnonCredsCredentialRecord } from '../../repository' +import { AnonCredsHolderServiceSymbol } from '../../services' +import { fetchCredentialDefinition } from '../../utils/anonCredsObjects' +import { + getIndyNamespaceFromIndyDid, + getQualifiedDidIndyDid, + getUnqualifiedRevocationRegistryDefinitionId, + isIndyDid, + isUnqualifiedCredentialDefinitionId, + isUnqualifiedIndyDid, + parseIndyRevocationRegistryId, +} from '../../utils/indyIdentifiers' +import { W3cAnonCredsCredentialMetadataKey } from '../../utils/metadata' +import { getW3cRecordAnonCredsTags } from '../../utils/w3cAnonCredsUtils' + +async function getIndyNamespace( + agentContext: AgentContext, + legacyCredentialDefinitionId: string, + legacyIssuerId: string +) { + const cacheModuleConfig = agentContext.dependencyManager.resolve(CacheModuleConfig) + const cache = cacheModuleConfig.cache + + const indyCacheKey = `IndyVdrPoolService:${legacyIssuerId}` + const sovCacheKey = `IndySdkPoolService:${legacyIssuerId}` + + const cachedNymResponse: Record | null = + (await cache.get(agentContext, indyCacheKey)) ?? (await cache.get(agentContext, sovCacheKey)) + + if (!cachedNymResponse?.indyNamespace || typeof cachedNymResponse?.indyNamespace !== 'string') { + const credentialDefinitionReturn = await fetchCredentialDefinition(agentContext, legacyCredentialDefinitionId) + const namespace = credentialDefinitionReturn.indyNamespace + + if (!namespace) { + throw new CredoError( + 'Could not determine the indyNamespace required for storing anoncreds in the new w3c format.' + ) + } + + return namespace + } else { + return cachedNymResponse.indyNamespace + } +} + +async function migrateLegacyToW3cCredential(agentContext: AgentContext, legacyRecord: AnonCredsCredentialRecord) { + const legacyTags = legacyRecord.getTags() + + let indyNamespace: string | undefined + let qualifiedSchemaId: string + let qualifiedSchemaIssuerId: string + let qualifiedCredentialDefinitionId: string + let qualifiedIssuerId: string + let qualifiedRevocationRegistryId: string | undefined + + if ( + !isUnqualifiedCredentialDefinitionId(legacyTags.credentialDefinitionId) && + !isUnqualifiedIndyDid(legacyTags.issuerId) + ) { + if (isIndyDid(legacyTags.issuerId)) { + indyNamespace = getIndyNamespaceFromIndyDid(legacyTags.issuerId) + } + } else { + indyNamespace = await getIndyNamespace(agentContext, legacyTags.credentialDefinitionId, legacyTags.issuerId) + } + + if (indyNamespace) { + qualifiedCredentialDefinitionId = getQualifiedDidIndyDid(legacyTags.credentialDefinitionId, indyNamespace) + qualifiedIssuerId = getQualifiedDidIndyDid(legacyTags.issuerId, indyNamespace) + qualifiedRevocationRegistryId = legacyTags.revocationRegistryId + ? getQualifiedDidIndyDid(legacyTags.revocationRegistryId, indyNamespace) + : undefined + qualifiedSchemaId = getQualifiedDidIndyDid(legacyTags.schemaId, indyNamespace) + qualifiedSchemaIssuerId = getQualifiedDidIndyDid(legacyTags.schemaIssuerId, indyNamespace) + } else { + qualifiedCredentialDefinitionId = legacyTags.credentialDefinitionId + qualifiedIssuerId = legacyTags.issuerId + qualifiedRevocationRegistryId = legacyTags.revocationRegistryId + qualifiedSchemaId = legacyTags.schemaId + qualifiedSchemaIssuerId = legacyTags.schemaIssuerId + } + + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + const w3cJsonLdCredential = await anonCredsHolderService.legacyToW3cCredential(agentContext, { + credential: legacyRecord.credential, + issuerId: qualifiedIssuerId, + }) + + if (Array.isArray(w3cJsonLdCredential.credentialSubject)) { + throw new CredoError('Credential subject must be an object, not an array.') + } + + const anonCredsTags = getW3cRecordAnonCredsTags({ + credentialSubject: w3cJsonLdCredential.credentialSubject, + issuerId: w3cJsonLdCredential.issuerId, + schemaId: qualifiedSchemaId, + schema: { + issuerId: qualifiedSchemaIssuerId, + name: legacyTags.schemaName, + version: legacyTags.schemaVersion, + }, + credentialRevocationId: legacyTags.credentialRevocationId, + revocationRegistryId: qualifiedRevocationRegistryId, + credentialDefinitionId: qualifiedCredentialDefinitionId, + linkSecretId: legacyTags.linkSecretId, + methodName: legacyTags.methodName, + }) + + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const w3cCredentialRecord = await w3cCredentialService.storeCredential(agentContext, { + credential: w3cJsonLdCredential, + }) + + for (const [key, meta] of Object.entries(legacyRecord.metadata.data)) { + w3cCredentialRecord.metadata.set(key, meta) + } + + const anonCredsCredentialMetadata: W3cAnonCredsCredentialMetadata = { + credentialRevocationId: anonCredsTags.anonCredsCredentialRevocationId, + linkSecretId: anonCredsTags.anonCredsLinkSecretId, + methodName: anonCredsTags.anonCredsMethodName, + } + + w3cCredentialRecord.setTags(anonCredsTags) + w3cCredentialRecord.metadata.set(W3cAnonCredsCredentialMetadataKey, anonCredsCredentialMetadata) + + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + await w3cCredentialRepository.update(agentContext, w3cCredentialRecord) + + // Find the credential exchange record bound to this anoncreds credential and update it to point to the newly created w3c record + const credentialExchangeRepository = agentContext.dependencyManager.resolve(CredentialRepository) + const [relatedCredentialExchangeRecord] = await credentialExchangeRepository.findByQuery(agentContext, { + credentialIds: [legacyRecord.credentialId], + }) + + if (relatedCredentialExchangeRecord) { + // Replace the related binding by the new one + const credentialBindingIndex = relatedCredentialExchangeRecord.credentials.findIndex( + (binding) => binding.credentialRecordId === legacyRecord.credentialId + ) + if (credentialBindingIndex !== -1) { + relatedCredentialExchangeRecord.credentials[credentialBindingIndex] = { + credentialRecordType: 'w3c', + credentialRecordId: w3cCredentialRecord.id, + } + + // If using Indy dids, store both qualified/unqualified revRegId forms + // to allow retrieving it from revocation notification service + if (legacyTags.revocationRegistryId && indyNamespace) { + const { credentialDefinitionTag, namespaceIdentifier, revocationRegistryTag, schemaSeqNo } = + parseIndyRevocationRegistryId(legacyTags.revocationRegistryId) + + relatedCredentialExchangeRecord.setTags({ + anonCredsRevocationRegistryId: getQualifiedDidIndyDid(legacyTags.revocationRegistryId, indyNamespace), + anonCredsUnqualifiedRevocationRegistryId: getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ), + }) + } + + await credentialExchangeRepository.update(agentContext, relatedCredentialExchangeRecord) + } + } +} + +/** + * Stores all anoncreds credentials in the new w3c format + */ +export async function storeAnonCredsInW3cFormatV0_5(agent: Agent) { + agent.config.logger.info('Migration of legacy AnonCreds records to the new W3C format version 0.5') + + const anoncredsRepository = agent.dependencyManager.resolve(AnonCredsCredentialRepository) + + agent.config.logger.debug(`Fetching all anoncreds credential records from storage`) + const records = await anoncredsRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${records.length} legacy anonCreds credential records to update.`) + + for (const record of records) { + agent.config.logger.debug( + `Re-saving anonCreds credential record with id ${record.id} in the new w3c format, and deleting the legacy record` + ) + try { + await migrateLegacyToW3cCredential(agent.context, record) + await anoncredsRepository.delete(agent.context, record) + agent.config.logger.debug( + `Successfully migrated w3c credential record with id ${record.id} to storage version 0.5` + ) + } catch (error) { + agent.config.logger.error( + `Failed to migrate w3c credential record with id ${record.id} to storage version 0.5`, + error + ) + } + } +} diff --git a/packages/anoncreds/src/updates/0.4-0.5/index.ts b/packages/anoncreds/src/updates/0.4-0.5/index.ts new file mode 100644 index 0000000000..9f846c7a09 --- /dev/null +++ b/packages/anoncreds/src/updates/0.4-0.5/index.ts @@ -0,0 +1,7 @@ +import type { BaseAgent } from '@credo-ts/core' + +import { storeAnonCredsInW3cFormatV0_5 } from './anonCredsCredentialRecord' + +export async function updateAnonCredsModuleV0_4ToV0_5(agent: Agent): Promise { + await storeAnonCredsInW3cFormatV0_5(agent) +} diff --git a/packages/anoncreds/src/updates/__tests__/0.3.test.ts b/packages/anoncreds/src/updates/__tests__/0.3.test.ts new file mode 100644 index 0000000000..625b69327b --- /dev/null +++ b/packages/anoncreds/src/updates/__tests__/0.3.test.ts @@ -0,0 +1,239 @@ +import { DependencyManager, InjectionSymbols, Agent, UpdateAssistant, utils } from '@credo-ts/core' +import { readFileSync } from 'fs' +import path from 'path' + +import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' +import { RegisteredAskarTestWallet } from '../../../../askar/tests/helpers' +import { agentDependencies, getAskarWalletConfig } from '../../../../core/tests' +import { InMemoryAnonCredsRegistry } from '../../../tests/InMemoryAnonCredsRegistry' +import { anoncreds } from '../../../tests/helpers' +import { AnonCredsModule } from '../../AnonCredsModule' +import { + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsVerifierServiceSymbol, +} from '../../services' + +// Backup date / time is the unique identifier for a backup, needs to be unique for every test +const backupDate = new Date('2023-03-19T22:50:20.522Z') +jest.useFakeTimers().setSystemTime(backupDate) + +describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { + it(`should correctly update the credential exchange records for holders`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + let uuidCounter = 1 + const uuidSpy = jest.spyOn(utils, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const holderRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/holder-anoncreds-2-credentials-0.3.json'), + 'utf8' + ) + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + dependencyManager.registerInstance(AnonCredsIssuerServiceSymbol, {}) + dependencyManager.registerInstance(AnonCredsHolderServiceSymbol, {}) + dependencyManager.registerInstance(AnonCredsVerifierServiceSymbol, {}) + + const agent = new Agent( + { + config: { + label: 'Test Agent', + walletConfig: getAskarWalletConfig('0.3 Update AnonCreds - Holder', { inMemory: false, random: 'static' }), + }, + dependencies: agentDependencies, + modules: { + // We need to include the AnonCredsModule to run the updates + anoncreds: new AnonCredsModule({ + registries: [new InMemoryAnonCredsRegistry()], + anoncreds, + }), + }, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(holderRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.isUpToDate('0.4')).toBe(false) + expect(await updateAssistant.getNeededUpdates('0.4')).toEqual([ + { + fromVersion: '0.3.1', + toVersion: '0.4', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update({ updateToVersion: '0.4' }) + + expect(await updateAssistant.isUpToDate('0.4')).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.4')).toEqual([]) + + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + uuidSpy.mockReset() + }) + + it(`should correctly update the schema and credential definition, and create link secret records for issuers`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + let uuidCounter = 1 + const uuidSpy = jest.spyOn(utils, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const issuerRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/issuer-anoncreds-2-schema-credential-definition-credentials-0.3.json'), + 'utf8' + ) + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + dependencyManager.registerInstance(AnonCredsIssuerServiceSymbol, {}) + dependencyManager.registerInstance(AnonCredsHolderServiceSymbol, {}) + dependencyManager.registerInstance(AnonCredsVerifierServiceSymbol, {}) + + const agent = new Agent( + { + config: { + label: 'Test Agent', + walletConfig: getAskarWalletConfig('0.3 Update AnonCreds - Issuer', { inMemory: false, random: 'static' }), + }, + dependencies: agentDependencies, + modules: { + // We need to include the AnonCredsModule to run the updates + anoncreds: new AnonCredsModule({ + anoncreds, + registries: [ + // We need to be able to resolve the credential definition so we can correctly + new InMemoryAnonCredsRegistry({ + existingCredentialDefinitions: { + 'did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/CLAIM_DEF/728265/TAG': { + schemaId: 'did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/Test Schema/5.0', + type: 'CL', + tag: 'TAG', + value: { + primary: { + n: '92212366077388130017820454980772482128748816766820141476572599854614095851660955000471493059368591899172871902601780138917819366396362308478329294184309858890996528496805316851980442998603067852135492500241351106196875782591605768921500179261268030423733287913264566336690041275292095018304899931956463465418485815424864260174164039300668997079647515281912887296402163314193409758676035183692610399804909476026418386307889108672419432084350222061008099663029495600327790438170442656903258282723208685959709427842790363181237326817713760262728130215152068903053780106153722598661062532884431955981726066921637468626277', + s: '51390585781167888666038495435187170763184923351566453067945476469346756595806461020566734704158200027078692575370502193819960413516290740555746465017482403889478846290536023708403164732218491843776868132606601025003681747438312581577370961516850128243993069117644352618102176047630881347535103984514944899145266563740618494984195198066875837169587608421653434298405108448043919659694417868161307274719186874014050768478275366248108923366328095899343801270111152240906954275776825865228792303252410200003812030838965966766135547588341334766187306815530098180130152857685278588510653805870629396608258594629734808653690', + r: { + master_secret: + '61760181601132349837705650289020474131050187135887129471275844481815813236212130783118399756778708344638568886652376797607377320325668612002653752234977886335615451602379984880071434500085608574636210148262041392898193694256008614118948399335181637372037261847305940365423773073896368876304671332779131812342778821167205383614143093932646167069176375555949468490333033638790088487176980785886865670928635382374747549737473235069853277820515331625504955674335885563904945632728269515723913822149934246500994026445014344664596837782532383727917670585587931554459150014400148586199456993200824425072825041491149065115358', + name: '26931653629593338073547610164492146524581067674323312766422801723649824593245481234130445257275008372300577748467390938672361842062002005882497002927312107798057743381013725196864084323240188855871993429346248168719358184490582297236588103100736704037766893167139178159330117766371806271005063205199099350905918805615139883380562348264630567225617537443345104841331985857206740142310735949731954114795552226430346325242557801443933408634628778255674180716568613268278944764455783252702248656985033565125477742417595184280107251126994232013125430027211388949790163391384834400043466265407965987657397646084753620067162', + age: '12830581846716232289919923091802380953776468678758115385731032778424701987000173171859986490394782070339145726689704906636521504338663443469452098276346339448054923530423862972901740020260863939784049655599141309168321131841197392728580317478651190091260391159517458959241170623799027865010022955890184958710784660242539198197998462816406524943537217991903198815091955260278449922637325465043293444707204707128649276474679898162587929569212222042385297095967670138838722149998051089657830225229881876437390119475653879155105350339634203813849831587911926503279160004910687478611349149984784835918594248713746244647783', + }, + rctxt: + '49138795132156579347604024288478735151511429635862925688354411685205551763173458098934068417340097826251030547752551543780926866551808708614689637810970695962341030571486307177314332719168625736959985286432056963760600243473038903885347227651607234887915878119362501367507071709125019506105125043394599512754034429977523734855754182754166158276654375145600716372728023694171066421047665189687655246390105632221713801254689564447819382923248801463300558408016868673087319876644152902663657524012266707505607127264589517707325298805787788577090696580253467312664036297509153665682462337661380935241888630672980409135218', + z: '60039858321231958911193979301402644724013798961769784342413248136534681852773598059805490735235936787666273383388316713664379360735859198156203333524277752965063504355175962212112042368638829236003950022345790744597825843498279654720032726822247321101635671237626308268641767351508666548662103083107416168951088459343716911392807952489009684909391952363633692353090657169830487309162716174148340837088238136793727262599036868196525437496909391247737814314203700293659965465494637540937762691328712617352605531361117679740841379808332881579693119257467828678864789270752346248637901288389165259844857126172669320275054', + }, + }, + issuerId: 'did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H', + }, + 'did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/CLAIM_DEF/728266/TAG2222': { + schemaId: 'did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/AnotherSchema/5.12', + type: 'CL', + tag: 'TAG2222', + value: { + primary: { + n: '92672464557302826159958381706610232890780336783477671819498833000372263812875113518039840314305532823865676182383425212337361529127538393953888294696727490398569250059424479369124987018050461872589017845243006613503064725987487445193151580781503573638936354603906684667833347097853363102011613363551325414493877438329911648643160153986822516630519571283817404410939266429143144325310144873915523634615108054232698216677813083664813055224968248142239446186423096615162232894052206134565411335121805318762068246410255953720296084525738290155785653879950387998340378428740625858243516259978797729172915362664095388670853', + s: '14126994029068124564262196574803727042317991235159231485233854758856355239996741822278406673337232628669751727662479515044513565209261235580848666630891738643990084502393352476512637677170660741636200618878417433799077613673205726221908822955109963272016538705991333626487531499501561952303907487494079241110050020874027756313402672435051524680914533743665605349121374703526870439925807395782970618162620991315112088226807823652545755186406850860290372739405126488851340032404507898084409367889215777693868794728141508635105180827151292046483128114528214465463152927678575672993454367871685772245405671312263615738674', + r: { + master_secret: + '26619502892062275386286102324954654427871501074061444846499515284182097331967223335934051936866595058991987589854477281430063143491959604612779394547177027208671151839864660333634457188140162529133121090987235146837242477233778516233683361556079466930407338673047472758762971774183683006400366713364299999136369605402942210978218705656266115751492424192940375368169431001551131077280268253962541139755004287154221749191778445668471756569604156885298127934116907544590473960073154419342138695278066485640775060389330807300193554886282756714343171543381166744147102049996134009291163457413551838522312496539196521595692', + age: '66774168049579501626527407565561158517617240253618394664527561632035323705337586053746273530704030779131642005263474574499533256973752287111528352278167213322154697290967283640418150957238004730763043665983334023181560033670971095508406493073727137576662898702804435263291473328275724172150330235410304531103984478435316648590218258879268883696376276091511367418038567366131461327869666106899795056026111553656932251156588986604454718398629113510266779047268855074155849278155719183039926867214509122089958991364786653941718444527779068428328047815224843863247382688134945397530917090461254004883032104714157971400208', + name: '86741028136853574348723360731891313985090403925160846711944073250686426070668157504590860843944722066104971819518996745252253900749842002049747953678564857190954502037349272982356665401492886602390599170831356482930058593126740772109115907363756874709445041702269262783286817223011097284796236690595266721670997137095592005971209969288260603902458413116126663192645410011918509026240763669966445865557485752253073758758805818980495379553872266089697405986128733558878942127067722757597848458411141451957344742184798866278323991155218917859626726262257431337439505881892995617030558234045945209395337282759265659447047', + height: + '36770374391380149834988196363447736840005566975684817148359676140020826239618728242171844190597784913998189387814084045750250841733745991085876913508447852492274928778550079342017977247125002133117906534740912461625630470754160325262589990928728689070499835994964192507742581994860212500470412940278375419595406129858839275229421691764136274418279944569154327695608011398611897919792595046386574831604431186160019573221025054141054966299987505071844770166968281403659227192031982497703452822527121064221030191938050276126255137769594174387744686048921264418842943478063585931864099188919773279516048122408000535396365', + }, + rctxt: + '71013751275772779114070724661642241189015436101735233481124050655632421295506098157799226697991094582116557937036881377025107827713675564553986787961039221830812177248435167562891351835998258222703796710987072076518659197627933717399137564619646356496210281862112127733957003638837075816198062819168957810762822613691407808469027306413697001991060047213339777833838291591976754857934071589843434238025803790508552421154902537027548698271140571140256835534208651964449214890690159171682094521879102663244464066621388809286987873635426369915309596945084951678722672915158041830248278889303704844284468270547467324686757', + z: '90415953543044389740703639345387867170174070770040351538453902580989033567810029650534915348296084212079064544906463014824475317557221991571331212308335167828473551776349999211544897426382305096215624787217055491736755052175278235595298571339706430785816901931128536495808042995635624112114867111658850659510246291844949806165980806847525704751270260070165853067310918184720602183083989806069386048683955313982129380729637761521928446431397104973906871109766946008926113644488012281655650467201044142029022180536946134328567554182760495139058952910079169456941591963348364521142012653606596379566245637724637892435425', + }, + revocation: { + g: '1 1864FF219549D1BC1E492955305FC5EED27C114580F206532D2F5D983A1DD3BD 1 0414758D7B6B254A9CA81E1084721A97CA312497C21BB9B16096636C59F9D105 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + g_dash: + '1 2327DA248E721E3935D81C5579DD3707882FFB962B518D37FB1112D96CC63611 1 164989452135CF5D840A20EE354DBF26BEEC74DE7FD53672E55224BEE0228128 1 0634D5E85C210319BFD2535AFD8F7F79590B2F5CC61AF794218CC50B43FBB8C6 1 0A63F1C0FC2C4540156C7A2E2A2DF1DDF99879C25B4F622933707DD6074A0F1B 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + h: '1 0A031B1932CDFEE76C448CA0B13A7DDC81615036DA17B81DB2E5DFC7D1F6CD6F 1 06F46C9CC7D32A11C7D2A308D4C71BEE42B3BD9DD54141284D92D64D3AC2CE04 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + h0: '1 1C88CA353EF878B74E7F515C88E2CBF11FDC3047E3C5057B34ECC2635B4F8FA5 1 1D645261FBC6164EC493BB700B5D8D5C8BF876FD9BA034B107753C79A53B0321 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + h1: '1 16AC82FE7769689173EABA532E7A489DF87F81AE891C1FDA90FE9813F6761D71 1 147E45451C76CD3A9B0649B12E27EA0BF4E85E632D1B2BEC3EC9FFFA51780ACE 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + h2: '1 2522C4FAA35392EE9B35DAC9CD8E270364598A5ED019CB34695E9C01D43C16DC 1 21D353FB299C9E39C976055BF4555198C63F912DBE3471E930185EF5A20470E5 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + htilde: + '1 24D87DBC6283534AE2AA38C45E52D83CC1E70BD589C813F412CC68563F52A2CA 1 05189BC1AAEE8E2A6CB92F65A8C0A18E4125EE61E5CEF1809EF68B388844D1B1 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + h_cap: + '1 1E3272ABDFD9BF05DB5A7667335A48B9026C9EA2C8DB9FA6E59323BBEB955FE2 1 031BD12497C5BBD68BEA2D0D41713CDFFDCBE462D603C54E9CA5F50DE792E1AB 1 05A917EBAA7D4B321E34F37ADC0C3212CE297E67C7D7FEC4E28AD4CE863B7516 1 16780B2C5BF22F7868BF7F442987AF1382F6465A581F6824245EFB90D4BB8B62 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + u: '1 1F654067166C73E14C4600C2349F0756763653A0B66F8872D99F9642F3BD2013 1 24B074FFB3EE1E5E7A17A06F4BCB4082478224BD4711619286266B59E3110777 1 001B07BEE5A1E36C0BBC31E56E039B39BB0A1BA2F491C2F674EC5CB89150FC2F 1 0F4F1E71A11EB1215DE5A081B7651E1E22C30FCCC5566E13F0A8062DB67B9E32 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + pk: '1 0A165BF9A5546F44298356622C58CA29D2C8D194402CAFCAF5944BE65239474E 1 24BA0620893059732B89897F601F37EF92F9F29B4526E094DA9DC612EB5A90CD 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + y: '1 020240A177435C7D5B1DBDB78A5F0A34A353447991E670BA09E69CCD03FA6800 1 1501D3C784703A097EDDE368B27B85229030C2942C4874CB913C7AAB8C3EF61A 1 109DB12EF355D8A477E353970300E8C0AC2E48793D3DC13416BFF75145BAD753 1 079C6F242737A5D97AC34CDE4FDE4BEC057A399E73E4EF87E7024048163A005F 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + }, + }, + issuerId: 'did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H', + }, + }, + }), + ], + }), + }, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(issuerRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.isUpToDate('0.4')).toBe(false) + expect(await updateAssistant.getNeededUpdates('0.4')).toEqual([ + { + fromVersion: '0.3.1', + toVersion: '0.4', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update({ updateToVersion: '0.4' }) + + expect(await updateAssistant.isUpToDate('0.4')).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.4')).toEqual([]) + + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + uuidSpy.mockReset() + }) +}) diff --git a/packages/anoncreds/src/updates/__tests__/0.4.test.ts b/packages/anoncreds/src/updates/__tests__/0.4.test.ts new file mode 100644 index 0000000000..63015c8d0a --- /dev/null +++ b/packages/anoncreds/src/updates/__tests__/0.4.test.ts @@ -0,0 +1,122 @@ +import { + DependencyManager, + InjectionSymbols, + Agent, + UpdateAssistant, + CacheModule, + InMemoryLruCache, + W3cCredentialRecord, +} from '@credo-ts/core' +import { readFileSync } from 'fs' +import path from 'path' + +import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' +import { RegisteredAskarTestWallet } from '../../../../askar/tests/helpers' +import { agentDependencies, getAskarWalletConfig } from '../../../../core/tests' +import { InMemoryAnonCredsRegistry } from '../../../tests/InMemoryAnonCredsRegistry' +import { anoncreds } from '../../../tests/helpers' +import { AnonCredsModule } from '../../AnonCredsModule' +import { + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsVerifierServiceSymbol, +} from '../../services' + +// Backup date / time is the unique identifier for a backup, needs to be unique for every test +const backupDate = new Date('2024-02-28T22:50:20.522Z') +jest.useFakeTimers().setSystemTime(backupDate) + +// We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. +let uuidCounter = 1 +jest.mock('../../../../core/src/utils/uuid', () => { + return { + uuid: jest.fn().mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`), + } +}) + +describe('UpdateAssistant | AnonCreds | v0.4 - v0.5', () => { + it(`should correctly update the credential exchange records for holders`, async () => { + const holderRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/holder-anoncreds-2-anoncreds-records.0.4.json'), + 'utf8' + ) + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + dependencyManager.registerInstance(AnonCredsIssuerServiceSymbol, {}) + dependencyManager.registerInstance(AnonCredsHolderServiceSymbol, {}) + dependencyManager.registerInstance(AnonCredsVerifierServiceSymbol, {}) + + const agent = new Agent( + { + config: { + label: 'Test Agent', + walletConfig: getAskarWalletConfig('0.4 Update AnonCreds - Holder', { inMemory: false, random: 'static' }), + }, + dependencies: agentDependencies, + modules: { + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 10 }), + }), + // We need to include the AnonCredsModule to run the updates + anoncreds: new AnonCredsModule({ + registries: [new InMemoryAnonCredsRegistry()], + anoncreds, + }), + }, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(holderRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.isUpToDate('0.5')).toBe(false) + expect(await updateAssistant.getNeededUpdates('0.5')).toEqual([ + { + fromVersion: '0.4', + toVersion: '0.5', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update({ updateToVersion: '0.5' }) + + expect(await updateAssistant.isUpToDate('0.5')).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.5')).toEqual([]) + + // We mock the system time, however the issuanceDate is set by AnonCreds RS in rust, so we need to + // manually set the issuanceDate to the current date (which is mocked) to not have inconsistent snapshot + for (const record of Object.values( + storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records + )) { + if (record.type !== W3cCredentialRecord.type) continue + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recordValue = record.value as any + recordValue.credential.issuanceDate = new Date() + } + + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + }) +}) diff --git a/packages/anoncreds/src/updates/__tests__/__fixtures__/holder-anoncreds-2-anoncreds-records.0.4.json b/packages/anoncreds/src/updates/__tests__/__fixtures__/holder-anoncreds-2-anoncreds-records.0.4.json new file mode 100644 index 0000000000..c1d8b4ba57 --- /dev/null +++ b/packages/anoncreds/src/updates/__tests__/__fixtures__/holder-anoncreds-2-anoncreds-records.0.4.json @@ -0,0 +1,134 @@ +{ + "STORAGE_VERSION_RECORD_ID": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "STORAGE_VERSION_RECORD_ID", + "createdAt": "2023-03-18T18:53:44.041Z", + "storageVersion": "0.4", + "updatedAt": "2023-03-18T18:53:44.041Z" + }, + "id": "STORAGE_VERSION_RECORD_ID", + "type": "StorageVersionRecord", + "tags": {} + }, + "4b93719f-9b76-4c4f-9b9d-f7015dc6bcca": { + "value": { + "_tags": { + "attr": [""], + "attr::test::value": "test", + "credentialDefinitionId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/CLAIM_DEF/400832/test", + "credentialId": "c5775c27-93d1-46e0-bb00-65e408a58d97", + "issuerId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx", + "linkSecretId": "ae343b1d-e2af-4706-9aae-2010a7f2c882", + "methodName": "indy", + "schemaId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/SCHEMA/test0.1599221872308001/1.0", + "schemaIssuerId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx", + "schemaName": "test0.1599221872308001", + "schemaVersion": "1.0" + }, + "metadata": {}, + "id": "4b93719f-9b76-4c4f-9b9d-f7015dc6bcca", + "credentialId": "c5775c27-93d1-46e0-bb00-65e408a58d97", + "credential": { + "schema_id": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/SCHEMA/test0.1599221872308001/1.0", + "cred_def_id": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/CLAIM_DEF/400832/test", + "rev_reg_id": null, + "values": { + "test": { + "raw": "test", + "encoded": "72155939486846849509759369733266486982821795810448245423168957390607644363272" + } + }, + "signature": { + "p_credential": { + "m_2": "85676623484277624682444820444702467499357910274165013418379125134075325543729", + "a": "29078583023644723417256963951834769429544893561312608041999039725779045478957665610539345669249033643845871310638205359827913345539893604784172221177874151867323789783352069858114677062382329201941336114640094448982492548621745418140287523358455796985452292603814050613022499206350481966450949147798607248367393022400871389441985483251995666110068431356102420069450232712370410071038529237338590022255675451148362600727584994665177285853081799683623674291603640314733666468918666175103622831604402215322289509594157875212406584558973682327127532803373424575230946721521968680075608178586165565162932643378236428403331", + "e": "259344723055062059907025491480697571938277889515152306249728583105665800713306759149981690559193987143012367913206299323899696942213235956742930103927082934306204189453914854568633", + "v": "10113246594919968694243749150533421251903212516176280450944343564431109505938841891625247667364379412129405405348477095610002433568347856509159611335758818756926825063560770314873165029229176344268719893707027252437404047327923906866676449185116008460538437595639211714306227451873006607255100899512311105919649129622793316610076053733506986530767535638972131892478976764607887355989030338392858709274725683511041231799301175492600538984695657494046447608704545830499026706733632944905447307052632450973098625532585070622424882750416943328031030319205075858421333346786928868231555780815068857501287044930625867151895584283719258208379421233099516506594646921971059399550756016776860669930757562757559953692743338803382655236340538001870484617627827300880520094049323022699847759090147376675910494368188266848296015139094" + }, + "r_credential": null + }, + "signature_correctness_proof": { + "se": "21478040510275170612337916082115329005924729160138728500731966689768864876350314127582065222070358453091326557476132640512915164393469162400241555486465031352127309617863096642685471579736279753483791845394120052052850579761097059767637641620637246275967251656241939449999728934398401723998520798896177945650388769630721777342623105417576378300802138205906595722648799399722998758345581952810879612170626417882503950639389717949927995386086595370764259106482228888541923217556032007348358437745103150644457074511256162173366561635465131422798361283669038048806849619337895126642478632186566293253597826848134434844979", + "c": "71574462310595963848889785756072396101961922057659218227849729861834355800201" + }, + "rev_reg": null, + "witness": null + }, + "linkSecretId": "ae343b1d-e2af-4706-9aae-2010a7f2c882", + "methodName": "indy", + "updatedAt": "2024-02-28T09:36:42.994Z" + }, + "id": "4b93719f-9b76-4c4f-9b9d-f7015dc6bcca", + "tags": { + "attr": [""], + "attr::test::value": "test", + "credentialDefinitionId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/CLAIM_DEF/400832/test", + "credentialId": "c5775c27-93d1-46e0-bb00-65e408a58d97", + "issuerId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx", + "linkSecretId": "ae343b1d-e2af-4706-9aae-2010a7f2c882", + "methodName": "indy", + "schemaId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/SCHEMA/test0.1599221872308001/1.0", + "schemaIssuerId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx", + "schemaName": "test0.1599221872308001", + "schemaVersion": "1.0", + "revocationRegistryId": null, + "attr::test::marker": true + }, + "type": "AnonCredsCredentialRecord" + }, + "14720230-54b4-4735-8072-0e1902982429": { + "value": { + "metadata": {}, + "id": "9ec1f4c6-6b58-4c54-84d0-2fa74f83f70f", + "credentialId": "e1548638-fbe6-4e26-96fc-d536d453b380", + "credential": { + "schema_id": "6LHqdUeWDWsL94zRc1ULEx:2:test0.1599221872308001:1.0", + "cred_def_id": "6LHqdUeWDWsL94zRc1ULEx:3:CL:400832:test", + "rev_reg_id": null, + "values": { + "test": { + "raw": "test", + "encoded": "72155939486846849509759369733266486982821795810448245423168957390607644363272" + } + }, + "signature": { + "p_credential": { + "m_2": "85676623484277624682444820444702467499357910274165013418379125134075325543729", + "a": "29078583023644723417256963951834769429544893561312608041999039725779045478957665610539345669249033643845871310638205359827913345539893604784172221177874151867323789783352069858114677062382329201941336114640094448982492548621745418140287523358455796985452292603814050613022499206350481966450949147798607248367393022400871389441985483251995666110068431356102420069450232712370410071038529237338590022255675451148362600727584994665177285853081799683623674291603640314733666468918666175103622831604402215322289509594157875212406584558973682327127532803373424575230946721521968680075608178586165565162932643378236428403331", + "e": "259344723055062059907025491480697571938277889515152306249728583105665800713306759149981690559193987143012367913206299323899696942213235956742930103927082934306204189453914854568633", + "v": "10113246594919968694243749150533421251903212516176280450944343564431109505938841891625247667364379412129405405348477095610002433568347856509159611335758818756926825063560770314873165029229176344268719893707027252437404047327923906866676449185116008460538437595639211714306227451873006607255100899512311105919649129622793316610076053733506986530767535638972131892478976764607887355989030338392858709274725683511041231799301175492600538984695657494046447608704545830499026706733632944905447307052632450973098625532585070622424882750416943328031030319205075858421333346786928868231555780815068857501287044930625867151895584283719258208379421233099516506594646921971059399550756016776860669930757562757559953692743338803382655236340538001870484617627827300880520094049323022699847759090147376675910494368188266848296015139094" + }, + "r_credential": null + }, + "signature_correctness_proof": { + "se": "21478040510275170612337916082115329005924729160138728500731966689768864876350314127582065222070358453091326557476132640512915164393469162400241555486465031352127309617863096642685471579736279753483791845394120052052850579761097059767637641620637246275967251656241939449999728934398401723998520798896177945650388769630721777342623105417576378300802138205906595722648799399722998758345581952810879612170626417882503950639389717949927995386086595370764259106482228888541923217556032007348358437745103150644457074511256162173366561635465131422798361283669038048806849619337895126642478632186566293253597826848134434844979", + "c": "71574462310595963848889785756072396101961922057659218227849729861834355800201" + }, + "rev_reg": null, + "witness": null + }, + "linkSecretId": "ae343b1d-e2af-4706-9aae-2010a7f2c882", + "methodName": "indy", + "updatedAt": "2024-02-28T09:36:42.994Z" + }, + "id": "bb30d3ee-651a-4ffc-b1d9-30e4df23a045", + "tags": { + "attr": [""], + "attr::test::value": "test", + "credentialDefinitionId": "6LHqdUeWDWsL94zRc1ULEx:3:CL:400832:test", + "credentialId": "e1548638-fbe6-4e26-96fc-d536d453b380", + "issuerId": "6LHqdUeWDWsL94zRc1ULEx", + "linkSecretId": "ae343b1d-e2af-4706-9aae-2010a7f2c882", + "methodName": "indy", + "schemaId": "6LHqdUeWDWsL94zRc1ULEx:2:test0.1599221872308001:1.0", + "schemaIssuerId": "6LHqdUeWDWsL94zRc1ULEx", + "schemaName": "test0.1599221872308001", + "schemaVersion": "1.0", + "revocationRegistryId": null, + "attr::test::marker": true + }, + "type": "AnonCredsCredentialRecord" + } +} diff --git a/packages/anoncreds/src/updates/__tests__/__fixtures__/holder-anoncreds-2-credentials-0.3.json b/packages/anoncreds/src/updates/__tests__/__fixtures__/holder-anoncreds-2-credentials-0.3.json new file mode 100644 index 0000000000..0bd19082fc --- /dev/null +++ b/packages/anoncreds/src/updates/__tests__/__fixtures__/holder-anoncreds-2-credentials-0.3.json @@ -0,0 +1,304 @@ +{ + "STORAGE_VERSION_RECORD_ID": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "STORAGE_VERSION_RECORD_ID", + "createdAt": "2023-03-18T18:53:44.041Z", + "storageVersion": "0.3.1", + "updatedAt": "2023-03-18T18:53:44.041Z" + }, + "id": "STORAGE_VERSION_RECORD_ID", + "type": "StorageVersionRecord", + "tags": {} + }, + "8788182f-1397-4265-9cea-10831b55f2df": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "8788182f-1397-4265-9cea-10831b55f2df", + "createdAt": "2023-03-18T18:54:00.025Z", + "associatedRecordId": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "role": "receiver", + "message": { + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "@id": "c5fc78be-b355-4411-86f3-3d97482b9841", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "John" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "99" + } + ] + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjI6VGVzdCBTY2hlbWE6NS4wIiwiY3JlZF9kZWZfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjM6Q0w6NzI4MjY1OlRBRyIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiODUxODAxMDMyNzEzNDg5NzYxOTg5MzAzNjMzMDkzOTEyOTExMDUxNjI0OTQ0OTYzMTgzNzM2MDY3NDkwOTc2MDYxODEwMDgxODkxMzQiLCJ4el9jYXAiOiI4NDk0NDg4MjQzNTk2NTkwOTc2MjQzMjc0NDg4ODk2Mjc1NTcyODAyMTQ1ODE5NDQzNTE0NzQxMzk1NDI1NjM5MzQwMTczMDIzMTQ5NzI3MDY5NzMzMzQwODgzMTU4MzQ1NTYzOTA5OTcxNDMzNTg1MjMwODAxNTYyMTM0NjczNjM1ODg5NTA3Njg5ODQwOTgyODU5Mzg1NjA1MTc1NTkxNDYxOTkyMDExNzU2Mzg1MTI3MTQ3ODgxNDMwODEzNjYxNzY0MDU5MzE0ODk4MTc2NzQzMTQ5MjYzMDMwMDQ1NzMwMDMzMzI2NzgyMzg1OTY0NjcxMzg1ODQ2MzcxNjQ5MzQxMTg2MDM5NjE4MTQwOTIwMDUxMzg1MDAwNTYxMDcyMTc5NTEyMzc5Nzk0OTU4NjE1ODIyODI2OTExNzIwNTQyNTE0MTQ1NDc5MTAxOTUyMzM4MDMwMDY1MDk5NjcxOTU2OTMxMzE2NjE5MjM0NTQ0NTE5NTQ1ODQ1MzA4MzgxMjQyNTM0NDcyOTc3NjY0MjAwMjc2MTMyOTgxODE1ODAzNTIxOTExMzk4ODkxMjE0NjE1NzA1MDM2ODM2ODU1NDU1NzY4ODg4MTUxNDgzODAyNDcyODQyMzczNzE0MTI0NTYwMzIyNTI3NDE4MTEwNzYyMjgyNzY4NzMyNTIzMDQyMDA3MDY2OTk2ODIxMTQwMzE1NDg0NzI4NTM2NzIwNDI3MDg5MTI2NDk1NTAzMjc0ODQ4MDM3MjUzOTM3NjI3MDU2ODUzMTQ4NjE5NDA4NDYxOTI5NzEzMjM4MjEwNDc4MjcyMTIxNTUwNjQzODc4ODM1NDYwMzY1OTIwMjE3NTk5NDYyNDUzMDMyNDQ4MjYyMTM3NjE5ODY0OTU4MzA1MDE3MjA4OTYwNDc1MTQxODgwMTMiLCJ4cl9jYXAiOltbIm5hbWUiLCI1MDcyNzU2NDE2NDA2ODIxNzU1OTc0MzUxMTg0NjE1NjA4NDY2NTk3Mzk0NzA2MTY1NDg2ODAzMjc3MjMyMzQyOTk4MDA0MzY0OTU0MTczMzc0NDIwOTc5NTkwMDcyODgxNDgxNDA0MTg2OTExODg5NzQ4MTgzMzQ1OTk5NzQ0NzgxMTQ1MTMwNzEyNDIzODY0Nzc1MzQzNjAzNTk2NDM3Mzg4OTgzNTExNDAzODA0NjEyNjU1MDE5NzQ4MTI5NDk3ODY2NTcwMDQyMjcwNDQxNDQ5MjYwODY0NzgyMzI5MjAxNDEzMTc5ODU3NzA0MjM5OTMyMTg4NTc4NzE3MDczNzM3NjUyNzY5MzY5NDg4OTgxNzg2NDQwNTExODAzMjMzNDMxNzA4NDk4MTU2NTA0OTUzNzkzNjU2NjQ2NzMyNTU4MzQwNDI2MDI1MjA3NTk0OTIwMDY4OTc2OTQ4Nzg2OTUxNzM3MDIwNDQ0NTA5NzYyMDQ2MzIzNzA0MDQ3MjU1ODU3NDE5ODE3MDc5NTI3NDgzNTE1NDY2NTAyMDkzOTY1NDMzMzk3MjQ1MzA4MjQ5MDgyMTQ4Mjc4NDA1MzI5Njg1Mjc0MDYwNjk0MzI0MTI2ODgxMjkyMDIyMjY1ODczMjk5MDU0NDU1OTA5NzkyNjUwNjAyMTk0NjUzMjYxMDk0ODYwOTc2NzA4ODE1ODgwMjExMTY0MTkwMDM0NjY0MzI2MDc3NjcwNzkyMDE4NTE2MzMzNDI3NjkwODYwMjIxODEwMzk5MDgxMjc5NjAwNTYzMjk3MjI0NjM0MDM0NjcxNTIwODE5MzU3NzQ0Njk2NzU1Njg1NDI2NjIzMzAwMjQ3MDUwODE4NTQ2MDM2NjA0NjMxNjcyNzE5MjI0NDA4NTE2NDM4NTgxMDM5Njk4NzI0MSJdLFsibWFzdGVyX3NlY3JldCIsIjU2MzYzNTgyMDQ5Mjg4OTY1OTg1MDA4NzgyMzU0NjgyNjMwNDkxMzQ3MTM1NDIxNTAyMDEyMTIwMzI4MDI4ODIyMjUyMzg4NjgwNTMwNTgxMTcwMTgwNDU1MTcyNTc3ODkyMTEyMTY1OTM0Mjk5NjUyNzAxNDExMzUyNDkzMzkyODU0ODI4NzMyMDQzMDI0MDI0MzM0MzMzNzc0NjEyOTEzOTUyMjAzNjM1NDk2MDQ0ODMzMDI5NDE2NjUwOTU5NjE0ODgzNTUwOTMxNzgzNTA5MzE1Nzg4MDEyODQ0MzAwMDQwMDE5MTY5MTc3NTI1OTgxMTU3OTkwNjQzMDcyMjQyNzcxMjU0MTYyNzMxOTU4NzI2Nzc1NjYwMjkxODIzMDcyNDk1Mzg0NzM5MTcwODc4ODMxNzkxMjQzMjEzMjU5MzA5ODQxNjU3MjUwOTg1NzMxMjEyNzE2MDM2MDY3MDUxNjM2NzA0MjA1NDEzMDk2MDU3MTA2NTM2MTI2ODUyNDU0NzcwMzQzMTMwMTczMjAwNjEzMDIxOTE4MzgzMDQxOTU4MTkwOTE2NzQ0NjU4NTI0ODA1NjM4Mzk2OTY3OTA3MzIwNjY1MDU1MzcwMjY0NjAxMDczMjc5NDMyNjM5MjM3Njc1NTA0OTg1NzQyNTI4NjYwMTAyMDEzNzIxMzA2MTE4MTg0NDk1MTEyNDQ2NDYyNDc2NTkwMjYxODkxMjA0OTQxOTA4MjMyNzMzNDA3MTg4MDA3NzE2NTA2OTUzMDY0Nzc5NDk5ODExNzI0ODI5NjcyNjY2NzIyNjIzOTAxMTc1OTk0NTIyNjkwMjk1ODI0MDgyNzY5NjQ0NDYxOTAxMDk2NzI3MTE5NzAzMjUzNzI4NjY3MTU1MzA5MDYzNDUyNDY2MDY3NzU5NzIwOTgyNDA3MiJdLFsiYWdlIiwiMTM2NTQxMjE0MjM5MTcyNDQxNzQ1MjU3MjcyMDI3MTA4NDYwMzU0MjgxMTA2OTA2MzYwNDIwMDE0NjUyMDIxMDgyNDEzODM2ODEyMjk3NjY3ODk2MTYzNDkzMjM4NDIxNDI4NjMyNTMxODE0ODk4NzkwMDg4OTg2NjgyMTE2OTAyMzc4NDgwNTE4OTUxNDExNzg1OTk3NTk5MDMyNDYxNjExNjIyMDUyNjMzMDQ5ODYxMzc5MTQzNzI4MTM5MTUyMDkyMzI0ODc3MjMxMTYwNTgzNzA5NjE0MzA1NzQ1MjA5MjQwNjU2MDU4NjY3OTMwODEzNzYyNDY5MDc2ODc5MTk1Nzg0Nzg4NTE2NjI3MjgxMDY0NjE3MzgzMDc4Njc5MTkwODIwMzQwNTgwNDY2MjU3ODU3NjA1MTc2MTg4NTI3OTMxMDI4MTMzNTY5Njc0Mzg2ODAwMTA2MDE2MDg1Nzc0OTcyMzI1NTAyNDA2MTY0OTY0MjU2OTUxNDI3ODAxMTQzNTQxMzUzMzI0Nzg0MzA5OTY4MjIyOTU1NDk4Njk3NTAwMDUwMzc0MDg0NjIwNzQ4MTk0NzIyMTI2NjE2OTY3OTY3Mzc1NTM3Nzc5NTc4NTMwMDIxODExNTA2MTIxNjcxMDUwNDgzNTM2MjA3Njc3MTg5NDQwNjEwNzk0NTcyNzI5MTgzMzAyMjM1MDkxMDg4NTU2ODc5NTg3OTE3MDMzMzQyODcyMzg2NDQ5MTQ0NzgwMDYyNjc4MzA3NzE4MzU1MjQ5MTUxNjc5MDA1MzkxNDA5NDE4OTQxMjEzNDkxMjQyMjg2NTAwODcyMzQxNDI3Nzk1MjQ1ODYzODE2MDY2NDY3NDkxOTg4OTU3MDEwNDIxNDA3NDkyMDUxOTc0NTMwNjIxOTk1ODU0ODczNTM5Mjk3MyJdXX0sIm5vbmNlIjoiNzk3NjAzMjE3NzA5MzM1MzAwMTcwODI4In0=" + } + } + ], + "~service": { + "recipientKeys": ["GVbwqUMqzxKaEVWjn1aPBfjJpYQHVejinpx8GCeEuQjW"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3000" + } + }, + "updatedAt": "2023-03-18T18:54:00.025Z" + }, + "id": "8788182f-1397-4265-9cea-10831b55f2df", + "type": "DidCommMessageRecord", + "tags": { + "role": "receiver", + "associatedRecordId": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "protocolName": "issue-credential", + "messageName": "offer-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "messageType": "https://didcomm.org/issue-credential/1.0/offer-credential", + "messageId": "c5fc78be-b355-4411-86f3-3d97482b9841" + } + }, + "2c250bf3-da8b-46ac-999d-509e4e6daafa": { + "value": { + "metadata": { + "_internal/indyRequest": { + "master_secret_blinding_data": { + "v_prime": "6088566065720309491695644944398283228337587174153857313170975821102428665682789111613194763354086540665993822078019981371868225077833338619179176775427438467982451441607103798898879602785159234518625137830139620180247716943526165654371269235270542103763086097868993123576876140373079243750364373248313759006451117374448224809216784667062369066076812328680472952148248732117690061334364498707450807760707599232005951883007442927332478453073050250159545354197772368724822531644722135760544102661829321297308144745035201971564171469931191452967102169235498946760810509797149446495254099095221645804379785022515460071863075055785600423275733199", + "vr_prime": null + }, + "nonce": "131502096406868204437821", + "master_secret_name": "walletId28c602347-3f6e-429f-93cd-d5aa7856ef3f" + }, + "_internal/indyCredential": { + "credentialDefinitionId": "A4CYPASJYRZRt98YWrac3H:3:CL:728265:TAG", + "schemaId": "A4CYPASJYRZRt98YWrac3H:2:Test Schema:5.0" + } + }, + "credentials": [ + { + "credentialRecordType": "indy", + "credentialRecordId": "f54d231b-ef4f-4da5-adad-b10a1edaeb18" + } + ], + "id": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "createdAt": "2023-03-18T18:54:00.023Z", + "state": "done", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "protocolVersion": "v1", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "John" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "99" + } + ], + "updatedAt": "2023-03-18T18:54:01.370Z" + }, + "id": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "type": "CredentialRecord", + "tags": { + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "state": "done", + "credentialIds": ["f54d231b-ef4f-4da5-adad-b10a1edaeb18"] + } + }, + "e1e7b5cb-9489-4cb5-9edd-77aa9b3edb64": { + "value": { + "metadata": {}, + "id": "e1e7b5cb-9489-4cb5-9edd-77aa9b3edb64", + "createdAt": "2023-03-18T18:54:01.098Z", + "associatedRecordId": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "role": "sender", + "message": { + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "@id": "2a6a3dad-8838-489b-aeea-deef649b0dc1", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiY3F4ZW8ybzVIWmVjYWc3VHo5aTJXcTRqejQxTml4THJjZW5HV0s2QmJKVyIsImNyZWRfZGVmX2lkIjoiQTRDWVBBU0pZUlpSdDk4WVdyYWMzSDozOkNMOjcyODI2NTpUQUciLCJibGluZGVkX21zIjp7InUiOiI3OTE4NzUwNzgzMzQxMjU3ODU3OTUzMjc2OTU5MjcxOTcyMDQwMjQxMTU1MzcyODEwOTQ2NTEwMjk5MDA1ODEyMTcwNjkwMjIzMTQ2ODU0NDk1MTI1NjQ1MTg3ODQxNzk0NjA3OTUwOTQ1OTM3MDYxMjk1ODgwMjIxMzE2NTg1MTIyNDY1Mzk1MjAwNDQ3MDIwNzAxNDA0NjEwMDc4MzkyMjA4NzkxMjk5NzYwMzM4OTIxNDMzMDc1Njk2ODU0NTY3MTIzNjYxNTYwNDMwNDE3NzQwMzc5MzA4NDQzODcyOTU1NzAwNTk1MTg2NzcxODY3MzM5NzQ5NDgzODYxNTQ2NTE2MTU4NTM5MjkyNDQzNTQ3OTg3MDUwMzE4OTAyOTI5OTgyNzMzMDk1ODk4MDIyMjg2OTk1OTQwMjkzMTg3NTg5NDgwNTgwNTAwNjM0NzAyNjQxNDM0MTgxNjIwMTU4OTU3MzUyNTE1OTA4NDE2MjI4MDQ0NDA2OTU4MTk1MDg4Mjc0ODI4Njc3OTQxMDgxOTczOTg3NjU1MDEzNDUxMzA4ODQyMjYyMzY4MTQzOTExNjIxMTE0NzYyNTk3Nzg1MDczMTM4MDg3NTQ2MDIyMzc1NjQxODQ5ODI2OTg2MjYwMDE5NTAzNzE3OTk0NjM3MzIyNDExNTgzNTY0MTQ1NjcwMTM5OTQ1MjQxOTU2Njk2MDQ3MTQzNjA4NjQ0MDM5OTg2NTYwODUyMzA1MTczMjUxMTUyMDIzODI5NjI3MzQyODM2NDI3MjkwNDQ5NTA3OTY0Nzk4MTQ2NjkzOTUxMDkwNzUwOTAyNiIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6Ijk4MjIzNzMzMDcwMDk1NjM0MzU5MTE2MDEyOTgzMDcyMjc5MjcxMzk1MTg0NjA0NDcxNDQ5NzA1Nzg0MTEyNDAyODMwNzIyMTUxMzIxIiwidl9kYXNoX2NhcCI6IjU5ODA0MTY4ODAxODk1NDAzMjkwMzczMTA3NzA4MzUwODc1NjY4MDcyNDY3ODAyOTcyOTIyNjUzNDE5ODU2MjMyNTIzNDI4OTUxODQ4NjEyNDE1MjM5Nzk3Mjk5ODY2MzIxNjU5NDQ1MTM1NzQ4NzU2MDY1NjgyOTY5MjY4ODI5MTYyMDA0NjQ4NzYwMzg4NTg4NDkyNjg1NDI1MTg1MDU2OTAxOTkxMjcwMzYwMDk3MDc5NjEyNTYxMzY4NzU1OTcwMjY5MjI4MDYzMjMyODU0NzI0NDkyOTA5Njg5MDMyOTg4NjYyMjk5Mzg3MDU2NDEwNjU5MDU3MDUwNjE0MDQ2NzE1NjA0NTgyMzM2NTg4MjMxMjI3MTEzMDEzMDQxMTA0NTU2NzM1MDE3ODUwNzUzODcwNjc2ODYxNDA4MjA0NzkzMDIzNTYwMDEwNTEzODAwNzA4MjAyNjAyNjQ0Mjg2NzI4NjAyOTk5MzU5MDcwODQxMTQ5MTAzMjA5MzY0ODkyMzMzMDYwMDgzMTA5NDIzOTQ5NzE4NTk5NjEzMzk2NjIyMjc4MTExMzk5ODU0MTcyMjMyNTQzOTk1Njc5NDk3Mjk1Nzc1NzA0MjA0MTQxOTU2NDI1MDc4NjYzMzgwMDA1Nzc2ODY2MTcxNTY4OTU1NjE4NjAwMTA2NzkxMjIyNDkyODA2NzI1ODU1NDY2Nzk4OTEzMTc2NDcxMDY3MTk5ODQ2ODEwNDI5MDIzMDc3ODI3OTc1OTIzMDIzNjU3MTg0NjkwNzE0MjkxNDk0MDc5MTM1NzYyOTUxNTc0MjMzNjMwMjExNDQ1Njc3NzE1Mzg3Mzc1NjkyMjAzODE3NDczNDk5NDExNzE5MzIwMjczNzExOTIzMzM3MzYzMTAyOTkwMDcyMjE2MjYzMzUxMzMxNTk4ODk1OTU3MzU1MDc1NTEzODE0NTUwNzkyMCIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiNTY0MTY1NjA2NjM0MTUwODgzMjg2MjM1NDM1MjUyODQ3MjAxMTk4NTQyNDAxMjYzNTY2MDQ2MTA3OTU3NzI4NTQ0MTMwMjgzNjUyNjQzOTI0NDc2NTU2NTIzNzg0ODI1OTgyMzMwMzc4NDI4OTM0MDkxNDcwNDM0OTAwMTM3NzkwNDkxMjM4NTA4ODk2ODQxMzkwNjQ4MTA1NzY5ODYzMzI1OTAzODI1Mjk1MjU2OTQ0NSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMzE1MDIwOTY0MDY4NjgyMDQ0Mzc4MjEifQ==" + } + } + ], + "~thread": { + "thid": "c5fc78be-b355-4411-86f3-3d97482b9841" + }, + "~service": { + "recipientKeys": ["cqxeo2o5HZecag7Tz9i2Wq4jz41NixLrcenGWK6BbJW"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001" + } + }, + "updatedAt": "2023-03-18T18:54:01.099Z" + }, + "id": "e1e7b5cb-9489-4cb5-9edd-77aa9b3edb64", + "type": "DidCommMessageRecord", + "tags": { + "role": "sender", + "associatedRecordId": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "protocolName": "issue-credential", + "messageName": "request-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "messageType": "https://didcomm.org/issue-credential/1.0/request-credential", + "messageId": "2a6a3dad-8838-489b-aeea-deef649b0dc1" + } + }, + "669093c0-b1f6-437a-b285-9cef598bb748": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "669093c0-b1f6-437a-b285-9cef598bb748", + "createdAt": "2023-03-18T18:54:01.134Z", + "associatedRecordId": "93a327ad-2bf3-4ec4-b01c-bdd58ba2f6e3", + "role": "receiver", + "message": { + "@type": "https://didcomm.org/issue-credential/2.0/offer-credential", + "@id": "4d2c80b7-4a25-42ac-b8cf-a68b1374b9b7", + "formats": [ + { + "attach_id": "8430ddb8-b0c3-4074-8ded-f4dcfe80303d", + "format": "hlindy/cred-abstract@v2.0" + } + ], + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/2.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "John" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "99" + }, + { + "mime-type": "text/plain", + "name": "height", + "value": "180" + } + ] + }, + "offers~attach": [ + { + "@id": "8430ddb8-b0c3-4074-8ded-f4dcfe80303d", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjI6QW5vdGhlclNjaGVtYTo1LjEyIiwiY3JlZF9kZWZfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjM6Q0w6NzI4MjY2OlRBRzIyMjIiLCJrZXlfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjQwMTQ0MTA0NDg3MjM0NDU2MTc1NzYwMDc1NzMxNjUyNjg1MTk0MjE5MTk5NDk3NDczNTM4NjU4ODM3OTIyODMzNTEzMDg0Nzk2MDQ5IiwieHpfY2FwIjoiMzgxOTQyMjM1Mzc3MzYwODEyNjY0MDA4MjYzNDQxMDg2MDMwOTMyMjAxNzgzMjM3ODQxODQ5NDg3ODk2ODg1MTYwODY2MTY1MDM3NzI2MTIxNjU0MjcwOTg5NDY3NjAzNDExOTAzODk4MzUwMDAzNDIwODg3MzI4NTUwMTY2MTI1ODMyMjAxOTQzMTkwNzAxMDU4NTAwMDE5ODM1NjA1ODczNDYzOTkwODg3NzQ0NjY3MzU0MjM2Njc3MzcyODg0ODQyNjE5NTEwMTUwOTA2MjI1OTMzMjc1ODEyNjg2NDg3NTg5NjY3ODI3MjAwODcwOTQ0OTIyMjk5MzI3OTI4MDQ1MTk1OTIwMDI3NTc0MDQwNDA4ODU5MzAwMzY1MDYwODc3Nzg2ODkwOTE1MDU5NTA2ODc1OTI0NzE2OTI1MDM2MTc4Njg2NDE5NTYyMzcwODI4MTMzODY2Nzg3NzkyMDcwNjAyNDQzNTkzMTk2NzEzNzcyNDM2NTYzODI0MzkwMDIyNzg4MjU2MzA4NjU4OTc0OTEzMTk1ODYxODUwMTQ3ODE1Mjg5NzQwOTA4NDk1MjQ3NTAyNjYyNDc3NzQ2NTI5ODA3Mzg0OTgxODI5MDc3NTQ4OTI2NzExMDkzNzQ5MjM1ODU4NjUwNDc5NzE5NDI4MzUwMzAwNzUyNjQ0OTg1MTQ5MTMxNjA1NjUzMDIxMDYxNzkwMjY3MzQyNTY4NTkyNTY2MTQ0MDM5NzY4OTg0NTMyNDMzNzk0MzUzNjQ2Nzg1MjA3NDgzOTk2ODQ0OTcxNTgzNzY3NDQ5ODYyODgxMjMxMjI1MzM4MzAzMTQ4NzA0ODczMDEzNDM3MDgyNzY1MTk4OTY2MzE5NTM0OTkyNjk4MzMzMDQ0MDI3MjIyNTYyNTIzNzk3ODk5Mjk2MTQ1NDU5IiwieHJfY2FwIjpbWyJtYXN0ZXJfc2VjcmV0IiwiOTE5MTc5NzQ4MTE5NTg5MTY3Njc5MjQxODk5NzY0ODIwNTk0MDc5OTQxNzIzOTgzOTYyNzQ1MTczODM0NDQxMDQ3MjU4MDcyOTE4OTUzNjIzOTQ4MDMyNzI2NDgyNzI2MTEwOTk2Mjk3MDU3NTYwNjcwNzAxOTU1MTkxNDc0NjM0MzQ0ODMxMzg3NTk4NzI2MzMxMjc0NjI4NDU3Njk5NzczMDA1NDMwMDIxNzMwMzg4MzcwMTEyMjc3MzI2MzU4OTgwMTA3ODIzNzUzODc3MTU0NjIwMDkzMjE5MjYyNjAxNDM2NzMyNTgzNDI4Nzc4NDA4OTc0NTQyNzkzMDk0NTQ5MTczOTA3MzQ3OTUxNTc1NjM5NzU2NDg5MTA0Mzk0MTY3NzExMzY1MjM3OTI1MjAwNjk4OTg5NTI5MTQ3OTIzNTYzNDMyODgyMzgwMTg0NzU0NzkzODMwMTE3MTQ1MDAwMTI0NDYxNjkzOTcxMDQ5MjgzNDk1NTE4MDQxMDc5ODUyMzAwMjk0NDM1MjYzOTIwNDU0NTU3MzUxNDQ1MDM3NDI4MDg3OTk2Mzg2NjY3NjU3Nzk5OTYyNzQzNzIyNzA3NzczOTEzMzc0NzIxODUyNTQ3MjkwMTY5MjI5NTAzMTQxOTMwODYzNTk4NTExNjc4NDEyMDE0MzE2MDM2MzYxMzczNDcwOTQwMDEyODcwMDgwMDA2MzE0NzYxNzYzNzUyNzYwODk5MTQ3NzA1MTA0NzQyNjAxNjkxMzMxNjkzMDIwMjg2MjA2NzQ2NzE0MzI3NjU2MjA2NTMzMjk3NDg4MjU2NTM2NTQ3MzY4MjM2OTQ2MDM5NzAzMzc0OTMzNTE0NTc2NDg2NjQyNTY4MjgyNTY2MjMyNDU1NTU5MDY4MzE3NzU5NDM0ODU4NTI3MDg2NjQ0Il0sWyJoZWlnaHQiLCI5MjMwMzkyNDc1NjI4ODc1MjA4OTM0NjM0NzE4MjYzNzA4MDIzOTI1MDU0NjY2NDgzMzgxMzIyMzc3MDg1MjMxMjU4MTM4MzgwOTU1NTk3NDQxNTEyOTYwNDA2MjI3MjUwODgyNjA3NjExMDkwODk3MTM1NDcxNzAwMDIzNDcwOTM2ODg4MDE3NDY5Nzk0ODYzNDk4NzUyNTI3Njc3MjMwMTEwNzg0ODQzNzI0NDUyNTUzODYyOTA2MzM5MDc0OTIzNDU4NTQ3NDYzODcwNzU3OTg5MzMxNzk4OTI2MjM4MjUxMTM2NTYzNjM2MjIyOTQwNDkwMzY3MjQ2OTg0OTU2NTE5MTAzODcwNDE0MDM5NzM2MDE2MDY5MzA2NjQ0NjQzODI4OTgxMTE3OTM3NzYyNDAzODY1Mjc1MDU5MjEyOTY2NzIxOTU3MzM0MTM2ODEyMDI0OTE0MzA4MzAxMzk5MzM4NzMyOTIzNTA0MjA5MDM5ODMxMTc5NjU1NTkyNjg0MjMyMTIzMTI2Mjc4ODQzNDMyOTUwMTk1Mjg3MzE4ODI3NTM2MTMwNDQ3NzM3MTgwMjk3MDE0ODEzNDg3NDQyOTg2NjQ1NzQyNjEyMzE5NzQxNDY2MDMyNTg5OTU0NzYwNjE4MDU0MDUxMjAzMTE1NTAxNDcxNDExMzg3NzU0NDk5MzAwNTU4MTc5NjM5NDAxOTM0NTAzMTMyMDEzMjAzOTg2NzkyMTEzMDAzNTkwODg1NTc3NjgyMzU2NDY3MjA5NTUwNjQxODQxMDYyNTkzNDYyODIwODg3NzgxNDYyODM3ODkzODcxNDM4MzM3Mjc5MTcwMTExMTQ5MTU4NDMzNDE0ODI1NTkyNjcyODU2MzM5OTM4NTgyODg2NzM3OTIwMjc1MzI0MjEwMTUzMjE5MjI2OTYiXSxbImFnZSIsIjkxNTg1ODk3NDkwNzE0ODA3OTY2MDYzOTg5MjE1NTMxNDkyOTQwMDI5NDcyMTM4MjgwNjcxNjcyMjQ0NjY5MDc5NzIyNTQyMDU0NTU3NjY0MTcxMDI1NzM1NjQ4NTIwMTM4ODQ4ODAxNzIyMTc4MTcxMTA5NTc0MTMyNTExMzM1MDEwNTc5NzExMzcyODM5MjI3MDExOTg4MTUyMTEwMzI4MTE5MjkyMjI4NjM3MDU4MDQ3NzYwODYwOTQ0NTY3MzQxMjY4MTY4Mjk3NjE5MDM2ODEwMjYwODM2NDI1NDkwMzU3NjE4NzM4NTYxNTY2MTUxODQ3MzIxNzM1MjQ5ODk1MDU5NTY2OTQxODI5MjE0Nzc0MTA0NzYyNTQwMjcyMjk2NjE1NTE3NjUwMDcyNDQyMTI0NjY5MDEzMTc1ODAyMDk5MDQxMzk3MzE5ODQ0OTA2MDgwOTYxNTcyMTcwNjg2NzgzNDM1Mjg2MDUyMzE5ODY3ODExMDE5MjAxMDYwODM2OTM3Mzc0MzM0NDM5MTQxMDAzMTI3NTcyNjgzNTgwODI0OTkwOTg3MjE5MzU4NzkzOTM2NTU4Nzk3MjI0MDQzNTM1ODA5NzMyNzgxMjE1NzEwNjI1MjQzODYwNTk4MTk0MjU2MjAwODkwOTA3ODAzMDcyMTAzNzc3MzkwODk4MDczOTgyNjY3Njc1ODg0MjI3MjU0Mzc2OTI5Mjg3ODQyNDE0MTE0MjcwNDQwMTEzNDUxNjk4NzE5Nzc5NjQyNTI4MDA4NDM3Mzk5NjI0NTE3OTM4Nzg5MDc3ODE5ODA0MDY5MzcxOTM0NzExMTIyNTQyODU0OTg4MDA0Mjc4NDkwMjAxNTk2NjE0MjUwODc3NDYxMDczNjc3NTUzNzYxMTMyMTA5Nzg3NTQ2ODE1ODk5Njc2NCJdLFsibmFtZSIsIjYyNzgwNTIwMTM3MzI3NTUzMDc3MDg4NTE4NDg1NDYyMTA0NjEzMjEyNzY3ODUwMzYwNTc3NDQ4MDUxNTk5MTMxMTM1NTI2NzQ3Nzc2NzMzMDg1MDMwODcyMDE1OTM2MTI2NzE0MTIxMDgxMzg3ODU2MTkwMTkzMzI3ODY3OTE0NTEzODM2NTQ1OTY4Mjg1NTc5ODEyODMxMDI4ODc2Nzg1NzI3OTQ2MTEwNzg5Mzc0MjcyODgzMzkyOTgwNDkwODk3NDkwMTc5MDQ0ODM0NTgwMzQ2ODY4NDI2ODc0ODU4NTY1OTg4NTUyMDcwNjI1NDczNjM4MDM3Njc5NTU1NTk2MzE5MTc3Nzc5OTcxMTIxMjQzMjgyMTIyOTQ2NjY0ODMxOTgxMTg3MzQ3MzcyMjkxMjYwOTM3MzkzNDA1ODk5OTI0NjM4MzE3ODI5MDczODMxMjI4ODc1Njg5MTcyMTg4NjIyMDI5NzcxNzM5MTQ5NDY2Mzg3NTM5NjkyNDQ5NDU5MjczNDI5NzM5MjMzNjkyNTEzNDA5OTkyNDgxNTQ4ODk0NjAzNjM3MTYzNjA4MTM0MTAzMTk3Nzc3NTM4OTYwMDcyMjcyMzYyNzM4NDM1MTM3MDcyNzIxMjExMDYxNTg4MDE3ODczODg3MTEwNDA2OTk1NDQ4ODIwMDEzMDA5MjgyMzk0OTczMDMwMDI5MTY3NjQ5NzY1OTI1MTUxMzY4NTg5OTkyNzMyMDE1ODAwNjAzNzYxOTI3MTg3MDM4MDkxNDY3MDE1MjA3MzIwNDczMDM0NDA3MDIyNDA0NjQ4MTI0NTk2NjQwNjU1NjY1MTIzMzY5Njc0ODI2NDE3MjE2ODUxNTM4Njc1NTM3NzAwOTg4MTQzNzE1NTE3NzMwMTM4NjA4NzkxMjcyMzM0MDUyMzY4OCJdXX0sIm5vbmNlIjoiNDE0MzQ4Njg0NDk2OTAxNjkyMjI2OTY0In0=" + } + } + ], + "~thread": { + "thid": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b" + }, + "~service": { + "recipientKeys": ["DXubCT3ahg6N7aASVFVei1GNUTecne8m3iRWjVNiAw31"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3000" + } + }, + "updatedAt": "2023-03-18T18:54:01.134Z" + }, + "id": "669093c0-b1f6-437a-b285-9cef598bb748", + "type": "DidCommMessageRecord", + "tags": { + "role": "receiver", + "associatedRecordId": "93a327ad-2bf3-4ec4-b01c-bdd58ba2f6e3", + "threadId": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + "protocolName": "issue-credential", + "messageName": "offer-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "messageType": "https://didcomm.org/issue-credential/2.0/offer-credential", + "messageId": "4d2c80b7-4a25-42ac-b8cf-a68b1374b9b7" + } + }, + "93a327ad-2bf3-4ec4-b01c-bdd58ba2f6e3": { + "value": { + "_tags": {}, + "metadata": {}, + "credentials": [], + "id": "93a327ad-2bf3-4ec4-b01c-bdd58ba2f6e3", + "createdAt": "2023-03-18T18:54:01.133Z", + "state": "offer-received", + "threadId": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + "protocolVersion": "v2", + "updatedAt": "2023-03-18T18:54:01.136Z" + }, + "id": "93a327ad-2bf3-4ec4-b01c-bdd58ba2f6e3", + "type": "CredentialRecord", + "tags": { + "threadId": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + "state": "offer-received", + "credentialIds": [] + } + }, + "b71d455b-9437-4ef8-b4f3-b6a0dd6bbfb3": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "b71d455b-9437-4ef8-b4f3-b6a0dd6bbfb3", + "createdAt": "2023-03-18T18:54:01.369Z", + "associatedRecordId": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "role": "receiver", + "message": { + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "@id": "578fc144-1e01-418c-b564-1523eb1e95b8", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjI6VGVzdCBTY2hlbWE6NS4wIiwiY3JlZF9kZWZfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjM6Q0w6NzI4MjY1OlRBRyIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJhZ2UiOnsicmF3IjoiOTkiLCJlbmNvZGVkIjoiOTkifSwibmFtZSI6eyJyYXciOiJKb2huIiwiZW5jb2RlZCI6Ijc2MzU1NzEzOTAzNTYxODY1ODY2NzQxMjkyOTg4NzQ2MTkxOTcyNTIzMDE1MDk4Nzg5NDU4MjQwMDc3NDc4ODI2NTEzMTE0NzQzMjU4In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjMyMTIwMDQ1ODc4MzIxMjcyMzA1ODI5MTc3NzMzMTIwNzE1OTY5NDEyNjkwNjUyNDQ4OTc0MTA4NzEzNjU0ODc3NTg2MzIzMTI3ODk2IiwiYSI6IjIyMjY0MTYwNjIwODcwNDUyNTExMjcyMzE1MzMzMDA0MjQzMzY3NTM2NzM3NDMwNjM1NjExMjcwMDkwOTE4NDMwNzc0ODEzMjAzNjQwNjMxMjIyNDczMzk0MjQ4MTgzMDIzMjIyNzExNDUwMzQxMDcxOTQyNDQwMDgwMjY2Nzk1Mzg5Mzg5Njc1NjYwOTUzNTQyMDE4OTA3NjQ3NzI4OTQ4NjY1MzA2Njg0NjExNDU1NTI5NzM5OTY1NDcyMjQ2NDQxMzE1NzAxMzM1ODc1MDY3MjExMDk3NzcyOTgwMjU1NDIxMDMzMTI1MjAyMTQzNDk3NjMyOTAyMjM1NDAyMzU5OTA1MzY5MzE4MjI1NTc4MjUxNjY4NTYzNzc1NTY0MDM2MjUxNzE0Mzk3MTEzNjQ3OTg0MjcxMTE5MTU2NDQ3NjI1OTk1NjE5MjAwMDk4MTgzNzY1NjkzMTg1ODEzNjA1NDU3OTQwMzE0MDU2MDkzMTI2MzQ3OTU5MzYwODIyMzg0OTEzODg3Mjg3ODI2NjkyNDIyNDMyNDUwMDA5OTYxNjQ2MjMzNTE3MjY3NDU1OTkyMjA3MTE3Mzk5NzU1NjY3MTA3MzM1NTQ0MzEwNDQwNDE1NDE5NTk5NTA1OTgxMzkwMjk5NDUxNzQyODg4NDg0MTc0NTU5MDA5NDgwNjU5MDk2Nzg2ODI2MDgxNzc3MzcwNTk1MTU3OTg5NjQ1MDYxNDI2OTA2ODM2MDk5NTU5MDQ0MDI4ODM2MzYwOTM2MDkwOTkxNjA1OTU0NTM2OTQxMjgxODQwNzk2MjkxODc0ODk2NDEzNTM5IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDExMzE1MTE0OTUxODg0MDQ5ODkyNjAwNTY3NTgzMTc4ODkwMyIsInYiOiI2NTE5ODc4MzYyODQ5NzExNDY4NDkyNDM1OTM3MDU4NzMzOTYxMTkxNjA3NjI4MzUzNjkxMDg1MzM5MDMwNDU0OTkyODc0ODYyODkyNDg4ODYzNTA3MDQ1MTM1MzA4ODI1NDA2NzYwMTQwNDQzNzM0NDYzODE5NTM2MzE0NzcxMTQ3MDk4MjU2ODMzNTc2MjIwNDI5ODQyNjc3NzMwMzQwODYwNjE2NTcxNzc5NjU4OTIxNDY4Mjc0NTUwOTc5NjYyMDkxNzEwNDU5MDk2MDgzMzYzNTc1Mjc0MjQzNzIyMzIzOTIxMjY5MDYyMjE0NjQyNzQyMTI0MzQ4MTY0MDUxNzE3MTk5MTkzODY3NTM3NTEzNjYzOTY1ODQzMDI5MjAxODA0OTE2MTEzNzMxODYzOTUzNjQ5MDkwNDgzNzMyMTkxNTQ2MTEwMjAxNTg0NzMxODg4NTE5NjA2MjE1OTkyNTgxNzk2MDg2NzUzOTE5NzUxMjkwMDI3MDI4NzczMTAwOTc5ODI5MzQ5NzA0MTUyMDEzNjg2MzU1MzM1MjIyNjU5MDY2NzE0NDQ2NDc4NzY3MTE5NDE4MjY3OTg5NTAyNzc4MjMzNzM3MjM4MjU1MTQxNzQyMjk4NTU3MDY2NzA2MTM0NzYwMjQwMzY3OTMzMzc5NzYzMTc5MTI1NTI4MDQwMzkxNjQwNTIyNTM5NjE5NTU0NTE0NTk4OTUxNTg0NjA3MjYwNzk1NzE1MDMyMjM4NTQ3ODMyMzA0NTY2MzQ4NjYzMTc0NzQwMDE2MzQ2NTU2MTM1ODc4MzgxNTYzODQ2NzU0MzQzMjk0NTIzNjc0NDI3NjQxNjAxNjAwNjE2NzI3NjEyMzc0MzI2NzY4ODA5NjAyNTE5MTAzOTk3NDY4OTg1NTg3Nzg4MjI3Njc5MzQ4NTgwNzk1OTkyOTkxNzMzMDg5MTUyMTg2MDg4OTU2MTg2MTQ0OTkyMDI5OTI2OTUxOTU0OTQyNjYwMDUxOTM0MDc5NzkxODI1NzA2MTExNzg0MDU2NDM2OTA2MDgxMDQ2MDQ5ODI0ODE1NDE0MTc5NTMzMDA2ODE4NzQ3NzgwNTQ5In0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjEwMTUyMDI0OTk1MTUxMzcyOTgwNzI5NDM1MTQ5MzgyMzQxMDQ3MDIzNjI2NjA4MDc3ODg1NzQ2Mjg4MTE3MjI2OTAyNzM4OTY5OTEwMTU0NjgzNzM2MzI4MzQ2MDg0MjcwMDA3MTY2NTg3ODI5MjE2MzEzNDc4MDk3Njc0MTI0ODU2ODIyMjA4NzI0Nzk1NTE0ODgzOTYwMzE5NDc5OTg4NzAzNDUyNjI4NjYxMDc3MTg3OTIyMzA1NDc5MDE2NzQzOTk0NzYwMzE5NzI1OTExODk0MjM2NDMxMDkxMTIyNTUxNTU0NzgwODg0NjQ2MjE0MTUzMDUzMTM2NDMwMTk4OTA5MTM0OTk4OTM2NjY3MzI4ODI2MDcwNzEzMzk0NDg0NDI0ODUxNjkxMzUxNDc0NjAxMjIwODk2NTIyMDYzNDA5NzA4NDA1Njk2MzY5MjA0MzU0NzE1MDkxMzk2Mzc4Mzc3MzA0ODk3MzMwOTM0Mjc2NTQyNjE2NjAxNTk1ODI5NzgxOTg3NTMyNzkxMzIyNTgzOTE1Njk1OTY2MjM3MTc4Njg1NTMzNTE3MTQxNTAyNDE3MzQxMDIzMTA1MTczMjMwMTcwNzUzODYwMjgxNDAxODk4MDE5OTQwNjA2MzczOTYwMzYxNjA3NTE2NjgyMDg4MTc1NzU4ODA0Mzg4MTM5MTQ0MDkwMjg5MzI5NzMzNTQ1NDg4MjUyNjczNDIyODkzMzc1MzE5ODQ2OTMwOTIyNjIwNzAzMTEwMDgwODU5OTE4ODQ0MzgyOTQ3ODczMjAwNzA4MTY2MzA0NDk4ODk0MDA4NTMyIiwiYyI6IjIyNDQyNTM5MzYwMzYzNjQyODI1ODkxNTc5ODgzMDE5Mjc3Mjk0NTQ2MjUwMDEzNTM3MzI2OTY2NzM3MzE0NTUxMjEwMjU3MjU2NDU5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9" + } + } + ], + "~thread": { + "thid": "c5fc78be-b355-4411-86f3-3d97482b9841" + }, + "~please_ack": { + "on": ["RECEIPT"] + }, + "~service": { + "recipientKeys": ["GVbwqUMqzxKaEVWjn1aPBfjJpYQHVejinpx8GCeEuQjW"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3000" + } + }, + "updatedAt": "2023-03-18T18:54:01.369Z" + }, + "id": "b71d455b-9437-4ef8-b4f3-b6a0dd6bbfb3", + "type": "DidCommMessageRecord", + "tags": { + "role": "receiver", + "associatedRecordId": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "protocolName": "issue-credential", + "messageName": "issue-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "messageType": "https://didcomm.org/issue-credential/1.0/issue-credential", + "messageId": "578fc144-1e01-418c-b564-1523eb1e95b8" + } + } +} diff --git a/packages/anoncreds/src/updates/__tests__/__fixtures__/issuer-anoncreds-2-schema-credential-definition-credentials-0.3.json b/packages/anoncreds/src/updates/__tests__/__fixtures__/issuer-anoncreds-2-schema-credential-definition-credentials-0.3.json new file mode 100644 index 0000000000..b4bd2e2d08 --- /dev/null +++ b/packages/anoncreds/src/updates/__tests__/__fixtures__/issuer-anoncreds-2-schema-credential-definition-credentials-0.3.json @@ -0,0 +1,431 @@ +{ + "STORAGE_VERSION_RECORD_ID": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "STORAGE_VERSION_RECORD_ID", + "createdAt": "2023-03-18T18:53:43.140Z", + "storageVersion": "0.3.1", + "updatedAt": "2023-03-18T18:53:43.140Z" + }, + "id": "STORAGE_VERSION_RECORD_ID", + "type": "StorageVersionRecord", + "tags": {} + }, + "fcdba9cd-3132-4e46-9677-f78c5a146cf0": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "fcdba9cd-3132-4e46-9677-f78c5a146cf0", + "schema": { + "ver": "1.0", + "id": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/Test Schema/5.0", + "name": "Test Schema", + "version": "5.0", + "attrNames": ["name", "age"], + "seqNo": 728265 + }, + "updatedAt": "2023-03-18T18:53:45.521Z" + }, + "id": "fcdba9cd-3132-4e46-9677-f78c5a146cf0", + "type": "AnonCredsSchemaRecord", + "tags": { + "schemaId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/Test Schema/5.0", + "schemaIssuerDid": "did", + "schemaName": "Test Schema", + "schemaVersion": "5.0" + } + }, + "de4c170b-b277-4220-b9dc-7e645ff4f041": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "de4c170b-b277-4220-b9dc-7e645ff4f041", + "schema": { + "ver": "1.0", + "id": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/AnotherSchema/5.12", + "name": "AnotherSchema", + "version": "5.12", + "attrNames": ["name", "height", "age"], + "seqNo": 728266 + }, + "updatedAt": "2023-03-18T18:53:48.938Z" + }, + "id": "de4c170b-b277-4220-b9dc-7e645ff4f041", + "type": "AnonCredsSchemaRecord", + "tags": { + "schemaId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/AnotherSchema/5.12", + "schemaIssuerDid": "did", + "schemaName": "AnotherSchema", + "schemaVersion": "5.12" + } + }, + "6ef35f59-a732-42f0-9c5e-4540cd3a672f": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "6ef35f59-a732-42f0-9c5e-4540cd3a672f", + "credentialDefinition": { + "ver": "1.0", + "id": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/CLAIM_DEF/728265/TAG", + "schemaId": "728265", + "type": "CL", + "tag": "TAG", + "value": { + "primary": { + "n": "92212366077388130017820454980772482128748816766820141476572599854614095851660955000471493059368591899172871902601780138917819366396362308478329294184309858890996528496805316851980442998603067852135492500241351106196875782591605768921500179261268030423733287913264566336690041275292095018304899931956463465418485815424864260174164039300668997079647515281912887296402163314193409758676035183692610399804909476026418386307889108672419432084350222061008099663029495600327790438170442656903258282723208685959709427842790363181237326817713760262728130215152068903053780106153722598661062532884431955981726066921637468626277", + "s": "51390585781167888666038495435187170763184923351566453067945476469346756595806461020566734704158200027078692575370502193819960413516290740555746465017482403889478846290536023708403164732218491843776868132606601025003681747438312581577370961516850128243993069117644352618102176047630881347535103984514944899145266563740618494984195198066875837169587608421653434298405108448043919659694417868161307274719186874014050768478275366248108923366328095899343801270111152240906954275776825865228792303252410200003812030838965966766135547588341334766187306815530098180130152857685278588510653805870629396608258594629734808653690", + "r": { + "master_secret": "61760181601132349837705650289020474131050187135887129471275844481815813236212130783118399756778708344638568886652376797607377320325668612002653752234977886335615451602379984880071434500085608574636210148262041392898193694256008614118948399335181637372037261847305940365423773073896368876304671332779131812342778821167205383614143093932646167069176375555949468490333033638790088487176980785886865670928635382374747549737473235069853277820515331625504955674335885563904945632728269515723913822149934246500994026445014344664596837782532383727917670585587931554459150014400148586199456993200824425072825041491149065115358", + "name": "26931653629593338073547610164492146524581067674323312766422801723649824593245481234130445257275008372300577748467390938672361842062002005882497002927312107798057743381013725196864084323240188855871993429346248168719358184490582297236588103100736704037766893167139178159330117766371806271005063205199099350905918805615139883380562348264630567225617537443345104841331985857206740142310735949731954114795552226430346325242557801443933408634628778255674180716568613268278944764455783252702248656985033565125477742417595184280107251126994232013125430027211388949790163391384834400043466265407965987657397646084753620067162", + "age": "12830581846716232289919923091802380953776468678758115385731032778424701987000173171859986490394782070339145726689704906636521504338663443469452098276346339448054923530423862972901740020260863939784049655599141309168321131841197392728580317478651190091260391159517458959241170623799027865010022955890184958710784660242539198197998462816406524943537217991903198815091955260278449922637325465043293444707204707128649276474679898162587929569212222042385297095967670138838722149998051089657830225229881876437390119475653879155105350339634203813849831587911926503279160004910687478611349149984784835918594248713746244647783" + }, + "rctxt": "49138795132156579347604024288478735151511429635862925688354411685205551763173458098934068417340097826251030547752551543780926866551808708614689637810970695962341030571486307177314332719168625736959985286432056963760600243473038903885347227651607234887915878119362501367507071709125019506105125043394599512754034429977523734855754182754166158276654375145600716372728023694171066421047665189687655246390105632221713801254689564447819382923248801463300558408016868673087319876644152902663657524012266707505607127264589517707325298805787788577090696580253467312664036297509153665682462337661380935241888630672980409135218", + "z": "60039858321231958911193979301402644724013798961769784342413248136534681852773598059805490735235936787666273383388316713664379360735859198156203333524277752965063504355175962212112042368638829236003950022345790744597825843498279654720032726822247321101635671237626308268641767351508666548662103083107416168951088459343716911392807952489009684909391952363633692353090657169830487309162716174148340837088238136793727262599036868196525437496909391247737814314203700293659965465494637540937762691328712617352605531361117679740841379808332881579693119257467828678864789270752346248637901288389165259844857126172669320275054" + } + } + }, + "updatedAt": "2023-03-18T18:53:55.036Z" + }, + "id": "6ef35f59-a732-42f0-9c5e-4540cd3a672f", + "type": "AnonCredsCredentialDefinitionRecord", + "tags": { + "credentialDefinitionId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/CLAIM_DEF/728265/TAG" + } + }, + "1545e17d-fc88-4020-a1f7-e6dbcf1e5266": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "1545e17d-fc88-4020-a1f7-e6dbcf1e5266", + "credentialDefinition": { + "ver": "1.0", + "id": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/CLAIM_DEF/728266/TAG2222", + "schemaId": "728266", + "type": "CL", + "tag": "TAG2222", + "value": { + "primary": { + "n": "92672464557302826159958381706610232890780336783477671819498833000372263812875113518039840314305532823865676182383425212337361529127538393953888294696727490398569250059424479369124987018050461872589017845243006613503064725987487445193151580781503573638936354603906684667833347097853363102011613363551325414493877438329911648643160153986822516630519571283817404410939266429143144325310144873915523634615108054232698216677813083664813055224968248142239446186423096615162232894052206134565411335121805318762068246410255953720296084525738290155785653879950387998340378428740625858243516259978797729172915362664095388670853", + "s": "14126994029068124564262196574803727042317991235159231485233854758856355239996741822278406673337232628669751727662479515044513565209261235580848666630891738643990084502393352476512637677170660741636200618878417433799077613673205726221908822955109963272016538705991333626487531499501561952303907487494079241110050020874027756313402672435051524680914533743665605349121374703526870439925807395782970618162620991315112088226807823652545755186406850860290372739405126488851340032404507898084409367889215777693868794728141508635105180827151292046483128114528214465463152927678575672993454367871685772245405671312263615738674", + "r": { + "master_secret": "26619502892062275386286102324954654427871501074061444846499515284182097331967223335934051936866595058991987589854477281430063143491959604612779394547177027208671151839864660333634457188140162529133121090987235146837242477233778516233683361556079466930407338673047472758762971774183683006400366713364299999136369605402942210978218705656266115751492424192940375368169431001551131077280268253962541139755004287154221749191778445668471756569604156885298127934116907544590473960073154419342138695278066485640775060389330807300193554886282756714343171543381166744147102049996134009291163457413551838522312496539196521595692", + "age": "66774168049579501626527407565561158517617240253618394664527561632035323705337586053746273530704030779131642005263474574499533256973752287111528352278167213322154697290967283640418150957238004730763043665983334023181560033670971095508406493073727137576662898702804435263291473328275724172150330235410304531103984478435316648590218258879268883696376276091511367418038567366131461327869666106899795056026111553656932251156588986604454718398629113510266779047268855074155849278155719183039926867214509122089958991364786653941718444527779068428328047815224843863247382688134945397530917090461254004883032104714157971400208", + "name": "86741028136853574348723360731891313985090403925160846711944073250686426070668157504590860843944722066104971819518996745252253900749842002049747953678564857190954502037349272982356665401492886602390599170831356482930058593126740772109115907363756874709445041702269262783286817223011097284796236690595266721670997137095592005971209969288260603902458413116126663192645410011918509026240763669966445865557485752253073758758805818980495379553872266089697405986128733558878942127067722757597848458411141451957344742184798866278323991155218917859626726262257431337439505881892995617030558234045945209395337282759265659447047", + "height": "36770374391380149834988196363447736840005566975684817148359676140020826239618728242171844190597784913998189387814084045750250841733745991085876913508447852492274928778550079342017977247125002133117906534740912461625630470754160325262589990928728689070499835994964192507742581994860212500470412940278375419595406129858839275229421691764136274418279944569154327695608011398611897919792595046386574831604431186160019573221025054141054966299987505071844770166968281403659227192031982497703452822527121064221030191938050276126255137769594174387744686048921264418842943478063585931864099188919773279516048122408000535396365" + }, + "rctxt": "71013751275772779114070724661642241189015436101735233481124050655632421295506098157799226697991094582116557937036881377025107827713675564553986787961039221830812177248435167562891351835998258222703796710987072076518659197627933717399137564619646356496210281862112127733957003638837075816198062819168957810762822613691407808469027306413697001991060047213339777833838291591976754857934071589843434238025803790508552421154902537027548698271140571140256835534208651964449214890690159171682094521879102663244464066621388809286987873635426369915309596945084951678722672915158041830248278889303704844284468270547467324686757", + "z": "90415953543044389740703639345387867170174070770040351538453902580989033567810029650534915348296084212079064544906463014824475317557221991571331212308335167828473551776349999211544897426382305096215624787217055491736755052175278235595298571339706430785816901931128536495808042995635624112114867111658850659510246291844949806165980806847525704751270260070165853067310918184720602183083989806069386048683955313982129380729637761521928446431397104973906871109766946008926113644488012281655650467201044142029022180536946134328567554182760495139058952910079169456941591963348364521142012653606596379566245637724637892435425" + }, + "revocation": { + "g": "1 1864FF219549D1BC1E492955305FC5EED27C114580F206532D2F5D983A1DD3BD 1 0414758D7B6B254A9CA81E1084721A97CA312497C21BB9B16096636C59F9D105 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "g_dash": "1 2327DA248E721E3935D81C5579DD3707882FFB962B518D37FB1112D96CC63611 1 164989452135CF5D840A20EE354DBF26BEEC74DE7FD53672E55224BEE0228128 1 0634D5E85C210319BFD2535AFD8F7F79590B2F5CC61AF794218CC50B43FBB8C6 1 0A63F1C0FC2C4540156C7A2E2A2DF1DDF99879C25B4F622933707DD6074A0F1B 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000", + "h": "1 0A031B1932CDFEE76C448CA0B13A7DDC81615036DA17B81DB2E5DFC7D1F6CD6F 1 06F46C9CC7D32A11C7D2A308D4C71BEE42B3BD9DD54141284D92D64D3AC2CE04 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "h0": "1 1C88CA353EF878B74E7F515C88E2CBF11FDC3047E3C5057B34ECC2635B4F8FA5 1 1D645261FBC6164EC493BB700B5D8D5C8BF876FD9BA034B107753C79A53B0321 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "h1": "1 16AC82FE7769689173EABA532E7A489DF87F81AE891C1FDA90FE9813F6761D71 1 147E45451C76CD3A9B0649B12E27EA0BF4E85E632D1B2BEC3EC9FFFA51780ACE 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "h2": "1 2522C4FAA35392EE9B35DAC9CD8E270364598A5ED019CB34695E9C01D43C16DC 1 21D353FB299C9E39C976055BF4555198C63F912DBE3471E930185EF5A20470E5 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "htilde": "1 24D87DBC6283534AE2AA38C45E52D83CC1E70BD589C813F412CC68563F52A2CA 1 05189BC1AAEE8E2A6CB92F65A8C0A18E4125EE61E5CEF1809EF68B388844D1B1 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "h_cap": "1 1E3272ABDFD9BF05DB5A7667335A48B9026C9EA2C8DB9FA6E59323BBEB955FE2 1 031BD12497C5BBD68BEA2D0D41713CDFFDCBE462D603C54E9CA5F50DE792E1AB 1 05A917EBAA7D4B321E34F37ADC0C3212CE297E67C7D7FEC4E28AD4CE863B7516 1 16780B2C5BF22F7868BF7F442987AF1382F6465A581F6824245EFB90D4BB8B62 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000", + "u": "1 1F654067166C73E14C4600C2349F0756763653A0B66F8872D99F9642F3BD2013 1 24B074FFB3EE1E5E7A17A06F4BCB4082478224BD4711619286266B59E3110777 1 001B07BEE5A1E36C0BBC31E56E039B39BB0A1BA2F491C2F674EC5CB89150FC2F 1 0F4F1E71A11EB1215DE5A081B7651E1E22C30FCCC5566E13F0A8062DB67B9E32 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000", + "pk": "1 0A165BF9A5546F44298356622C58CA29D2C8D194402CAFCAF5944BE65239474E 1 24BA0620893059732B89897F601F37EF92F9F29B4526E094DA9DC612EB5A90CD 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "y": "1 020240A177435C7D5B1DBDB78A5F0A34A353447991E670BA09E69CCD03FA6800 1 1501D3C784703A097EDDE368B27B85229030C2942C4874CB913C7AAB8C3EF61A 1 109DB12EF355D8A477E353970300E8C0AC2E48793D3DC13416BFF75145BAD753 1 079C6F242737A5D97AC34CDE4FDE4BEC057A399E73E4EF87E7024048163A005F 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000" + } + } + }, + "updatedAt": "2023-03-18T18:53:59.067Z" + }, + "id": "1545e17d-fc88-4020-a1f7-e6dbcf1e5266", + "type": "AnonCredsCredentialDefinitionRecord", + "tags": { + "credentialDefinitionId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/CLAIM_DEF/728266/TAG2222" + } + }, + "d7353d4a-24fc-405f-9bf5-f99fae726349": { + "value": { + "metadata": {}, + "id": "d7353d4a-24fc-405f-9bf5-f99fae726349", + "createdAt": "2023-03-18T18:53:59.857Z", + "associatedRecordId": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "role": "sender", + "message": { + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "@id": "c5fc78be-b355-4411-86f3-3d97482b9841", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "name": "name", + "value": "John" + }, + { + "name": "age", + "value": "99" + } + ] + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjI6VGVzdCBTY2hlbWE6NS4wIiwiY3JlZF9kZWZfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjM6Q0w6NzI4MjY1OlRBRyIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiODUxODAxMDMyNzEzNDg5NzYxOTg5MzAzNjMzMDkzOTEyOTExMDUxNjI0OTQ0OTYzMTgzNzM2MDY3NDkwOTc2MDYxODEwMDgxODkxMzQiLCJ4el9jYXAiOiI4NDk0NDg4MjQzNTk2NTkwOTc2MjQzMjc0NDg4ODk2Mjc1NTcyODAyMTQ1ODE5NDQzNTE0NzQxMzk1NDI1NjM5MzQwMTczMDIzMTQ5NzI3MDY5NzMzMzQwODgzMTU4MzQ1NTYzOTA5OTcxNDMzNTg1MjMwODAxNTYyMTM0NjczNjM1ODg5NTA3Njg5ODQwOTgyODU5Mzg1NjA1MTc1NTkxNDYxOTkyMDExNzU2Mzg1MTI3MTQ3ODgxNDMwODEzNjYxNzY0MDU5MzE0ODk4MTc2NzQzMTQ5MjYzMDMwMDQ1NzMwMDMzMzI2NzgyMzg1OTY0NjcxMzg1ODQ2MzcxNjQ5MzQxMTg2MDM5NjE4MTQwOTIwMDUxMzg1MDAwNTYxMDcyMTc5NTEyMzc5Nzk0OTU4NjE1ODIyODI2OTExNzIwNTQyNTE0MTQ1NDc5MTAxOTUyMzM4MDMwMDY1MDk5NjcxOTU2OTMxMzE2NjE5MjM0NTQ0NTE5NTQ1ODQ1MzA4MzgxMjQyNTM0NDcyOTc3NjY0MjAwMjc2MTMyOTgxODE1ODAzNTIxOTExMzk4ODkxMjE0NjE1NzA1MDM2ODM2ODU1NDU1NzY4ODg4MTUxNDgzODAyNDcyODQyMzczNzE0MTI0NTYwMzIyNTI3NDE4MTEwNzYyMjgyNzY4NzMyNTIzMDQyMDA3MDY2OTk2ODIxMTQwMzE1NDg0NzI4NTM2NzIwNDI3MDg5MTI2NDk1NTAzMjc0ODQ4MDM3MjUzOTM3NjI3MDU2ODUzMTQ4NjE5NDA4NDYxOTI5NzEzMjM4MjEwNDc4MjcyMTIxNTUwNjQzODc4ODM1NDYwMzY1OTIwMjE3NTk5NDYyNDUzMDMyNDQ4MjYyMTM3NjE5ODY0OTU4MzA1MDE3MjA4OTYwNDc1MTQxODgwMTMiLCJ4cl9jYXAiOltbIm5hbWUiLCI1MDcyNzU2NDE2NDA2ODIxNzU1OTc0MzUxMTg0NjE1NjA4NDY2NTk3Mzk0NzA2MTY1NDg2ODAzMjc3MjMyMzQyOTk4MDA0MzY0OTU0MTczMzc0NDIwOTc5NTkwMDcyODgxNDgxNDA0MTg2OTExODg5NzQ4MTgzMzQ1OTk5NzQ0NzgxMTQ1MTMwNzEyNDIzODY0Nzc1MzQzNjAzNTk2NDM3Mzg4OTgzNTExNDAzODA0NjEyNjU1MDE5NzQ4MTI5NDk3ODY2NTcwMDQyMjcwNDQxNDQ5MjYwODY0NzgyMzI5MjAxNDEzMTc5ODU3NzA0MjM5OTMyMTg4NTc4NzE3MDczNzM3NjUyNzY5MzY5NDg4OTgxNzg2NDQwNTExODAzMjMzNDMxNzA4NDk4MTU2NTA0OTUzNzkzNjU2NjQ2NzMyNTU4MzQwNDI2MDI1MjA3NTk0OTIwMDY4OTc2OTQ4Nzg2OTUxNzM3MDIwNDQ0NTA5NzYyMDQ2MzIzNzA0MDQ3MjU1ODU3NDE5ODE3MDc5NTI3NDgzNTE1NDY2NTAyMDkzOTY1NDMzMzk3MjQ1MzA4MjQ5MDgyMTQ4Mjc4NDA1MzI5Njg1Mjc0MDYwNjk0MzI0MTI2ODgxMjkyMDIyMjY1ODczMjk5MDU0NDU1OTA5NzkyNjUwNjAyMTk0NjUzMjYxMDk0ODYwOTc2NzA4ODE1ODgwMjExMTY0MTkwMDM0NjY0MzI2MDc3NjcwNzkyMDE4NTE2MzMzNDI3NjkwODYwMjIxODEwMzk5MDgxMjc5NjAwNTYzMjk3MjI0NjM0MDM0NjcxNTIwODE5MzU3NzQ0Njk2NzU1Njg1NDI2NjIzMzAwMjQ3MDUwODE4NTQ2MDM2NjA0NjMxNjcyNzE5MjI0NDA4NTE2NDM4NTgxMDM5Njk4NzI0MSJdLFsibWFzdGVyX3NlY3JldCIsIjU2MzYzNTgyMDQ5Mjg4OTY1OTg1MDA4NzgyMzU0NjgyNjMwNDkxMzQ3MTM1NDIxNTAyMDEyMTIwMzI4MDI4ODIyMjUyMzg4NjgwNTMwNTgxMTcwMTgwNDU1MTcyNTc3ODkyMTEyMTY1OTM0Mjk5NjUyNzAxNDExMzUyNDkzMzkyODU0ODI4NzMyMDQzMDI0MDI0MzM0MzMzNzc0NjEyOTEzOTUyMjAzNjM1NDk2MDQ0ODMzMDI5NDE2NjUwOTU5NjE0ODgzNTUwOTMxNzgzNTA5MzE1Nzg4MDEyODQ0MzAwMDQwMDE5MTY5MTc3NTI1OTgxMTU3OTkwNjQzMDcyMjQyNzcxMjU0MTYyNzMxOTU4NzI2Nzc1NjYwMjkxODIzMDcyNDk1Mzg0NzM5MTcwODc4ODMxNzkxMjQzMjEzMjU5MzA5ODQxNjU3MjUwOTg1NzMxMjEyNzE2MDM2MDY3MDUxNjM2NzA0MjA1NDEzMDk2MDU3MTA2NTM2MTI2ODUyNDU0NzcwMzQzMTMwMTczMjAwNjEzMDIxOTE4MzgzMDQxOTU4MTkwOTE2NzQ0NjU4NTI0ODA1NjM4Mzk2OTY3OTA3MzIwNjY1MDU1MzcwMjY0NjAxMDczMjc5NDMyNjM5MjM3Njc1NTA0OTg1NzQyNTI4NjYwMTAyMDEzNzIxMzA2MTE4MTg0NDk1MTEyNDQ2NDYyNDc2NTkwMjYxODkxMjA0OTQxOTA4MjMyNzMzNDA3MTg4MDA3NzE2NTA2OTUzMDY0Nzc5NDk5ODExNzI0ODI5NjcyNjY2NzIyNjIzOTAxMTc1OTk0NTIyNjkwMjk1ODI0MDgyNzY5NjQ0NDYxOTAxMDk2NzI3MTE5NzAzMjUzNzI4NjY3MTU1MzA5MDYzNDUyNDY2MDY3NzU5NzIwOTgyNDA3MiJdLFsiYWdlIiwiMTM2NTQxMjE0MjM5MTcyNDQxNzQ1MjU3MjcyMDI3MTA4NDYwMzU0MjgxMTA2OTA2MzYwNDIwMDE0NjUyMDIxMDgyNDEzODM2ODEyMjk3NjY3ODk2MTYzNDkzMjM4NDIxNDI4NjMyNTMxODE0ODk4NzkwMDg4OTg2NjgyMTE2OTAyMzc4NDgwNTE4OTUxNDExNzg1OTk3NTk5MDMyNDYxNjExNjIyMDUyNjMzMDQ5ODYxMzc5MTQzNzI4MTM5MTUyMDkyMzI0ODc3MjMxMTYwNTgzNzA5NjE0MzA1NzQ1MjA5MjQwNjU2MDU4NjY3OTMwODEzNzYyNDY5MDc2ODc5MTk1Nzg0Nzg4NTE2NjI3MjgxMDY0NjE3MzgzMDc4Njc5MTkwODIwMzQwNTgwNDY2MjU3ODU3NjA1MTc2MTg4NTI3OTMxMDI4MTMzNTY5Njc0Mzg2ODAwMTA2MDE2MDg1Nzc0OTcyMzI1NTAyNDA2MTY0OTY0MjU2OTUxNDI3ODAxMTQzNTQxMzUzMzI0Nzg0MzA5OTY4MjIyOTU1NDk4Njk3NTAwMDUwMzc0MDg0NjIwNzQ4MTk0NzIyMTI2NjE2OTY3OTY3Mzc1NTM3Nzc5NTc4NTMwMDIxODExNTA2MTIxNjcxMDUwNDgzNTM2MjA3Njc3MTg5NDQwNjEwNzk0NTcyNzI5MTgzMzAyMjM1MDkxMDg4NTU2ODc5NTg3OTE3MDMzMzQyODcyMzg2NDQ5MTQ0NzgwMDYyNjc4MzA3NzE4MzU1MjQ5MTUxNjc5MDA1MzkxNDA5NDE4OTQxMjEzNDkxMjQyMjg2NTAwODcyMzQxNDI3Nzk1MjQ1ODYzODE2MDY2NDY3NDkxOTg4OTU3MDEwNDIxNDA3NDkyMDUxOTc0NTMwNjIxOTk1ODU0ODczNTM5Mjk3MyJdXX0sIm5vbmNlIjoiNzk3NjAzMjE3NzA5MzM1MzAwMTcwODI4In0=" + } + } + ], + "~service": { + "recipientKeys": ["GVbwqUMqzxKaEVWjn1aPBfjJpYQHVejinpx8GCeEuQjW"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3000" + } + }, + "updatedAt": "2023-03-18T18:54:00.011Z" + }, + "id": "d7353d4a-24fc-405f-9bf5-f99fae726349", + "type": "DidCommMessageRecord", + "tags": { + "role": "sender", + "associatedRecordId": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "protocolName": "issue-credential", + "messageName": "offer-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "messageType": "https://didcomm.org/issue-credential/1.0/offer-credential", + "messageId": "c5fc78be-b355-4411-86f3-3d97482b9841" + } + }, + "be76cfbf-111b-4332-b1fe-7a1fea272188": { + "value": { + "metadata": { + "_internal/indyCredential": { + "schemaId": "A4CYPASJYRZRt98YWrac3H:2:Test Schema:5.0", + "credentialDefinitionId": "A4CYPASJYRZRt98YWrac3H:3:CL:728265:TAG" + } + }, + "credentials": [], + "id": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "createdAt": "2023-03-18T18:53:59.068Z", + "state": "done", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "protocolVersion": "v1", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "John" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "99" + } + ], + "updatedAt": "2023-03-18T18:54:01.378Z" + }, + "id": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "type": "CredentialRecord", + "tags": { + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "state": "done", + "credentialIds": [] + } + }, + "e531476a-8147-44db-9e3f-2c8f97fa8f94": { + "value": { + "metadata": {}, + "id": "e531476a-8147-44db-9e3f-2c8f97fa8f94", + "createdAt": "2023-03-18T18:54:00.005Z", + "associatedRecordId": "a56d83c5-2427-4f06-9a90-585623cf854a", + "role": "sender", + "message": { + "@type": "https://didcomm.org/issue-credential/2.0/offer-credential", + "@id": "4d2c80b7-4a25-42ac-b8cf-a68b1374b9b7", + "formats": [ + { + "attach_id": "8430ddb8-b0c3-4074-8ded-f4dcfe80303d", + "format": "hlindy/cred-abstract@v2.0" + } + ], + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/2.0/credential-preview", + "attributes": [ + { + "name": "name", + "value": "John" + }, + { + "name": "age", + "value": "99" + }, + { + "name": "height", + "value": "180" + } + ] + }, + "offers~attach": [ + { + "@id": "8430ddb8-b0c3-4074-8ded-f4dcfe80303d", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjI6QW5vdGhlclNjaGVtYTo1LjEyIiwiY3JlZF9kZWZfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjM6Q0w6NzI4MjY2OlRBRzIyMjIiLCJrZXlfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjQwMTQ0MTA0NDg3MjM0NDU2MTc1NzYwMDc1NzMxNjUyNjg1MTk0MjE5MTk5NDk3NDczNTM4NjU4ODM3OTIyODMzNTEzMDg0Nzk2MDQ5IiwieHpfY2FwIjoiMzgxOTQyMjM1Mzc3MzYwODEyNjY0MDA4MjYzNDQxMDg2MDMwOTMyMjAxNzgzMjM3ODQxODQ5NDg3ODk2ODg1MTYwODY2MTY1MDM3NzI2MTIxNjU0MjcwOTg5NDY3NjAzNDExOTAzODk4MzUwMDAzNDIwODg3MzI4NTUwMTY2MTI1ODMyMjAxOTQzMTkwNzAxMDU4NTAwMDE5ODM1NjA1ODczNDYzOTkwODg3NzQ0NjY3MzU0MjM2Njc3MzcyODg0ODQyNjE5NTEwMTUwOTA2MjI1OTMzMjc1ODEyNjg2NDg3NTg5NjY3ODI3MjAwODcwOTQ0OTIyMjk5MzI3OTI4MDQ1MTk1OTIwMDI3NTc0MDQwNDA4ODU5MzAwMzY1MDYwODc3Nzg2ODkwOTE1MDU5NTA2ODc1OTI0NzE2OTI1MDM2MTc4Njg2NDE5NTYyMzcwODI4MTMzODY2Nzg3NzkyMDcwNjAyNDQzNTkzMTk2NzEzNzcyNDM2NTYzODI0MzkwMDIyNzg4MjU2MzA4NjU4OTc0OTEzMTk1ODYxODUwMTQ3ODE1Mjg5NzQwOTA4NDk1MjQ3NTAyNjYyNDc3NzQ2NTI5ODA3Mzg0OTgxODI5MDc3NTQ4OTI2NzExMDkzNzQ5MjM1ODU4NjUwNDc5NzE5NDI4MzUwMzAwNzUyNjQ0OTg1MTQ5MTMxNjA1NjUzMDIxMDYxNzkwMjY3MzQyNTY4NTkyNTY2MTQ0MDM5NzY4OTg0NTMyNDMzNzk0MzUzNjQ2Nzg1MjA3NDgzOTk2ODQ0OTcxNTgzNzY3NDQ5ODYyODgxMjMxMjI1MzM4MzAzMTQ4NzA0ODczMDEzNDM3MDgyNzY1MTk4OTY2MzE5NTM0OTkyNjk4MzMzMDQ0MDI3MjIyNTYyNTIzNzk3ODk5Mjk2MTQ1NDU5IiwieHJfY2FwIjpbWyJtYXN0ZXJfc2VjcmV0IiwiOTE5MTc5NzQ4MTE5NTg5MTY3Njc5MjQxODk5NzY0ODIwNTk0MDc5OTQxNzIzOTgzOTYyNzQ1MTczODM0NDQxMDQ3MjU4MDcyOTE4OTUzNjIzOTQ4MDMyNzI2NDgyNzI2MTEwOTk2Mjk3MDU3NTYwNjcwNzAxOTU1MTkxNDc0NjM0MzQ0ODMxMzg3NTk4NzI2MzMxMjc0NjI4NDU3Njk5NzczMDA1NDMwMDIxNzMwMzg4MzcwMTEyMjc3MzI2MzU4OTgwMTA3ODIzNzUzODc3MTU0NjIwMDkzMjE5MjYyNjAxNDM2NzMyNTgzNDI4Nzc4NDA4OTc0NTQyNzkzMDk0NTQ5MTczOTA3MzQ3OTUxNTc1NjM5NzU2NDg5MTA0Mzk0MTY3NzExMzY1MjM3OTI1MjAwNjk4OTg5NTI5MTQ3OTIzNTYzNDMyODgyMzgwMTg0NzU0NzkzODMwMTE3MTQ1MDAwMTI0NDYxNjkzOTcxMDQ5MjgzNDk1NTE4MDQxMDc5ODUyMzAwMjk0NDM1MjYzOTIwNDU0NTU3MzUxNDQ1MDM3NDI4MDg3OTk2Mzg2NjY3NjU3Nzk5OTYyNzQzNzIyNzA3NzczOTEzMzc0NzIxODUyNTQ3MjkwMTY5MjI5NTAzMTQxOTMwODYzNTk4NTExNjc4NDEyMDE0MzE2MDM2MzYxMzczNDcwOTQwMDEyODcwMDgwMDA2MzE0NzYxNzYzNzUyNzYwODk5MTQ3NzA1MTA0NzQyNjAxNjkxMzMxNjkzMDIwMjg2MjA2NzQ2NzE0MzI3NjU2MjA2NTMzMjk3NDg4MjU2NTM2NTQ3MzY4MjM2OTQ2MDM5NzAzMzc0OTMzNTE0NTc2NDg2NjQyNTY4MjgyNTY2MjMyNDU1NTU5MDY4MzE3NzU5NDM0ODU4NTI3MDg2NjQ0Il0sWyJoZWlnaHQiLCI5MjMwMzkyNDc1NjI4ODc1MjA4OTM0NjM0NzE4MjYzNzA4MDIzOTI1MDU0NjY2NDgzMzgxMzIyMzc3MDg1MjMxMjU4MTM4MzgwOTU1NTk3NDQxNTEyOTYwNDA2MjI3MjUwODgyNjA3NjExMDkwODk3MTM1NDcxNzAwMDIzNDcwOTM2ODg4MDE3NDY5Nzk0ODYzNDk4NzUyNTI3Njc3MjMwMTEwNzg0ODQzNzI0NDUyNTUzODYyOTA2MzM5MDc0OTIzNDU4NTQ3NDYzODcwNzU3OTg5MzMxNzk4OTI2MjM4MjUxMTM2NTYzNjM2MjIyOTQwNDkwMzY3MjQ2OTg0OTU2NTE5MTAzODcwNDE0MDM5NzM2MDE2MDY5MzA2NjQ0NjQzODI4OTgxMTE3OTM3NzYyNDAzODY1Mjc1MDU5MjEyOTY2NzIxOTU3MzM0MTM2ODEyMDI0OTE0MzA4MzAxMzk5MzM4NzMyOTIzNTA0MjA5MDM5ODMxMTc5NjU1NTkyNjg0MjMyMTIzMTI2Mjc4ODQzNDMyOTUwMTk1Mjg3MzE4ODI3NTM2MTMwNDQ3NzM3MTgwMjk3MDE0ODEzNDg3NDQyOTg2NjQ1NzQyNjEyMzE5NzQxNDY2MDMyNTg5OTU0NzYwNjE4MDU0MDUxMjAzMTE1NTAxNDcxNDExMzg3NzU0NDk5MzAwNTU4MTc5NjM5NDAxOTM0NTAzMTMyMDEzMjAzOTg2NzkyMTEzMDAzNTkwODg1NTc3NjgyMzU2NDY3MjA5NTUwNjQxODQxMDYyNTkzNDYyODIwODg3NzgxNDYyODM3ODkzODcxNDM4MzM3Mjc5MTcwMTExMTQ5MTU4NDMzNDE0ODI1NTkyNjcyODU2MzM5OTM4NTgyODg2NzM3OTIwMjc1MzI0MjEwMTUzMjE5MjI2OTYiXSxbImFnZSIsIjkxNTg1ODk3NDkwNzE0ODA3OTY2MDYzOTg5MjE1NTMxNDkyOTQwMDI5NDcyMTM4MjgwNjcxNjcyMjQ0NjY5MDc5NzIyNTQyMDU0NTU3NjY0MTcxMDI1NzM1NjQ4NTIwMTM4ODQ4ODAxNzIyMTc4MTcxMTA5NTc0MTMyNTExMzM1MDEwNTc5NzExMzcyODM5MjI3MDExOTg4MTUyMTEwMzI4MTE5MjkyMjI4NjM3MDU4MDQ3NzYwODYwOTQ0NTY3MzQxMjY4MTY4Mjk3NjE5MDM2ODEwMjYwODM2NDI1NDkwMzU3NjE4NzM4NTYxNTY2MTUxODQ3MzIxNzM1MjQ5ODk1MDU5NTY2OTQxODI5MjE0Nzc0MTA0NzYyNTQwMjcyMjk2NjE1NTE3NjUwMDcyNDQyMTI0NjY5MDEzMTc1ODAyMDk5MDQxMzk3MzE5ODQ0OTA2MDgwOTYxNTcyMTcwNjg2NzgzNDM1Mjg2MDUyMzE5ODY3ODExMDE5MjAxMDYwODM2OTM3Mzc0MzM0NDM5MTQxMDAzMTI3NTcyNjgzNTgwODI0OTkwOTg3MjE5MzU4NzkzOTM2NTU4Nzk3MjI0MDQzNTM1ODA5NzMyNzgxMjE1NzEwNjI1MjQzODYwNTk4MTk0MjU2MjAwODkwOTA3ODAzMDcyMTAzNzc3MzkwODk4MDczOTgyNjY3Njc1ODg0MjI3MjU0Mzc2OTI5Mjg3ODQyNDE0MTE0MjcwNDQwMTEzNDUxNjk4NzE5Nzc5NjQyNTI4MDA4NDM3Mzk5NjI0NTE3OTM4Nzg5MDc3ODE5ODA0MDY5MzcxOTM0NzExMTIyNTQyODU0OTg4MDA0Mjc4NDkwMjAxNTk2NjE0MjUwODc3NDYxMDczNjc3NTUzNzYxMTMyMTA5Nzg3NTQ2ODE1ODk5Njc2NCJdLFsibmFtZSIsIjYyNzgwNTIwMTM3MzI3NTUzMDc3MDg4NTE4NDg1NDYyMTA0NjEzMjEyNzY3ODUwMzYwNTc3NDQ4MDUxNTk5MTMxMTM1NTI2NzQ3Nzc2NzMzMDg1MDMwODcyMDE1OTM2MTI2NzE0MTIxMDgxMzg3ODU2MTkwMTkzMzI3ODY3OTE0NTEzODM2NTQ1OTY4Mjg1NTc5ODEyODMxMDI4ODc2Nzg1NzI3OTQ2MTEwNzg5Mzc0MjcyODgzMzkyOTgwNDkwODk3NDkwMTc5MDQ0ODM0NTgwMzQ2ODY4NDI2ODc0ODU4NTY1OTg4NTUyMDcwNjI1NDczNjM4MDM3Njc5NTU1NTk2MzE5MTc3Nzc5OTcxMTIxMjQzMjgyMTIyOTQ2NjY0ODMxOTgxMTg3MzQ3MzcyMjkxMjYwOTM3MzkzNDA1ODk5OTI0NjM4MzE3ODI5MDczODMxMjI4ODc1Njg5MTcyMTg4NjIyMDI5NzcxNzM5MTQ5NDY2Mzg3NTM5NjkyNDQ5NDU5MjczNDI5NzM5MjMzNjkyNTEzNDA5OTkyNDgxNTQ4ODk0NjAzNjM3MTYzNjA4MTM0MTAzMTk3Nzc3NTM4OTYwMDcyMjcyMzYyNzM4NDM1MTM3MDcyNzIxMjExMDYxNTg4MDE3ODczODg3MTEwNDA2OTk1NDQ4ODIwMDEzMDA5MjgyMzk0OTczMDMwMDI5MTY3NjQ5NzY1OTI1MTUxMzY4NTg5OTkyNzMyMDE1ODAwNjAzNzYxOTI3MTg3MDM4MDkxNDY3MDE1MjA3MzIwNDczMDM0NDA3MDIyNDA0NjQ4MTI0NTk2NjQwNjU1NjY1MTIzMzY5Njc0ODI2NDE3MjE2ODUxNTM4Njc1NTM3NzAwOTg4MTQzNzE1NTE3NzMwMTM4NjA4NzkxMjcyMzM0MDUyMzY4OCJdXX0sIm5vbmNlIjoiNDE0MzQ4Njg0NDk2OTAxNjkyMjI2OTY0In0=" + } + } + ], + "~thread": { + "thid": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b" + }, + "~service": { + "recipientKeys": ["DXubCT3ahg6N7aASVFVei1GNUTecne8m3iRWjVNiAw31"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3000" + } + }, + "updatedAt": "2023-03-18T18:54:00.014Z" + }, + "id": "e531476a-8147-44db-9e3f-2c8f97fa8f94", + "type": "DidCommMessageRecord", + "tags": { + "role": "sender", + "associatedRecordId": "a56d83c5-2427-4f06-9a90-585623cf854a", + "threadId": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + "protocolName": "issue-credential", + "messageName": "offer-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "messageType": "https://didcomm.org/issue-credential/2.0/offer-credential", + "messageId": "4d2c80b7-4a25-42ac-b8cf-a68b1374b9b7" + } + }, + "a56d83c5-2427-4f06-9a90-585623cf854a": { + "value": { + "_tags": {}, + "metadata": { + "_internal/indyCredential": { + "schemaId": "A4CYPASJYRZRt98YWrac3H:2:AnotherSchema:5.12", + "credentialDefinitionId": "A4CYPASJYRZRt98YWrac3H:3:CL:728266:TAG2222" + } + }, + "credentials": [], + "id": "a56d83c5-2427-4f06-9a90-585623cf854a", + "createdAt": "2023-03-18T18:53:59.859Z", + "state": "offer-sent", + "threadId": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + "protocolVersion": "v2", + "credentialAttributes": [ + { + "name": "name", + "value": "John" + }, + { + "name": "age", + "value": "99" + }, + { + "name": "height", + "value": "180" + } + ], + "updatedAt": "2023-03-18T18:54:00.007Z" + }, + "id": "a56d83c5-2427-4f06-9a90-585623cf854a", + "type": "CredentialRecord", + "tags": { + "threadId": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + "state": "offer-sent", + "credentialIds": [] + } + }, + "598dbcc3-5272-4503-9c67-b0cb69a9d3d6": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "598dbcc3-5272-4503-9c67-b0cb69a9d3d6", + "createdAt": "2023-03-18T18:54:01.126Z", + "associatedRecordId": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "role": "receiver", + "message": { + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "@id": "2a6a3dad-8838-489b-aeea-deef649b0dc1", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiY3F4ZW8ybzVIWmVjYWc3VHo5aTJXcTRqejQxTml4THJjZW5HV0s2QmJKVyIsImNyZWRfZGVmX2lkIjoiQTRDWVBBU0pZUlpSdDk4WVdyYWMzSDozOkNMOjcyODI2NTpUQUciLCJibGluZGVkX21zIjp7InUiOiI3OTE4NzUwNzgzMzQxMjU3ODU3OTUzMjc2OTU5MjcxOTcyMDQwMjQxMTU1MzcyODEwOTQ2NTEwMjk5MDA1ODEyMTcwNjkwMjIzMTQ2ODU0NDk1MTI1NjQ1MTg3ODQxNzk0NjA3OTUwOTQ1OTM3MDYxMjk1ODgwMjIxMzE2NTg1MTIyNDY1Mzk1MjAwNDQ3MDIwNzAxNDA0NjEwMDc4MzkyMjA4NzkxMjk5NzYwMzM4OTIxNDMzMDc1Njk2ODU0NTY3MTIzNjYxNTYwNDMwNDE3NzQwMzc5MzA4NDQzODcyOTU1NzAwNTk1MTg2NzcxODY3MzM5NzQ5NDgzODYxNTQ2NTE2MTU4NTM5MjkyNDQzNTQ3OTg3MDUwMzE4OTAyOTI5OTgyNzMzMDk1ODk4MDIyMjg2OTk1OTQwMjkzMTg3NTg5NDgwNTgwNTAwNjM0NzAyNjQxNDM0MTgxNjIwMTU4OTU3MzUyNTE1OTA4NDE2MjI4MDQ0NDA2OTU4MTk1MDg4Mjc0ODI4Njc3OTQxMDgxOTczOTg3NjU1MDEzNDUxMzA4ODQyMjYyMzY4MTQzOTExNjIxMTE0NzYyNTk3Nzg1MDczMTM4MDg3NTQ2MDIyMzc1NjQxODQ5ODI2OTg2MjYwMDE5NTAzNzE3OTk0NjM3MzIyNDExNTgzNTY0MTQ1NjcwMTM5OTQ1MjQxOTU2Njk2MDQ3MTQzNjA4NjQ0MDM5OTg2NTYwODUyMzA1MTczMjUxMTUyMDIzODI5NjI3MzQyODM2NDI3MjkwNDQ5NTA3OTY0Nzk4MTQ2NjkzOTUxMDkwNzUwOTAyNiIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6Ijk4MjIzNzMzMDcwMDk1NjM0MzU5MTE2MDEyOTgzMDcyMjc5MjcxMzk1MTg0NjA0NDcxNDQ5NzA1Nzg0MTEyNDAyODMwNzIyMTUxMzIxIiwidl9kYXNoX2NhcCI6IjU5ODA0MTY4ODAxODk1NDAzMjkwMzczMTA3NzA4MzUwODc1NjY4MDcyNDY3ODAyOTcyOTIyNjUzNDE5ODU2MjMyNTIzNDI4OTUxODQ4NjEyNDE1MjM5Nzk3Mjk5ODY2MzIxNjU5NDQ1MTM1NzQ4NzU2MDY1NjgyOTY5MjY4ODI5MTYyMDA0NjQ4NzYwMzg4NTg4NDkyNjg1NDI1MTg1MDU2OTAxOTkxMjcwMzYwMDk3MDc5NjEyNTYxMzY4NzU1OTcwMjY5MjI4MDYzMjMyODU0NzI0NDkyOTA5Njg5MDMyOTg4NjYyMjk5Mzg3MDU2NDEwNjU5MDU3MDUwNjE0MDQ2NzE1NjA0NTgyMzM2NTg4MjMxMjI3MTEzMDEzMDQxMTA0NTU2NzM1MDE3ODUwNzUzODcwNjc2ODYxNDA4MjA0NzkzMDIzNTYwMDEwNTEzODAwNzA4MjAyNjAyNjQ0Mjg2NzI4NjAyOTk5MzU5MDcwODQxMTQ5MTAzMjA5MzY0ODkyMzMzMDYwMDgzMTA5NDIzOTQ5NzE4NTk5NjEzMzk2NjIyMjc4MTExMzk5ODU0MTcyMjMyNTQzOTk1Njc5NDk3Mjk1Nzc1NzA0MjA0MTQxOTU2NDI1MDc4NjYzMzgwMDA1Nzc2ODY2MTcxNTY4OTU1NjE4NjAwMTA2NzkxMjIyNDkyODA2NzI1ODU1NDY2Nzk4OTEzMTc2NDcxMDY3MTk5ODQ2ODEwNDI5MDIzMDc3ODI3OTc1OTIzMDIzNjU3MTg0NjkwNzE0MjkxNDk0MDc5MTM1NzYyOTUxNTc0MjMzNjMwMjExNDQ1Njc3NzE1Mzg3Mzc1NjkyMjAzODE3NDczNDk5NDExNzE5MzIwMjczNzExOTIzMzM3MzYzMTAyOTkwMDcyMjE2MjYzMzUxMzMxNTk4ODk1OTU3MzU1MDc1NTEzODE0NTUwNzkyMCIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiNTY0MTY1NjA2NjM0MTUwODgzMjg2MjM1NDM1MjUyODQ3MjAxMTk4NTQyNDAxMjYzNTY2MDQ2MTA3OTU3NzI4NTQ0MTMwMjgzNjUyNjQzOTI0NDc2NTU2NTIzNzg0ODI1OTgyMzMwMzc4NDI4OTM0MDkxNDcwNDM0OTAwMTM3NzkwNDkxMjM4NTA4ODk2ODQxMzkwNjQ4MTA1NzY5ODYzMzI1OTAzODI1Mjk1MjU2OTQ0NSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMzE1MDIwOTY0MDY4NjgyMDQ0Mzc4MjEifQ==" + } + } + ], + "~thread": { + "thid": "c5fc78be-b355-4411-86f3-3d97482b9841" + }, + "~service": { + "recipientKeys": ["cqxeo2o5HZecag7Tz9i2Wq4jz41NixLrcenGWK6BbJW"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001" + } + }, + "updatedAt": "2023-03-18T18:54:01.126Z" + }, + "id": "598dbcc3-5272-4503-9c67-b0cb69a9d3d6", + "type": "DidCommMessageRecord", + "tags": { + "role": "receiver", + "associatedRecordId": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "protocolName": "issue-credential", + "messageName": "request-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "messageType": "https://didcomm.org/issue-credential/1.0/request-credential", + "messageId": "2a6a3dad-8838-489b-aeea-deef649b0dc1" + } + }, + "b88030f7-dc33-4e7e-bf4d-cdfaa6e51ebf": { + "value": { + "metadata": {}, + "id": "b88030f7-dc33-4e7e-bf4d-cdfaa6e51ebf", + "createdAt": "2023-03-18T18:54:01.192Z", + "associatedRecordId": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "role": "sender", + "message": { + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "@id": "578fc144-1e01-418c-b564-1523eb1e95b8", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjI6VGVzdCBTY2hlbWE6NS4wIiwiY3JlZF9kZWZfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjM6Q0w6NzI4MjY1OlRBRyIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJhZ2UiOnsicmF3IjoiOTkiLCJlbmNvZGVkIjoiOTkifSwibmFtZSI6eyJyYXciOiJKb2huIiwiZW5jb2RlZCI6Ijc2MzU1NzEzOTAzNTYxODY1ODY2NzQxMjkyOTg4NzQ2MTkxOTcyNTIzMDE1MDk4Nzg5NDU4MjQwMDc3NDc4ODI2NTEzMTE0NzQzMjU4In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjMyMTIwMDQ1ODc4MzIxMjcyMzA1ODI5MTc3NzMzMTIwNzE1OTY5NDEyNjkwNjUyNDQ4OTc0MTA4NzEzNjU0ODc3NTg2MzIzMTI3ODk2IiwiYSI6IjIyMjY0MTYwNjIwODcwNDUyNTExMjcyMzE1MzMzMDA0MjQzMzY3NTM2NzM3NDMwNjM1NjExMjcwMDkwOTE4NDMwNzc0ODEzMjAzNjQwNjMxMjIyNDczMzk0MjQ4MTgzMDIzMjIyNzExNDUwMzQxMDcxOTQyNDQwMDgwMjY2Nzk1Mzg5Mzg5Njc1NjYwOTUzNTQyMDE4OTA3NjQ3NzI4OTQ4NjY1MzA2Njg0NjExNDU1NTI5NzM5OTY1NDcyMjQ2NDQxMzE1NzAxMzM1ODc1MDY3MjExMDk3NzcyOTgwMjU1NDIxMDMzMTI1MjAyMTQzNDk3NjMyOTAyMjM1NDAyMzU5OTA1MzY5MzE4MjI1NTc4MjUxNjY4NTYzNzc1NTY0MDM2MjUxNzE0Mzk3MTEzNjQ3OTg0MjcxMTE5MTU2NDQ3NjI1OTk1NjE5MjAwMDk4MTgzNzY1NjkzMTg1ODEzNjA1NDU3OTQwMzE0MDU2MDkzMTI2MzQ3OTU5MzYwODIyMzg0OTEzODg3Mjg3ODI2NjkyNDIyNDMyNDUwMDA5OTYxNjQ2MjMzNTE3MjY3NDU1OTkyMjA3MTE3Mzk5NzU1NjY3MTA3MzM1NTQ0MzEwNDQwNDE1NDE5NTk5NTA1OTgxMzkwMjk5NDUxNzQyODg4NDg0MTc0NTU5MDA5NDgwNjU5MDk2Nzg2ODI2MDgxNzc3MzcwNTk1MTU3OTg5NjQ1MDYxNDI2OTA2ODM2MDk5NTU5MDQ0MDI4ODM2MzYwOTM2MDkwOTkxNjA1OTU0NTM2OTQxMjgxODQwNzk2MjkxODc0ODk2NDEzNTM5IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDExMzE1MTE0OTUxODg0MDQ5ODkyNjAwNTY3NTgzMTc4ODkwMyIsInYiOiI2NTE5ODc4MzYyODQ5NzExNDY4NDkyNDM1OTM3MDU4NzMzOTYxMTkxNjA3NjI4MzUzNjkxMDg1MzM5MDMwNDU0OTkyODc0ODYyODkyNDg4ODYzNTA3MDQ1MTM1MzA4ODI1NDA2NzYwMTQwNDQzNzM0NDYzODE5NTM2MzE0NzcxMTQ3MDk4MjU2ODMzNTc2MjIwNDI5ODQyNjc3NzMwMzQwODYwNjE2NTcxNzc5NjU4OTIxNDY4Mjc0NTUwOTc5NjYyMDkxNzEwNDU5MDk2MDgzMzYzNTc1Mjc0MjQzNzIyMzIzOTIxMjY5MDYyMjE0NjQyNzQyMTI0MzQ4MTY0MDUxNzE3MTk5MTkzODY3NTM3NTEzNjYzOTY1ODQzMDI5MjAxODA0OTE2MTEzNzMxODYzOTUzNjQ5MDkwNDgzNzMyMTkxNTQ2MTEwMjAxNTg0NzMxODg4NTE5NjA2MjE1OTkyNTgxNzk2MDg2NzUzOTE5NzUxMjkwMDI3MDI4NzczMTAwOTc5ODI5MzQ5NzA0MTUyMDEzNjg2MzU1MzM1MjIyNjU5MDY2NzE0NDQ2NDc4NzY3MTE5NDE4MjY3OTg5NTAyNzc4MjMzNzM3MjM4MjU1MTQxNzQyMjk4NTU3MDY2NzA2MTM0NzYwMjQwMzY3OTMzMzc5NzYzMTc5MTI1NTI4MDQwMzkxNjQwNTIyNTM5NjE5NTU0NTE0NTk4OTUxNTg0NjA3MjYwNzk1NzE1MDMyMjM4NTQ3ODMyMzA0NTY2MzQ4NjYzMTc0NzQwMDE2MzQ2NTU2MTM1ODc4MzgxNTYzODQ2NzU0MzQzMjk0NTIzNjc0NDI3NjQxNjAxNjAwNjE2NzI3NjEyMzc0MzI2NzY4ODA5NjAyNTE5MTAzOTk3NDY4OTg1NTg3Nzg4MjI3Njc5MzQ4NTgwNzk1OTkyOTkxNzMzMDg5MTUyMTg2MDg4OTU2MTg2MTQ0OTkyMDI5OTI2OTUxOTU0OTQyNjYwMDUxOTM0MDc5NzkxODI1NzA2MTExNzg0MDU2NDM2OTA2MDgxMDQ2MDQ5ODI0ODE1NDE0MTc5NTMzMDA2ODE4NzQ3NzgwNTQ5In0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjEwMTUyMDI0OTk1MTUxMzcyOTgwNzI5NDM1MTQ5MzgyMzQxMDQ3MDIzNjI2NjA4MDc3ODg1NzQ2Mjg4MTE3MjI2OTAyNzM4OTY5OTEwMTU0NjgzNzM2MzI4MzQ2MDg0MjcwMDA3MTY2NTg3ODI5MjE2MzEzNDc4MDk3Njc0MTI0ODU2ODIyMjA4NzI0Nzk1NTE0ODgzOTYwMzE5NDc5OTg4NzAzNDUyNjI4NjYxMDc3MTg3OTIyMzA1NDc5MDE2NzQzOTk0NzYwMzE5NzI1OTExODk0MjM2NDMxMDkxMTIyNTUxNTU0NzgwODg0NjQ2MjE0MTUzMDUzMTM2NDMwMTk4OTA5MTM0OTk4OTM2NjY3MzI4ODI2MDcwNzEzMzk0NDg0NDI0ODUxNjkxMzUxNDc0NjAxMjIwODk2NTIyMDYzNDA5NzA4NDA1Njk2MzY5MjA0MzU0NzE1MDkxMzk2Mzc4Mzc3MzA0ODk3MzMwOTM0Mjc2NTQyNjE2NjAxNTk1ODI5NzgxOTg3NTMyNzkxMzIyNTgzOTE1Njk1OTY2MjM3MTc4Njg1NTMzNTE3MTQxNTAyNDE3MzQxMDIzMTA1MTczMjMwMTcwNzUzODYwMjgxNDAxODk4MDE5OTQwNjA2MzczOTYwMzYxNjA3NTE2NjgyMDg4MTc1NzU4ODA0Mzg4MTM5MTQ0MDkwMjg5MzI5NzMzNTQ1NDg4MjUyNjczNDIyODkzMzc1MzE5ODQ2OTMwOTIyNjIwNzAzMTEwMDgwODU5OTE4ODQ0MzgyOTQ3ODczMjAwNzA4MTY2MzA0NDk4ODk0MDA4NTMyIiwiYyI6IjIyNDQyNTM5MzYwMzYzNjQyODI1ODkxNTc5ODgzMDE5Mjc3Mjk0NTQ2MjUwMDEzNTM3MzI2OTY2NzM3MzE0NTUxMjEwMjU3MjU2NDU5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9" + } + } + ], + "~thread": { + "thid": "c5fc78be-b355-4411-86f3-3d97482b9841" + }, + "~please_ack": { + "on": ["RECEIPT"] + }, + "~service": { + "recipientKeys": ["GVbwqUMqzxKaEVWjn1aPBfjJpYQHVejinpx8GCeEuQjW"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3000" + } + }, + "updatedAt": "2023-03-18T18:54:01.192Z" + }, + "id": "b88030f7-dc33-4e7e-bf4d-cdfaa6e51ebf", + "type": "DidCommMessageRecord", + "tags": { + "role": "sender", + "associatedRecordId": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "protocolName": "issue-credential", + "messageName": "issue-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "messageType": "https://didcomm.org/issue-credential/1.0/issue-credential", + "messageId": "578fc144-1e01-418c-b564-1523eb1e95b8" + } + } +} diff --git a/packages/anoncreds/src/updates/__tests__/__snapshots__/0.3.test.ts.snap b/packages/anoncreds/src/updates/__tests__/__snapshots__/0.3.test.ts.snap new file mode 100644 index 0000000000..0bb9565761 --- /dev/null +++ b/packages/anoncreds/src/updates/__tests__/__snapshots__/0.3.test.ts.snap @@ -0,0 +1,832 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UpdateAssistant | AnonCreds | v0.3.1 - v0.4 should correctly update the credential exchange records for holders 1`] = ` +{ + "1-4e4f-41d9-94c4-f49351b811f1": { + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "isDefault": true, + "linkSecretId": "Wallet: 0.3 Update AnonCreds - Holder - static", + }, + "type": "AnonCredsLinkSecretRecord", + "value": { + "_tags": { + "isDefault": true, + }, + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "linkSecretId": "Wallet: 0.3 Update AnonCreds - Holder - static", + "metadata": {}, + "updatedAt": "2023-03-19T22:50:20.522Z", + "value": undefined, + }, + }, + "2c250bf3-da8b-46ac-999d-509e4e6daafa": { + "id": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "tags": { + "connectionId": undefined, + "credentialIds": [ + "f54d231b-ef4f-4da5-adad-b10a1edaeb18", + ], + "parentThreadId": undefined, + "role": undefined, + "state": "done", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + }, + "type": "CredentialRecord", + "value": { + "createdAt": "2023-03-18T18:54:00.023Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "John", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "99", + }, + ], + "credentials": [ + { + "credentialRecordId": "f54d231b-ef4f-4da5-adad-b10a1edaeb18", + "credentialRecordType": "anoncreds", + }, + ], + "id": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "metadata": { + "_anoncreds/credential": { + "credentialDefinitionId": "A4CYPASJYRZRt98YWrac3H:3:CL:728265:TAG", + "schemaId": "A4CYPASJYRZRt98YWrac3H:2:Test Schema:5.0", + }, + "_anoncreds/credentialRequest": { + "link_secret_blinding_data": { + "v_prime": "6088566065720309491695644944398283228337587174153857313170975821102428665682789111613194763354086540665993822078019981371868225077833338619179176775427438467982451441607103798898879602785159234518625137830139620180247716943526165654371269235270542103763086097868993123576876140373079243750364373248313759006451117374448224809216784667062369066076812328680472952148248732117690061334364498707450807760707599232005951883007442927332478453073050250159545354197772368724822531644722135760544102661829321297308144745035201971564171469931191452967102169235498946760810509797149446495254099095221645804379785022515460071863075055785600423275733199", + "vr_prime": null, + }, + "link_secret_name": "walletId28c602347-3f6e-429f-93cd-d5aa7856ef3f", + "nonce": "131502096406868204437821", + }, + }, + "protocolVersion": "v1", + "state": "done", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "updatedAt": "2023-03-19T22:50:20.522Z", + }, + }, + "669093c0-b1f6-437a-b285-9cef598bb748": { + "id": "669093c0-b1f6-437a-b285-9cef598bb748", + "tags": { + "associatedRecordId": "93a327ad-2bf3-4ec4-b01c-bdd58ba2f6e3", + "messageId": "4d2c80b7-4a25-42ac-b8cf-a68b1374b9b7", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/offer-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "93a327ad-2bf3-4ec4-b01c-bdd58ba2f6e3", + "createdAt": "2023-03-18T18:54:01.134Z", + "id": "669093c0-b1f6-437a-b285-9cef598bb748", + "message": { + "@id": "4d2c80b7-4a25-42ac-b8cf-a68b1374b9b7", + "@type": "https://didcomm.org/issue-credential/2.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/2.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "John", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "99", + }, + { + "mime-type": "text/plain", + "name": "height", + "value": "180", + }, + ], + }, + "formats": [ + { + "attach_id": "8430ddb8-b0c3-4074-8ded-f4dcfe80303d", + "format": "hlindy/cred-abstract@v2.0", + }, + ], + "offers~attach": [ + { + "@id": "8430ddb8-b0c3-4074-8ded-f4dcfe80303d", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjI6QW5vdGhlclNjaGVtYTo1LjEyIiwiY3JlZF9kZWZfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjM6Q0w6NzI4MjY2OlRBRzIyMjIiLCJrZXlfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjQwMTQ0MTA0NDg3MjM0NDU2MTc1NzYwMDc1NzMxNjUyNjg1MTk0MjE5MTk5NDk3NDczNTM4NjU4ODM3OTIyODMzNTEzMDg0Nzk2MDQ5IiwieHpfY2FwIjoiMzgxOTQyMjM1Mzc3MzYwODEyNjY0MDA4MjYzNDQxMDg2MDMwOTMyMjAxNzgzMjM3ODQxODQ5NDg3ODk2ODg1MTYwODY2MTY1MDM3NzI2MTIxNjU0MjcwOTg5NDY3NjAzNDExOTAzODk4MzUwMDAzNDIwODg3MzI4NTUwMTY2MTI1ODMyMjAxOTQzMTkwNzAxMDU4NTAwMDE5ODM1NjA1ODczNDYzOTkwODg3NzQ0NjY3MzU0MjM2Njc3MzcyODg0ODQyNjE5NTEwMTUwOTA2MjI1OTMzMjc1ODEyNjg2NDg3NTg5NjY3ODI3MjAwODcwOTQ0OTIyMjk5MzI3OTI4MDQ1MTk1OTIwMDI3NTc0MDQwNDA4ODU5MzAwMzY1MDYwODc3Nzg2ODkwOTE1MDU5NTA2ODc1OTI0NzE2OTI1MDM2MTc4Njg2NDE5NTYyMzcwODI4MTMzODY2Nzg3NzkyMDcwNjAyNDQzNTkzMTk2NzEzNzcyNDM2NTYzODI0MzkwMDIyNzg4MjU2MzA4NjU4OTc0OTEzMTk1ODYxODUwMTQ3ODE1Mjg5NzQwOTA4NDk1MjQ3NTAyNjYyNDc3NzQ2NTI5ODA3Mzg0OTgxODI5MDc3NTQ4OTI2NzExMDkzNzQ5MjM1ODU4NjUwNDc5NzE5NDI4MzUwMzAwNzUyNjQ0OTg1MTQ5MTMxNjA1NjUzMDIxMDYxNzkwMjY3MzQyNTY4NTkyNTY2MTQ0MDM5NzY4OTg0NTMyNDMzNzk0MzUzNjQ2Nzg1MjA3NDgzOTk2ODQ0OTcxNTgzNzY3NDQ5ODYyODgxMjMxMjI1MzM4MzAzMTQ4NzA0ODczMDEzNDM3MDgyNzY1MTk4OTY2MzE5NTM0OTkyNjk4MzMzMDQ0MDI3MjIyNTYyNTIzNzk3ODk5Mjk2MTQ1NDU5IiwieHJfY2FwIjpbWyJtYXN0ZXJfc2VjcmV0IiwiOTE5MTc5NzQ4MTE5NTg5MTY3Njc5MjQxODk5NzY0ODIwNTk0MDc5OTQxNzIzOTgzOTYyNzQ1MTczODM0NDQxMDQ3MjU4MDcyOTE4OTUzNjIzOTQ4MDMyNzI2NDgyNzI2MTEwOTk2Mjk3MDU3NTYwNjcwNzAxOTU1MTkxNDc0NjM0MzQ0ODMxMzg3NTk4NzI2MzMxMjc0NjI4NDU3Njk5NzczMDA1NDMwMDIxNzMwMzg4MzcwMTEyMjc3MzI2MzU4OTgwMTA3ODIzNzUzODc3MTU0NjIwMDkzMjE5MjYyNjAxNDM2NzMyNTgzNDI4Nzc4NDA4OTc0NTQyNzkzMDk0NTQ5MTczOTA3MzQ3OTUxNTc1NjM5NzU2NDg5MTA0Mzk0MTY3NzExMzY1MjM3OTI1MjAwNjk4OTg5NTI5MTQ3OTIzNTYzNDMyODgyMzgwMTg0NzU0NzkzODMwMTE3MTQ1MDAwMTI0NDYxNjkzOTcxMDQ5MjgzNDk1NTE4MDQxMDc5ODUyMzAwMjk0NDM1MjYzOTIwNDU0NTU3MzUxNDQ1MDM3NDI4MDg3OTk2Mzg2NjY3NjU3Nzk5OTYyNzQzNzIyNzA3NzczOTEzMzc0NzIxODUyNTQ3MjkwMTY5MjI5NTAzMTQxOTMwODYzNTk4NTExNjc4NDEyMDE0MzE2MDM2MzYxMzczNDcwOTQwMDEyODcwMDgwMDA2MzE0NzYxNzYzNzUyNzYwODk5MTQ3NzA1MTA0NzQyNjAxNjkxMzMxNjkzMDIwMjg2MjA2NzQ2NzE0MzI3NjU2MjA2NTMzMjk3NDg4MjU2NTM2NTQ3MzY4MjM2OTQ2MDM5NzAzMzc0OTMzNTE0NTc2NDg2NjQyNTY4MjgyNTY2MjMyNDU1NTU5MDY4MzE3NzU5NDM0ODU4NTI3MDg2NjQ0Il0sWyJoZWlnaHQiLCI5MjMwMzkyNDc1NjI4ODc1MjA4OTM0NjM0NzE4MjYzNzA4MDIzOTI1MDU0NjY2NDgzMzgxMzIyMzc3MDg1MjMxMjU4MTM4MzgwOTU1NTk3NDQxNTEyOTYwNDA2MjI3MjUwODgyNjA3NjExMDkwODk3MTM1NDcxNzAwMDIzNDcwOTM2ODg4MDE3NDY5Nzk0ODYzNDk4NzUyNTI3Njc3MjMwMTEwNzg0ODQzNzI0NDUyNTUzODYyOTA2MzM5MDc0OTIzNDU4NTQ3NDYzODcwNzU3OTg5MzMxNzk4OTI2MjM4MjUxMTM2NTYzNjM2MjIyOTQwNDkwMzY3MjQ2OTg0OTU2NTE5MTAzODcwNDE0MDM5NzM2MDE2MDY5MzA2NjQ0NjQzODI4OTgxMTE3OTM3NzYyNDAzODY1Mjc1MDU5MjEyOTY2NzIxOTU3MzM0MTM2ODEyMDI0OTE0MzA4MzAxMzk5MzM4NzMyOTIzNTA0MjA5MDM5ODMxMTc5NjU1NTkyNjg0MjMyMTIzMTI2Mjc4ODQzNDMyOTUwMTk1Mjg3MzE4ODI3NTM2MTMwNDQ3NzM3MTgwMjk3MDE0ODEzNDg3NDQyOTg2NjQ1NzQyNjEyMzE5NzQxNDY2MDMyNTg5OTU0NzYwNjE4MDU0MDUxMjAzMTE1NTAxNDcxNDExMzg3NzU0NDk5MzAwNTU4MTc5NjM5NDAxOTM0NTAzMTMyMDEzMjAzOTg2NzkyMTEzMDAzNTkwODg1NTc3NjgyMzU2NDY3MjA5NTUwNjQxODQxMDYyNTkzNDYyODIwODg3NzgxNDYyODM3ODkzODcxNDM4MzM3Mjc5MTcwMTExMTQ5MTU4NDMzNDE0ODI1NTkyNjcyODU2MzM5OTM4NTgyODg2NzM3OTIwMjc1MzI0MjEwMTUzMjE5MjI2OTYiXSxbImFnZSIsIjkxNTg1ODk3NDkwNzE0ODA3OTY2MDYzOTg5MjE1NTMxNDkyOTQwMDI5NDcyMTM4MjgwNjcxNjcyMjQ0NjY5MDc5NzIyNTQyMDU0NTU3NjY0MTcxMDI1NzM1NjQ4NTIwMTM4ODQ4ODAxNzIyMTc4MTcxMTA5NTc0MTMyNTExMzM1MDEwNTc5NzExMzcyODM5MjI3MDExOTg4MTUyMTEwMzI4MTE5MjkyMjI4NjM3MDU4MDQ3NzYwODYwOTQ0NTY3MzQxMjY4MTY4Mjk3NjE5MDM2ODEwMjYwODM2NDI1NDkwMzU3NjE4NzM4NTYxNTY2MTUxODQ3MzIxNzM1MjQ5ODk1MDU5NTY2OTQxODI5MjE0Nzc0MTA0NzYyNTQwMjcyMjk2NjE1NTE3NjUwMDcyNDQyMTI0NjY5MDEzMTc1ODAyMDk5MDQxMzk3MzE5ODQ0OTA2MDgwOTYxNTcyMTcwNjg2NzgzNDM1Mjg2MDUyMzE5ODY3ODExMDE5MjAxMDYwODM2OTM3Mzc0MzM0NDM5MTQxMDAzMTI3NTcyNjgzNTgwODI0OTkwOTg3MjE5MzU4NzkzOTM2NTU4Nzk3MjI0MDQzNTM1ODA5NzMyNzgxMjE1NzEwNjI1MjQzODYwNTk4MTk0MjU2MjAwODkwOTA3ODAzMDcyMTAzNzc3MzkwODk4MDczOTgyNjY3Njc1ODg0MjI3MjU0Mzc2OTI5Mjg3ODQyNDE0MTE0MjcwNDQwMTEzNDUxNjk4NzE5Nzc5NjQyNTI4MDA4NDM3Mzk5NjI0NTE3OTM4Nzg5MDc3ODE5ODA0MDY5MzcxOTM0NzExMTIyNTQyODU0OTg4MDA0Mjc4NDkwMjAxNTk2NjE0MjUwODc3NDYxMDczNjc3NTUzNzYxMTMyMTA5Nzg3NTQ2ODE1ODk5Njc2NCJdLFsibmFtZSIsIjYyNzgwNTIwMTM3MzI3NTUzMDc3MDg4NTE4NDg1NDYyMTA0NjEzMjEyNzY3ODUwMzYwNTc3NDQ4MDUxNTk5MTMxMTM1NTI2NzQ3Nzc2NzMzMDg1MDMwODcyMDE1OTM2MTI2NzE0MTIxMDgxMzg3ODU2MTkwMTkzMzI3ODY3OTE0NTEzODM2NTQ1OTY4Mjg1NTc5ODEyODMxMDI4ODc2Nzg1NzI3OTQ2MTEwNzg5Mzc0MjcyODgzMzkyOTgwNDkwODk3NDkwMTc5MDQ0ODM0NTgwMzQ2ODY4NDI2ODc0ODU4NTY1OTg4NTUyMDcwNjI1NDczNjM4MDM3Njc5NTU1NTk2MzE5MTc3Nzc5OTcxMTIxMjQzMjgyMTIyOTQ2NjY0ODMxOTgxMTg3MzQ3MzcyMjkxMjYwOTM3MzkzNDA1ODk5OTI0NjM4MzE3ODI5MDczODMxMjI4ODc1Njg5MTcyMTg4NjIyMDI5NzcxNzM5MTQ5NDY2Mzg3NTM5NjkyNDQ5NDU5MjczNDI5NzM5MjMzNjkyNTEzNDA5OTkyNDgxNTQ4ODk0NjAzNjM3MTYzNjA4MTM0MTAzMTk3Nzc3NTM4OTYwMDcyMjcyMzYyNzM4NDM1MTM3MDcyNzIxMjExMDYxNTg4MDE3ODczODg3MTEwNDA2OTk1NDQ4ODIwMDEzMDA5MjgyMzk0OTczMDMwMDI5MTY3NjQ5NzY1OTI1MTUxMzY4NTg5OTkyNzMyMDE1ODAwNjAzNzYxOTI3MTg3MDM4MDkxNDY3MDE1MjA3MzIwNDczMDM0NDA3MDIyNDA0NjQ4MTI0NTk2NjQwNjU1NjY1MTIzMzY5Njc0ODI2NDE3MjE2ODUxNTM4Njc1NTM3NzAwOTg4MTQzNzE1NTE3NzMwMTM4NjA4NzkxMjcyMzM0MDUyMzY4OCJdXX0sIm5vbmNlIjoiNDE0MzQ4Njg0NDk2OTAxNjkyMjI2OTY0In0=", + }, + "mime-type": "application/json", + }, + ], + "~service": { + "recipientKeys": [ + "DXubCT3ahg6N7aASVFVei1GNUTecne8m3iRWjVNiAw31", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3000", + }, + "~thread": { + "thid": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2023-03-18T18:54:01.134Z", + }, + }, + "8788182f-1397-4265-9cea-10831b55f2df": { + "id": "8788182f-1397-4265-9cea-10831b55f2df", + "tags": { + "associatedRecordId": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "messageId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/offer-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "createdAt": "2023-03-18T18:54:00.025Z", + "id": "8788182f-1397-4265-9cea-10831b55f2df", + "message": { + "@id": "c5fc78be-b355-4411-86f3-3d97482b9841", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "John", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "99", + }, + ], + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjI6VGVzdCBTY2hlbWE6NS4wIiwiY3JlZF9kZWZfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjM6Q0w6NzI4MjY1OlRBRyIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiODUxODAxMDMyNzEzNDg5NzYxOTg5MzAzNjMzMDkzOTEyOTExMDUxNjI0OTQ0OTYzMTgzNzM2MDY3NDkwOTc2MDYxODEwMDgxODkxMzQiLCJ4el9jYXAiOiI4NDk0NDg4MjQzNTk2NTkwOTc2MjQzMjc0NDg4ODk2Mjc1NTcyODAyMTQ1ODE5NDQzNTE0NzQxMzk1NDI1NjM5MzQwMTczMDIzMTQ5NzI3MDY5NzMzMzQwODgzMTU4MzQ1NTYzOTA5OTcxNDMzNTg1MjMwODAxNTYyMTM0NjczNjM1ODg5NTA3Njg5ODQwOTgyODU5Mzg1NjA1MTc1NTkxNDYxOTkyMDExNzU2Mzg1MTI3MTQ3ODgxNDMwODEzNjYxNzY0MDU5MzE0ODk4MTc2NzQzMTQ5MjYzMDMwMDQ1NzMwMDMzMzI2NzgyMzg1OTY0NjcxMzg1ODQ2MzcxNjQ5MzQxMTg2MDM5NjE4MTQwOTIwMDUxMzg1MDAwNTYxMDcyMTc5NTEyMzc5Nzk0OTU4NjE1ODIyODI2OTExNzIwNTQyNTE0MTQ1NDc5MTAxOTUyMzM4MDMwMDY1MDk5NjcxOTU2OTMxMzE2NjE5MjM0NTQ0NTE5NTQ1ODQ1MzA4MzgxMjQyNTM0NDcyOTc3NjY0MjAwMjc2MTMyOTgxODE1ODAzNTIxOTExMzk4ODkxMjE0NjE1NzA1MDM2ODM2ODU1NDU1NzY4ODg4MTUxNDgzODAyNDcyODQyMzczNzE0MTI0NTYwMzIyNTI3NDE4MTEwNzYyMjgyNzY4NzMyNTIzMDQyMDA3MDY2OTk2ODIxMTQwMzE1NDg0NzI4NTM2NzIwNDI3MDg5MTI2NDk1NTAzMjc0ODQ4MDM3MjUzOTM3NjI3MDU2ODUzMTQ4NjE5NDA4NDYxOTI5NzEzMjM4MjEwNDc4MjcyMTIxNTUwNjQzODc4ODM1NDYwMzY1OTIwMjE3NTk5NDYyNDUzMDMyNDQ4MjYyMTM3NjE5ODY0OTU4MzA1MDE3MjA4OTYwNDc1MTQxODgwMTMiLCJ4cl9jYXAiOltbIm5hbWUiLCI1MDcyNzU2NDE2NDA2ODIxNzU1OTc0MzUxMTg0NjE1NjA4NDY2NTk3Mzk0NzA2MTY1NDg2ODAzMjc3MjMyMzQyOTk4MDA0MzY0OTU0MTczMzc0NDIwOTc5NTkwMDcyODgxNDgxNDA0MTg2OTExODg5NzQ4MTgzMzQ1OTk5NzQ0NzgxMTQ1MTMwNzEyNDIzODY0Nzc1MzQzNjAzNTk2NDM3Mzg4OTgzNTExNDAzODA0NjEyNjU1MDE5NzQ4MTI5NDk3ODY2NTcwMDQyMjcwNDQxNDQ5MjYwODY0NzgyMzI5MjAxNDEzMTc5ODU3NzA0MjM5OTMyMTg4NTc4NzE3MDczNzM3NjUyNzY5MzY5NDg4OTgxNzg2NDQwNTExODAzMjMzNDMxNzA4NDk4MTU2NTA0OTUzNzkzNjU2NjQ2NzMyNTU4MzQwNDI2MDI1MjA3NTk0OTIwMDY4OTc2OTQ4Nzg2OTUxNzM3MDIwNDQ0NTA5NzYyMDQ2MzIzNzA0MDQ3MjU1ODU3NDE5ODE3MDc5NTI3NDgzNTE1NDY2NTAyMDkzOTY1NDMzMzk3MjQ1MzA4MjQ5MDgyMTQ4Mjc4NDA1MzI5Njg1Mjc0MDYwNjk0MzI0MTI2ODgxMjkyMDIyMjY1ODczMjk5MDU0NDU1OTA5NzkyNjUwNjAyMTk0NjUzMjYxMDk0ODYwOTc2NzA4ODE1ODgwMjExMTY0MTkwMDM0NjY0MzI2MDc3NjcwNzkyMDE4NTE2MzMzNDI3NjkwODYwMjIxODEwMzk5MDgxMjc5NjAwNTYzMjk3MjI0NjM0MDM0NjcxNTIwODE5MzU3NzQ0Njk2NzU1Njg1NDI2NjIzMzAwMjQ3MDUwODE4NTQ2MDM2NjA0NjMxNjcyNzE5MjI0NDA4NTE2NDM4NTgxMDM5Njk4NzI0MSJdLFsibWFzdGVyX3NlY3JldCIsIjU2MzYzNTgyMDQ5Mjg4OTY1OTg1MDA4NzgyMzU0NjgyNjMwNDkxMzQ3MTM1NDIxNTAyMDEyMTIwMzI4MDI4ODIyMjUyMzg4NjgwNTMwNTgxMTcwMTgwNDU1MTcyNTc3ODkyMTEyMTY1OTM0Mjk5NjUyNzAxNDExMzUyNDkzMzkyODU0ODI4NzMyMDQzMDI0MDI0MzM0MzMzNzc0NjEyOTEzOTUyMjAzNjM1NDk2MDQ0ODMzMDI5NDE2NjUwOTU5NjE0ODgzNTUwOTMxNzgzNTA5MzE1Nzg4MDEyODQ0MzAwMDQwMDE5MTY5MTc3NTI1OTgxMTU3OTkwNjQzMDcyMjQyNzcxMjU0MTYyNzMxOTU4NzI2Nzc1NjYwMjkxODIzMDcyNDk1Mzg0NzM5MTcwODc4ODMxNzkxMjQzMjEzMjU5MzA5ODQxNjU3MjUwOTg1NzMxMjEyNzE2MDM2MDY3MDUxNjM2NzA0MjA1NDEzMDk2MDU3MTA2NTM2MTI2ODUyNDU0NzcwMzQzMTMwMTczMjAwNjEzMDIxOTE4MzgzMDQxOTU4MTkwOTE2NzQ0NjU4NTI0ODA1NjM4Mzk2OTY3OTA3MzIwNjY1MDU1MzcwMjY0NjAxMDczMjc5NDMyNjM5MjM3Njc1NTA0OTg1NzQyNTI4NjYwMTAyMDEzNzIxMzA2MTE4MTg0NDk1MTEyNDQ2NDYyNDc2NTkwMjYxODkxMjA0OTQxOTA4MjMyNzMzNDA3MTg4MDA3NzE2NTA2OTUzMDY0Nzc5NDk5ODExNzI0ODI5NjcyNjY2NzIyNjIzOTAxMTc1OTk0NTIyNjkwMjk1ODI0MDgyNzY5NjQ0NDYxOTAxMDk2NzI3MTE5NzAzMjUzNzI4NjY3MTU1MzA5MDYzNDUyNDY2MDY3NzU5NzIwOTgyNDA3MiJdLFsiYWdlIiwiMTM2NTQxMjE0MjM5MTcyNDQxNzQ1MjU3MjcyMDI3MTA4NDYwMzU0MjgxMTA2OTA2MzYwNDIwMDE0NjUyMDIxMDgyNDEzODM2ODEyMjk3NjY3ODk2MTYzNDkzMjM4NDIxNDI4NjMyNTMxODE0ODk4NzkwMDg4OTg2NjgyMTE2OTAyMzc4NDgwNTE4OTUxNDExNzg1OTk3NTk5MDMyNDYxNjExNjIyMDUyNjMzMDQ5ODYxMzc5MTQzNzI4MTM5MTUyMDkyMzI0ODc3MjMxMTYwNTgzNzA5NjE0MzA1NzQ1MjA5MjQwNjU2MDU4NjY3OTMwODEzNzYyNDY5MDc2ODc5MTk1Nzg0Nzg4NTE2NjI3MjgxMDY0NjE3MzgzMDc4Njc5MTkwODIwMzQwNTgwNDY2MjU3ODU3NjA1MTc2MTg4NTI3OTMxMDI4MTMzNTY5Njc0Mzg2ODAwMTA2MDE2MDg1Nzc0OTcyMzI1NTAyNDA2MTY0OTY0MjU2OTUxNDI3ODAxMTQzNTQxMzUzMzI0Nzg0MzA5OTY4MjIyOTU1NDk4Njk3NTAwMDUwMzc0MDg0NjIwNzQ4MTk0NzIyMTI2NjE2OTY3OTY3Mzc1NTM3Nzc5NTc4NTMwMDIxODExNTA2MTIxNjcxMDUwNDgzNTM2MjA3Njc3MTg5NDQwNjEwNzk0NTcyNzI5MTgzMzAyMjM1MDkxMDg4NTU2ODc5NTg3OTE3MDMzMzQyODcyMzg2NDQ5MTQ0NzgwMDYyNjc4MzA3NzE4MzU1MjQ5MTUxNjc5MDA1MzkxNDA5NDE4OTQxMjEzNDkxMjQyMjg2NTAwODcyMzQxNDI3Nzk1MjQ1ODYzODE2MDY2NDY3NDkxOTg4OTU3MDEwNDIxNDA3NDkyMDUxOTc0NTMwNjIxOTk1ODU0ODczNTM5Mjk3MyJdXX0sIm5vbmNlIjoiNzk3NjAzMjE3NzA5MzM1MzAwMTcwODI4In0=", + }, + "mime-type": "application/json", + }, + ], + "~service": { + "recipientKeys": [ + "GVbwqUMqzxKaEVWjn1aPBfjJpYQHVejinpx8GCeEuQjW", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3000", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2023-03-18T18:54:00.025Z", + }, + }, + "93a327ad-2bf3-4ec4-b01c-bdd58ba2f6e3": { + "id": "93a327ad-2bf3-4ec4-b01c-bdd58ba2f6e3", + "tags": { + "connectionId": undefined, + "credentialIds": [], + "parentThreadId": undefined, + "role": undefined, + "state": "offer-received", + "threadId": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + }, + "type": "CredentialRecord", + "value": { + "createdAt": "2023-03-18T18:54:01.133Z", + "credentials": [], + "id": "93a327ad-2bf3-4ec4-b01c-bdd58ba2f6e3", + "metadata": {}, + "protocolVersion": "v2", + "state": "offer-received", + "threadId": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + "updatedAt": "2023-03-19T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2023-03-18T18:53:44.041Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.4", + "updatedAt": "2023-03-19T22:50:20.522Z", + }, + }, + "b71d455b-9437-4ef8-b4f3-b6a0dd6bbfb3": { + "id": "b71d455b-9437-4ef8-b4f3-b6a0dd6bbfb3", + "tags": { + "associatedRecordId": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "messageId": "578fc144-1e01-418c-b564-1523eb1e95b8", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/issue-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "createdAt": "2023-03-18T18:54:01.369Z", + "id": "b71d455b-9437-4ef8-b4f3-b6a0dd6bbfb3", + "message": { + "@id": "578fc144-1e01-418c-b564-1523eb1e95b8", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjI6VGVzdCBTY2hlbWE6NS4wIiwiY3JlZF9kZWZfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjM6Q0w6NzI4MjY1OlRBRyIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJhZ2UiOnsicmF3IjoiOTkiLCJlbmNvZGVkIjoiOTkifSwibmFtZSI6eyJyYXciOiJKb2huIiwiZW5jb2RlZCI6Ijc2MzU1NzEzOTAzNTYxODY1ODY2NzQxMjkyOTg4NzQ2MTkxOTcyNTIzMDE1MDk4Nzg5NDU4MjQwMDc3NDc4ODI2NTEzMTE0NzQzMjU4In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjMyMTIwMDQ1ODc4MzIxMjcyMzA1ODI5MTc3NzMzMTIwNzE1OTY5NDEyNjkwNjUyNDQ4OTc0MTA4NzEzNjU0ODc3NTg2MzIzMTI3ODk2IiwiYSI6IjIyMjY0MTYwNjIwODcwNDUyNTExMjcyMzE1MzMzMDA0MjQzMzY3NTM2NzM3NDMwNjM1NjExMjcwMDkwOTE4NDMwNzc0ODEzMjAzNjQwNjMxMjIyNDczMzk0MjQ4MTgzMDIzMjIyNzExNDUwMzQxMDcxOTQyNDQwMDgwMjY2Nzk1Mzg5Mzg5Njc1NjYwOTUzNTQyMDE4OTA3NjQ3NzI4OTQ4NjY1MzA2Njg0NjExNDU1NTI5NzM5OTY1NDcyMjQ2NDQxMzE1NzAxMzM1ODc1MDY3MjExMDk3NzcyOTgwMjU1NDIxMDMzMTI1MjAyMTQzNDk3NjMyOTAyMjM1NDAyMzU5OTA1MzY5MzE4MjI1NTc4MjUxNjY4NTYzNzc1NTY0MDM2MjUxNzE0Mzk3MTEzNjQ3OTg0MjcxMTE5MTU2NDQ3NjI1OTk1NjE5MjAwMDk4MTgzNzY1NjkzMTg1ODEzNjA1NDU3OTQwMzE0MDU2MDkzMTI2MzQ3OTU5MzYwODIyMzg0OTEzODg3Mjg3ODI2NjkyNDIyNDMyNDUwMDA5OTYxNjQ2MjMzNTE3MjY3NDU1OTkyMjA3MTE3Mzk5NzU1NjY3MTA3MzM1NTQ0MzEwNDQwNDE1NDE5NTk5NTA1OTgxMzkwMjk5NDUxNzQyODg4NDg0MTc0NTU5MDA5NDgwNjU5MDk2Nzg2ODI2MDgxNzc3MzcwNTk1MTU3OTg5NjQ1MDYxNDI2OTA2ODM2MDk5NTU5MDQ0MDI4ODM2MzYwOTM2MDkwOTkxNjA1OTU0NTM2OTQxMjgxODQwNzk2MjkxODc0ODk2NDEzNTM5IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDExMzE1MTE0OTUxODg0MDQ5ODkyNjAwNTY3NTgzMTc4ODkwMyIsInYiOiI2NTE5ODc4MzYyODQ5NzExNDY4NDkyNDM1OTM3MDU4NzMzOTYxMTkxNjA3NjI4MzUzNjkxMDg1MzM5MDMwNDU0OTkyODc0ODYyODkyNDg4ODYzNTA3MDQ1MTM1MzA4ODI1NDA2NzYwMTQwNDQzNzM0NDYzODE5NTM2MzE0NzcxMTQ3MDk4MjU2ODMzNTc2MjIwNDI5ODQyNjc3NzMwMzQwODYwNjE2NTcxNzc5NjU4OTIxNDY4Mjc0NTUwOTc5NjYyMDkxNzEwNDU5MDk2MDgzMzYzNTc1Mjc0MjQzNzIyMzIzOTIxMjY5MDYyMjE0NjQyNzQyMTI0MzQ4MTY0MDUxNzE3MTk5MTkzODY3NTM3NTEzNjYzOTY1ODQzMDI5MjAxODA0OTE2MTEzNzMxODYzOTUzNjQ5MDkwNDgzNzMyMTkxNTQ2MTEwMjAxNTg0NzMxODg4NTE5NjA2MjE1OTkyNTgxNzk2MDg2NzUzOTE5NzUxMjkwMDI3MDI4NzczMTAwOTc5ODI5MzQ5NzA0MTUyMDEzNjg2MzU1MzM1MjIyNjU5MDY2NzE0NDQ2NDc4NzY3MTE5NDE4MjY3OTg5NTAyNzc4MjMzNzM3MjM4MjU1MTQxNzQyMjk4NTU3MDY2NzA2MTM0NzYwMjQwMzY3OTMzMzc5NzYzMTc5MTI1NTI4MDQwMzkxNjQwNTIyNTM5NjE5NTU0NTE0NTk4OTUxNTg0NjA3MjYwNzk1NzE1MDMyMjM4NTQ3ODMyMzA0NTY2MzQ4NjYzMTc0NzQwMDE2MzQ2NTU2MTM1ODc4MzgxNTYzODQ2NzU0MzQzMjk0NTIzNjc0NDI3NjQxNjAxNjAwNjE2NzI3NjEyMzc0MzI2NzY4ODA5NjAyNTE5MTAzOTk3NDY4OTg1NTg3Nzg4MjI3Njc5MzQ4NTgwNzk1OTkyOTkxNzMzMDg5MTUyMTg2MDg4OTU2MTg2MTQ0OTkyMDI5OTI2OTUxOTU0OTQyNjYwMDUxOTM0MDc5NzkxODI1NzA2MTExNzg0MDU2NDM2OTA2MDgxMDQ2MDQ5ODI0ODE1NDE0MTc5NTMzMDA2ODE4NzQ3NzgwNTQ5In0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjEwMTUyMDI0OTk1MTUxMzcyOTgwNzI5NDM1MTQ5MzgyMzQxMDQ3MDIzNjI2NjA4MDc3ODg1NzQ2Mjg4MTE3MjI2OTAyNzM4OTY5OTEwMTU0NjgzNzM2MzI4MzQ2MDg0MjcwMDA3MTY2NTg3ODI5MjE2MzEzNDc4MDk3Njc0MTI0ODU2ODIyMjA4NzI0Nzk1NTE0ODgzOTYwMzE5NDc5OTg4NzAzNDUyNjI4NjYxMDc3MTg3OTIyMzA1NDc5MDE2NzQzOTk0NzYwMzE5NzI1OTExODk0MjM2NDMxMDkxMTIyNTUxNTU0NzgwODg0NjQ2MjE0MTUzMDUzMTM2NDMwMTk4OTA5MTM0OTk4OTM2NjY3MzI4ODI2MDcwNzEzMzk0NDg0NDI0ODUxNjkxMzUxNDc0NjAxMjIwODk2NTIyMDYzNDA5NzA4NDA1Njk2MzY5MjA0MzU0NzE1MDkxMzk2Mzc4Mzc3MzA0ODk3MzMwOTM0Mjc2NTQyNjE2NjAxNTk1ODI5NzgxOTg3NTMyNzkxMzIyNTgzOTE1Njk1OTY2MjM3MTc4Njg1NTMzNTE3MTQxNTAyNDE3MzQxMDIzMTA1MTczMjMwMTcwNzUzODYwMjgxNDAxODk4MDE5OTQwNjA2MzczOTYwMzYxNjA3NTE2NjgyMDg4MTc1NzU4ODA0Mzg4MTM5MTQ0MDkwMjg5MzI5NzMzNTQ1NDg4MjUyNjczNDIyODkzMzc1MzE5ODQ2OTMwOTIyNjIwNzAzMTEwMDgwODU5OTE4ODQ0MzgyOTQ3ODczMjAwNzA4MTY2MzA0NDk4ODk0MDA4NTMyIiwiYyI6IjIyNDQyNTM5MzYwMzYzNjQyODI1ODkxNTc5ODgzMDE5Mjc3Mjk0NTQ2MjUwMDEzNTM3MzI2OTY2NzM3MzE0NTUxMjEwMjU3MjU2NDU5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": { + "on": [ + "RECEIPT", + ], + }, + "~service": { + "recipientKeys": [ + "GVbwqUMqzxKaEVWjn1aPBfjJpYQHVejinpx8GCeEuQjW", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3000", + }, + "~thread": { + "thid": "c5fc78be-b355-4411-86f3-3d97482b9841", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2023-03-18T18:54:01.369Z", + }, + }, + "e1e7b5cb-9489-4cb5-9edd-77aa9b3edb64": { + "id": "e1e7b5cb-9489-4cb5-9edd-77aa9b3edb64", + "tags": { + "associatedRecordId": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "messageId": "2a6a3dad-8838-489b-aeea-deef649b0dc1", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/request-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "2c250bf3-da8b-46ac-999d-509e4e6daafa", + "createdAt": "2023-03-18T18:54:01.098Z", + "id": "e1e7b5cb-9489-4cb5-9edd-77aa9b3edb64", + "message": { + "@id": "2a6a3dad-8838-489b-aeea-deef649b0dc1", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiY3F4ZW8ybzVIWmVjYWc3VHo5aTJXcTRqejQxTml4THJjZW5HV0s2QmJKVyIsImNyZWRfZGVmX2lkIjoiQTRDWVBBU0pZUlpSdDk4WVdyYWMzSDozOkNMOjcyODI2NTpUQUciLCJibGluZGVkX21zIjp7InUiOiI3OTE4NzUwNzgzMzQxMjU3ODU3OTUzMjc2OTU5MjcxOTcyMDQwMjQxMTU1MzcyODEwOTQ2NTEwMjk5MDA1ODEyMTcwNjkwMjIzMTQ2ODU0NDk1MTI1NjQ1MTg3ODQxNzk0NjA3OTUwOTQ1OTM3MDYxMjk1ODgwMjIxMzE2NTg1MTIyNDY1Mzk1MjAwNDQ3MDIwNzAxNDA0NjEwMDc4MzkyMjA4NzkxMjk5NzYwMzM4OTIxNDMzMDc1Njk2ODU0NTY3MTIzNjYxNTYwNDMwNDE3NzQwMzc5MzA4NDQzODcyOTU1NzAwNTk1MTg2NzcxODY3MzM5NzQ5NDgzODYxNTQ2NTE2MTU4NTM5MjkyNDQzNTQ3OTg3MDUwMzE4OTAyOTI5OTgyNzMzMDk1ODk4MDIyMjg2OTk1OTQwMjkzMTg3NTg5NDgwNTgwNTAwNjM0NzAyNjQxNDM0MTgxNjIwMTU4OTU3MzUyNTE1OTA4NDE2MjI4MDQ0NDA2OTU4MTk1MDg4Mjc0ODI4Njc3OTQxMDgxOTczOTg3NjU1MDEzNDUxMzA4ODQyMjYyMzY4MTQzOTExNjIxMTE0NzYyNTk3Nzg1MDczMTM4MDg3NTQ2MDIyMzc1NjQxODQ5ODI2OTg2MjYwMDE5NTAzNzE3OTk0NjM3MzIyNDExNTgzNTY0MTQ1NjcwMTM5OTQ1MjQxOTU2Njk2MDQ3MTQzNjA4NjQ0MDM5OTg2NTYwODUyMzA1MTczMjUxMTUyMDIzODI5NjI3MzQyODM2NDI3MjkwNDQ5NTA3OTY0Nzk4MTQ2NjkzOTUxMDkwNzUwOTAyNiIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6Ijk4MjIzNzMzMDcwMDk1NjM0MzU5MTE2MDEyOTgzMDcyMjc5MjcxMzk1MTg0NjA0NDcxNDQ5NzA1Nzg0MTEyNDAyODMwNzIyMTUxMzIxIiwidl9kYXNoX2NhcCI6IjU5ODA0MTY4ODAxODk1NDAzMjkwMzczMTA3NzA4MzUwODc1NjY4MDcyNDY3ODAyOTcyOTIyNjUzNDE5ODU2MjMyNTIzNDI4OTUxODQ4NjEyNDE1MjM5Nzk3Mjk5ODY2MzIxNjU5NDQ1MTM1NzQ4NzU2MDY1NjgyOTY5MjY4ODI5MTYyMDA0NjQ4NzYwMzg4NTg4NDkyNjg1NDI1MTg1MDU2OTAxOTkxMjcwMzYwMDk3MDc5NjEyNTYxMzY4NzU1OTcwMjY5MjI4MDYzMjMyODU0NzI0NDkyOTA5Njg5MDMyOTg4NjYyMjk5Mzg3MDU2NDEwNjU5MDU3MDUwNjE0MDQ2NzE1NjA0NTgyMzM2NTg4MjMxMjI3MTEzMDEzMDQxMTA0NTU2NzM1MDE3ODUwNzUzODcwNjc2ODYxNDA4MjA0NzkzMDIzNTYwMDEwNTEzODAwNzA4MjAyNjAyNjQ0Mjg2NzI4NjAyOTk5MzU5MDcwODQxMTQ5MTAzMjA5MzY0ODkyMzMzMDYwMDgzMTA5NDIzOTQ5NzE4NTk5NjEzMzk2NjIyMjc4MTExMzk5ODU0MTcyMjMyNTQzOTk1Njc5NDk3Mjk1Nzc1NzA0MjA0MTQxOTU2NDI1MDc4NjYzMzgwMDA1Nzc2ODY2MTcxNTY4OTU1NjE4NjAwMTA2NzkxMjIyNDkyODA2NzI1ODU1NDY2Nzk4OTEzMTc2NDcxMDY3MTk5ODQ2ODEwNDI5MDIzMDc3ODI3OTc1OTIzMDIzNjU3MTg0NjkwNzE0MjkxNDk0MDc5MTM1NzYyOTUxNTc0MjMzNjMwMjExNDQ1Njc3NzE1Mzg3Mzc1NjkyMjAzODE3NDczNDk5NDExNzE5MzIwMjczNzExOTIzMzM3MzYzMTAyOTkwMDcyMjE2MjYzMzUxMzMxNTk4ODk1OTU3MzU1MDc1NTEzODE0NTUwNzkyMCIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiNTY0MTY1NjA2NjM0MTUwODgzMjg2MjM1NDM1MjUyODQ3MjAxMTk4NTQyNDAxMjYzNTY2MDQ2MTA3OTU3NzI4NTQ0MTMwMjgzNjUyNjQzOTI0NDc2NTU2NTIzNzg0ODI1OTgyMzMwMzc4NDI4OTM0MDkxNDcwNDM0OTAwMTM3NzkwNDkxMjM4NTA4ODk2ODQxMzkwNjQ4MTA1NzY5ODYzMzI1OTAzODI1Mjk1MjU2OTQ0NSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMzE1MDIwOTY0MDY4NjgyMDQ0Mzc4MjEifQ==", + }, + "mime-type": "application/json", + }, + ], + "~service": { + "recipientKeys": [ + "cqxeo2o5HZecag7Tz9i2Wq4jz41NixLrcenGWK6BbJW", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001", + }, + "~thread": { + "thid": "c5fc78be-b355-4411-86f3-3d97482b9841", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2023-03-18T18:54:01.099Z", + }, + }, +} +`; + +exports[`UpdateAssistant | AnonCreds | v0.3.1 - v0.4 should correctly update the schema and credential definition, and create link secret records for issuers 1`] = ` +{ + "1-4e4f-41d9-94c4-f49351b811f1": { + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "isDefault": true, + "linkSecretId": "Wallet: 0.3 Update AnonCreds - Issuer - static", + }, + "type": "AnonCredsLinkSecretRecord", + "value": { + "_tags": { + "isDefault": true, + }, + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "linkSecretId": "Wallet: 0.3 Update AnonCreds - Issuer - static", + "metadata": {}, + "updatedAt": "2023-03-19T22:50:20.522Z", + "value": undefined, + }, + }, + "1545e17d-fc88-4020-a1f7-e6dbcf1e5266": { + "id": "1545e17d-fc88-4020-a1f7-e6dbcf1e5266", + "tags": { + "credentialDefinitionId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/CLAIM_DEF/728266/TAG2222", + "issuerId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H", + "methodName": "indy", + "schemaId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/AnotherSchema/5.12", + "tag": "TAG2222", + "unqualifiedCredentialDefinitionId": "A4CYPASJYRZRt98YWrac3H:3:CL:728266:TAG2222", + }, + "type": "AnonCredsCredentialDefinitionRecord", + "value": { + "credentialDefinition": { + "issuerId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H", + "schemaId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/AnotherSchema/5.12", + "tag": "TAG2222", + "type": "CL", + "value": { + "primary": { + "n": "92672464557302826159958381706610232890780336783477671819498833000372263812875113518039840314305532823865676182383425212337361529127538393953888294696727490398569250059424479369124987018050461872589017845243006613503064725987487445193151580781503573638936354603906684667833347097853363102011613363551325414493877438329911648643160153986822516630519571283817404410939266429143144325310144873915523634615108054232698216677813083664813055224968248142239446186423096615162232894052206134565411335121805318762068246410255953720296084525738290155785653879950387998340378428740625858243516259978797729172915362664095388670853", + "r": { + "age": "66774168049579501626527407565561158517617240253618394664527561632035323705337586053746273530704030779131642005263474574499533256973752287111528352278167213322154697290967283640418150957238004730763043665983334023181560033670971095508406493073727137576662898702804435263291473328275724172150330235410304531103984478435316648590218258879268883696376276091511367418038567366131461327869666106899795056026111553656932251156588986604454718398629113510266779047268855074155849278155719183039926867214509122089958991364786653941718444527779068428328047815224843863247382688134945397530917090461254004883032104714157971400208", + "height": "36770374391380149834988196363447736840005566975684817148359676140020826239618728242171844190597784913998189387814084045750250841733745991085876913508447852492274928778550079342017977247125002133117906534740912461625630470754160325262589990928728689070499835994964192507742581994860212500470412940278375419595406129858839275229421691764136274418279944569154327695608011398611897919792595046386574831604431186160019573221025054141054966299987505071844770166968281403659227192031982497703452822527121064221030191938050276126255137769594174387744686048921264418842943478063585931864099188919773279516048122408000535396365", + "master_secret": "26619502892062275386286102324954654427871501074061444846499515284182097331967223335934051936866595058991987589854477281430063143491959604612779394547177027208671151839864660333634457188140162529133121090987235146837242477233778516233683361556079466930407338673047472758762971774183683006400366713364299999136369605402942210978218705656266115751492424192940375368169431001551131077280268253962541139755004287154221749191778445668471756569604156885298127934116907544590473960073154419342138695278066485640775060389330807300193554886282756714343171543381166744147102049996134009291163457413551838522312496539196521595692", + "name": "86741028136853574348723360731891313985090403925160846711944073250686426070668157504590860843944722066104971819518996745252253900749842002049747953678564857190954502037349272982356665401492886602390599170831356482930058593126740772109115907363756874709445041702269262783286817223011097284796236690595266721670997137095592005971209969288260603902458413116126663192645410011918509026240763669966445865557485752253073758758805818980495379553872266089697405986128733558878942127067722757597848458411141451957344742184798866278323991155218917859626726262257431337439505881892995617030558234045945209395337282759265659447047", + }, + "rctxt": "71013751275772779114070724661642241189015436101735233481124050655632421295506098157799226697991094582116557937036881377025107827713675564553986787961039221830812177248435167562891351835998258222703796710987072076518659197627933717399137564619646356496210281862112127733957003638837075816198062819168957810762822613691407808469027306413697001991060047213339777833838291591976754857934071589843434238025803790508552421154902537027548698271140571140256835534208651964449214890690159171682094521879102663244464066621388809286987873635426369915309596945084951678722672915158041830248278889303704844284468270547467324686757", + "s": "14126994029068124564262196574803727042317991235159231485233854758856355239996741822278406673337232628669751727662479515044513565209261235580848666630891738643990084502393352476512637677170660741636200618878417433799077613673205726221908822955109963272016538705991333626487531499501561952303907487494079241110050020874027756313402672435051524680914533743665605349121374703526870439925807395782970618162620991315112088226807823652545755186406850860290372739405126488851340032404507898084409367889215777693868794728141508635105180827151292046483128114528214465463152927678575672993454367871685772245405671312263615738674", + "z": "90415953543044389740703639345387867170174070770040351538453902580989033567810029650534915348296084212079064544906463014824475317557221991571331212308335167828473551776349999211544897426382305096215624787217055491736755052175278235595298571339706430785816901931128536495808042995635624112114867111658850659510246291844949806165980806847525704751270260070165853067310918184720602183083989806069386048683955313982129380729637761521928446431397104973906871109766946008926113644488012281655650467201044142029022180536946134328567554182760495139058952910079169456941591963348364521142012653606596379566245637724637892435425", + }, + "revocation": { + "g": "1 1864FF219549D1BC1E492955305FC5EED27C114580F206532D2F5D983A1DD3BD 1 0414758D7B6B254A9CA81E1084721A97CA312497C21BB9B16096636C59F9D105 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "g_dash": "1 2327DA248E721E3935D81C5579DD3707882FFB962B518D37FB1112D96CC63611 1 164989452135CF5D840A20EE354DBF26BEEC74DE7FD53672E55224BEE0228128 1 0634D5E85C210319BFD2535AFD8F7F79590B2F5CC61AF794218CC50B43FBB8C6 1 0A63F1C0FC2C4540156C7A2E2A2DF1DDF99879C25B4F622933707DD6074A0F1B 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000", + "h": "1 0A031B1932CDFEE76C448CA0B13A7DDC81615036DA17B81DB2E5DFC7D1F6CD6F 1 06F46C9CC7D32A11C7D2A308D4C71BEE42B3BD9DD54141284D92D64D3AC2CE04 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "h0": "1 1C88CA353EF878B74E7F515C88E2CBF11FDC3047E3C5057B34ECC2635B4F8FA5 1 1D645261FBC6164EC493BB700B5D8D5C8BF876FD9BA034B107753C79A53B0321 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "h1": "1 16AC82FE7769689173EABA532E7A489DF87F81AE891C1FDA90FE9813F6761D71 1 147E45451C76CD3A9B0649B12E27EA0BF4E85E632D1B2BEC3EC9FFFA51780ACE 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "h2": "1 2522C4FAA35392EE9B35DAC9CD8E270364598A5ED019CB34695E9C01D43C16DC 1 21D353FB299C9E39C976055BF4555198C63F912DBE3471E930185EF5A20470E5 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "h_cap": "1 1E3272ABDFD9BF05DB5A7667335A48B9026C9EA2C8DB9FA6E59323BBEB955FE2 1 031BD12497C5BBD68BEA2D0D41713CDFFDCBE462D603C54E9CA5F50DE792E1AB 1 05A917EBAA7D4B321E34F37ADC0C3212CE297E67C7D7FEC4E28AD4CE863B7516 1 16780B2C5BF22F7868BF7F442987AF1382F6465A581F6824245EFB90D4BB8B62 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000", + "htilde": "1 24D87DBC6283534AE2AA38C45E52D83CC1E70BD589C813F412CC68563F52A2CA 1 05189BC1AAEE8E2A6CB92F65A8C0A18E4125EE61E5CEF1809EF68B388844D1B1 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "pk": "1 0A165BF9A5546F44298356622C58CA29D2C8D194402CAFCAF5944BE65239474E 1 24BA0620893059732B89897F601F37EF92F9F29B4526E094DA9DC612EB5A90CD 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8", + "u": "1 1F654067166C73E14C4600C2349F0756763653A0B66F8872D99F9642F3BD2013 1 24B074FFB3EE1E5E7A17A06F4BCB4082478224BD4711619286266B59E3110777 1 001B07BEE5A1E36C0BBC31E56E039B39BB0A1BA2F491C2F674EC5CB89150FC2F 1 0F4F1E71A11EB1215DE5A081B7651E1E22C30FCCC5566E13F0A8062DB67B9E32 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000", + "y": "1 020240A177435C7D5B1DBDB78A5F0A34A353447991E670BA09E69CCD03FA6800 1 1501D3C784703A097EDDE368B27B85229030C2942C4874CB913C7AAB8C3EF61A 1 109DB12EF355D8A477E353970300E8C0AC2E48793D3DC13416BFF75145BAD753 1 079C6F242737A5D97AC34CDE4FDE4BEC057A399E73E4EF87E7024048163A005F 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + }, + "credentialDefinitionId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/CLAIM_DEF/728266/TAG2222", + "id": "1545e17d-fc88-4020-a1f7-e6dbcf1e5266", + "metadata": {}, + "methodName": "indy", + "updatedAt": "2023-03-19T22:50:20.522Z", + }, + }, + "598dbcc3-5272-4503-9c67-b0cb69a9d3d6": { + "id": "598dbcc3-5272-4503-9c67-b0cb69a9d3d6", + "tags": { + "associatedRecordId": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "messageId": "2a6a3dad-8838-489b-aeea-deef649b0dc1", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/request-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "createdAt": "2023-03-18T18:54:01.126Z", + "id": "598dbcc3-5272-4503-9c67-b0cb69a9d3d6", + "message": { + "@id": "2a6a3dad-8838-489b-aeea-deef649b0dc1", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiY3F4ZW8ybzVIWmVjYWc3VHo5aTJXcTRqejQxTml4THJjZW5HV0s2QmJKVyIsImNyZWRfZGVmX2lkIjoiQTRDWVBBU0pZUlpSdDk4WVdyYWMzSDozOkNMOjcyODI2NTpUQUciLCJibGluZGVkX21zIjp7InUiOiI3OTE4NzUwNzgzMzQxMjU3ODU3OTUzMjc2OTU5MjcxOTcyMDQwMjQxMTU1MzcyODEwOTQ2NTEwMjk5MDA1ODEyMTcwNjkwMjIzMTQ2ODU0NDk1MTI1NjQ1MTg3ODQxNzk0NjA3OTUwOTQ1OTM3MDYxMjk1ODgwMjIxMzE2NTg1MTIyNDY1Mzk1MjAwNDQ3MDIwNzAxNDA0NjEwMDc4MzkyMjA4NzkxMjk5NzYwMzM4OTIxNDMzMDc1Njk2ODU0NTY3MTIzNjYxNTYwNDMwNDE3NzQwMzc5MzA4NDQzODcyOTU1NzAwNTk1MTg2NzcxODY3MzM5NzQ5NDgzODYxNTQ2NTE2MTU4NTM5MjkyNDQzNTQ3OTg3MDUwMzE4OTAyOTI5OTgyNzMzMDk1ODk4MDIyMjg2OTk1OTQwMjkzMTg3NTg5NDgwNTgwNTAwNjM0NzAyNjQxNDM0MTgxNjIwMTU4OTU3MzUyNTE1OTA4NDE2MjI4MDQ0NDA2OTU4MTk1MDg4Mjc0ODI4Njc3OTQxMDgxOTczOTg3NjU1MDEzNDUxMzA4ODQyMjYyMzY4MTQzOTExNjIxMTE0NzYyNTk3Nzg1MDczMTM4MDg3NTQ2MDIyMzc1NjQxODQ5ODI2OTg2MjYwMDE5NTAzNzE3OTk0NjM3MzIyNDExNTgzNTY0MTQ1NjcwMTM5OTQ1MjQxOTU2Njk2MDQ3MTQzNjA4NjQ0MDM5OTg2NTYwODUyMzA1MTczMjUxMTUyMDIzODI5NjI3MzQyODM2NDI3MjkwNDQ5NTA3OTY0Nzk4MTQ2NjkzOTUxMDkwNzUwOTAyNiIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6Ijk4MjIzNzMzMDcwMDk1NjM0MzU5MTE2MDEyOTgzMDcyMjc5MjcxMzk1MTg0NjA0NDcxNDQ5NzA1Nzg0MTEyNDAyODMwNzIyMTUxMzIxIiwidl9kYXNoX2NhcCI6IjU5ODA0MTY4ODAxODk1NDAzMjkwMzczMTA3NzA4MzUwODc1NjY4MDcyNDY3ODAyOTcyOTIyNjUzNDE5ODU2MjMyNTIzNDI4OTUxODQ4NjEyNDE1MjM5Nzk3Mjk5ODY2MzIxNjU5NDQ1MTM1NzQ4NzU2MDY1NjgyOTY5MjY4ODI5MTYyMDA0NjQ4NzYwMzg4NTg4NDkyNjg1NDI1MTg1MDU2OTAxOTkxMjcwMzYwMDk3MDc5NjEyNTYxMzY4NzU1OTcwMjY5MjI4MDYzMjMyODU0NzI0NDkyOTA5Njg5MDMyOTg4NjYyMjk5Mzg3MDU2NDEwNjU5MDU3MDUwNjE0MDQ2NzE1NjA0NTgyMzM2NTg4MjMxMjI3MTEzMDEzMDQxMTA0NTU2NzM1MDE3ODUwNzUzODcwNjc2ODYxNDA4MjA0NzkzMDIzNTYwMDEwNTEzODAwNzA4MjAyNjAyNjQ0Mjg2NzI4NjAyOTk5MzU5MDcwODQxMTQ5MTAzMjA5MzY0ODkyMzMzMDYwMDgzMTA5NDIzOTQ5NzE4NTk5NjEzMzk2NjIyMjc4MTExMzk5ODU0MTcyMjMyNTQzOTk1Njc5NDk3Mjk1Nzc1NzA0MjA0MTQxOTU2NDI1MDc4NjYzMzgwMDA1Nzc2ODY2MTcxNTY4OTU1NjE4NjAwMTA2NzkxMjIyNDkyODA2NzI1ODU1NDY2Nzk4OTEzMTc2NDcxMDY3MTk5ODQ2ODEwNDI5MDIzMDc3ODI3OTc1OTIzMDIzNjU3MTg0NjkwNzE0MjkxNDk0MDc5MTM1NzYyOTUxNTc0MjMzNjMwMjExNDQ1Njc3NzE1Mzg3Mzc1NjkyMjAzODE3NDczNDk5NDExNzE5MzIwMjczNzExOTIzMzM3MzYzMTAyOTkwMDcyMjE2MjYzMzUxMzMxNTk4ODk1OTU3MzU1MDc1NTEzODE0NTUwNzkyMCIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiNTY0MTY1NjA2NjM0MTUwODgzMjg2MjM1NDM1MjUyODQ3MjAxMTk4NTQyNDAxMjYzNTY2MDQ2MTA3OTU3NzI4NTQ0MTMwMjgzNjUyNjQzOTI0NDc2NTU2NTIzNzg0ODI1OTgyMzMwMzc4NDI4OTM0MDkxNDcwNDM0OTAwMTM3NzkwNDkxMjM4NTA4ODk2ODQxMzkwNjQ4MTA1NzY5ODYzMzI1OTAzODI1Mjk1MjU2OTQ0NSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMzE1MDIwOTY0MDY4NjgyMDQ0Mzc4MjEifQ==", + }, + "mime-type": "application/json", + }, + ], + "~service": { + "recipientKeys": [ + "cqxeo2o5HZecag7Tz9i2Wq4jz41NixLrcenGWK6BbJW", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001", + }, + "~thread": { + "thid": "c5fc78be-b355-4411-86f3-3d97482b9841", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2023-03-18T18:54:01.126Z", + }, + }, + "6ef35f59-a732-42f0-9c5e-4540cd3a672f": { + "id": "6ef35f59-a732-42f0-9c5e-4540cd3a672f", + "tags": { + "credentialDefinitionId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/CLAIM_DEF/728265/TAG", + "issuerId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H", + "methodName": "indy", + "schemaId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/Test Schema/5.0", + "tag": "TAG", + "unqualifiedCredentialDefinitionId": "A4CYPASJYRZRt98YWrac3H:3:CL:728265:TAG", + }, + "type": "AnonCredsCredentialDefinitionRecord", + "value": { + "credentialDefinition": { + "issuerId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H", + "schemaId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/Test Schema/5.0", + "tag": "TAG", + "type": "CL", + "value": { + "primary": { + "n": "92212366077388130017820454980772482128748816766820141476572599854614095851660955000471493059368591899172871902601780138917819366396362308478329294184309858890996528496805316851980442998603067852135492500241351106196875782591605768921500179261268030423733287913264566336690041275292095018304899931956463465418485815424864260174164039300668997079647515281912887296402163314193409758676035183692610399804909476026418386307889108672419432084350222061008099663029495600327790438170442656903258282723208685959709427842790363181237326817713760262728130215152068903053780106153722598661062532884431955981726066921637468626277", + "r": { + "age": "12830581846716232289919923091802380953776468678758115385731032778424701987000173171859986490394782070339145726689704906636521504338663443469452098276346339448054923530423862972901740020260863939784049655599141309168321131841197392728580317478651190091260391159517458959241170623799027865010022955890184958710784660242539198197998462816406524943537217991903198815091955260278449922637325465043293444707204707128649276474679898162587929569212222042385297095967670138838722149998051089657830225229881876437390119475653879155105350339634203813849831587911926503279160004910687478611349149984784835918594248713746244647783", + "master_secret": "61760181601132349837705650289020474131050187135887129471275844481815813236212130783118399756778708344638568886652376797607377320325668612002653752234977886335615451602379984880071434500085608574636210148262041392898193694256008614118948399335181637372037261847305940365423773073896368876304671332779131812342778821167205383614143093932646167069176375555949468490333033638790088487176980785886865670928635382374747549737473235069853277820515331625504955674335885563904945632728269515723913822149934246500994026445014344664596837782532383727917670585587931554459150014400148586199456993200824425072825041491149065115358", + "name": "26931653629593338073547610164492146524581067674323312766422801723649824593245481234130445257275008372300577748467390938672361842062002005882497002927312107798057743381013725196864084323240188855871993429346248168719358184490582297236588103100736704037766893167139178159330117766371806271005063205199099350905918805615139883380562348264630567225617537443345104841331985857206740142310735949731954114795552226430346325242557801443933408634628778255674180716568613268278944764455783252702248656985033565125477742417595184280107251126994232013125430027211388949790163391384834400043466265407965987657397646084753620067162", + }, + "rctxt": "49138795132156579347604024288478735151511429635862925688354411685205551763173458098934068417340097826251030547752551543780926866551808708614689637810970695962341030571486307177314332719168625736959985286432056963760600243473038903885347227651607234887915878119362501367507071709125019506105125043394599512754034429977523734855754182754166158276654375145600716372728023694171066421047665189687655246390105632221713801254689564447819382923248801463300558408016868673087319876644152902663657524012266707505607127264589517707325298805787788577090696580253467312664036297509153665682462337661380935241888630672980409135218", + "s": "51390585781167888666038495435187170763184923351566453067945476469346756595806461020566734704158200027078692575370502193819960413516290740555746465017482403889478846290536023708403164732218491843776868132606601025003681747438312581577370961516850128243993069117644352618102176047630881347535103984514944899145266563740618494984195198066875837169587608421653434298405108448043919659694417868161307274719186874014050768478275366248108923366328095899343801270111152240906954275776825865228792303252410200003812030838965966766135547588341334766187306815530098180130152857685278588510653805870629396608258594629734808653690", + "z": "60039858321231958911193979301402644724013798961769784342413248136534681852773598059805490735235936787666273383388316713664379360735859198156203333524277752965063504355175962212112042368638829236003950022345790744597825843498279654720032726822247321101635671237626308268641767351508666548662103083107416168951088459343716911392807952489009684909391952363633692353090657169830487309162716174148340837088238136793727262599036868196525437496909391247737814314203700293659965465494637540937762691328712617352605531361117679740841379808332881579693119257467828678864789270752346248637901288389165259844857126172669320275054", + }, + }, + }, + "credentialDefinitionId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/CLAIM_DEF/728265/TAG", + "id": "6ef35f59-a732-42f0-9c5e-4540cd3a672f", + "metadata": {}, + "methodName": "indy", + "updatedAt": "2023-03-19T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2023-03-18T18:53:43.140Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.4", + "updatedAt": "2023-03-19T22:50:20.522Z", + }, + }, + "a56d83c5-2427-4f06-9a90-585623cf854a": { + "id": "a56d83c5-2427-4f06-9a90-585623cf854a", + "tags": { + "connectionId": undefined, + "credentialIds": [], + "parentThreadId": undefined, + "role": undefined, + "state": "offer-sent", + "threadId": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + }, + "type": "CredentialRecord", + "value": { + "createdAt": "2023-03-18T18:53:59.859Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "John", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "99", + }, + { + "mime-type": "text/plain", + "name": "height", + "value": "180", + }, + ], + "credentials": [], + "id": "a56d83c5-2427-4f06-9a90-585623cf854a", + "metadata": { + "_anoncreds/credential": { + "credentialDefinitionId": "A4CYPASJYRZRt98YWrac3H:3:CL:728266:TAG2222", + "schemaId": "A4CYPASJYRZRt98YWrac3H:2:AnotherSchema:5.12", + }, + }, + "protocolVersion": "v2", + "state": "offer-sent", + "threadId": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + "updatedAt": "2023-03-19T22:50:20.522Z", + }, + }, + "b88030f7-dc33-4e7e-bf4d-cdfaa6e51ebf": { + "id": "b88030f7-dc33-4e7e-bf4d-cdfaa6e51ebf", + "tags": { + "associatedRecordId": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "messageId": "578fc144-1e01-418c-b564-1523eb1e95b8", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/issue-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "createdAt": "2023-03-18T18:54:01.192Z", + "id": "b88030f7-dc33-4e7e-bf4d-cdfaa6e51ebf", + "message": { + "@id": "578fc144-1e01-418c-b564-1523eb1e95b8", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjI6VGVzdCBTY2hlbWE6NS4wIiwiY3JlZF9kZWZfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjM6Q0w6NzI4MjY1OlRBRyIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJhZ2UiOnsicmF3IjoiOTkiLCJlbmNvZGVkIjoiOTkifSwibmFtZSI6eyJyYXciOiJKb2huIiwiZW5jb2RlZCI6Ijc2MzU1NzEzOTAzNTYxODY1ODY2NzQxMjkyOTg4NzQ2MTkxOTcyNTIzMDE1MDk4Nzg5NDU4MjQwMDc3NDc4ODI2NTEzMTE0NzQzMjU4In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjMyMTIwMDQ1ODc4MzIxMjcyMzA1ODI5MTc3NzMzMTIwNzE1OTY5NDEyNjkwNjUyNDQ4OTc0MTA4NzEzNjU0ODc3NTg2MzIzMTI3ODk2IiwiYSI6IjIyMjY0MTYwNjIwODcwNDUyNTExMjcyMzE1MzMzMDA0MjQzMzY3NTM2NzM3NDMwNjM1NjExMjcwMDkwOTE4NDMwNzc0ODEzMjAzNjQwNjMxMjIyNDczMzk0MjQ4MTgzMDIzMjIyNzExNDUwMzQxMDcxOTQyNDQwMDgwMjY2Nzk1Mzg5Mzg5Njc1NjYwOTUzNTQyMDE4OTA3NjQ3NzI4OTQ4NjY1MzA2Njg0NjExNDU1NTI5NzM5OTY1NDcyMjQ2NDQxMzE1NzAxMzM1ODc1MDY3MjExMDk3NzcyOTgwMjU1NDIxMDMzMTI1MjAyMTQzNDk3NjMyOTAyMjM1NDAyMzU5OTA1MzY5MzE4MjI1NTc4MjUxNjY4NTYzNzc1NTY0MDM2MjUxNzE0Mzk3MTEzNjQ3OTg0MjcxMTE5MTU2NDQ3NjI1OTk1NjE5MjAwMDk4MTgzNzY1NjkzMTg1ODEzNjA1NDU3OTQwMzE0MDU2MDkzMTI2MzQ3OTU5MzYwODIyMzg0OTEzODg3Mjg3ODI2NjkyNDIyNDMyNDUwMDA5OTYxNjQ2MjMzNTE3MjY3NDU1OTkyMjA3MTE3Mzk5NzU1NjY3MTA3MzM1NTQ0MzEwNDQwNDE1NDE5NTk5NTA1OTgxMzkwMjk5NDUxNzQyODg4NDg0MTc0NTU5MDA5NDgwNjU5MDk2Nzg2ODI2MDgxNzc3MzcwNTk1MTU3OTg5NjQ1MDYxNDI2OTA2ODM2MDk5NTU5MDQ0MDI4ODM2MzYwOTM2MDkwOTkxNjA1OTU0NTM2OTQxMjgxODQwNzk2MjkxODc0ODk2NDEzNTM5IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDExMzE1MTE0OTUxODg0MDQ5ODkyNjAwNTY3NTgzMTc4ODkwMyIsInYiOiI2NTE5ODc4MzYyODQ5NzExNDY4NDkyNDM1OTM3MDU4NzMzOTYxMTkxNjA3NjI4MzUzNjkxMDg1MzM5MDMwNDU0OTkyODc0ODYyODkyNDg4ODYzNTA3MDQ1MTM1MzA4ODI1NDA2NzYwMTQwNDQzNzM0NDYzODE5NTM2MzE0NzcxMTQ3MDk4MjU2ODMzNTc2MjIwNDI5ODQyNjc3NzMwMzQwODYwNjE2NTcxNzc5NjU4OTIxNDY4Mjc0NTUwOTc5NjYyMDkxNzEwNDU5MDk2MDgzMzYzNTc1Mjc0MjQzNzIyMzIzOTIxMjY5MDYyMjE0NjQyNzQyMTI0MzQ4MTY0MDUxNzE3MTk5MTkzODY3NTM3NTEzNjYzOTY1ODQzMDI5MjAxODA0OTE2MTEzNzMxODYzOTUzNjQ5MDkwNDgzNzMyMTkxNTQ2MTEwMjAxNTg0NzMxODg4NTE5NjA2MjE1OTkyNTgxNzk2MDg2NzUzOTE5NzUxMjkwMDI3MDI4NzczMTAwOTc5ODI5MzQ5NzA0MTUyMDEzNjg2MzU1MzM1MjIyNjU5MDY2NzE0NDQ2NDc4NzY3MTE5NDE4MjY3OTg5NTAyNzc4MjMzNzM3MjM4MjU1MTQxNzQyMjk4NTU3MDY2NzA2MTM0NzYwMjQwMzY3OTMzMzc5NzYzMTc5MTI1NTI4MDQwMzkxNjQwNTIyNTM5NjE5NTU0NTE0NTk4OTUxNTg0NjA3MjYwNzk1NzE1MDMyMjM4NTQ3ODMyMzA0NTY2MzQ4NjYzMTc0NzQwMDE2MzQ2NTU2MTM1ODc4MzgxNTYzODQ2NzU0MzQzMjk0NTIzNjc0NDI3NjQxNjAxNjAwNjE2NzI3NjEyMzc0MzI2NzY4ODA5NjAyNTE5MTAzOTk3NDY4OTg1NTg3Nzg4MjI3Njc5MzQ4NTgwNzk1OTkyOTkxNzMzMDg5MTUyMTg2MDg4OTU2MTg2MTQ0OTkyMDI5OTI2OTUxOTU0OTQyNjYwMDUxOTM0MDc5NzkxODI1NzA2MTExNzg0MDU2NDM2OTA2MDgxMDQ2MDQ5ODI0ODE1NDE0MTc5NTMzMDA2ODE4NzQ3NzgwNTQ5In0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjEwMTUyMDI0OTk1MTUxMzcyOTgwNzI5NDM1MTQ5MzgyMzQxMDQ3MDIzNjI2NjA4MDc3ODg1NzQ2Mjg4MTE3MjI2OTAyNzM4OTY5OTEwMTU0NjgzNzM2MzI4MzQ2MDg0MjcwMDA3MTY2NTg3ODI5MjE2MzEzNDc4MDk3Njc0MTI0ODU2ODIyMjA4NzI0Nzk1NTE0ODgzOTYwMzE5NDc5OTg4NzAzNDUyNjI4NjYxMDc3MTg3OTIyMzA1NDc5MDE2NzQzOTk0NzYwMzE5NzI1OTExODk0MjM2NDMxMDkxMTIyNTUxNTU0NzgwODg0NjQ2MjE0MTUzMDUzMTM2NDMwMTk4OTA5MTM0OTk4OTM2NjY3MzI4ODI2MDcwNzEzMzk0NDg0NDI0ODUxNjkxMzUxNDc0NjAxMjIwODk2NTIyMDYzNDA5NzA4NDA1Njk2MzY5MjA0MzU0NzE1MDkxMzk2Mzc4Mzc3MzA0ODk3MzMwOTM0Mjc2NTQyNjE2NjAxNTk1ODI5NzgxOTg3NTMyNzkxMzIyNTgzOTE1Njk1OTY2MjM3MTc4Njg1NTMzNTE3MTQxNTAyNDE3MzQxMDIzMTA1MTczMjMwMTcwNzUzODYwMjgxNDAxODk4MDE5OTQwNjA2MzczOTYwMzYxNjA3NTE2NjgyMDg4MTc1NzU4ODA0Mzg4MTM5MTQ0MDkwMjg5MzI5NzMzNTQ1NDg4MjUyNjczNDIyODkzMzc1MzE5ODQ2OTMwOTIyNjIwNzAzMTEwMDgwODU5OTE4ODQ0MzgyOTQ3ODczMjAwNzA4MTY2MzA0NDk4ODk0MDA4NTMyIiwiYyI6IjIyNDQyNTM5MzYwMzYzNjQyODI1ODkxNTc5ODgzMDE5Mjc3Mjk0NTQ2MjUwMDEzNTM3MzI2OTY2NzM3MzE0NTUxMjEwMjU3MjU2NDU5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": { + "on": [ + "RECEIPT", + ], + }, + "~service": { + "recipientKeys": [ + "GVbwqUMqzxKaEVWjn1aPBfjJpYQHVejinpx8GCeEuQjW", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3000", + }, + "~thread": { + "thid": "c5fc78be-b355-4411-86f3-3d97482b9841", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2023-03-18T18:54:01.192Z", + }, + }, + "be76cfbf-111b-4332-b1fe-7a1fea272188": { + "id": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "tags": { + "connectionId": undefined, + "credentialIds": [], + "parentThreadId": undefined, + "role": undefined, + "state": "done", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + }, + "type": "CredentialRecord", + "value": { + "createdAt": "2023-03-18T18:53:59.068Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "John", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "99", + }, + ], + "credentials": [], + "id": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "metadata": { + "_anoncreds/credential": { + "credentialDefinitionId": "A4CYPASJYRZRt98YWrac3H:3:CL:728265:TAG", + "schemaId": "A4CYPASJYRZRt98YWrac3H:2:Test Schema:5.0", + }, + }, + "protocolVersion": "v1", + "state": "done", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "updatedAt": "2023-03-19T22:50:20.522Z", + }, + }, + "d7353d4a-24fc-405f-9bf5-f99fae726349": { + "id": "d7353d4a-24fc-405f-9bf5-f99fae726349", + "tags": { + "associatedRecordId": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "messageId": "c5fc78be-b355-4411-86f3-3d97482b9841", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/offer-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "c5fc78be-b355-4411-86f3-3d97482b9841", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "be76cfbf-111b-4332-b1fe-7a1fea272188", + "createdAt": "2023-03-18T18:53:59.857Z", + "id": "d7353d4a-24fc-405f-9bf5-f99fae726349", + "message": { + "@id": "c5fc78be-b355-4411-86f3-3d97482b9841", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "name": "name", + "value": "John", + }, + { + "name": "age", + "value": "99", + }, + ], + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjI6VGVzdCBTY2hlbWE6NS4wIiwiY3JlZF9kZWZfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjM6Q0w6NzI4MjY1OlRBRyIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiODUxODAxMDMyNzEzNDg5NzYxOTg5MzAzNjMzMDkzOTEyOTExMDUxNjI0OTQ0OTYzMTgzNzM2MDY3NDkwOTc2MDYxODEwMDgxODkxMzQiLCJ4el9jYXAiOiI4NDk0NDg4MjQzNTk2NTkwOTc2MjQzMjc0NDg4ODk2Mjc1NTcyODAyMTQ1ODE5NDQzNTE0NzQxMzk1NDI1NjM5MzQwMTczMDIzMTQ5NzI3MDY5NzMzMzQwODgzMTU4MzQ1NTYzOTA5OTcxNDMzNTg1MjMwODAxNTYyMTM0NjczNjM1ODg5NTA3Njg5ODQwOTgyODU5Mzg1NjA1MTc1NTkxNDYxOTkyMDExNzU2Mzg1MTI3MTQ3ODgxNDMwODEzNjYxNzY0MDU5MzE0ODk4MTc2NzQzMTQ5MjYzMDMwMDQ1NzMwMDMzMzI2NzgyMzg1OTY0NjcxMzg1ODQ2MzcxNjQ5MzQxMTg2MDM5NjE4MTQwOTIwMDUxMzg1MDAwNTYxMDcyMTc5NTEyMzc5Nzk0OTU4NjE1ODIyODI2OTExNzIwNTQyNTE0MTQ1NDc5MTAxOTUyMzM4MDMwMDY1MDk5NjcxOTU2OTMxMzE2NjE5MjM0NTQ0NTE5NTQ1ODQ1MzA4MzgxMjQyNTM0NDcyOTc3NjY0MjAwMjc2MTMyOTgxODE1ODAzNTIxOTExMzk4ODkxMjE0NjE1NzA1MDM2ODM2ODU1NDU1NzY4ODg4MTUxNDgzODAyNDcyODQyMzczNzE0MTI0NTYwMzIyNTI3NDE4MTEwNzYyMjgyNzY4NzMyNTIzMDQyMDA3MDY2OTk2ODIxMTQwMzE1NDg0NzI4NTM2NzIwNDI3MDg5MTI2NDk1NTAzMjc0ODQ4MDM3MjUzOTM3NjI3MDU2ODUzMTQ4NjE5NDA4NDYxOTI5NzEzMjM4MjEwNDc4MjcyMTIxNTUwNjQzODc4ODM1NDYwMzY1OTIwMjE3NTk5NDYyNDUzMDMyNDQ4MjYyMTM3NjE5ODY0OTU4MzA1MDE3MjA4OTYwNDc1MTQxODgwMTMiLCJ4cl9jYXAiOltbIm5hbWUiLCI1MDcyNzU2NDE2NDA2ODIxNzU1OTc0MzUxMTg0NjE1NjA4NDY2NTk3Mzk0NzA2MTY1NDg2ODAzMjc3MjMyMzQyOTk4MDA0MzY0OTU0MTczMzc0NDIwOTc5NTkwMDcyODgxNDgxNDA0MTg2OTExODg5NzQ4MTgzMzQ1OTk5NzQ0NzgxMTQ1MTMwNzEyNDIzODY0Nzc1MzQzNjAzNTk2NDM3Mzg4OTgzNTExNDAzODA0NjEyNjU1MDE5NzQ4MTI5NDk3ODY2NTcwMDQyMjcwNDQxNDQ5MjYwODY0NzgyMzI5MjAxNDEzMTc5ODU3NzA0MjM5OTMyMTg4NTc4NzE3MDczNzM3NjUyNzY5MzY5NDg4OTgxNzg2NDQwNTExODAzMjMzNDMxNzA4NDk4MTU2NTA0OTUzNzkzNjU2NjQ2NzMyNTU4MzQwNDI2MDI1MjA3NTk0OTIwMDY4OTc2OTQ4Nzg2OTUxNzM3MDIwNDQ0NTA5NzYyMDQ2MzIzNzA0MDQ3MjU1ODU3NDE5ODE3MDc5NTI3NDgzNTE1NDY2NTAyMDkzOTY1NDMzMzk3MjQ1MzA4MjQ5MDgyMTQ4Mjc4NDA1MzI5Njg1Mjc0MDYwNjk0MzI0MTI2ODgxMjkyMDIyMjY1ODczMjk5MDU0NDU1OTA5NzkyNjUwNjAyMTk0NjUzMjYxMDk0ODYwOTc2NzA4ODE1ODgwMjExMTY0MTkwMDM0NjY0MzI2MDc3NjcwNzkyMDE4NTE2MzMzNDI3NjkwODYwMjIxODEwMzk5MDgxMjc5NjAwNTYzMjk3MjI0NjM0MDM0NjcxNTIwODE5MzU3NzQ0Njk2NzU1Njg1NDI2NjIzMzAwMjQ3MDUwODE4NTQ2MDM2NjA0NjMxNjcyNzE5MjI0NDA4NTE2NDM4NTgxMDM5Njk4NzI0MSJdLFsibWFzdGVyX3NlY3JldCIsIjU2MzYzNTgyMDQ5Mjg4OTY1OTg1MDA4NzgyMzU0NjgyNjMwNDkxMzQ3MTM1NDIxNTAyMDEyMTIwMzI4MDI4ODIyMjUyMzg4NjgwNTMwNTgxMTcwMTgwNDU1MTcyNTc3ODkyMTEyMTY1OTM0Mjk5NjUyNzAxNDExMzUyNDkzMzkyODU0ODI4NzMyMDQzMDI0MDI0MzM0MzMzNzc0NjEyOTEzOTUyMjAzNjM1NDk2MDQ0ODMzMDI5NDE2NjUwOTU5NjE0ODgzNTUwOTMxNzgzNTA5MzE1Nzg4MDEyODQ0MzAwMDQwMDE5MTY5MTc3NTI1OTgxMTU3OTkwNjQzMDcyMjQyNzcxMjU0MTYyNzMxOTU4NzI2Nzc1NjYwMjkxODIzMDcyNDk1Mzg0NzM5MTcwODc4ODMxNzkxMjQzMjEzMjU5MzA5ODQxNjU3MjUwOTg1NzMxMjEyNzE2MDM2MDY3MDUxNjM2NzA0MjA1NDEzMDk2MDU3MTA2NTM2MTI2ODUyNDU0NzcwMzQzMTMwMTczMjAwNjEzMDIxOTE4MzgzMDQxOTU4MTkwOTE2NzQ0NjU4NTI0ODA1NjM4Mzk2OTY3OTA3MzIwNjY1MDU1MzcwMjY0NjAxMDczMjc5NDMyNjM5MjM3Njc1NTA0OTg1NzQyNTI4NjYwMTAyMDEzNzIxMzA2MTE4MTg0NDk1MTEyNDQ2NDYyNDc2NTkwMjYxODkxMjA0OTQxOTA4MjMyNzMzNDA3MTg4MDA3NzE2NTA2OTUzMDY0Nzc5NDk5ODExNzI0ODI5NjcyNjY2NzIyNjIzOTAxMTc1OTk0NTIyNjkwMjk1ODI0MDgyNzY5NjQ0NDYxOTAxMDk2NzI3MTE5NzAzMjUzNzI4NjY3MTU1MzA5MDYzNDUyNDY2MDY3NzU5NzIwOTgyNDA3MiJdLFsiYWdlIiwiMTM2NTQxMjE0MjM5MTcyNDQxNzQ1MjU3MjcyMDI3MTA4NDYwMzU0MjgxMTA2OTA2MzYwNDIwMDE0NjUyMDIxMDgyNDEzODM2ODEyMjk3NjY3ODk2MTYzNDkzMjM4NDIxNDI4NjMyNTMxODE0ODk4NzkwMDg4OTg2NjgyMTE2OTAyMzc4NDgwNTE4OTUxNDExNzg1OTk3NTk5MDMyNDYxNjExNjIyMDUyNjMzMDQ5ODYxMzc5MTQzNzI4MTM5MTUyMDkyMzI0ODc3MjMxMTYwNTgzNzA5NjE0MzA1NzQ1MjA5MjQwNjU2MDU4NjY3OTMwODEzNzYyNDY5MDc2ODc5MTk1Nzg0Nzg4NTE2NjI3MjgxMDY0NjE3MzgzMDc4Njc5MTkwODIwMzQwNTgwNDY2MjU3ODU3NjA1MTc2MTg4NTI3OTMxMDI4MTMzNTY5Njc0Mzg2ODAwMTA2MDE2MDg1Nzc0OTcyMzI1NTAyNDA2MTY0OTY0MjU2OTUxNDI3ODAxMTQzNTQxMzUzMzI0Nzg0MzA5OTY4MjIyOTU1NDk4Njk3NTAwMDUwMzc0MDg0NjIwNzQ4MTk0NzIyMTI2NjE2OTY3OTY3Mzc1NTM3Nzc5NTc4NTMwMDIxODExNTA2MTIxNjcxMDUwNDgzNTM2MjA3Njc3MTg5NDQwNjEwNzk0NTcyNzI5MTgzMzAyMjM1MDkxMDg4NTU2ODc5NTg3OTE3MDMzMzQyODcyMzg2NDQ5MTQ0NzgwMDYyNjc4MzA3NzE4MzU1MjQ5MTUxNjc5MDA1MzkxNDA5NDE4OTQxMjEzNDkxMjQyMjg2NTAwODcyMzQxNDI3Nzk1MjQ1ODYzODE2MDY2NDY3NDkxOTg4OTU3MDEwNDIxNDA3NDkyMDUxOTc0NTMwNjIxOTk1ODU0ODczNTM5Mjk3MyJdXX0sIm5vbmNlIjoiNzk3NjAzMjE3NzA5MzM1MzAwMTcwODI4In0=", + }, + "mime-type": "application/json", + }, + ], + "~service": { + "recipientKeys": [ + "GVbwqUMqzxKaEVWjn1aPBfjJpYQHVejinpx8GCeEuQjW", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3000", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2023-03-18T18:54:00.011Z", + }, + }, + "de4c170b-b277-4220-b9dc-7e645ff4f041": { + "id": "de4c170b-b277-4220-b9dc-7e645ff4f041", + "tags": { + "issuerId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H", + "methodName": "indy", + "schemaId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/AnotherSchema/5.12", + "schemaIssuerDid": undefined, + "schemaName": "AnotherSchema", + "schemaVersion": "5.12", + "unqualifiedSchemaId": "A4CYPASJYRZRt98YWrac3H:2:AnotherSchema:5.12", + }, + "type": "AnonCredsSchemaRecord", + "value": { + "id": "de4c170b-b277-4220-b9dc-7e645ff4f041", + "metadata": {}, + "methodName": "indy", + "schema": { + "attrNames": [ + "name", + "height", + "age", + ], + "issuerId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H", + "name": "AnotherSchema", + "version": "5.12", + }, + "schemaId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/AnotherSchema/5.12", + "updatedAt": "2023-03-19T22:50:20.522Z", + }, + }, + "e531476a-8147-44db-9e3f-2c8f97fa8f94": { + "id": "e531476a-8147-44db-9e3f-2c8f97fa8f94", + "tags": { + "associatedRecordId": "a56d83c5-2427-4f06-9a90-585623cf854a", + "messageId": "4d2c80b7-4a25-42ac-b8cf-a68b1374b9b7", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/offer-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "a56d83c5-2427-4f06-9a90-585623cf854a", + "createdAt": "2023-03-18T18:54:00.005Z", + "id": "e531476a-8147-44db-9e3f-2c8f97fa8f94", + "message": { + "@id": "4d2c80b7-4a25-42ac-b8cf-a68b1374b9b7", + "@type": "https://didcomm.org/issue-credential/2.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/2.0/credential-preview", + "attributes": [ + { + "name": "name", + "value": "John", + }, + { + "name": "age", + "value": "99", + }, + { + "name": "height", + "value": "180", + }, + ], + }, + "formats": [ + { + "attach_id": "8430ddb8-b0c3-4074-8ded-f4dcfe80303d", + "format": "hlindy/cred-abstract@v2.0", + }, + ], + "offers~attach": [ + { + "@id": "8430ddb8-b0c3-4074-8ded-f4dcfe80303d", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjI6QW5vdGhlclNjaGVtYTo1LjEyIiwiY3JlZF9kZWZfaWQiOiJBNENZUEFTSllSWlJ0OThZV3JhYzNIOjM6Q0w6NzI4MjY2OlRBRzIyMjIiLCJrZXlfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjQwMTQ0MTA0NDg3MjM0NDU2MTc1NzYwMDc1NzMxNjUyNjg1MTk0MjE5MTk5NDk3NDczNTM4NjU4ODM3OTIyODMzNTEzMDg0Nzk2MDQ5IiwieHpfY2FwIjoiMzgxOTQyMjM1Mzc3MzYwODEyNjY0MDA4MjYzNDQxMDg2MDMwOTMyMjAxNzgzMjM3ODQxODQ5NDg3ODk2ODg1MTYwODY2MTY1MDM3NzI2MTIxNjU0MjcwOTg5NDY3NjAzNDExOTAzODk4MzUwMDAzNDIwODg3MzI4NTUwMTY2MTI1ODMyMjAxOTQzMTkwNzAxMDU4NTAwMDE5ODM1NjA1ODczNDYzOTkwODg3NzQ0NjY3MzU0MjM2Njc3MzcyODg0ODQyNjE5NTEwMTUwOTA2MjI1OTMzMjc1ODEyNjg2NDg3NTg5NjY3ODI3MjAwODcwOTQ0OTIyMjk5MzI3OTI4MDQ1MTk1OTIwMDI3NTc0MDQwNDA4ODU5MzAwMzY1MDYwODc3Nzg2ODkwOTE1MDU5NTA2ODc1OTI0NzE2OTI1MDM2MTc4Njg2NDE5NTYyMzcwODI4MTMzODY2Nzg3NzkyMDcwNjAyNDQzNTkzMTk2NzEzNzcyNDM2NTYzODI0MzkwMDIyNzg4MjU2MzA4NjU4OTc0OTEzMTk1ODYxODUwMTQ3ODE1Mjg5NzQwOTA4NDk1MjQ3NTAyNjYyNDc3NzQ2NTI5ODA3Mzg0OTgxODI5MDc3NTQ4OTI2NzExMDkzNzQ5MjM1ODU4NjUwNDc5NzE5NDI4MzUwMzAwNzUyNjQ0OTg1MTQ5MTMxNjA1NjUzMDIxMDYxNzkwMjY3MzQyNTY4NTkyNTY2MTQ0MDM5NzY4OTg0NTMyNDMzNzk0MzUzNjQ2Nzg1MjA3NDgzOTk2ODQ0OTcxNTgzNzY3NDQ5ODYyODgxMjMxMjI1MzM4MzAzMTQ4NzA0ODczMDEzNDM3MDgyNzY1MTk4OTY2MzE5NTM0OTkyNjk4MzMzMDQ0MDI3MjIyNTYyNTIzNzk3ODk5Mjk2MTQ1NDU5IiwieHJfY2FwIjpbWyJtYXN0ZXJfc2VjcmV0IiwiOTE5MTc5NzQ4MTE5NTg5MTY3Njc5MjQxODk5NzY0ODIwNTk0MDc5OTQxNzIzOTgzOTYyNzQ1MTczODM0NDQxMDQ3MjU4MDcyOTE4OTUzNjIzOTQ4MDMyNzI2NDgyNzI2MTEwOTk2Mjk3MDU3NTYwNjcwNzAxOTU1MTkxNDc0NjM0MzQ0ODMxMzg3NTk4NzI2MzMxMjc0NjI4NDU3Njk5NzczMDA1NDMwMDIxNzMwMzg4MzcwMTEyMjc3MzI2MzU4OTgwMTA3ODIzNzUzODc3MTU0NjIwMDkzMjE5MjYyNjAxNDM2NzMyNTgzNDI4Nzc4NDA4OTc0NTQyNzkzMDk0NTQ5MTczOTA3MzQ3OTUxNTc1NjM5NzU2NDg5MTA0Mzk0MTY3NzExMzY1MjM3OTI1MjAwNjk4OTg5NTI5MTQ3OTIzNTYzNDMyODgyMzgwMTg0NzU0NzkzODMwMTE3MTQ1MDAwMTI0NDYxNjkzOTcxMDQ5MjgzNDk1NTE4MDQxMDc5ODUyMzAwMjk0NDM1MjYzOTIwNDU0NTU3MzUxNDQ1MDM3NDI4MDg3OTk2Mzg2NjY3NjU3Nzk5OTYyNzQzNzIyNzA3NzczOTEzMzc0NzIxODUyNTQ3MjkwMTY5MjI5NTAzMTQxOTMwODYzNTk4NTExNjc4NDEyMDE0MzE2MDM2MzYxMzczNDcwOTQwMDEyODcwMDgwMDA2MzE0NzYxNzYzNzUyNzYwODk5MTQ3NzA1MTA0NzQyNjAxNjkxMzMxNjkzMDIwMjg2MjA2NzQ2NzE0MzI3NjU2MjA2NTMzMjk3NDg4MjU2NTM2NTQ3MzY4MjM2OTQ2MDM5NzAzMzc0OTMzNTE0NTc2NDg2NjQyNTY4MjgyNTY2MjMyNDU1NTU5MDY4MzE3NzU5NDM0ODU4NTI3MDg2NjQ0Il0sWyJoZWlnaHQiLCI5MjMwMzkyNDc1NjI4ODc1MjA4OTM0NjM0NzE4MjYzNzA4MDIzOTI1MDU0NjY2NDgzMzgxMzIyMzc3MDg1MjMxMjU4MTM4MzgwOTU1NTk3NDQxNTEyOTYwNDA2MjI3MjUwODgyNjA3NjExMDkwODk3MTM1NDcxNzAwMDIzNDcwOTM2ODg4MDE3NDY5Nzk0ODYzNDk4NzUyNTI3Njc3MjMwMTEwNzg0ODQzNzI0NDUyNTUzODYyOTA2MzM5MDc0OTIzNDU4NTQ3NDYzODcwNzU3OTg5MzMxNzk4OTI2MjM4MjUxMTM2NTYzNjM2MjIyOTQwNDkwMzY3MjQ2OTg0OTU2NTE5MTAzODcwNDE0MDM5NzM2MDE2MDY5MzA2NjQ0NjQzODI4OTgxMTE3OTM3NzYyNDAzODY1Mjc1MDU5MjEyOTY2NzIxOTU3MzM0MTM2ODEyMDI0OTE0MzA4MzAxMzk5MzM4NzMyOTIzNTA0MjA5MDM5ODMxMTc5NjU1NTkyNjg0MjMyMTIzMTI2Mjc4ODQzNDMyOTUwMTk1Mjg3MzE4ODI3NTM2MTMwNDQ3NzM3MTgwMjk3MDE0ODEzNDg3NDQyOTg2NjQ1NzQyNjEyMzE5NzQxNDY2MDMyNTg5OTU0NzYwNjE4MDU0MDUxMjAzMTE1NTAxNDcxNDExMzg3NzU0NDk5MzAwNTU4MTc5NjM5NDAxOTM0NTAzMTMyMDEzMjAzOTg2NzkyMTEzMDAzNTkwODg1NTc3NjgyMzU2NDY3MjA5NTUwNjQxODQxMDYyNTkzNDYyODIwODg3NzgxNDYyODM3ODkzODcxNDM4MzM3Mjc5MTcwMTExMTQ5MTU4NDMzNDE0ODI1NTkyNjcyODU2MzM5OTM4NTgyODg2NzM3OTIwMjc1MzI0MjEwMTUzMjE5MjI2OTYiXSxbImFnZSIsIjkxNTg1ODk3NDkwNzE0ODA3OTY2MDYzOTg5MjE1NTMxNDkyOTQwMDI5NDcyMTM4MjgwNjcxNjcyMjQ0NjY5MDc5NzIyNTQyMDU0NTU3NjY0MTcxMDI1NzM1NjQ4NTIwMTM4ODQ4ODAxNzIyMTc4MTcxMTA5NTc0MTMyNTExMzM1MDEwNTc5NzExMzcyODM5MjI3MDExOTg4MTUyMTEwMzI4MTE5MjkyMjI4NjM3MDU4MDQ3NzYwODYwOTQ0NTY3MzQxMjY4MTY4Mjk3NjE5MDM2ODEwMjYwODM2NDI1NDkwMzU3NjE4NzM4NTYxNTY2MTUxODQ3MzIxNzM1MjQ5ODk1MDU5NTY2OTQxODI5MjE0Nzc0MTA0NzYyNTQwMjcyMjk2NjE1NTE3NjUwMDcyNDQyMTI0NjY5MDEzMTc1ODAyMDk5MDQxMzk3MzE5ODQ0OTA2MDgwOTYxNTcyMTcwNjg2NzgzNDM1Mjg2MDUyMzE5ODY3ODExMDE5MjAxMDYwODM2OTM3Mzc0MzM0NDM5MTQxMDAzMTI3NTcyNjgzNTgwODI0OTkwOTg3MjE5MzU4NzkzOTM2NTU4Nzk3MjI0MDQzNTM1ODA5NzMyNzgxMjE1NzEwNjI1MjQzODYwNTk4MTk0MjU2MjAwODkwOTA3ODAzMDcyMTAzNzc3MzkwODk4MDczOTgyNjY3Njc1ODg0MjI3MjU0Mzc2OTI5Mjg3ODQyNDE0MTE0MjcwNDQwMTEzNDUxNjk4NzE5Nzc5NjQyNTI4MDA4NDM3Mzk5NjI0NTE3OTM4Nzg5MDc3ODE5ODA0MDY5MzcxOTM0NzExMTIyNTQyODU0OTg4MDA0Mjc4NDkwMjAxNTk2NjE0MjUwODc3NDYxMDczNjc3NTUzNzYxMTMyMTA5Nzg3NTQ2ODE1ODk5Njc2NCJdLFsibmFtZSIsIjYyNzgwNTIwMTM3MzI3NTUzMDc3MDg4NTE4NDg1NDYyMTA0NjEzMjEyNzY3ODUwMzYwNTc3NDQ4MDUxNTk5MTMxMTM1NTI2NzQ3Nzc2NzMzMDg1MDMwODcyMDE1OTM2MTI2NzE0MTIxMDgxMzg3ODU2MTkwMTkzMzI3ODY3OTE0NTEzODM2NTQ1OTY4Mjg1NTc5ODEyODMxMDI4ODc2Nzg1NzI3OTQ2MTEwNzg5Mzc0MjcyODgzMzkyOTgwNDkwODk3NDkwMTc5MDQ0ODM0NTgwMzQ2ODY4NDI2ODc0ODU4NTY1OTg4NTUyMDcwNjI1NDczNjM4MDM3Njc5NTU1NTk2MzE5MTc3Nzc5OTcxMTIxMjQzMjgyMTIyOTQ2NjY0ODMxOTgxMTg3MzQ3MzcyMjkxMjYwOTM3MzkzNDA1ODk5OTI0NjM4MzE3ODI5MDczODMxMjI4ODc1Njg5MTcyMTg4NjIyMDI5NzcxNzM5MTQ5NDY2Mzg3NTM5NjkyNDQ5NDU5MjczNDI5NzM5MjMzNjkyNTEzNDA5OTkyNDgxNTQ4ODk0NjAzNjM3MTYzNjA4MTM0MTAzMTk3Nzc3NTM4OTYwMDcyMjcyMzYyNzM4NDM1MTM3MDcyNzIxMjExMDYxNTg4MDE3ODczODg3MTEwNDA2OTk1NDQ4ODIwMDEzMDA5MjgyMzk0OTczMDMwMDI5MTY3NjQ5NzY1OTI1MTUxMzY4NTg5OTkyNzMyMDE1ODAwNjAzNzYxOTI3MTg3MDM4MDkxNDY3MDE1MjA3MzIwNDczMDM0NDA3MDIyNDA0NjQ4MTI0NTk2NjQwNjU1NjY1MTIzMzY5Njc0ODI2NDE3MjE2ODUxNTM4Njc1NTM3NzAwOTg4MTQzNzE1NTE3NzMwMTM4NjA4NzkxMjcyMzM0MDUyMzY4OCJdXX0sIm5vbmNlIjoiNDE0MzQ4Njg0NDk2OTAxNjkyMjI2OTY0In0=", + }, + "mime-type": "application/json", + }, + ], + "~service": { + "recipientKeys": [ + "DXubCT3ahg6N7aASVFVei1GNUTecne8m3iRWjVNiAw31", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3000", + }, + "~thread": { + "thid": "f9f79a46-a4d8-4ee7-9745-1b9cdf03676b", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2023-03-18T18:54:00.014Z", + }, + }, + "fcdba9cd-3132-4e46-9677-f78c5a146cf0": { + "id": "fcdba9cd-3132-4e46-9677-f78c5a146cf0", + "tags": { + "issuerId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H", + "methodName": "indy", + "schemaId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/Test Schema/5.0", + "schemaIssuerDid": undefined, + "schemaName": "Test Schema", + "schemaVersion": "5.0", + "unqualifiedSchemaId": "A4CYPASJYRZRt98YWrac3H:2:Test Schema:5.0", + }, + "type": "AnonCredsSchemaRecord", + "value": { + "id": "fcdba9cd-3132-4e46-9677-f78c5a146cf0", + "metadata": {}, + "methodName": "indy", + "schema": { + "attrNames": [ + "name", + "age", + ], + "issuerId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H", + "name": "Test Schema", + "version": "5.0", + }, + "schemaId": "did:indy:bcovrin:test:A4CYPASJYRZRt98YWrac3H/anoncreds/v0/SCHEMA/Test Schema/5.0", + "updatedAt": "2023-03-19T22:50:20.522Z", + }, + }, +} +`; diff --git a/packages/anoncreds/src/updates/__tests__/__snapshots__/0.4.test.ts.snap b/packages/anoncreds/src/updates/__tests__/__snapshots__/0.4.test.ts.snap new file mode 100644 index 0000000000..b5c3a9a5eb --- /dev/null +++ b/packages/anoncreds/src/updates/__tests__/__snapshots__/0.4.test.ts.snap @@ -0,0 +1,154 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UpdateAssistant | AnonCreds | v0.4 - v0.5 should correctly update the credential exchange records for holders 1`] = ` +{ + "1-4e4f-41d9-94c4-f49351b811f1": { + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "anonCredsAttr::test::marker": true, + "anonCredsAttr::test::value": "test", + "anonCredsCredentialDefinitionId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/CLAIM_DEF/400832/test", + "anonCredsCredentialRevocationId": undefined, + "anonCredsLinkSecretId": "ae343b1d-e2af-4706-9aae-2010a7f2c882", + "anonCredsMethodName": "indy", + "anonCredsRevocationRegistryId": undefined, + "anonCredsSchemaId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/SCHEMA/test0.1599221872308001/1.0", + "anonCredsSchemaIssuerId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx", + "anonCredsSchemaName": "test0.1599221872308001", + "anonCredsSchemaVersion": "1.0", + "anonCredsUnqualifiedCredentialDefinitionId": "6LHqdUeWDWsL94zRc1ULEx:3:CL:400832:test", + "anonCredsUnqualifiedIssuerId": "6LHqdUeWDWsL94zRc1ULEx", + "anonCredsUnqualifiedRevocationRegistryId": undefined, + "anonCredsUnqualifiedSchemaId": "6LHqdUeWDWsL94zRc1ULEx:2:test0.1599221872308001:1.0", + "anonCredsUnqualifiedSchemaIssuerId": "6LHqdUeWDWsL94zRc1ULEx", + "claimFormat": "ldp_vc", + "contexts": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/data-integrity/v2", + ], + "cryptosuites": [ + "anoncreds-2023", + ], + "expandedTypes": [ + "https://www.w3.org/2018/credentials#VerifiableCredential", + ], + "givenId": undefined, + "issuerId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx", + "proofTypes": [ + "DataIntegrityProof", + ], + "schemaIds": [], + "subjectIds": [], + "types": [ + "VerifiableCredential", + ], + }, + "type": "W3cCredentialRecord", + "value": { + "createdAt": "2024-02-28T22:50:20.522Z", + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/data-integrity/v2", + { + "@vocab": "https://www.w3.org/ns/credentials/issuer-dependent#", + }, + ], + "credentialSubject": { + "test": "test", + }, + "issuanceDate": 2024-02-28T22:50:20.522Z, + "issuer": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx", + "proof": [ + { + "cryptosuite": "anoncreds-2023", + "proofPurpose": "assertionMethod", + "proofValue": "ukgGEqXNjaGVtYV9pZNlbZGlkOmluZHk6YmNvdnJpbjp0ZXN0OjZMSHFkVWVXRFdzTDk0elJjMVVMRXgvYW5vbmNyZWRzL3YwL1NDSEVNQS90ZXN0MC4xNTk5MjIxODcyMzA4MDAxLzEuMKtjcmVkX2RlZl9pZNlPZGlkOmluZHk6YmNvdnJpbjp0ZXN0OjZMSHFkVWVXRFdzTDk0elJjMVVMRXgvYW5vbmNyZWRzL3YwL0NMQUlNX0RFRi80MDA4MzIvdGVzdKlzaWduYXR1cmWCrHBfY3JlZGVudGlhbISjbV8y3AAgzL1rQBvM-szpzMImzNrMgcylCi7M8syxzKEQzLBBzKNTzOTM48zbzIlaNDzM1F91MaFh3AEAzOZYzMR6USQgEHTMoRbMrsydzKtnXBAGzITM7C4kPczaWG_Mh2nM0czezItlzLcnPMzVzIJSan06zMDMolbM48z3e8ynzOnMm8y6zOLMqMyizM3M-MyvzNnMrB3MtsyUc0ciO8yYK8ztzIHM4lg1PMyezJ7M0MyYecyrEsz4zOXMvldEF8yZIszbzNPMlT4xzIhfzMYCGA7M9cy3SczmzM3Mql0eScyJZ2UKKHRKGcy6zN8ozLvMvlLM_sy3WTjM_gfMrMzQMszKzKTMxDY2F8zmTWTM3EHMlwB4zLYAEsyeZcyIZBTMtlrMpczfSVdMzN1_zPQMzJ0QEAfM9MzIzIrMwsyeZ8zDH8zPzIRjUi42zJfM5wITADQlzMLM5grM8wrM-MzbC8zUB8ypzOPMj8yscMz7zMNbHAxCLQkpMER2zO7Ml3nMoB4yzI1GzJ7MkG3M61XM21jMhszZzLAvzJBgzK_M1MzlzMnM8gTMs8yCXQNdzLtuzL3MjMzYQsyDoWXcAEsQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSMsydzL0HzPpfzJTM8CLM4czlJsyyzLmhdtwBVQ_M-EvM1cyczK3MqcyccwMqzIvM6kzM6UdwOcyDzPM1zN_Mnzt1zLvM2MyrIHzMoszPzOXMpMzhzJsBMMz5aszuzMbM38yrzNV3WMyFSczMzINpMMyOAszZOszaSMy1zIYBzKvMxHNuzKHM58zuzIvMvcyvzPkfFMyqzN_Mi1nMxADM1szOODERcsz3zJvMtcyCL0dUzJjMzMzwIsy8zJPMxMz_zMbMzcyWGyDM-S5FzOLM5szzYszbTczyLA8bMgAmRczCGHXMmwFhGQfMilMjHVBWcFXM28yWzOjMsMyJzKETHQsezOfMhcybesz7zKXM6wxwbsz8RszHMGBFzObM_mfMq8zWbMzpzKdDzOEwdizM0czeNwRWCR0DzM41bMzNzJczzOczzP7MgUErzK9cAhkDcg1GzLl7IszBPUBua1vMyCfMtszUzIvM6QcDVlzM_szUGczAzObMvszrzLHMl8yWeC0-KlDM7cy4VUPMsD_MmzjM-cz_zPnM0sz3zJPMvmRGzKnMqSdDzJnMpcybf3VnzL_MkMzazK4qI8ySzOjMnszHzMhTFWRozIbM7G1sMD3MjQrMucz4zPDMnMzNIMyPzKIfzJR_TcyMzNjMrVrMycyaKsy6zLjMlSEtKsy4zNfMsMyrY8y0zN5bVFXMgMzAzIzM4hrM2ylyzPJvD8yWVsyRFqxyX2NyZWRlbnRpYWzAu3NpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZoKic2XcAQDMqiPMi8zrzOPMgcy3zOo9GMz4zOlEKUjM3My2zNgAR8yZzJnM4D5fzOLM5szeEGnMscy6zN8mDszrzL3MrszfTHrM7MzBdMyvMcyizIXM3UE4asyXRSJnzK3MygZuXncNzLPMgWZIXQQPHMy3JcyCYF_MhDNnYCsMLl8LzLfM98yhGnZfzPfMrsygzKEAzI_M50nM28yxzLnMpFJ_ScyjEWfMiio2zIfMxszLzLBrIEFJD8zbzMEDPjDMiszIzKcTbCpXT3hzLiIld8zDzLbMyMyqHV3MzHnMwszzdCQEMlpGzOMRzJwCE8zizNXM4HnM8kYSzLcyzNNYzNrM7GbM1xTMwszkzKt5zNjM92QgzOZuCMzlzKQIRhZszP_MpMzbzLXM1sy0NyIHIszyzJBlzMLMjBrM4RI2Q8z2MMyRLRfM7iNkIMzEzORbzPkEesy5zJnMoMy5zMt7zMxYzLjM7zzMoC7Mt8y7OSNtHx84zI_M5m3MgszxM6Fj3AAgzJ49zLXMxljMmGvMqsy1BD3MpszXzMfM7MztzIwBQMyAdMzJCVXM38y0Y8ybzMhoeMyJ", + "type": "DataIntegrityProof", + "verificationMethod": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/CLAIM_DEF/400832/test", + }, + ], + "type": [ + "VerifiableCredential", + ], + }, + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "metadata": { + "_w3c/anonCredsMetadata": { + "credentialRevocationId": undefined, + "linkSecretId": "ae343b1d-e2af-4706-9aae-2010a7f2c882", + "methodName": "indy", + }, + }, + "updatedAt": "2024-02-28T22:50:20.522Z", + }, + }, + "14720230-54b4-4735-8072-0e1902982429": { + "id": "bb30d3ee-651a-4ffc-b1d9-30e4df23a045", + "tags": { + "attr": [ + "", + ], + "attr::test::marker": true, + "attr::test::value": "test", + "credentialDefinitionId": "6LHqdUeWDWsL94zRc1ULEx:3:CL:400832:test", + "credentialId": "e1548638-fbe6-4e26-96fc-d536d453b380", + "issuerId": "6LHqdUeWDWsL94zRc1ULEx", + "linkSecretId": "ae343b1d-e2af-4706-9aae-2010a7f2c882", + "methodName": "indy", + "revocationRegistryId": null, + "schemaId": "6LHqdUeWDWsL94zRc1ULEx:2:test0.1599221872308001:1.0", + "schemaIssuerId": "6LHqdUeWDWsL94zRc1ULEx", + "schemaName": "test0.1599221872308001", + "schemaVersion": "1.0", + }, + "type": "AnonCredsCredentialRecord", + "value": { + "credential": { + "cred_def_id": "6LHqdUeWDWsL94zRc1ULEx:3:CL:400832:test", + "rev_reg": null, + "rev_reg_id": null, + "schema_id": "6LHqdUeWDWsL94zRc1ULEx:2:test0.1599221872308001:1.0", + "signature": { + "p_credential": { + "a": "29078583023644723417256963951834769429544893561312608041999039725779045478957665610539345669249033643845871310638205359827913345539893604784172221177874151867323789783352069858114677062382329201941336114640094448982492548621745418140287523358455796985452292603814050613022499206350481966450949147798607248367393022400871389441985483251995666110068431356102420069450232712370410071038529237338590022255675451148362600727584994665177285853081799683623674291603640314733666468918666175103622831604402215322289509594157875212406584558973682327127532803373424575230946721521968680075608178586165565162932643378236428403331", + "e": "259344723055062059907025491480697571938277889515152306249728583105665800713306759149981690559193987143012367913206299323899696942213235956742930103927082934306204189453914854568633", + "m_2": "85676623484277624682444820444702467499357910274165013418379125134075325543729", + "v": "10113246594919968694243749150533421251903212516176280450944343564431109505938841891625247667364379412129405405348477095610002433568347856509159611335758818756926825063560770314873165029229176344268719893707027252437404047327923906866676449185116008460538437595639211714306227451873006607255100899512311105919649129622793316610076053733506986530767535638972131892478976764607887355989030338392858709274725683511041231799301175492600538984695657494046447608704545830499026706733632944905447307052632450973098625532585070622424882750416943328031030319205075858421333346786928868231555780815068857501287044930625867151895584283719258208379421233099516506594646921971059399550756016776860669930757562757559953692743338803382655236340538001870484617627827300880520094049323022699847759090147376675910494368188266848296015139094", + }, + "r_credential": null, + }, + "signature_correctness_proof": { + "c": "71574462310595963848889785756072396101961922057659218227849729861834355800201", + "se": "21478040510275170612337916082115329005924729160138728500731966689768864876350314127582065222070358453091326557476132640512915164393469162400241555486465031352127309617863096642685471579736279753483791845394120052052850579761097059767637641620637246275967251656241939449999728934398401723998520798896177945650388769630721777342623105417576378300802138205906595722648799399722998758345581952810879612170626417882503950639389717949927995386086595370764259106482228888541923217556032007348358437745103150644457074511256162173366561635465131422798361283669038048806849619337895126642478632186566293253597826848134434844979", + }, + "values": { + "test": { + "encoded": "72155939486846849509759369733266486982821795810448245423168957390607644363272", + "raw": "test", + }, + }, + "witness": null, + }, + "credentialId": "e1548638-fbe6-4e26-96fc-d536d453b380", + "id": "9ec1f4c6-6b58-4c54-84d0-2fa74f83f70f", + "linkSecretId": "ae343b1d-e2af-4706-9aae-2010a7f2c882", + "metadata": {}, + "methodName": "indy", + "updatedAt": "2024-02-28T09:36:42.994Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2023-03-18T18:53:44.041Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.5", + "updatedAt": "2024-02-28T22:50:20.522Z", + }, + }, +} +`; diff --git a/packages/anoncreds/src/utils/__tests__/W3cAnonCredsCredentialRecord.test.ts b/packages/anoncreds/src/utils/__tests__/W3cAnonCredsCredentialRecord.test.ts new file mode 100644 index 0000000000..19bff68335 --- /dev/null +++ b/packages/anoncreds/src/utils/__tests__/W3cAnonCredsCredentialRecord.test.ts @@ -0,0 +1,75 @@ +import { JsonTransformer, W3cCredentialRecord, W3cJsonLdVerifiableCredential } from '@credo-ts/core' + +import { Ed25519Signature2018Fixtures } from '../../../../core/src/modules/vc/data-integrity/__tests__/fixtures' +import { W3cAnonCredsCredentialMetadataKey } from '../metadata' +import { getAnonCredsTagsFromRecord, type AnonCredsCredentialTags } from '../w3cAnonCredsUtils' + +describe('AnoncredsW3cCredentialRecord', () => { + it('should return default tags (w3cAnoncredsCredential)', () => { + const credential = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ) + + const anoncredsCredentialRecordTags: AnonCredsCredentialTags = { + anonCredsSchemaIssuerId: 'schemaIssuerId', + anonCredsSchemaName: 'schemaName', + anonCredsSchemaVersion: 'schemaVersion', + anonCredsSchemaId: 'schemaId', + anonCredsCredentialDefinitionId: 'credentialDefinitionId', + anonCredsCredentialRevocationId: 'credentialRevocationId', + anonCredsLinkSecretId: 'linkSecretId', + anonCredsMethodName: 'methodName', + anonCredsRevocationRegistryId: 'revocationRegistryId', + } + + const w3cCredentialRecord = new W3cCredentialRecord({ + credential, + tags: { + expandedTypes: ['https://expanded.tag#1'], + }, + }) + + const anonCredsCredentialMetadata = { + credentialRevocationId: anoncredsCredentialRecordTags.anonCredsCredentialRevocationId, + linkSecretId: anoncredsCredentialRecordTags.anonCredsLinkSecretId, + methodName: anoncredsCredentialRecordTags.anonCredsMethodName, + } + + w3cCredentialRecord.setTags(anoncredsCredentialRecordTags) + w3cCredentialRecord.metadata.set(W3cAnonCredsCredentialMetadataKey, anonCredsCredentialMetadata) + + const anoncredsCredentialTags = { + anonCredsLinkSecretId: 'linkSecretId', + anonCredsMethodName: 'methodName', + anonCredsSchemaId: 'schemaId', + anonCredsSchemaIssuerId: 'schemaIssuerId', + anonCredsSchemaName: 'schemaName', + anonCredsSchemaVersion: 'schemaVersion', + anonCredsCredentialDefinitionId: 'credentialDefinitionId', + anonCredsRevocationRegistryId: 'revocationRegistryId', + anonCredsCredentialRevocationId: 'credentialRevocationId', + } + + const anonCredsTags = getAnonCredsTagsFromRecord(w3cCredentialRecord) + expect(anonCredsTags).toEqual({ + ...anoncredsCredentialTags, + }) + + expect(w3cCredentialRecord.metadata.get(W3cAnonCredsCredentialMetadataKey)).toEqual(anonCredsCredentialMetadata) + + expect(w3cCredentialRecord.getTags()).toEqual({ + claimFormat: 'ldp_vc', + issuerId: credential.issuerId, + subjectIds: credential.credentialSubjectIds, + schemaIds: credential.credentialSchemaIds, + contexts: credential.contexts, + proofTypes: credential.proofTypes, + givenId: credential.id, + types: ['VerifiableCredential', 'UniversityDegreeCredential'], + cryptosuites: [], + expandedTypes: ['https://expanded.tag#1'], + ...anoncredsCredentialTags, + }) + }) +}) diff --git a/packages/anoncreds/src/utils/__tests__/anonCredsCredentialValue.test.ts b/packages/anoncreds/src/utils/__tests__/anonCredsCredentialValue.test.ts new file mode 100644 index 0000000000..81ee9ff846 --- /dev/null +++ b/packages/anoncreds/src/utils/__tests__/anonCredsCredentialValue.test.ts @@ -0,0 +1,124 @@ +import { encodeCredentialValue, mapAttributeRawValuesToAnonCredsCredentialValues } from '../credential' + +const testVectors = { + 'str 0.0': { + raw: '0.0', + encoded: '62838607218564353630028473473939957328943626306458686867332534889076311281879', + }, + // conversion error! + // this does not work in js + // 'float 0.0': { + // raw: 0.0, + // encoded: '62838607218564353630028473473939957328943626306458686867332534889076311281879', + // }, + 'max i32': { + raw: 2147483647, + encoded: '2147483647', + }, + 'max i32 + 1': { + raw: 2147483648, + encoded: '26221484005389514539852548961319751347124425277437769688639924217837557266135', + }, + 'min i32': { + raw: -2147483648, + encoded: '-2147483648', + }, + 'min i32 - 1': { + raw: -2147483649, + encoded: '68956915425095939579909400566452872085353864667122112803508671228696852865689', + }, + address2: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + zip: { + raw: '87121', + encoded: '87121', + }, + city: { + raw: 'SLC', + encoded: '101327353979588246869873249766058188995681113722618593621043638294296500696424', + }, + address1: { + raw: '101 Tela Lane', + encoded: '63690509275174663089934667471948380740244018358024875547775652380902762701972', + }, + state: { + raw: 'UT', + encoded: '93856629670657830351991220989031130499313559332549427637940645777813964461231', + }, + Empty: { + raw: '', + encoded: '102987336249554097029535212322581322789799900648198034993379397001115665086549', + }, + Undefined: { + raw: undefined, + encoded: '99769404535520360775991420569103450442789945655240760487761322098828903685777', + }, + Null: { + raw: null, + encoded: '99769404535520360775991420569103450442789945655240760487761322098828903685777', + }, + 'bool True': { + raw: true, + encoded: '1', + }, + 'bool False': { + raw: false, + encoded: '0', + }, + 'str True': { + raw: 'True', + encoded: '27471875274925838976481193902417661171675582237244292940724984695988062543640', + }, + 'str False': { + raw: 'False', + encoded: '43710460381310391454089928988014746602980337898724813422905404670995938820350', + }, + + 'chr 0': { + raw: String.fromCharCode(0), + encoded: '49846369543417741186729467304575255505141344055555831574636310663216789168157', + }, + 'chr 1': { + raw: String.fromCharCode(1), + encoded: '34356466678672179216206944866734405838331831190171667647615530531663699592602', + }, + 'chr 2': { + raw: String.fromCharCode(2), + encoded: '99398763056634537812744552006896172984671876672520535998211840060697129507206', + }, +} + +describe('utils', () => { + test('encoding algorithm', async () => { + Object.values(testVectors).forEach((vector) => { + expect(encodeCredentialValue(vector.raw)).toEqual(vector.encoded) + }) + }) + + test('test attribute record value mapping', () => { + const attrsExpected = { + address2: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + zip: { + raw: '87121', + encoded: '87121', + }, + state: { + raw: 'UT', + encoded: '93856629670657830351991220989031130499313559332549427637940645777813964461231', + }, + } + + const attrs = { + address2: '101 Wilson Lane', + zip: '87121', + state: 'UT', + } + + expect(mapAttributeRawValuesToAnonCredsCredentialValues(attrs)).toMatchObject(attrsExpected) + }) +}) diff --git a/packages/anoncreds/src/utils/__tests__/areRequestsEqual.test.ts b/packages/anoncreds/src/utils/__tests__/areRequestsEqual.test.ts new file mode 100644 index 0000000000..51f9c3317e --- /dev/null +++ b/packages/anoncreds/src/utils/__tests__/areRequestsEqual.test.ts @@ -0,0 +1,419 @@ +import type { AnonCredsProofRequest } from '../../models' + +import { areAnonCredsProofRequestsEqual } from '../areRequestsEqual' + +const proofRequest = { + name: 'Proof Request', + version: '1.0.0', + nonce: 'nonce', + ver: '1.0', + non_revoked: {}, + requested_attributes: { + a: { + names: ['name1', 'name2'], + restrictions: [ + { + cred_def_id: 'cred_def_id1', + }, + { + schema_id: 'schema_id', + }, + ], + }, + }, + requested_predicates: { + p: { + name: 'Hello', + p_type: '<', + p_value: 10, + restrictions: [ + { + cred_def_id: 'string2', + }, + { + cred_def_id: 'string', + }, + ], + }, + }, +} satisfies AnonCredsProofRequest + +describe('util | areAnonCredsProofRequestsEqual', () => { + test('does not compare name, ver, version and nonce', () => { + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + name: 'Proof Request 2', + version: '2.0.0', + nonce: 'nonce2', + ver: '2.0', + }) + ).toBe(true) + }) + + test('check top level non_revocation interval', () => { + // empty object is semantically equal to undefined + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + non_revoked: {}, + }) + ).toBe(true) + + // properties inside object are different + expect( + areAnonCredsProofRequestsEqual( + { + ...proofRequest, + non_revoked: { + to: 5, + }, + }, + { + ...proofRequest, + non_revoked: { + from: 5, + }, + } + ) + ).toBe(false) + + // One has non_revoked, other doesn't + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + non_revoked: { + from: 5, + }, + }) + ).toBe(false) + }) + + test('ignores attribute group name differences', () => { + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + b: proofRequest.requested_attributes.a, + }, + }) + ).toBe(true) + }) + + test('ignores attribute restriction order', () => { + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + restrictions: [...proofRequest.requested_attributes.a.restrictions].reverse(), + }, + }, + }) + ).toBe(true) + }) + + test('ignores attribute restriction undefined vs empty array', () => { + expect( + areAnonCredsProofRequestsEqual( + { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + restrictions: undefined, + }, + }, + }, + { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + restrictions: [], + }, + }, + } + ) + ).toBe(true) + }) + + test('ignores attribute names order', () => { + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + names: ['name2', 'name1'], + }, + }, + }) + ).toBe(true) + }) + + test('checks attribute non_revocation interval', () => { + // empty object is semantically equal to undefined + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + non_revoked: {}, + }, + }, + }) + ).toBe(true) + + // properties inside object are different + expect( + areAnonCredsProofRequestsEqual( + { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + non_revoked: { + to: 5, + }, + }, + }, + }, + { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + non_revoked: { + from: 5, + }, + }, + }, + } + ) + ).toBe(false) + + // One has non_revoked, other doesn't + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + non_revoked: { + from: 5, + }, + }, + }, + }) + ).toBe(false) + }) + + test('checks attribute restriction differences', () => { + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + restrictions: [ + { + cred_def_id: 'cred_def_id1', + }, + { + cred_def_id: 'cred_def_id2', + }, + ], + }, + }, + }) + ).toBe(false) + }) + + test('checks attribute name differences', () => { + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + names: ['name3'], + }, + }, + }) + ).toBe(false) + + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + name: 'name3', + names: undefined, + }, + }, + }) + ).toBe(false) + }) + + test('ignores predicate group name differences', () => { + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_predicates: { + a: proofRequest.requested_predicates.p, + }, + }) + ).toBe(true) + }) + + test('ignores predicate restriction order', () => { + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + restrictions: [...proofRequest.requested_predicates.p.restrictions].reverse(), + }, + }, + }) + ).toBe(true) + }) + + test('ignores predicate restriction undefined vs empty array', () => { + expect( + areAnonCredsProofRequestsEqual( + { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + restrictions: undefined, + }, + }, + }, + { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + restrictions: [], + }, + }, + } + ) + ).toBe(true) + }) + + test('checks predicate restriction differences', () => { + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + p: { + ...proofRequest.requested_predicates.p, + restrictions: [ + { + cred_def_id: 'cred_def_id1', + }, + { + cred_def_id: 'cred_def_id2', + }, + ], + }, + }, + }) + ).toBe(false) + }) + + test('checks predicate name differences', () => { + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + name: 'name3', + }, + }, + }) + ).toBe(false) + }) + + test('checks predicate non_revocation interval', () => { + // empty object is semantically equal to undefined + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + non_revoked: {}, + }, + }, + }) + ).toBe(true) + + // properties inside object are different + expect( + areAnonCredsProofRequestsEqual( + { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + non_revoked: { + to: 5, + }, + }, + }, + }, + { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + non_revoked: { + from: 5, + }, + }, + }, + } + ) + ).toBe(false) + + // One has non_revoked, other doesn't + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + non_revoked: { + from: 5, + }, + }, + }, + }) + ).toBe(false) + }) + + test('checks predicate p_type and p_value', () => { + expect( + areAnonCredsProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + p_type: '<', + p_value: 134134, + }, + }, + }) + ).toBe(false) + }) +}) diff --git a/packages/anoncreds/src/utils/__tests__/credential.test.ts b/packages/anoncreds/src/utils/__tests__/credential.test.ts new file mode 100644 index 0000000000..6da7283dd3 --- /dev/null +++ b/packages/anoncreds/src/utils/__tests__/credential.test.ts @@ -0,0 +1,229 @@ +import { CredentialPreviewAttribute } from '@credo-ts/core' + +import { + assertCredentialValuesMatch, + checkValidCredentialValueEncoding, + convertAttributesToCredentialValues, +} from '../credential' + +/** + * Sample test cases for encoding/decoding of verifiable credential claims - Aries RFCs 0036 and 0037 + * @see https://gist.github.com/swcurran/78e5a9e8d11236f003f6a6263c6619a6 + */ +const testEncodings: { [key: string]: { raw: string | number | boolean | null; encoded: string } } = { + address2: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + zip: { + raw: '87121', + encoded: '87121', + }, + city: { + raw: 'SLC', + encoded: '101327353979588246869873249766058188995681113722618593621043638294296500696424', + }, + address1: { + raw: '101 Tela Lane', + encoded: '63690509275174663089934667471948380740244018358024875547775652380902762701972', + }, + state: { + raw: 'UT', + encoded: '93856629670657830351991220989031130499313559332549427637940645777813964461231', + }, + Empty: { + raw: '', + encoded: '102987336249554097029535212322581322789799900648198034993379397001115665086549', + }, + Null: { + raw: null, + encoded: '99769404535520360775991420569103450442789945655240760487761322098828903685777', + }, + 'bool True': { + raw: true, + encoded: '1', + }, + 'bool False': { + raw: false, + encoded: '0', + }, + 'str True': { + raw: 'True', + encoded: '27471875274925838976481193902417661171675582237244292940724984695988062543640', + }, + 'str False': { + raw: 'False', + encoded: '43710460381310391454089928988014746602980337898724813422905404670995938820350', + }, + 'max i32': { + raw: 2147483647, + encoded: '2147483647', + }, + 'max i32 + 1': { + raw: 2147483648, + encoded: '26221484005389514539852548961319751347124425277437769688639924217837557266135', + }, + 'min i32': { + raw: -2147483648, + encoded: '-2147483648', + }, + 'min i32 - 1': { + raw: -2147483649, + encoded: '68956915425095939579909400566452872085353864667122112803508671228696852865689', + }, + 'float 0.1': { + raw: 0.1, + encoded: '9382477430624249591204401974786823110077201914483282671737639310288175260432', + }, + 'str 0.1': { + raw: '0.1', + encoded: '9382477430624249591204401974786823110077201914483282671737639310288175260432', + }, + 'str 1.0': { + raw: '1.0', + encoded: '94532235908853478633102631881008651863941875830027892478278578250784387892726', + }, + 'str 1': { + raw: '1', + encoded: '1', + }, + 'leading zero number string': { + raw: '012345', + encoded: '12345', + }, + 'chr 0': { + raw: String.fromCharCode(0), + encoded: '49846369543417741186729467304575255505141344055555831574636310663216789168157', + }, + 'chr 1': { + raw: String.fromCharCode(1), + encoded: '34356466678672179216206944866734405838331831190171667647615530531663699592602', + }, + 'chr 2': { + raw: String.fromCharCode(2), + encoded: '99398763056634537812744552006896172984671876672520535998211840060697129507206', + }, +} + +describe('Utils | Credentials', () => { + describe('convertAttributesToCredentialValues', () => { + test('returns object with raw and encoded attributes', () => { + const attributes = [ + new CredentialPreviewAttribute({ + name: 'name', + mimeType: 'text/plain', + value: '101 Wilson Lane', + }), + new CredentialPreviewAttribute({ + name: 'age', + mimeType: 'text/plain', + value: '1234', + }), + ] + + expect(convertAttributesToCredentialValues(attributes)).toEqual({ + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + }) + }) + }) + + describe('assertCredentialValuesMatch', () => { + test('does not throw if attributes match', () => { + const firstValues = { + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).not.toThrow() + }) + + test('throws if number of values in the entries do not match', () => { + const firstValues = { + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).toThrow( + 'Number of values in first entry (1) does not match number of values in second entry (2)' + ) + }) + + test('throws if second value does not contain key from first value', () => { + const firstValues = { + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + anotherName: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).toThrow( + "Second cred values object has no value for key 'name'" + ) + }) + + test('throws if encoded values do not match', () => { + const firstValues = { + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + age: { raw: '1234', encoded: '12345' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).toThrow( + "Encoded credential values for key 'age' do not match" + ) + }) + + test('throws if raw values do not match', () => { + const firstValues = { + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + age: { raw: '12345', encoded: '1234' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).toThrow( + "Raw credential values for key 'age' do not match" + ) + }) + }) + + describe('checkValidEncoding', () => { + // Formatted for test.each + const testEntries = Object.entries(testEncodings).map( + ([name, { raw, encoded }]) => [name, raw, encoded] as [string, string | number | boolean | null, string] + ) + + test.each(testEntries)('returns true for valid encoding %s', (_, raw, encoded) => { + expect(checkValidCredentialValueEncoding(raw, encoded)).toEqual(true) + }) + }) +}) diff --git a/packages/anoncreds/src/utils/__tests__/credentialPreviewAttributes.test.ts b/packages/anoncreds/src/utils/__tests__/credentialPreviewAttributes.test.ts new file mode 100644 index 0000000000..419f9af0b7 --- /dev/null +++ b/packages/anoncreds/src/utils/__tests__/credentialPreviewAttributes.test.ts @@ -0,0 +1,143 @@ +import { areCredentialPreviewAttributesEqual } from '../credentialPreviewAttributes' + +describe('areCredentialPreviewAttributesEqual', () => { + test('returns true if the attributes are equal', () => { + const firstAttributes = [ + { + name: 'firstName', + value: 'firstValue', + mimeType: 'text/grass', + }, + { + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }, + ] + + const secondAttribute = [ + { + name: 'firstName', + value: 'firstValue', + mimeType: 'text/grass', + }, + { + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }, + ] + + expect(areCredentialPreviewAttributesEqual(firstAttributes, secondAttribute)).toBe(true) + }) + + test('returns false if the attribute name and value are equal but the mime type is different', () => { + const firstAttributes = [ + { + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }, + ] + + const secondAttribute = [ + { + name: 'secondName', + value: 'secondValue', + mimeType: 'text/notGrass', + }, + ] + + expect(areCredentialPreviewAttributesEqual(firstAttributes, secondAttribute)).toBe(false) + }) + + test('returns false if the attribute name and mime type are equal but the value is different', () => { + const firstAttributes = [ + { + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }, + ] + + const secondAttribute = [ + { + name: 'secondName', + value: 'thirdValue', + mimeType: 'text/grass', + }, + ] + + expect(areCredentialPreviewAttributesEqual(firstAttributes, secondAttribute)).toBe(false) + }) + + test('returns false if the value and mime type are equal but the name is different', () => { + const firstAttributes = [ + { + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }, + ] + + const secondAttribute = [ + { + name: 'thirdName', + value: 'secondValue', + mimeType: 'text/grass', + }, + ] + + expect(areCredentialPreviewAttributesEqual(firstAttributes, secondAttribute)).toBe(false) + }) + + test('returns false if the length of the attributes does not match', () => { + const firstAttributes = [ + { + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }, + ] + + const secondAttribute = [ + { + name: 'thirdName', + value: 'secondValue', + mimeType: 'text/grass', + }, + { + name: 'fourthName', + value: 'secondValue', + mimeType: 'text/grass', + }, + ] + + expect(areCredentialPreviewAttributesEqual(firstAttributes, secondAttribute)).toBe(false) + }) + + test('returns false if duplicate key names exist', () => { + const firstAttributes = [ + { + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }, + { + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }, + ] + + const secondAttribute = [ + { + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }, + ] + + expect(areCredentialPreviewAttributesEqual(firstAttributes, secondAttribute)).toBe(false) + }) +}) diff --git a/packages/anoncreds/src/utils/__tests__/hasDuplicateGroupNames.test.ts b/packages/anoncreds/src/utils/__tests__/hasDuplicateGroupNames.test.ts new file mode 100644 index 0000000000..c4deb02be7 --- /dev/null +++ b/packages/anoncreds/src/utils/__tests__/hasDuplicateGroupNames.test.ts @@ -0,0 +1,72 @@ +import type { AnonCredsProofRequest } from '../../models' + +import { assertNoDuplicateGroupsNamesInProofRequest } from '../hasDuplicateGroupNames' + +const credentialDefinitionId = '9vPXgSpQJPkJEALbLXueBp:3:CL:57753:tag1' + +describe('util | assertNoDuplicateGroupsNamesInProofRequest', () => { + describe('assertNoDuplicateGroupsNamesInProofRequest', () => { + test('attribute names match', () => { + const proofRequest = { + name: 'proof-request', + version: '1.0', + nonce: 'testtesttest12345', + requested_attributes: { + age1: { + name: 'age', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + age2: { + name: 'age', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: {}, + } satisfies AnonCredsProofRequest + + expect(() => assertNoDuplicateGroupsNamesInProofRequest(proofRequest)).not.toThrow() + }) + + test('attribute names match with predicates name', () => { + const proofRequest = { + name: 'proof-request', + version: '1.0', + nonce: 'testtesttest12345', + requested_attributes: { + attrib: { + name: 'age', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + predicate: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + } satisfies AnonCredsProofRequest + + expect(() => assertNoDuplicateGroupsNamesInProofRequest(proofRequest)).toThrowError( + 'The proof request contains duplicate predicates and attributes: age' + ) + }) + }) +}) diff --git a/packages/anoncreds/src/utils/__tests__/indyIdentifiers.test.ts b/packages/anoncreds/src/utils/__tests__/indyIdentifiers.test.ts new file mode 100644 index 0000000000..7f94486586 --- /dev/null +++ b/packages/anoncreds/src/utils/__tests__/indyIdentifiers.test.ts @@ -0,0 +1,147 @@ +import { + getUnqualifiedCredentialDefinitionId, + getUnqualifiedRevocationRegistryDefinitionId, + getUnqualifiedSchemaId, + parseIndyCredentialDefinitionId, + parseIndyRevocationRegistryId, + parseIndySchemaId, + unqualifiedCredentialDefinitionIdRegex, + unqualifiedIndyDidRegex, + unqualifiedRevocationRegistryIdRegex, + unqualifiedSchemaIdRegex, + unqualifiedSchemaVersionRegex, +} from '../indyIdentifiers' + +describe('Legacy Indy Identifier Regex', () => { + const invalidTest = 'test' + + test('test for legacyIndyCredentialDefinitionIdRegex', async () => { + const test = 'q7ATwTYbQDgiigVijUAej:3:CL:160971:1.0.0' + expect(test).toMatch(unqualifiedCredentialDefinitionIdRegex) + expect(unqualifiedCredentialDefinitionIdRegex.test(invalidTest)).toBeFalsy() + }) + + test('test for legacyIndyDidRegex', async () => { + const test = 'did:sov:q7ATwTYbQDgiigVijUAej' + expect(test).toMatch(unqualifiedIndyDidRegex) + expect(unqualifiedIndyDidRegex.test(invalidTest)).toBeFalsy() + }) + + test('test for legacyIndySchemaIdRegex', async () => { + const test = 'q7ATwTYbQDgiigVijUAej:2:test:1.0' + expect(test).toMatch(unqualifiedSchemaIdRegex) + expect(unqualifiedSchemaIdRegex.test(invalidTest)).toBeFalsy() + }) + + test('test for legacyIndySchemaIdRegex', async () => { + const test = 'N7baRMcyvPwWc8v85CtZ6e:4:N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID:CL_ACCUM:1-1024' + expect(test).toMatch(unqualifiedRevocationRegistryIdRegex) + expect(unqualifiedRevocationRegistryIdRegex.test(invalidTest)).toBeFalsy() + }) + + test('test for legacyIndySchemaVersionRegex', async () => { + const test = '1.0.0' + expect(test).toMatch(unqualifiedSchemaVersionRegex) + expect(unqualifiedSchemaVersionRegex.test(invalidTest)).toBeFalsy() + }) + + test('getUnqualifiedSchemaId returns a valid schema id given a did, name, and version', () => { + const did = '12345' + const name = 'backbench' + const version = '420' + + expect(getUnqualifiedSchemaId(did, name, version)).toEqual('12345:2:backbench:420') + }) + + test('getUnqualifiedCredentialDefinitionId returns a valid credential definition id given a did, seqNo, and tag', () => { + const did = '12345' + const seqNo = 420 + const tag = 'someTag' + + expect(getUnqualifiedCredentialDefinitionId(did, seqNo, tag)).toEqual('12345:3:CL:420:someTag') + }) + + test('getUnqualifiedRevocationRegistryId returns a valid credential definition id given a did, seqNo, and tag', () => { + const did = '12345' + const seqNo = 420 + const credentialDefinitionTag = 'someTag' + const tag = 'anotherTag' + + expect(getUnqualifiedRevocationRegistryDefinitionId(did, seqNo, credentialDefinitionTag, tag)).toEqual( + '12345:4:12345:3:CL:420:someTag:CL_ACCUM:anotherTag' + ) + }) + + describe('parseIndySchemaId', () => { + test('parses legacy schema id', () => { + expect(parseIndySchemaId('SDqTzbVuCowusqGBNbNDjH:2:schema-name:1.0')).toEqual({ + did: 'SDqTzbVuCowusqGBNbNDjH', + namespaceIdentifier: 'SDqTzbVuCowusqGBNbNDjH', + schemaName: 'schema-name', + schemaVersion: '1.0', + }) + }) + + test('parses did:indy schema id', () => { + expect( + parseIndySchemaId('did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/SCHEMA/schema-name/1.0') + ).toEqual({ + namespaceIdentifier: 'SDqTzbVuCowusqGBNbNDjH', + did: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH', + schemaName: 'schema-name', + schemaVersion: '1.0', + namespace: 'bcovrin:test', + }) + }) + }) + + describe('parseIndyCredentialDefinitionId', () => { + test('parses legacy credential definition id', () => { + expect(parseIndyCredentialDefinitionId('TL1EaPFCZ8Si5aUrqScBDt:3:CL:10:TAG')).toEqual({ + did: 'TL1EaPFCZ8Si5aUrqScBDt', + namespaceIdentifier: 'TL1EaPFCZ8Si5aUrqScBDt', + schemaSeqNo: '10', + tag: 'TAG', + }) + }) + + test('parses did:indy credential definition id', () => { + expect( + parseIndyCredentialDefinitionId('did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt/anoncreds/v0/CLAIM_DEF/10/TAG') + ).toEqual({ + namespaceIdentifier: 'TL1EaPFCZ8Si5aUrqScBDt', + did: 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt', + namespace: 'pool:localtest', + schemaSeqNo: '10', + tag: 'TAG', + }) + }) + }) + + describe('parseIndyRevocationRegistryId', () => { + test('parses legacy revocation registry id', () => { + expect( + parseIndyRevocationRegistryId('5nDyJVP1NrcPAttP3xwMB9:4:5nDyJVP1NrcPAttP3xwMB9:3:CL:56495:npdb:CL_ACCUM:TAG1') + ).toEqual({ + did: '5nDyJVP1NrcPAttP3xwMB9', + namespaceIdentifier: '5nDyJVP1NrcPAttP3xwMB9', + schemaSeqNo: '56495', + credentialDefinitionTag: 'npdb', + revocationRegistryTag: 'TAG1', + }) + }) + + test('parses did:indy revocation registry id', () => { + expect( + parseIndyRevocationRegistryId('did:indy:sovrin:5nDyJVP1NrcPAttP3xwMB9/anoncreds/v0/REV_REG_DEF/56495/npdb/TAG1') + ).toEqual({ + namespace: 'sovrin', + namespaceIdentifier: '5nDyJVP1NrcPAttP3xwMB9', + did: 'did:indy:sovrin:5nDyJVP1NrcPAttP3xwMB9', + schemaSeqNo: '56495', + credentialDefinitionTag: 'npdb', + revocationRegistryTag: 'TAG1', + }) + }) + }) +}) diff --git a/packages/anoncreds/src/utils/__tests__/revocationInterval.test.ts b/packages/anoncreds/src/utils/__tests__/revocationInterval.test.ts new file mode 100644 index 0000000000..0aa0b15d9e --- /dev/null +++ b/packages/anoncreds/src/utils/__tests__/revocationInterval.test.ts @@ -0,0 +1,37 @@ +import { assertBestPracticeRevocationInterval } from '../../utils' + +describe('assertBestPracticeRevocationInterval', () => { + test("throws if no 'to' value is specified", () => { + expect(() => + assertBestPracticeRevocationInterval({ + from: 10, + }) + ).toThrow() + }) + + test("throws if a 'from' value is specified and it is different from 'to'", () => { + expect(() => + assertBestPracticeRevocationInterval({ + to: 5, + from: 10, + }) + ).toThrow() + }) + + test('does not throw if only to is provided', () => { + expect(() => + assertBestPracticeRevocationInterval({ + to: 5, + }) + ).not.toThrow() + }) + + test('does not throw if from and to are equal', () => { + expect(() => + assertBestPracticeRevocationInterval({ + to: 10, + from: 10, + }) + ).not.toThrow() + }) +}) diff --git a/packages/anoncreds/src/utils/__tests__/sortRequestedCredentialsMatches.test.ts b/packages/anoncreds/src/utils/__tests__/sortRequestedCredentialsMatches.test.ts new file mode 100644 index 0000000000..89a56ab5c9 --- /dev/null +++ b/packages/anoncreds/src/utils/__tests__/sortRequestedCredentialsMatches.test.ts @@ -0,0 +1,60 @@ +import type { AnonCredsCredentialInfo, AnonCredsRequestedAttributeMatch } from '../../models' + +import { sortRequestedCredentialsMatches } from '../sortRequestedCredentialsMatches' + +const credentialInfo = { + updatedAt: new Date('2024-01-01T00:00:00Z'), + createdAt: new Date('2024-01-01T00:00:00Z'), +} as unknown as AnonCredsCredentialInfo + +const credentials: AnonCredsRequestedAttributeMatch[] = [ + { + credentialId: '1', + revealed: true, + revoked: true, + credentialInfo: { ...credentialInfo, updatedAt: new Date('2024-01-01T00:00:01Z') }, + }, + { + credentialId: '2', + revealed: true, + revoked: undefined, + credentialInfo: { ...credentialInfo, updatedAt: new Date('2024-01-01T00:00:01Z') }, + }, + { + credentialId: '3', + revealed: true, + revoked: false, + credentialInfo: { ...credentialInfo, updatedAt: new Date('2024-01-01T00:00:01Z') }, + }, + { + credentialId: '4', + revealed: true, + revoked: false, + credentialInfo, + }, + { + credentialId: '5', + revealed: true, + revoked: true, + credentialInfo, + }, + { + credentialId: '6', + revealed: true, + revoked: undefined, + credentialInfo, + }, +] + +describe('sortRequestedCredentialsMatches', () => { + test('sorts the credentials', () => { + expect(sortRequestedCredentialsMatches(credentials)).toEqual([ + credentials[1], + credentials[5], + credentials[2], + credentials[3], + credentials[0], + credentials[4], + ]) + }) +}) diff --git a/packages/anoncreds/src/utils/anonCredsObjects.ts b/packages/anoncreds/src/utils/anonCredsObjects.ts new file mode 100644 index 0000000000..4ad2c95c95 --- /dev/null +++ b/packages/anoncreds/src/utils/anonCredsObjects.ts @@ -0,0 +1,115 @@ +import type { AnonCredsCredentialDefinition, AnonCredsRevocationStatusList, AnonCredsSchema } from '../models' +import type { AgentContext } from '@credo-ts/core' + +import { CredoError } from '@credo-ts/core' + +import { AnonCredsRegistryService } from '../services' + +export async function fetchSchema(agentContext: AgentContext, schemaId: string) { + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + + const result = await registryService + .getRegistryForIdentifier(agentContext, schemaId) + .getSchema(agentContext, schemaId) + + if (!result || !result.schema) { + throw new CredoError(`Schema not found for id ${schemaId}: ${result.resolutionMetadata.message}`) + } + + return { + schema: result.schema, + schemaId: result.schemaId, + indyNamespace: result.schemaMetadata.didIndyNamespace as string | undefined, + } +} + +export async function fetchCredentialDefinition(agentContext: AgentContext, credentialDefinitionId: string) { + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + + const result = await registryService + .getRegistryForIdentifier(agentContext, credentialDefinitionId) + .getCredentialDefinition(agentContext, credentialDefinitionId) + + if (!result || !result.credentialDefinition) { + throw new CredoError(`Schema not found for id ${credentialDefinitionId}: ${result.resolutionMetadata.message}`) + } + + const indyNamespace = result.credentialDefinitionMetadata.didIndyNamespace + + return { + credentialDefinition: result.credentialDefinition, + credentialDefinitionId, + indyNamespace: indyNamespace && typeof indyNamespace === 'string' ? indyNamespace : undefined, + } +} + +export async function fetchRevocationRegistryDefinition( + agentContext: AgentContext, + revocationRegistryDefinitionId: string +) { + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + + const result = await registryService + .getRegistryForIdentifier(agentContext, revocationRegistryDefinitionId) + .getRevocationRegistryDefinition(agentContext, revocationRegistryDefinitionId) + + if (!result || !result.revocationRegistryDefinition) { + throw new CredoError( + `RevocationRegistryDefinition not found for id ${revocationRegistryDefinitionId}: ${result.resolutionMetadata.message}` + ) + } + + const indyNamespace = result.revocationRegistryDefinitionMetadata.didIndyNamespace + + return { + revocationRegistryDefinition: result.revocationRegistryDefinition, + revocationRegistryDefinitionId, + indyNamespace: indyNamespace && typeof indyNamespace === 'string' ? indyNamespace : undefined, + } +} + +export async function fetchRevocationStatusList( + agentContext: AgentContext, + revocationRegistryId: string, + timestamp: number +): Promise<{ revocationStatusList: AnonCredsRevocationStatusList }> { + const registry = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, revocationRegistryId) + + const { revocationStatusList, resolutionMetadata } = await registry.getRevocationStatusList( + agentContext, + revocationRegistryId, + timestamp + ) + + if (!revocationStatusList) { + throw new CredoError( + `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${resolutionMetadata.message}` + ) + } + + return { revocationStatusList } +} + +export async function fetchSchemas(agentContext: AgentContext, schemaIds: Set) { + const schemaFetchPromises = [...schemaIds].map(async (schemaId): Promise<[string, AnonCredsSchema]> => { + const { schema } = await fetchSchema(agentContext, schemaId) + return [schemaId, schema] + }) + + const schemas = Object.fromEntries(await Promise.all(schemaFetchPromises)) + return schemas +} + +export async function fetchCredentialDefinitions(agentContext: AgentContext, credentialDefinitionIds: Set) { + const credentialDefinitionEntries = [...credentialDefinitionIds].map( + async (credentialDefinitionId): Promise<[string, AnonCredsCredentialDefinition]> => { + const { credentialDefinition } = await fetchCredentialDefinition(agentContext, credentialDefinitionId) + return [credentialDefinitionId, credentialDefinition] + } + ) + + const credentialDefinitions = Object.fromEntries(await Promise.all(credentialDefinitionEntries)) + return credentialDefinitions +} diff --git a/packages/anoncreds/src/utils/areRequestsEqual.ts b/packages/anoncreds/src/utils/areRequestsEqual.ts new file mode 100644 index 0000000000..759312cf87 --- /dev/null +++ b/packages/anoncreds/src/utils/areRequestsEqual.ts @@ -0,0 +1,156 @@ +import type { AnonCredsNonRevokedInterval, AnonCredsProofRequest, AnonCredsProofRequestRestriction } from '../models' + +// Copied from the core package so we don't have to export these silly utils. We should probably move these to a separate package. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function areObjectsEqual(a: any, b: any): boolean { + if (typeof a == 'object' && a != null && typeof b == 'object' && b != null) { + if (Object.keys(a).length !== Object.keys(b).length) return false + for (const key in a) { + if (!(key in b) || !areObjectsEqual(a[key], b[key])) { + return false + } + } + for (const key in b) { + if (!(key in a) || !areObjectsEqual(b[key], a[key])) { + return false + } + } + return true + } else { + return a === b + } +} + +/** + * Checks whether two `names` arrays are equal. The order of the names doesn't matter. + */ +function areNamesEqual(namesA: string[] | undefined, namesB: string[] | undefined) { + if (namesA === undefined) return namesB === undefined || namesB.length === 0 + if (namesB === undefined) return namesA.length === 0 + + // Check if there are any duplicates + if (new Set(namesA).size !== namesA.length || new Set(namesB).size !== namesB.length) return false + + // Check if the number of names is equal between A & B + if (namesA.length !== namesB.length) return false + + return namesA.every((a) => namesB.includes(a)) +} + +/** + * Checks whether two proof requests are semantically equal. The `name`, `version` and `nonce`, `ver` fields are ignored. + * In addition the group names don't have to be the same between the different requests. + */ +export function areAnonCredsProofRequestsEqual( + requestA: AnonCredsProofRequest, + requestB: AnonCredsProofRequest +): boolean { + // Check if the top-level non-revocation interval is equal + if (!isNonRevokedEqual(requestA.non_revoked, requestB.non_revoked)) return false + + const attributeAList = Object.values(requestA.requested_attributes) + const attributeBList = Object.values(requestB.requested_attributes) + + // Check if the number of attribute groups is equal in both requests + if (attributeAList.length !== attributeBList.length) return false + + // Check if all attribute groups in A are also in B + const attributesMatch = attributeAList.every((a) => { + // find an attribute in B that matches this attribute + const bIndex = attributeBList.findIndex((b) => { + return ( + b.name === a.name && + areNamesEqual(a.names, b.names) && + isNonRevokedEqual(a.non_revoked, b.non_revoked) && + areRestrictionsEqual(a.restrictions, b.restrictions) + ) + }) + + // Match found + if (bIndex !== -1) { + attributeBList.splice(bIndex, 1) + return true + } + + // Match not found + return false + }) + + if (!attributesMatch) return false + + const predicatesA = Object.values(requestA.requested_predicates) + const predicatesB = Object.values(requestB.requested_predicates) + + if (predicatesA.length !== predicatesB.length) return false + const predicatesMatch = predicatesA.every((a) => { + // find a predicate in B that matches this predicate + const bIndex = predicatesB.findIndex((b) => { + return ( + a.name === b.name && + a.p_type === b.p_type && + a.p_value === b.p_value && + isNonRevokedEqual(a.non_revoked, b.non_revoked) && + areRestrictionsEqual(a.restrictions, b.restrictions) + ) + }) + + if (bIndex !== -1) { + predicatesB.splice(bIndex, 1) + return true + } + + return false + }) + + if (!predicatesMatch) return false + + return true +} + +/** + * Checks whether two non-revocation intervals are semantically equal. They are considered equal if: + * - Both are undefined + * - Both are empty objects + * - One if undefined and the other is an empty object + * - Both have the same from and to values + */ +function isNonRevokedEqual( + nonRevokedA: AnonCredsNonRevokedInterval | undefined, + nonRevokedB: AnonCredsNonRevokedInterval | undefined +) { + // Having an empty non-revoked object is the same as not having one + if (nonRevokedA === undefined) + return nonRevokedB === undefined || (nonRevokedB.from === undefined && nonRevokedB.to === undefined) + if (nonRevokedB === undefined) return nonRevokedA.from === undefined && nonRevokedA.to === undefined + + return nonRevokedA.from === nonRevokedB.from && nonRevokedA.to === nonRevokedB.to +} + +/** + * Check if two restriction lists are equal. The order of the restrictions does not matter. + */ +function areRestrictionsEqual( + restrictionsA: AnonCredsProofRequestRestriction[] | undefined, + restrictionsB: AnonCredsProofRequestRestriction[] | undefined +) { + // Having an undefined restrictions property or an empty array is the same + if (restrictionsA === undefined) return restrictionsB === undefined || restrictionsB.length === 0 + if (restrictionsB === undefined) return restrictionsA.length === 0 + + // Clone array to not modify input object + const bList = [...restrictionsB] + + // Check if all restrictions in A are also in B + return restrictionsA.every((a) => { + const bIndex = restrictionsB.findIndex((b) => areObjectsEqual(a, b)) + + // Match found + if (bIndex !== -1) { + bList.splice(bIndex, 1) + return true + } + + // Match not found + return false + }) +} diff --git a/packages/anoncreds/src/utils/composeAutoAccept.ts b/packages/anoncreds/src/utils/composeAutoAccept.ts new file mode 100644 index 0000000000..c93a2cbe49 --- /dev/null +++ b/packages/anoncreds/src/utils/composeAutoAccept.ts @@ -0,0 +1,21 @@ +import { AutoAcceptCredential, AutoAcceptProof } from '@credo-ts/core' + +/** + * Returns the credential auto accept config based on priority: + * - The record config takes first priority + * - Otherwise the agent config + * - Otherwise {@link AutoAcceptCredential.Never} is returned + */ +export function composeCredentialAutoAccept(recordConfig?: AutoAcceptCredential, agentConfig?: AutoAcceptCredential) { + return recordConfig ?? agentConfig ?? AutoAcceptCredential.Never +} + +/** + * Returns the proof auto accept config based on priority: + * - The record config takes first priority + * - Otherwise the agent config + * - Otherwise {@link AutoAcceptProof.Never} is returned + */ +export function composeProofAutoAccept(recordConfig?: AutoAcceptProof, agentConfig?: AutoAcceptProof) { + return recordConfig ?? agentConfig ?? AutoAcceptProof.Never +} diff --git a/packages/anoncreds/src/utils/createRequestFromPreview.ts b/packages/anoncreds/src/utils/createRequestFromPreview.ts new file mode 100644 index 0000000000..7144ec2150 --- /dev/null +++ b/packages/anoncreds/src/utils/createRequestFromPreview.ts @@ -0,0 +1,96 @@ +import type { + AnonCredsPresentationPreviewAttribute, + AnonCredsPresentationPreviewPredicate, +} from '../formats/AnonCredsProofFormat' +import type { AnonCredsNonRevokedInterval, AnonCredsProofRequest } from '../models' + +import { utils } from '@credo-ts/core' + +export function createRequestFromPreview({ + name, + version, + nonce, + attributes, + predicates, + nonRevokedInterval, +}: { + name: string + version: string + nonce: string + attributes: AnonCredsPresentationPreviewAttribute[] + predicates: AnonCredsPresentationPreviewPredicate[] + nonRevokedInterval?: AnonCredsNonRevokedInterval +}): AnonCredsProofRequest { + const proofRequest: AnonCredsProofRequest = { + name, + version, + nonce, + requested_attributes: {}, + requested_predicates: {}, + } + + /** + * Create mapping of attributes by referent. This required the + * attributes to come from the same credential. + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#referent + * + * { + * "referent1": [Attribute1, Attribute2], + * "referent2": [Attribute3] + * } + */ + const attributesByReferent: Record = {} + for (const proposedAttributes of attributes ?? []) { + const referent = proposedAttributes.referent ?? utils.uuid() + + const referentAttributes = attributesByReferent[referent] + + // Referent key already exist, add to list + if (referentAttributes) { + referentAttributes.push(proposedAttributes) + } + + // Referent key does not exist yet, create new entry + else { + attributesByReferent[referent] = [proposedAttributes] + } + } + + // Transform attributes by referent to requested attributes + for (const [referent, proposedAttributes] of Object.entries(attributesByReferent)) { + // Either attributeName or attributeNames will be undefined + const attributeName = proposedAttributes.length == 1 ? proposedAttributes[0].name : undefined + const attributeNames = proposedAttributes.length > 1 ? proposedAttributes.map((a) => a.name) : undefined + + proofRequest.requested_attributes[referent] = { + name: attributeName, + names: attributeNames, + restrictions: [ + { + cred_def_id: proposedAttributes[0].credentialDefinitionId, + }, + ], + } + } + + // Transform proposed predicates to requested predicates + for (const proposedPredicate of predicates ?? []) { + proofRequest.requested_predicates[utils.uuid()] = { + name: proposedPredicate.name, + p_type: proposedPredicate.predicate, + p_value: proposedPredicate.threshold, + restrictions: [ + { + cred_def_id: proposedPredicate.credentialDefinitionId, + }, + ], + } + } + + // TODO: local non_revoked? + if (nonRevokedInterval) { + proofRequest.non_revoked = nonRevokedInterval + } + + return proofRequest +} diff --git a/packages/anoncreds/src/utils/credential.ts b/packages/anoncreds/src/utils/credential.ts new file mode 100644 index 0000000000..93f520ee75 --- /dev/null +++ b/packages/anoncreds/src/utils/credential.ts @@ -0,0 +1,230 @@ +import type { AnonCredsSchema, AnonCredsCredentialValues } from '../models' +import type { CredentialPreviewAttributeOptions, LinkedAttachment } from '@credo-ts/core' + +import { Buffer, CredoError, Hasher, TypedArrayEncoder, encodeAttachment } from '@credo-ts/core' +import bigInt from 'big-integer' + +export type AnonCredsClaimRecord = Record + +export interface AnonCredsCredentialValue { + raw: string + encoded: string // Raw value as number in string +} + +const isString = (value: unknown): value is string => typeof value === 'string' +const isNumber = (value: unknown): value is number => typeof value === 'number' +const isBoolean = (value: unknown): value is boolean => typeof value === 'boolean' +const isNumeric = (value: string) => /^-?\d+$/.test(value) + +const isInt32 = (number: number) => { + const minI32 = -2147483648 + const maxI32 = 2147483647 + + // Check if number is integer and in range of int32 + return Number.isInteger(number) && number >= minI32 && number <= maxI32 +} + +// TODO: this function can only encode strings +// If encoding numbers we run into problems with 0.0 representing the same value as 0 and is implicitly converted to 0 +/** + * Encode value according to the encoding format described in Aries RFC 0036/0037 + * + * @param value + * @returns Encoded version of value + * + * @see https://github.com/hyperledger/aries-cloudagent-python/blob/0000f924a50b6ac5e6342bff90e64864672ee935/aries_cloudagent/messaging/util.py#L106-L136 + * @see https://github.com/hyperledger/aries-rfcs/blob/be4ad0a6fb2823bb1fc109364c96f077d5d8dffa/features/0037-present-proof/README.md#verifying-claims-of-indy-based-verifiable-credentials + * @see https://github.com/hyperledger/aries-rfcs/blob/be4ad0a6fb2823bb1fc109364c96f077d5d8dffa/features/0036-issue-credential/README.md#encoding-claims-for-indy-based-verifiable-credentials + */ +export function encodeCredentialValue(value: unknown) { + const isEmpty = (value: unknown) => isString(value) && value === '' + + // If bool return bool as number string + if (isBoolean(value)) { + return Number(value).toString() + } + + // If value is int32 return as number string + if (isNumber(value) && isInt32(value)) { + return value.toString() + } + + // If value is an int32 number string return as number string + if (isString(value) && !isEmpty(value) && !isNaN(Number(value)) && isNumeric(value) && isInt32(Number(value))) { + return Number(value).toString() + } + + if (isNumber(value)) { + value = value.toString() + } + + // If value is null we must use the string value 'None' + if (value === null || value === undefined) { + value = 'None' + } + + const buffer = TypedArrayEncoder.fromString(String(value)) + const hash = Hasher.hash(buffer, 'sha-256') + const hex = Buffer.from(hash).toString('hex') + + return bigInt(hex, 16).toString() +} + +export const mapAttributeRawValuesToAnonCredsCredentialValues = ( + record: AnonCredsClaimRecord +): Record => { + const credentialValues: Record = {} + + for (const [key, value] of Object.entries(record)) { + if (typeof value === 'object') { + throw new CredoError(`Unsupported value type: object for W3cAnonCreds Credential`) + } + credentialValues[key] = { + raw: value.toString(), + encoded: encodeCredentialValue(value), + } + } + + return credentialValues +} + +/** + * Converts int value to string + * Converts string value: + * - hash with sha256, + * - convert to byte array and reverse it + * - convert it to BigInteger and return as a string + * @param attributes + * + * @returns CredValues + */ +export function convertAttributesToCredentialValues( + attributes: CredentialPreviewAttributeOptions[] +): AnonCredsCredentialValues { + return attributes.reduce((credentialValues, attribute) => { + return { + [attribute.name]: { + raw: attribute.value, + encoded: encodeCredentialValue(attribute.value), + }, + ...credentialValues, + } + }, {}) +} + +/** + * Check whether the values of two credentials match (using {@link assertCredentialValuesMatch}) + * + * @returns a boolean whether the values are equal + * + */ +export function checkCredentialValuesMatch( + firstValues: AnonCredsCredentialValues, + secondValues: AnonCredsCredentialValues +): boolean { + try { + assertCredentialValuesMatch(firstValues, secondValues) + return true + } catch { + return false + } +} + +/** + * Assert two credential values objects match. + * + * @param firstValues The first values object + * @param secondValues The second values object + * + * @throws If not all values match + */ +export function assertCredentialValuesMatch( + firstValues: AnonCredsCredentialValues, + secondValues: AnonCredsCredentialValues +) { + const firstValuesKeys = Object.keys(firstValues) + const secondValuesKeys = Object.keys(secondValues) + + if (firstValuesKeys.length !== secondValuesKeys.length) { + throw new Error( + `Number of values in first entry (${firstValuesKeys.length}) does not match number of values in second entry (${secondValuesKeys.length})` + ) + } + + for (const key of firstValuesKeys) { + const firstValue = firstValues[key] + const secondValue = secondValues[key] + + if (!secondValue) { + throw new Error(`Second cred values object has no value for key '${key}'`) + } + + if (firstValue.encoded !== secondValue.encoded) { + throw new Error(`Encoded credential values for key '${key}' do not match`) + } + + if (firstValue.raw !== secondValue.raw) { + throw new Error(`Raw credential values for key '${key}' do not match`) + } + } +} + +/** + * Check whether the raw value matches the encoded version according to the encoding format described in Aries RFC 0037 + * Use this method to ensure the received proof (over the encoded) value is the same as the raw value of the data. + * + * @param raw + * @param encoded + * @returns Whether raw and encoded value match + * + * @see https://github.com/hyperledger/aries-framework-dotnet/blob/a18bef91e5b9e4a1892818df7408e2383c642dfa/src/Hyperledger.Aries/Utils/CredentialUtils.cs#L78-L89 + * @see https://github.com/hyperledger/aries-rfcs/blob/be4ad0a6fb2823bb1fc109364c96f077d5d8dffa/features/0037-present-proof/README.md#verifying-claims-of-indy-based-verifiable-credentials + */ +export function checkValidCredentialValueEncoding(raw: unknown, encoded: string) { + return encoded === encodeCredentialValue(raw) +} + +export function assertAttributesMatch(schema: AnonCredsSchema, attributes: CredentialPreviewAttributeOptions[]) { + const schemaAttributes = schema.attrNames + const credAttributes = attributes.map((a) => a.name) + + const difference = credAttributes + .filter((x) => !schemaAttributes.includes(x)) + .concat(schemaAttributes.filter((x) => !credAttributes.includes(x))) + + if (difference.length > 0) { + throw new CredoError( + `The credential preview attributes do not match the schema attributes (difference is: ${difference}, needs: ${schemaAttributes})` + ) + } +} + +/** + * Adds attribute(s) to the credential preview that is linked to the given attachment(s) + * + * @param attachments a list of the attachments that need to be linked to a credential + * @param preview the credential previews where the new linked credential has to be appended to + * + * @returns a modified version of the credential preview with the linked credentials + * */ +export function createAndLinkAttachmentsToPreview( + attachments: LinkedAttachment[], + previewAttributes: CredentialPreviewAttributeOptions[] +) { + const credentialPreviewAttributeNames = previewAttributes.map((attribute) => attribute.name) + const newPreviewAttributes = [...previewAttributes] + + attachments.forEach((linkedAttachment) => { + if (credentialPreviewAttributeNames.includes(linkedAttachment.attributeName)) { + throw new CredoError(`linkedAttachment ${linkedAttachment.attributeName} already exists in the preview`) + } else { + newPreviewAttributes.push({ + name: linkedAttachment.attributeName, + mimeType: linkedAttachment.attachment.mimeType, + value: encodeAttachment(linkedAttachment.attachment), + }) + } + }) + + return newPreviewAttributes +} diff --git a/packages/anoncreds/src/utils/credentialPreviewAttributes.ts b/packages/anoncreds/src/utils/credentialPreviewAttributes.ts new file mode 100644 index 0000000000..99d72819d0 --- /dev/null +++ b/packages/anoncreds/src/utils/credentialPreviewAttributes.ts @@ -0,0 +1,27 @@ +import type { CredentialPreviewAttributeOptions } from '@credo-ts/core' + +export function areCredentialPreviewAttributesEqual( + firstAttributes: CredentialPreviewAttributeOptions[], + secondAttributes: CredentialPreviewAttributeOptions[] +) { + if (firstAttributes.length !== secondAttributes.length) return false + + const secondAttributeMap = secondAttributes.reduce>( + (attributeMap, attribute) => ({ ...attributeMap, [attribute.name]: attribute }), + {} + ) + + // check if no duplicate keys exist + if (new Set(firstAttributes.map((attribute) => attribute.name)).size !== firstAttributes.length) return false + if (new Set(secondAttributes.map((attribute) => attribute.name)).size !== secondAttributes.length) return false + + for (const firstAttribute of firstAttributes) { + const secondAttribute = secondAttributeMap[firstAttribute.name] + + if (!secondAttribute) return false + if (firstAttribute.value !== secondAttribute.value) return false + if (firstAttribute.mimeType !== secondAttribute.mimeType) return false + } + + return true +} diff --git a/packages/anoncreds/src/utils/getCredentialsForAnonCredsRequest.ts b/packages/anoncreds/src/utils/getCredentialsForAnonCredsRequest.ts new file mode 100644 index 0000000000..549eacea2b --- /dev/null +++ b/packages/anoncreds/src/utils/getCredentialsForAnonCredsRequest.ts @@ -0,0 +1,154 @@ +import type { AnonCredsCredentialsForProofRequest, AnonCredsGetCredentialsForProofRequestOptions } from '../formats' +import type { + AnonCredsCredentialInfo, + AnonCredsProofRequest, + AnonCredsRequestedAttribute, + AnonCredsRequestedAttributeMatch, + AnonCredsRequestedPredicate, + AnonCredsRequestedPredicateMatch, +} from '../models' +import type { AnonCredsHolderService, GetCredentialsForProofRequestReturn } from '../services' +import type { AgentContext } from '@credo-ts/core' + +import { AnonCredsHolderServiceSymbol } from '../services' + +import { fetchRevocationStatusList } from './anonCredsObjects' +import { assertBestPracticeRevocationInterval } from './revocationInterval' +import { sortRequestedCredentialsMatches } from './sortRequestedCredentialsMatches' +import { dateToTimestamp } from './timestamp' + +const getCredentialsForProofRequestReferent = async ( + agentContext: AgentContext, + proofRequest: AnonCredsProofRequest, + attributeReferent: string +): Promise => { + const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const credentials = await holderService.getCredentialsForProofRequest(agentContext, { + proofRequest, + attributeReferent, + }) + + return credentials +} + +const getRevocationStatus = async ( + agentContext: AgentContext, + proofRequest: AnonCredsProofRequest, + requestedItem: AnonCredsRequestedAttribute | AnonCredsRequestedPredicate, + credentialInfo: AnonCredsCredentialInfo +) => { + const requestNonRevoked = requestedItem.non_revoked ?? proofRequest.non_revoked + const credentialRevocationId = credentialInfo.credentialRevocationId + const revocationRegistryId = credentialInfo.revocationRegistryId + + // If revocation interval is not present or the credential is not revocable then we + // don't need to fetch the revocation status + if (!requestNonRevoked || credentialRevocationId === null || !revocationRegistryId) { + return { isRevoked: undefined, timestamp: undefined } + } + + agentContext.config.logger.trace( + `Fetching credential revocation status for credential revocation id '${credentialRevocationId}' with revocation interval with from '${requestNonRevoked.from}' and to '${requestNonRevoked.to}'` + ) + + // Make sure the revocation interval follows best practices from Aries RFC 0441 + assertBestPracticeRevocationInterval(requestNonRevoked) + + const { revocationStatusList } = await fetchRevocationStatusList( + agentContext, + revocationRegistryId, + requestNonRevoked.to ?? dateToTimestamp(new Date()) + ) + + const isRevoked = revocationStatusList.revocationList[parseInt(credentialRevocationId)] === 1 + + agentContext.config.logger.trace( + `Credential with credential revocation index '${credentialRevocationId}' is ${ + isRevoked ? '' : 'not ' + }revoked with revocation interval with to '${requestNonRevoked.to}' & from '${requestNonRevoked.from}'` + ) + + return { + isRevoked, + timestamp: revocationStatusList.timestamp, + } +} + +export const getCredentialsForAnonCredsProofRequest = async ( + agentContext: AgentContext, + proofRequest: AnonCredsProofRequest, + options: AnonCredsGetCredentialsForProofRequestOptions +): Promise => { + const credentialsForProofRequest: AnonCredsCredentialsForProofRequest = { + attributes: {}, + predicates: {}, + } + + for (const [referent, requestedAttribute] of Object.entries(proofRequest.requested_attributes)) { + const credentials = await getCredentialsForProofRequestReferent(agentContext, proofRequest, referent) + + credentialsForProofRequest.attributes[referent] = sortRequestedCredentialsMatches( + await Promise.all( + credentials.map(async (credential) => { + const { isRevoked, timestamp } = await getRevocationStatus( + agentContext, + proofRequest, + requestedAttribute, + credential.credentialInfo + ) + + return { + credentialId: credential.credentialInfo.credentialId, + revealed: true, + credentialInfo: credential.credentialInfo, + timestamp, + revoked: isRevoked, + } satisfies AnonCredsRequestedAttributeMatch + }) + ) + ) + + // We only attach revoked state if non-revocation is requested. So if revoked is true it means + // the credential is not applicable to the proof request + if (options.filterByNonRevocationRequirements) { + credentialsForProofRequest.attributes[referent] = credentialsForProofRequest.attributes[referent].filter( + (r) => !r.revoked + ) + } + } + + for (const [referent, requestedPredicate] of Object.entries(proofRequest.requested_predicates)) { + const credentials = await getCredentialsForProofRequestReferent(agentContext, proofRequest, referent) + + credentialsForProofRequest.predicates[referent] = sortRequestedCredentialsMatches( + await Promise.all( + credentials.map(async (credential) => { + const { isRevoked, timestamp } = await getRevocationStatus( + agentContext, + proofRequest, + requestedPredicate, + credential.credentialInfo + ) + + return { + credentialId: credential.credentialInfo.credentialId, + credentialInfo: credential.credentialInfo, + timestamp, + revoked: isRevoked, + } satisfies AnonCredsRequestedPredicateMatch + }) + ) + ) + + // We only attach revoked state if non-revocation is requested. So if revoked is true it means + // the credential is not applicable to the proof request + if (options.filterByNonRevocationRequirements) { + credentialsForProofRequest.predicates[referent] = credentialsForProofRequest.predicates[referent].filter( + (r) => !r.revoked + ) + } + } + + return credentialsForProofRequest +} diff --git a/packages/anoncreds/src/utils/getRevocationRegistries.ts b/packages/anoncreds/src/utils/getRevocationRegistries.ts new file mode 100644 index 0000000000..699a98070e --- /dev/null +++ b/packages/anoncreds/src/utils/getRevocationRegistries.ts @@ -0,0 +1,212 @@ +import type { AnonCredsProof, AnonCredsProofRequest, AnonCredsSelectedCredentials } from '../models' +import type { CreateProofOptions, VerifyProofOptions } from '../services' +import type { AgentContext } from '@credo-ts/core' + +import { CredoError } from '@credo-ts/core' + +import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' +import { AnonCredsRegistryService } from '../services' + +import { assertBestPracticeRevocationInterval } from './revocationInterval' + +export async function getRevocationRegistriesForRequest( + agentContext: AgentContext, + proofRequest: AnonCredsProofRequest, + selectedCredentials: AnonCredsSelectedCredentials +) { + const revocationRegistries: CreateProofOptions['revocationRegistries'] = {} + + // NOTE: we don't want to mutate this object, when modifying we need to always deeply clone objects firsts. + let updatedSelectedCredentials = selectedCredentials + + try { + agentContext.config.logger.debug(`Retrieving revocation registries for proof request`, { + proofRequest, + selectedCredentials, + }) + + const referentCredentials = [] + + // Retrieve information for referents and push to single array + for (const [referent, selectedCredential] of Object.entries(selectedCredentials.attributes)) { + referentCredentials.push({ + type: 'attributes' as const, + referent, + selectedCredential, + nonRevoked: proofRequest.requested_attributes[referent].non_revoked ?? proofRequest.non_revoked, + }) + } + for (const [referent, selectedCredential] of Object.entries(selectedCredentials.predicates)) { + referentCredentials.push({ + type: 'predicates' as const, + referent, + selectedCredential, + nonRevoked: proofRequest.requested_predicates[referent].non_revoked ?? proofRequest.non_revoked, + }) + } + + const revocationRegistryPromises = [] + for (const { referent, selectedCredential, nonRevoked, type } of referentCredentials) { + if (!selectedCredential.credentialInfo) { + throw new CredoError( + `Credential for referent '${referent} does not have credential info for revocation state creation` + ) + } + + // Prefer referent-specific revocation interval over global revocation interval + const credentialRevocationId = selectedCredential.credentialInfo.credentialRevocationId + const revocationRegistryId = selectedCredential.credentialInfo.revocationRegistryId + const timestamp = selectedCredential.timestamp + + // If revocation interval is present and the credential is revocable then create revocation state + if (nonRevoked && credentialRevocationId && revocationRegistryId) { + agentContext.config.logger.trace( + `Presentation is requesting proof of non revocation for referent '${referent}', creating revocation state for credential`, + { + nonRevoked, + credentialRevocationId, + revocationRegistryId, + timestamp, + } + ) + + // Make sure the revocation interval follows best practices from Aries RFC 0441 + assertBestPracticeRevocationInterval(nonRevoked) + + const registry = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, revocationRegistryId) + + const getRevocationRegistry = async () => { + // Fetch revocation registry definition if not in revocation registries list yet + if (!revocationRegistries[revocationRegistryId]) { + const { revocationRegistryDefinition, resolutionMetadata } = await registry.getRevocationRegistryDefinition( + agentContext, + revocationRegistryId + ) + if (!revocationRegistryDefinition) { + throw new CredoError( + `Could not retrieve revocation registry definition for revocation registry ${revocationRegistryId}: ${resolutionMetadata.message}` + ) + } + + const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + const { tailsFilePath } = await tailsFileService.getTailsFile(agentContext, { + revocationRegistryDefinition, + }) + + // const tails = await this.indyUtilitiesService.downloadTails(tailsHash, tailsLocation) + revocationRegistries[revocationRegistryId] = { + definition: revocationRegistryDefinition, + tailsFilePath, + revocationStatusLists: {}, + } + } + + // In most cases we will have a timestamp, but if it's not defined, we use the nonRevoked.to value + const timestampToFetch = timestamp ?? nonRevoked.to + + // Fetch revocation status list if we don't already have a revocation status list for the given timestamp + if (!revocationRegistries[revocationRegistryId].revocationStatusLists[timestampToFetch]) { + const { revocationStatusList, resolutionMetadata: statusListResolutionMetadata } = + await registry.getRevocationStatusList(agentContext, revocationRegistryId, timestampToFetch) + + if (!revocationStatusList) { + throw new CredoError( + `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${statusListResolutionMetadata.message}` + ) + } + + revocationRegistries[revocationRegistryId].revocationStatusLists[revocationStatusList.timestamp] = + revocationStatusList + + // If we don't have a timestamp on the selected credential, we set it to the timestamp of the revocation status list + // this way we know which revocation status list to use when creating the proof. + if (!timestamp) { + updatedSelectedCredentials = { + ...updatedSelectedCredentials, + [type]: { + ...updatedSelectedCredentials[type], + [referent]: { + ...updatedSelectedCredentials[type][referent], + timestamp: revocationStatusList.timestamp, + }, + }, + } + } + } + } + revocationRegistryPromises.push(getRevocationRegistry()) + } + } + // await all revocation registry statuses asynchronously + await Promise.all(revocationRegistryPromises) + agentContext.config.logger.debug(`Retrieved revocation registries for proof request`, { + revocationRegistries, + }) + + return { revocationRegistries, updatedSelectedCredentials } + } catch (error) { + agentContext.config.logger.error(`Error retrieving revocation registry for proof request`, { + error, + proofRequest, + selectedCredentials, + }) + + throw error + } +} + +export async function getRevocationRegistriesForProof(agentContext: AgentContext, proof: AnonCredsProof) { + const revocationRegistries: VerifyProofOptions['revocationRegistries'] = {} + + const revocationRegistryPromises = [] + for (const identifier of proof.identifiers) { + const revocationRegistryId = identifier.rev_reg_id + const timestamp = identifier.timestamp + + // Skip if no revocation registry id is present + if (!revocationRegistryId || !timestamp) continue + + const registry = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, revocationRegistryId) + + const getRevocationRegistry = async () => { + // Fetch revocation registry definition if not already fetched + if (!revocationRegistries[revocationRegistryId]) { + const { revocationRegistryDefinition, resolutionMetadata } = await registry.getRevocationRegistryDefinition( + agentContext, + revocationRegistryId + ) + if (!revocationRegistryDefinition) { + throw new CredoError( + `Could not retrieve revocation registry definition for revocation registry ${revocationRegistryId}: ${resolutionMetadata.message}` + ) + } + + revocationRegistries[revocationRegistryId] = { + definition: revocationRegistryDefinition, + revocationStatusLists: {}, + } + } + + // Fetch revocation status list by timestamp if not already fetched + if (!revocationRegistries[revocationRegistryId].revocationStatusLists[timestamp]) { + const { revocationStatusList, resolutionMetadata: statusListResolutionMetadata } = + await registry.getRevocationStatusList(agentContext, revocationRegistryId, timestamp) + + if (!revocationStatusList) { + throw new CredoError( + `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${statusListResolutionMetadata.message}` + ) + } + + revocationRegistries[revocationRegistryId].revocationStatusLists[timestamp] = revocationStatusList + } + } + revocationRegistryPromises.push(getRevocationRegistry()) + } + await Promise.all(revocationRegistryPromises) + return revocationRegistries +} diff --git a/packages/anoncreds/src/utils/hasDuplicateGroupNames.ts b/packages/anoncreds/src/utils/hasDuplicateGroupNames.ts new file mode 100644 index 0000000000..8f1faf3549 --- /dev/null +++ b/packages/anoncreds/src/utils/hasDuplicateGroupNames.ts @@ -0,0 +1,27 @@ +import type { AnonCredsProofRequest } from '../models' + +import { CredoError } from '@credo-ts/core' + +function attributeNamesToArray(proofRequest: AnonCredsProofRequest) { + // Attributes can contain either a `name` string value or an `names` string array. We reduce it to a single array + // containing all attribute names from the requested attributes. + return Object.values(proofRequest.requested_attributes).reduce( + (names, a) => [...names, ...(a.name ? [a.name] : a.names ? a.names : [])], + [] + ) +} + +function predicateNamesToArray(proofRequest: AnonCredsProofRequest) { + return Array.from(new Set(Object.values(proofRequest.requested_predicates).map((a) => a.name))) +} + +// TODO: This is still not ideal. The requested groups can specify different credentials using restrictions. +export function assertNoDuplicateGroupsNamesInProofRequest(proofRequest: AnonCredsProofRequest) { + const attributes = attributeNamesToArray(proofRequest) + const predicates = predicateNamesToArray(proofRequest) + + const duplicates = predicates.filter((item) => attributes.indexOf(item) !== -1) + if (duplicates.length > 0) { + throw new CredoError(`The proof request contains duplicate predicates and attributes: ${duplicates.toString()}`) + } +} diff --git a/packages/anoncreds/src/utils/index.ts b/packages/anoncreds/src/utils/index.ts new file mode 100644 index 0000000000..62a50bbea8 --- /dev/null +++ b/packages/anoncreds/src/utils/index.ts @@ -0,0 +1,35 @@ +export { createRequestFromPreview } from './createRequestFromPreview' +export { sortRequestedCredentialsMatches } from './sortRequestedCredentialsMatches' +export { assertNoDuplicateGroupsNamesInProofRequest } from './hasDuplicateGroupNames' +export { areAnonCredsProofRequestsEqual } from './areRequestsEqual' +export { assertBestPracticeRevocationInterval } from './revocationInterval' +export { getRevocationRegistriesForRequest, getRevocationRegistriesForProof } from './getRevocationRegistries' +export { checkValidCredentialValueEncoding, AnonCredsCredentialValue } from './credential' +export { IsMap } from './isMap' +export { composeCredentialAutoAccept, composeProofAutoAccept } from './composeAutoAccept' +export { areCredentialPreviewAttributesEqual } from './credentialPreviewAttributes' +export { dateToTimestamp } from './timestamp' +export { storeLinkSecret } from './linkSecret' +export { + unqualifiedCredentialDefinitionIdRegex, + unqualifiedIndyDidRegex, + unqualifiedSchemaIdRegex, + unqualifiedSchemaVersionRegex, +} from './indyIdentifiers' + +export { + fetchCredentialDefinition, + fetchRevocationRegistryDefinition, + fetchSchema, + fetchRevocationStatusList, +} from './anonCredsObjects' +export { + AnonCredsCredentialMetadataKey, + AnonCredsCredentialRequestMetadataKey, + W3cAnonCredsCredentialMetadataKey, + AnonCredsCredentialMetadata, + AnonCredsCredentialRequestMetadata, + W3cAnonCredsCredentialMetadata, +} from './metadata' +export { getW3cRecordAnonCredsTags } from './w3cAnonCredsUtils' +export { getCredentialsForAnonCredsProofRequest } from './getCredentialsForAnonCredsRequest' diff --git a/packages/anoncreds/src/utils/indyIdentifiers.ts b/packages/anoncreds/src/utils/indyIdentifiers.ts new file mode 100644 index 0000000000..c31d26b03e --- /dev/null +++ b/packages/anoncreds/src/utils/indyIdentifiers.ts @@ -0,0 +1,393 @@ +import type { AnonCredsCredentialDefinition, AnonCredsRevocationRegistryDefinition, AnonCredsSchema } from '../models' + +import { CredoError } from '@credo-ts/core' + +const didIndyAnonCredsBase = + /(did:indy:((?:[a-z][_a-z0-9-]*)(?::[a-z][_a-z0-9-]*)?):([1-9A-HJ-NP-Za-km-z]{21,22}))\/anoncreds\/v0/ + +// :2:: +export const unqualifiedSchemaIdRegex = /^([a-zA-Z0-9]{21,22}):2:(.+):([0-9.]+)$/ +// did:indy::/anoncreds/v0/SCHEMA// +export const didIndySchemaIdRegex = new RegExp(`^${didIndyAnonCredsBase.source}/SCHEMA/(.+)/([0-9.]+)$`) + +export const unqualifiedSchemaVersionRegex = /^(\d+\.)?(\d+\.)?(\*|\d+)$/ +export const unqualifiedIndyDidRegex = /^(did:sov:)?[a-zA-Z0-9]{21,22}$/ + +// :3:CL:: +export const unqualifiedCredentialDefinitionIdRegex = /^([a-zA-Z0-9]{21,22}):3:CL:([1-9][0-9]*):(.+)$/ +// did:indy::/anoncreds/v0/CLAIM_DEF// +export const didIndyCredentialDefinitionIdRegex = new RegExp( + `^${didIndyAnonCredsBase.source}/CLAIM_DEF/([1-9][0-9]*)/(.+)$` +) + +// :4::3:CL:::CL_ACCUM: +export const unqualifiedRevocationRegistryIdRegex = + /^([a-zA-Z0-9]{21,22}):4:[a-zA-Z0-9]{21,22}:3:CL:([1-9][0-9]*):(.+):CL_ACCUM:(.+)$/ +// did:indy::/anoncreds/v0/REV_REG_DEF/// +export const didIndyRevocationRegistryIdRegex = new RegExp( + `^${didIndyAnonCredsBase.source}/REV_REG_DEF/([1-9][0-9]*)/(.+)/(.+)$` +) + +export const didIndyRegex = /^did:indy:((?:[a-z][_a-z0-9-]*)(?::[a-z][_a-z0-9-]*)?):([1-9A-HJ-NP-Za-km-z]{21,22})$/ + +export function getUnqualifiedSchemaId(unqualifiedDid: string, name: string, version: string) { + return `${unqualifiedDid}:2:${name}:${version}` +} + +export function getUnqualifiedCredentialDefinitionId( + unqualifiedDid: string, + schemaSeqNo: string | number, + tag: string +) { + return `${unqualifiedDid}:3:CL:${schemaSeqNo}:${tag}` +} + +// TZQuLp43UcYTdtc3HewcDz:4:TZQuLp43UcYTdtc3HewcDz:3:CL:98158:BaustellenzertifikateNU1:CL_ACCUM:1-100 +export function getUnqualifiedRevocationRegistryDefinitionId( + unqualifiedDid: string, + schemaSeqNo: string | number, + credentialDefinitionTag: string, + revocationRegistryTag: string +) { + return `${unqualifiedDid}:4:${unqualifiedDid}:3:CL:${schemaSeqNo}:${credentialDefinitionTag}:CL_ACCUM:${revocationRegistryTag}` +} + +export function isUnqualifiedIndyDid(did: string) { + return unqualifiedIndyDidRegex.test(did) +} + +export function isUnqualifiedCredentialDefinitionId(credentialDefinitionId: string) { + return unqualifiedCredentialDefinitionIdRegex.test(credentialDefinitionId) +} + +export function isUnqualifiedRevocationRegistryId(revocationRegistryId: string) { + return unqualifiedRevocationRegistryIdRegex.test(revocationRegistryId) +} + +export function isUnqualifiedSchemaId(schemaId: string) { + return unqualifiedSchemaIdRegex.test(schemaId) +} + +export function isDidIndySchemaId(schemaId: string) { + return didIndySchemaIdRegex.test(schemaId) +} + +export function isDidIndyCredentialDefinitionId(credentialDefinitionId: string) { + return didIndyCredentialDefinitionIdRegex.test(credentialDefinitionId) +} + +export function isDidIndyRevocationRegistryId(revocationRegistryId: string) { + return didIndyRevocationRegistryIdRegex.test(revocationRegistryId) +} + +export function parseIndyDid(did: string) { + const match = did.match(didIndyRegex) + if (match) { + const [, namespace, namespaceIdentifier] = match + return { namespace, namespaceIdentifier } + } else { + throw new CredoError(`${did} is not a valid did:indy did`) + } +} + +interface ParsedIndySchemaId { + did: string + namespaceIdentifier: string + schemaName: string + schemaVersion: string + namespace?: string +} + +export function parseIndySchemaId(schemaId: string): ParsedIndySchemaId { + const didIndyMatch = schemaId.match(didIndySchemaIdRegex) + if (didIndyMatch) { + const [, did, namespace, namespaceIdentifier, schemaName, schemaVersion] = didIndyMatch + + return { + did, + namespaceIdentifier, + schemaName, + schemaVersion, + namespace, + } + } + + const legacyMatch = schemaId.match(unqualifiedSchemaIdRegex) + if (legacyMatch) { + const [, did, schemaName, schemaVersion] = legacyMatch + + return { + did, + namespaceIdentifier: did, + schemaName, + schemaVersion, + } + } + + throw new Error(`Invalid schema id: ${schemaId}`) +} + +interface ParsedIndyCredentialDefinitionId { + did: string + namespaceIdentifier: string + schemaSeqNo: string + tag: string + namespace?: string +} + +export function parseIndyCredentialDefinitionId(credentialDefinitionId: string): ParsedIndyCredentialDefinitionId { + const didIndyMatch = credentialDefinitionId.match(didIndyCredentialDefinitionIdRegex) + if (didIndyMatch) { + const [, did, namespace, namespaceIdentifier, schemaSeqNo, tag] = didIndyMatch + + return { + did, + namespaceIdentifier, + schemaSeqNo, + tag, + namespace, + } + } + + const legacyMatch = credentialDefinitionId.match(unqualifiedCredentialDefinitionIdRegex) + if (legacyMatch) { + const [, did, schemaSeqNo, tag] = legacyMatch + + return { + did, + namespaceIdentifier: did, + schemaSeqNo, + tag, + } + } + + throw new Error(`Invalid credential definition id: ${credentialDefinitionId}`) +} + +interface ParsedIndyRevocationRegistryId { + did: string + namespaceIdentifier: string + schemaSeqNo: string + credentialDefinitionTag: string + revocationRegistryTag: string + namespace?: string +} + +export function parseIndyRevocationRegistryId(revocationRegistryId: string): ParsedIndyRevocationRegistryId { + const didIndyMatch = revocationRegistryId.match(didIndyRevocationRegistryIdRegex) + if (didIndyMatch) { + const [, did, namespace, namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag] = + didIndyMatch + + return { + did, + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag, + namespace, + } + } + + const legacyMatch = revocationRegistryId.match(unqualifiedRevocationRegistryIdRegex) + if (legacyMatch) { + const [, did, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag] = legacyMatch + + return { + did, + namespaceIdentifier: did, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag, + } + } + + throw new Error(`Invalid revocation registry id: ${revocationRegistryId}`) +} + +export function getIndyNamespaceFromIndyDid(identifier: string): string { + let namespace: string | undefined + if (isDidIndySchemaId(identifier)) { + namespace = parseIndySchemaId(identifier).namespace + } else if (isDidIndyCredentialDefinitionId(identifier)) { + namespace = parseIndyCredentialDefinitionId(identifier).namespace + } else if (isDidIndyRevocationRegistryId(identifier)) { + namespace = parseIndyRevocationRegistryId(identifier).namespace + } else { + namespace = parseIndyDid(identifier).namespace + } + if (!namespace) throw new CredoError(`Cannot get indy namespace of identifier '${identifier}'`) + return namespace +} + +export function getUnQualifiedDidIndyDid(identifier: string): string { + if (isUnqualifiedIndyDid(identifier)) return identifier + + if (isDidIndySchemaId(identifier)) { + const { schemaName, schemaVersion, namespaceIdentifier } = parseIndySchemaId(identifier) + return getUnqualifiedSchemaId(namespaceIdentifier, schemaName, schemaVersion) + } else if (isDidIndyCredentialDefinitionId(identifier)) { + const { schemaSeqNo, tag, namespaceIdentifier } = parseIndyCredentialDefinitionId(identifier) + return getUnqualifiedCredentialDefinitionId(namespaceIdentifier, schemaSeqNo, tag) + } else if (isDidIndyRevocationRegistryId(identifier)) { + const { namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag } = + parseIndyRevocationRegistryId(identifier) + return getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + } + + const { namespaceIdentifier } = parseIndyDid(identifier) + return namespaceIdentifier +} + +export function isIndyDid(identifier: string): boolean { + return identifier.startsWith('did:indy:') +} + +export function getQualifiedDidIndyDid(identifier: string, namespace: string): string { + if (isIndyDid(identifier)) return identifier + + if (!namespace || typeof namespace !== 'string') { + throw new CredoError('Missing required indy namespace') + } + + if (isUnqualifiedSchemaId(identifier)) { + const { namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(identifier) + const schemaId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/SCHEMA/${schemaName}/${schemaVersion}` + return schemaId + } else if (isUnqualifiedCredentialDefinitionId(identifier)) { + const { namespaceIdentifier, schemaSeqNo, tag } = parseIndyCredentialDefinitionId(identifier) + const credentialDefinitionId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/${tag}` + return credentialDefinitionId + } else if (isUnqualifiedRevocationRegistryId(identifier)) { + const { namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag } = + parseIndyRevocationRegistryId(identifier) + const revocationRegistryId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/REV_REG_DEF/${schemaSeqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` + return revocationRegistryId + } else if (isUnqualifiedIndyDid(identifier)) { + return `did:indy:${namespace}:${identifier}` + } else { + throw new CredoError(`Cannot created qualified indy identifier for '${identifier}' with namespace '${namespace}'`) + } +} + +// -- schema -- // + +export function isUnqualifiedDidIndySchema(schema: AnonCredsSchema) { + return isUnqualifiedIndyDid(schema.issuerId) +} + +export function getUnqualifiedDidIndySchema(schema: AnonCredsSchema): AnonCredsSchema { + if (isUnqualifiedDidIndySchema(schema)) return { ...schema } + if (!isIndyDid(schema.issuerId)) { + throw new CredoError(`IssuerId '${schema.issuerId}' is not a valid qualified did-indy did.`) + } + + const issuerId = getUnQualifiedDidIndyDid(schema.issuerId) + return { ...schema, issuerId } +} + +export function isQualifiedDidIndySchema(schema: AnonCredsSchema) { + return !isUnqualifiedIndyDid(schema.issuerId) +} + +export function getQualifiedDidIndySchema(schema: AnonCredsSchema, namespace: string): AnonCredsSchema { + if (isQualifiedDidIndySchema(schema)) return { ...schema } + + return { + ...schema, + issuerId: getQualifiedDidIndyDid(schema.issuerId, namespace), + } +} + +// -- credential definition -- // + +export function isUnqualifiedDidIndyCredentialDefinition(anonCredsCredentialDefinition: AnonCredsCredentialDefinition) { + return ( + isUnqualifiedIndyDid(anonCredsCredentialDefinition.issuerId) && + isUnqualifiedSchemaId(anonCredsCredentialDefinition.schemaId) + ) +} + +export function getUnqualifiedDidIndyCredentialDefinition( + anonCredsCredentialDefinition: AnonCredsCredentialDefinition +): AnonCredsCredentialDefinition { + if (isUnqualifiedDidIndyCredentialDefinition(anonCredsCredentialDefinition)) { + return { ...anonCredsCredentialDefinition } + } + + const issuerId = getUnQualifiedDidIndyDid(anonCredsCredentialDefinition.issuerId) + const schemaId = getUnQualifiedDidIndyDid(anonCredsCredentialDefinition.schemaId) + + return { ...anonCredsCredentialDefinition, issuerId, schemaId } +} + +export function isQualifiedDidIndyCredentialDefinition(anonCredsCredentialDefinition: AnonCredsCredentialDefinition) { + return ( + !isUnqualifiedIndyDid(anonCredsCredentialDefinition.issuerId) && + !isUnqualifiedSchemaId(anonCredsCredentialDefinition.schemaId) + ) +} + +export function getQualifiedDidIndyCredentialDefinition( + anonCredsCredentialDefinition: AnonCredsCredentialDefinition, + namespace: string +): AnonCredsCredentialDefinition { + if (isQualifiedDidIndyCredentialDefinition(anonCredsCredentialDefinition)) return { ...anonCredsCredentialDefinition } + + return { + ...anonCredsCredentialDefinition, + issuerId: getQualifiedDidIndyDid(anonCredsCredentialDefinition.issuerId, namespace), + schemaId: getQualifiedDidIndyDid(anonCredsCredentialDefinition.schemaId, namespace), + } +} + +// -- revocation registry definition -- // + +export function isUnqualifiedDidIndyRevocationRegistryDefinition( + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition +) { + return ( + isUnqualifiedIndyDid(revocationRegistryDefinition.issuerId) && + isUnqualifiedCredentialDefinitionId(revocationRegistryDefinition.credDefId) + ) +} + +export function getUnqualifiedDidIndyRevocationRegistryDefinition( + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition +): AnonCredsRevocationRegistryDefinition { + if (isUnqualifiedDidIndyRevocationRegistryDefinition(revocationRegistryDefinition)) { + return { ...revocationRegistryDefinition } + } + + const issuerId = getUnQualifiedDidIndyDid(revocationRegistryDefinition.issuerId) + const credDefId = getUnQualifiedDidIndyDid(revocationRegistryDefinition.credDefId) + + return { ...revocationRegistryDefinition, issuerId, credDefId } +} + +export function isQualifiedRevocationRegistryDefinition( + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition +) { + return ( + !isUnqualifiedIndyDid(revocationRegistryDefinition.issuerId) && + !isUnqualifiedCredentialDefinitionId(revocationRegistryDefinition.credDefId) + ) +} + +export function getQualifiedDidIndyRevocationRegistryDefinition( + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition, + namespace: string +): AnonCredsRevocationRegistryDefinition { + if (isQualifiedRevocationRegistryDefinition(revocationRegistryDefinition)) return { ...revocationRegistryDefinition } + + return { + ...revocationRegistryDefinition, + issuerId: getQualifiedDidIndyDid(revocationRegistryDefinition.issuerId, namespace), + credDefId: getQualifiedDidIndyDid(revocationRegistryDefinition.credDefId, namespace), + } +} diff --git a/packages/anoncreds/src/utils/isMap.ts b/packages/anoncreds/src/utils/isMap.ts new file mode 100644 index 0000000000..1ee81fe4a4 --- /dev/null +++ b/packages/anoncreds/src/utils/isMap.ts @@ -0,0 +1,19 @@ +import type { ValidationOptions } from 'class-validator' + +import { ValidateBy, buildMessage } from 'class-validator' + +/** + * Checks if a given value is a Map + */ +export function IsMap(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'isMap', + validator: { + validate: (value: unknown): boolean => value instanceof Map, + defaultMessage: buildMessage((eachPrefix) => eachPrefix + '$property must be a Map', validationOptions), + }, + }, + validationOptions + ) +} diff --git a/packages/anoncreds/src/utils/linkSecret.ts b/packages/anoncreds/src/utils/linkSecret.ts new file mode 100644 index 0000000000..8a4d87ad63 --- /dev/null +++ b/packages/anoncreds/src/utils/linkSecret.ts @@ -0,0 +1,53 @@ +import type { AgentContext } from '@credo-ts/core' + +import { AnonCredsRsError } from '../error/AnonCredsRsError' +import { AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository } from '../repository' + +export async function storeLinkSecret( + agentContext: AgentContext, + options: { linkSecretId: string; linkSecretValue?: string; setAsDefault?: boolean } +) { + const { linkSecretId, linkSecretValue, setAsDefault } = options + const linkSecretRepository = agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository) + + // In some cases we don't have the linkSecretValue. However we still want a record so we know which link secret ids are valid + const linkSecretRecord = new AnonCredsLinkSecretRecord({ linkSecretId, value: linkSecretValue }) + + // If it is the first link secret registered, set as default + const defaultLinkSecretRecord = await linkSecretRepository.findDefault(agentContext) + if (!defaultLinkSecretRecord || setAsDefault) { + linkSecretRecord.setTag('isDefault', true) + } + + // Set the current default link secret as not default + if (defaultLinkSecretRecord && setAsDefault) { + defaultLinkSecretRecord.setTag('isDefault', false) + await linkSecretRepository.update(agentContext, defaultLinkSecretRecord) + } + + await linkSecretRepository.save(agentContext, linkSecretRecord) + + return linkSecretRecord +} + +export function assertLinkSecretsMatch(agentContext: AgentContext, linkSecretIds: string[]) { + // Get all requested credentials and take linkSecret. If it's not the same for every credential, throw error + const linkSecretsMatch = linkSecretIds.every((linkSecretId) => linkSecretId === linkSecretIds[0]) + if (!linkSecretsMatch) { + throw new AnonCredsRsError('All credentials in a Proof should have been issued using the same Link Secret') + } + + return linkSecretIds[0] +} + +export async function getLinkSecret(agentContext: AgentContext, linkSecretId: string): Promise { + const linkSecretRecord = await agentContext.dependencyManager + .resolve(AnonCredsLinkSecretRepository) + .getByLinkSecretId(agentContext, linkSecretId) + + if (!linkSecretRecord.value) { + throw new AnonCredsRsError('Link Secret value not stored') + } + + return linkSecretRecord.value +} diff --git a/packages/anoncreds/src/utils/metadata.ts b/packages/anoncreds/src/utils/metadata.ts new file mode 100644 index 0000000000..c5e2276906 --- /dev/null +++ b/packages/anoncreds/src/utils/metadata.ts @@ -0,0 +1,44 @@ +import type { AnonCredsLinkSecretBlindingData } from '../models' + +export interface AnonCredsCredentialMetadata { + schemaId?: string + credentialDefinitionId?: string + revocationRegistryId?: string + credentialRevocationId?: string +} + +export interface AnonCredsCredentialRequestMetadata { + link_secret_blinding_data: AnonCredsLinkSecretBlindingData + link_secret_name: string + nonce: string +} + +export interface W3cAnonCredsCredentialMetadata { + methodName: string + credentialRevocationId?: string + linkSecretId: string +} + +// TODO: we may want to already support multiple credentials in the metadata of a credential +// record, as that's what the RFCs support. We already need to write a migration script for modules + +/** + * Metadata key for strong metadata on an AnonCreds credential. + * + * MUST be used with {@link AnonCredsCredentialMetadata} + */ +export const AnonCredsCredentialMetadataKey = '_anoncreds/credential' + +/** + * Metadata key for storing metadata on an AnonCreds credential request. + * + * MUST be used with {@link AnonCredsCredentialRequestMetadata} + */ +export const AnonCredsCredentialRequestMetadataKey = '_anoncreds/credentialRequest' + +/** + * Metadata key for storing the W3C AnonCreds credential metadata. + * + * MUST be used with {@link W3cAnonCredsCredentialMetadata} + */ +export const W3cAnonCredsCredentialMetadataKey = '_w3c/anonCredsMetadata' diff --git a/packages/anoncreds/src/utils/proofRequest.ts b/packages/anoncreds/src/utils/proofRequest.ts new file mode 100644 index 0000000000..982b49ba0e --- /dev/null +++ b/packages/anoncreds/src/utils/proofRequest.ts @@ -0,0 +1,27 @@ +import type { AnonCredsProofRequest } from '../models/exchange' + +import { + isUnqualifiedCredentialDefinitionId, + isUnqualifiedSchemaId, + isUnqualifiedIndyDid, + isUnqualifiedRevocationRegistryId, +} from './indyIdentifiers' + +export function proofRequestUsesUnqualifiedIdentifiers(proofRequest: AnonCredsProofRequest) { + // We assume that if any identifier is unqualified, all of them are unqualified as well + return [ + ...Object.values(proofRequest.requested_attributes), + ...Object.values(proofRequest.requested_predicates), + ].some((attribute) => + attribute.restrictions?.some( + (restriction) => + (restriction.cred_def_id && isUnqualifiedCredentialDefinitionId(restriction.cred_def_id)) || + (restriction.schema_id && isUnqualifiedSchemaId(restriction.schema_id)) || + (restriction.issuer_did && isUnqualifiedIndyDid(restriction.issuer_did)) || + (restriction.issuer_id && isUnqualifiedIndyDid(restriction.issuer_id)) || + (restriction.schema_issuer_did && isUnqualifiedIndyDid(restriction.schema_issuer_did)) || + (restriction.schema_issuer_id && isUnqualifiedIndyDid(restriction.schema_issuer_id)) || + (restriction.rev_reg_id && isUnqualifiedRevocationRegistryId(restriction.rev_reg_id)) + ) + ) +} diff --git a/packages/anoncreds/src/utils/proverDid.ts b/packages/anoncreds/src/utils/proverDid.ts new file mode 100644 index 0000000000..a5d852d5b1 --- /dev/null +++ b/packages/anoncreds/src/utils/proverDid.ts @@ -0,0 +1,12 @@ +import { TypedArrayEncoder, utils } from '@credo-ts/core' + +/** + * generates a string that adheres to the format of a legacy indy did. + * + * This can be used for the `prover_did` property that is required in the legacy anoncreds credential + * request. This doesn't actually have to be a did, but some frameworks (like ACA-Py) require it to be + * an unqualified indy did. + */ +export function generateLegacyProverDidLikeString() { + return TypedArrayEncoder.toBase58(TypedArrayEncoder.fromString(utils.uuid()).slice(0, 16)) +} diff --git a/packages/anoncreds/src/utils/revocationInterval.ts b/packages/anoncreds/src/utils/revocationInterval.ts new file mode 100644 index 0000000000..fdf1036157 --- /dev/null +++ b/packages/anoncreds/src/utils/revocationInterval.ts @@ -0,0 +1,25 @@ +import type { AnonCredsNonRevokedInterval } from '../models' + +import { CredoError } from '@credo-ts/core' + +// This sets the `to` value to be required. We do this check in the `assertBestPracticeRevocationInterval` method, +// and it makes it easier to work with the object in TS +interface BestPracticeNonRevokedInterval { + from?: number + to: number +} + +// Check revocation interval in accordance with https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0441-present-proof-best-practices/README.md#semantics-of-non-revocation-interval-endpoints +export function assertBestPracticeRevocationInterval( + revocationInterval: AnonCredsNonRevokedInterval +): asserts revocationInterval is BestPracticeNonRevokedInterval { + if (!revocationInterval.to) { + throw new CredoError(`Presentation requests proof of non-revocation with no 'to' value specified`) + } + + if ((revocationInterval.from || revocationInterval.from === 0) && revocationInterval.to !== revocationInterval.from) { + throw new CredoError( + `Presentation requests proof of non-revocation with an interval from: '${revocationInterval.from}' that does not match the interval to: '${revocationInterval.to}', as specified in Aries RFC 0441` + ) + } +} diff --git a/packages/anoncreds/src/utils/sortRequestedCredentialsMatches.ts b/packages/anoncreds/src/utils/sortRequestedCredentialsMatches.ts new file mode 100644 index 0000000000..e7f2d7a2f9 --- /dev/null +++ b/packages/anoncreds/src/utils/sortRequestedCredentialsMatches.ts @@ -0,0 +1,33 @@ +import type { AnonCredsRequestedAttributeMatch, AnonCredsRequestedPredicateMatch } from '../models' + +/** + * Sort requested attributes and predicates by `revoked` status. The order is: + * - first credentials with `revoked` set to undefined, this means no revocation status is needed for the credentials + * - then credentials with `revoked` set to false, this means the credentials are not revoked + * - then credentials with `revoked` set to true, this means the credentials are revoked + */ +export function sortRequestedCredentialsMatches< + Requested extends Array | Array +>(credentials: Requested) { + const credentialGoUp = -1 + const credentialGoDown = 1 + + // Clone as sort is in place + const credentialsClone = [...credentials] + + return credentialsClone.sort((credential, compareTo) => { + // Nothing needs to happen if values are the same + if (credential.revoked === compareTo.revoked) + return compareTo.credentialInfo.updatedAt.getTime() - credential.credentialInfo.updatedAt.getTime() + + // Undefined always is at the top + if (credential.revoked === undefined) return credentialGoUp + if (compareTo.revoked === undefined) return credentialGoDown + + // Then revoked + if (credential.revoked === false) return credentialGoUp + + // It means that compareTo is false and credential is true + return credentialGoDown + }) +} diff --git a/packages/anoncreds/src/utils/timestamp.ts b/packages/anoncreds/src/utils/timestamp.ts new file mode 100644 index 0000000000..9386fe68e3 --- /dev/null +++ b/packages/anoncreds/src/utils/timestamp.ts @@ -0,0 +1,2 @@ +// Timestamps are expressed as Unix epoch time (seconds since 1/1/1970) +export const dateToTimestamp = (date: Date) => Math.floor(date.getTime() / 1000) diff --git a/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts b/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts new file mode 100644 index 0000000000..de383a4ed1 --- /dev/null +++ b/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts @@ -0,0 +1,251 @@ +import type { AnonCredsClaimRecord } from './credential' +import type { W3cAnonCredsCredentialMetadata } from './metadata' +import type { AnonCredsCredentialInfo, AnonCredsSchema } from '../models' +import type { AnonCredsCredentialRecord } from '../repository' +import type { StoreCredentialOptions } from '../services' +import type { DefaultW3cCredentialTags, W3cCredentialSubject } from '@credo-ts/core' + +import { CredoError, W3cCredentialRecord, utils } from '@credo-ts/core' + +import { mapAttributeRawValuesToAnonCredsCredentialValues } from './credential' +import { + getQualifiedDidIndyCredentialDefinition, + getQualifiedDidIndyDid, + getQualifiedDidIndyRevocationRegistryDefinition, + getQualifiedDidIndySchema, + isUnqualifiedDidIndyCredentialDefinition, + isUnqualifiedDidIndyRevocationRegistryDefinition, + isUnqualifiedDidIndySchema, + isUnqualifiedCredentialDefinitionId, + isUnqualifiedRevocationRegistryId, + isIndyDid, + getUnQualifiedDidIndyDid, + isUnqualifiedIndyDid, +} from './indyIdentifiers' +import { W3cAnonCredsCredentialMetadataKey } from './metadata' + +export type AnonCredsCredentialTags = { + anonCredsLinkSecretId: string + anonCredsCredentialRevocationId?: string + anonCredsMethodName: string + + // the following keys can be used for every `attribute name` in credential. + [key: `anonCredsAttr::${string}::marker`]: true | undefined + [key: `anonCredsAttr::${string}::value`]: string | undefined + + anonCredsSchemaName: string + anonCredsSchemaVersion: string + + anonCredsSchemaId: string + anonCredsSchemaIssuerId: string + anonCredsCredentialDefinitionId: string + anonCredsRevocationRegistryId?: string + + anonCredsUnqualifiedIssuerId?: string + anonCredsUnqualifiedSchemaId?: string + anonCredsUnqualifiedSchemaIssuerId?: string + anonCredsUnqualifiedCredentialDefinitionId?: string + anonCredsUnqualifiedRevocationRegistryId?: string +} + +function anonCredsCredentialInfoFromW3cRecord( + w3cCredentialRecord: W3cCredentialRecord, + useUnqualifiedIdentifiers?: boolean +): AnonCredsCredentialInfo { + if (Array.isArray(w3cCredentialRecord.credential.credentialSubject)) { + throw new CredoError('Credential subject must be an object, not an array.') + } + + const anonCredsTags = getAnonCredsTagsFromRecord(w3cCredentialRecord) + if (!anonCredsTags) throw new CredoError('AnonCreds tags not found on credential record.') + + const anonCredsCredentialMetadata = w3cCredentialRecord.metadata.get( + W3cAnonCredsCredentialMetadataKey + ) + if (!anonCredsCredentialMetadata) throw new CredoError('AnonCreds metadata not found on credential record.') + + const credentialDefinitionId = + useUnqualifiedIdentifiers && anonCredsTags.anonCredsUnqualifiedCredentialDefinitionId + ? anonCredsTags.anonCredsUnqualifiedCredentialDefinitionId + : anonCredsTags.anonCredsCredentialDefinitionId + + const schemaId = + useUnqualifiedIdentifiers && anonCredsTags.anonCredsUnqualifiedSchemaId + ? anonCredsTags.anonCredsUnqualifiedSchemaId + : anonCredsTags.anonCredsSchemaId + + const revocationRegistryId = + useUnqualifiedIdentifiers && anonCredsTags.anonCredsUnqualifiedRevocationRegistryId + ? anonCredsTags.anonCredsUnqualifiedRevocationRegistryId + : anonCredsTags.anonCredsRevocationRegistryId ?? null + + return { + attributes: (w3cCredentialRecord.credential.credentialSubject.claims as AnonCredsClaimRecord) ?? {}, + credentialId: w3cCredentialRecord.id, + credentialDefinitionId, + schemaId, + revocationRegistryId, + credentialRevocationId: anonCredsCredentialMetadata.credentialRevocationId ?? null, + methodName: anonCredsCredentialMetadata.methodName, + linkSecretId: anonCredsCredentialMetadata.linkSecretId, + createdAt: w3cCredentialRecord.createdAt, + updatedAt: w3cCredentialRecord.updatedAt ?? w3cCredentialRecord.createdAt, + } +} + +function anonCredsCredentialInfoFromAnonCredsRecord( + anonCredsCredentialRecord: AnonCredsCredentialRecord +): AnonCredsCredentialInfo { + const attributes: { [key: string]: string } = {} + for (const attribute in anonCredsCredentialRecord.credential) { + attributes[attribute] = anonCredsCredentialRecord.credential.values[attribute].raw + } + + return { + attributes, + credentialDefinitionId: anonCredsCredentialRecord.credential.cred_def_id, + credentialId: anonCredsCredentialRecord.credentialId, + schemaId: anonCredsCredentialRecord.credential.schema_id, + credentialRevocationId: anonCredsCredentialRecord.credentialRevocationId ?? null, + revocationRegistryId: anonCredsCredentialRecord.credential.rev_reg_id ?? null, + methodName: anonCredsCredentialRecord.methodName, + linkSecretId: anonCredsCredentialRecord.linkSecretId, + createdAt: anonCredsCredentialRecord.createdAt, + updatedAt: anonCredsCredentialRecord.updatedAt ?? anonCredsCredentialRecord.createdAt, + } +} + +export function getAnoncredsCredentialInfoFromRecord( + credentialRecord: W3cCredentialRecord | AnonCredsCredentialRecord, + useUnqualifiedIdentifiersIfPresent?: boolean +): AnonCredsCredentialInfo { + if (credentialRecord instanceof W3cCredentialRecord) { + return anonCredsCredentialInfoFromW3cRecord(credentialRecord, useUnqualifiedIdentifiersIfPresent) + } else { + return anonCredsCredentialInfoFromAnonCredsRecord(credentialRecord) + } +} +export function getAnonCredsTagsFromRecord(record: W3cCredentialRecord) { + const anoncredsMetadata = record.metadata.get(W3cAnonCredsCredentialMetadataKey) + if (!anoncredsMetadata) return undefined + + const tags = record.getTags() as DefaultW3cCredentialTags & Partial + if ( + !tags.anonCredsLinkSecretId || + !tags.anonCredsMethodName || + !tags.anonCredsSchemaId || + !tags.anonCredsSchemaName || + !tags.anonCredsSchemaVersion || + !tags.anonCredsSchemaIssuerId || + !tags.anonCredsCredentialDefinitionId + ) { + return undefined + } + + return Object.fromEntries( + Object.entries(tags).filter(([key]) => key.startsWith('anonCreds')) + ) as AnonCredsCredentialTags +} + +export function getStoreCredentialOptions( + options: StoreCredentialOptions, + indyNamespace?: string +): StoreCredentialOptions { + const { + credentialRequestMetadata, + credentialDefinitionId, + schema, + credential, + credentialDefinition, + revocationRegistry, + } = options + + const storeCredentialOptions = { + credentialId: utils.uuid(), + credentialRequestMetadata, + credential, + credentialDefinitionId: isUnqualifiedCredentialDefinitionId(credentialDefinitionId) + ? getQualifiedDidIndyDid(credentialDefinitionId, indyNamespace as string) + : credentialDefinitionId, + credentialDefinition: isUnqualifiedDidIndyCredentialDefinition(credentialDefinition) + ? getQualifiedDidIndyCredentialDefinition(credentialDefinition, indyNamespace as string) + : credentialDefinition, + schema: isUnqualifiedDidIndySchema(schema) ? getQualifiedDidIndySchema(schema, indyNamespace as string) : schema, + revocationRegistry: revocationRegistry?.definition + ? { + definition: isUnqualifiedDidIndyRevocationRegistryDefinition(revocationRegistry.definition) + ? getQualifiedDidIndyRevocationRegistryDefinition(revocationRegistry.definition, indyNamespace as string) + : revocationRegistry.definition, + id: isUnqualifiedRevocationRegistryId(revocationRegistry.id) + ? getQualifiedDidIndyDid(revocationRegistry.id, indyNamespace as string) + : revocationRegistry.id, + } + : undefined, + } + + return storeCredentialOptions +} + +// The issuer of the schema does not always match the issuer of the credential definition thus the unqualified schema id needs to be derived from both values +function getUnqualifiedSchemaId(schemaIssuerId: string, schemaId: string) { + const schemaDid = schemaIssuerId.split(':')[3] + const split = getUnQualifiedDidIndyDid(schemaId).split(':') + split[0] = schemaDid + return split.join(':') +} + +export function getW3cRecordAnonCredsTags(options: { + credentialSubject: W3cCredentialSubject + issuerId: string + schemaId: string + schema: Omit + credentialDefinitionId: string + revocationRegistryId?: string + credentialRevocationId?: string + linkSecretId: string + methodName: string +}) { + const { + credentialSubject, + issuerId, + schema, + schemaId, + credentialDefinitionId, + revocationRegistryId, + credentialRevocationId, + linkSecretId, + methodName, + } = options + + const anonCredsCredentialRecordTags: AnonCredsCredentialTags = { + anonCredsLinkSecretId: linkSecretId, + anonCredsCredentialDefinitionId: credentialDefinitionId, + anonCredsSchemaId: schemaId, + anonCredsSchemaName: schema.name, + anonCredsSchemaIssuerId: schema.issuerId, + anonCredsSchemaVersion: schema.version, + anonCredsMethodName: methodName, + anonCredsRevocationRegistryId: revocationRegistryId, + anonCredsCredentialRevocationId: credentialRevocationId, + ...((isIndyDid(issuerId) || isUnqualifiedIndyDid(issuerId)) && { + anonCredsUnqualifiedIssuerId: getUnQualifiedDidIndyDid(issuerId), + anonCredsUnqualifiedCredentialDefinitionId: getUnQualifiedDidIndyDid(credentialDefinitionId), + anonCredsUnqualifiedSchemaId: getUnqualifiedSchemaId(schema.issuerId, schemaId), + anonCredsUnqualifiedSchemaIssuerId: getUnQualifiedDidIndyDid(schema.issuerId), + anonCredsUnqualifiedRevocationRegistryId: revocationRegistryId + ? getUnQualifiedDidIndyDid(revocationRegistryId) + : undefined, + }), + } + + const values = mapAttributeRawValuesToAnonCredsCredentialValues( + (credentialSubject.claims as AnonCredsClaimRecord) ?? {} + ) + + for (const [key, value] of Object.entries(values)) { + anonCredsCredentialRecordTags[`anonCredsAttr::${key}::value`] = value.raw + anonCredsCredentialRecordTags[`anonCredsAttr::${key}::marker`] = true + } + + return anonCredsCredentialRecordTags +} diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts new file mode 100644 index 0000000000..fa397be680 --- /dev/null +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -0,0 +1,383 @@ +import type { + AnonCredsCredentialDefinition, + AnonCredsRegistry, + AnonCredsRevocationRegistryDefinition, + AnonCredsRevocationStatusList, + AnonCredsSchema, + GetCredentialDefinitionReturn, + GetRevocationRegistryDefinitionReturn, + GetRevocationStatusListReturn, + GetSchemaReturn, + RegisterCredentialDefinitionOptions, + RegisterCredentialDefinitionReturn, + RegisterRevocationRegistryDefinitionOptions, + RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationStatusListOptions, + RegisterRevocationStatusListReturn, + RegisterSchemaOptions, + RegisterSchemaReturn, +} from '../src' +import type { AgentContext } from '@credo-ts/core' + +import { Hasher, utils } from '@credo-ts/core' +import BigNumber from 'bn.js' + +import { + getDidIndyCredentialDefinitionId, + getDidIndyRevocationRegistryDefinitionId, + getDidIndySchemaId, +} from '../../indy-vdr/src/anoncreds/utils/identifiers' +import { + getUnQualifiedDidIndyDid, + getUnqualifiedRevocationRegistryDefinitionId, + getUnqualifiedCredentialDefinitionId, + getUnqualifiedSchemaId, + parseIndyDid, + getUnqualifiedDidIndySchema, + parseIndyCredentialDefinitionId, + parseIndyRevocationRegistryId, + parseIndySchemaId, + isIndyDid, + isUnqualifiedCredentialDefinitionId, + isUnqualifiedSchemaId, +} from '../src/utils/indyIdentifiers' +import { dateToTimestamp } from '../src/utils/timestamp' + +/** + * In memory implementation of the {@link AnonCredsRegistry} interface. Useful for testing. + */ +export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { + public readonly methodName = 'inMemory' + + public readonly supportedIdentifier = /.+/ + + private schemas: Record + private credentialDefinitions: Record + private revocationRegistryDefinitions: Record + private revocationStatusLists: Record> + + public constructor({ + existingSchemas = {}, + existingCredentialDefinitions = {}, + existingRevocationRegistryDefinitions = {}, + existingRevocationStatusLists = {}, + }: { + existingSchemas?: Record + existingCredentialDefinitions?: Record + existingRevocationRegistryDefinitions?: Record + existingRevocationStatusLists?: Record> + } = {}) { + this.schemas = existingSchemas + this.credentialDefinitions = existingCredentialDefinitions + this.revocationRegistryDefinitions = existingRevocationRegistryDefinitions + this.revocationStatusLists = existingRevocationStatusLists + } + + public async getSchema(_agentContext: AgentContext, schemaId: string): Promise { + const schema = this.schemas[schemaId] + + if (!schema) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Schema with id ${schemaId} not found in memory registry`, + }, + schemaId, + schemaMetadata: {}, + } + } + + let didIndyNamespace: string | undefined = undefined + if (isUnqualifiedSchemaId(schemaId)) { + const { namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(schemaId) + const qualifiedSchemaEnding = `${namespaceIdentifier}/anoncreds/v0/SCHEMA/${schemaName}/${schemaVersion}` + const qualifiedSchemaId = Object.keys(this.schemas).find((schemaId) => schemaId.endsWith(qualifiedSchemaEnding)) + didIndyNamespace = qualifiedSchemaId ? parseIndySchemaId(qualifiedSchemaId).namespace : undefined + } else if (isIndyDid(schemaId)) { + didIndyNamespace = parseIndySchemaId(schemaId).namespace + } + + return { + resolutionMetadata: {}, + schema, + schemaId, + schemaMetadata: { ...(didIndyNamespace && { didIndyNamespace }) }, + } + } + + public async registerSchema( + _agentContext: AgentContext, + options: RegisterSchemaOptions + ): Promise { + const issuerId = options.schema.issuerId + + let schemaId: string + if (isIndyDid(issuerId)) { + const { namespace, namespaceIdentifier } = parseIndyDid(issuerId) + schemaId = getDidIndySchemaId(namespace, namespaceIdentifier, options.schema.name, options.schema.version) + this.schemas[getUnQualifiedDidIndyDid(schemaId)] = getUnqualifiedDidIndySchema(options.schema) + } else if (issuerId.startsWith('did:cheqd:')) { + schemaId = issuerId + '/resources/' + utils.uuid() + } else { + throw new Error(`Cannot register Schema. Unsupported issuerId '${issuerId}'`) + } + + this.schemas[schemaId] = options.schema + + return { + registrationMetadata: {}, + schemaMetadata: {}, + schemaState: { + state: 'finished', + schema: options.schema, + schemaId: schemaId, + }, + } + } + + public async getCredentialDefinition( + _agentContext: AgentContext, + credentialDefinitionId: string + ): Promise { + const credentialDefinition = this.credentialDefinitions[credentialDefinitionId] + + if (!credentialDefinition) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Credential definition with id ${credentialDefinitionId} not found in memory registry`, + }, + credentialDefinitionId, + credentialDefinitionMetadata: {}, + } + } + + let didIndyNamespace: string | undefined = undefined + if (isUnqualifiedCredentialDefinitionId(credentialDefinitionId)) { + const { namespaceIdentifier, schemaSeqNo, tag } = parseIndyCredentialDefinitionId(credentialDefinitionId) + const qualifiedCredDefEnding = `${namespaceIdentifier}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/${tag}` + const unqualifiedCredDefId = Object.keys(this.credentialDefinitions).find((credentialDefinitionId) => + credentialDefinitionId.endsWith(qualifiedCredDefEnding) + ) + didIndyNamespace = unqualifiedCredDefId + ? parseIndyCredentialDefinitionId(unqualifiedCredDefId).namespace + : undefined + } else if (isIndyDid(credentialDefinitionId)) { + didIndyNamespace = parseIndyCredentialDefinitionId(credentialDefinitionId).namespace + } + + return { + resolutionMetadata: {}, + credentialDefinition, + credentialDefinitionId, + credentialDefinitionMetadata: { ...(didIndyNamespace && { didIndyNamespace }) }, + } + } + + public async registerCredentialDefinition( + _agentContext: AgentContext, + options: RegisterCredentialDefinitionOptions + ): Promise { + const schemaId = options.credentialDefinition.schemaId + + let credentialDefinitionId: string + if (isIndyDid(options.credentialDefinition.issuerId)) { + const parsedSchema = parseIndySchemaId(options.credentialDefinition.schemaId) + const legacySchemaId = getUnqualifiedSchemaId( + parsedSchema.namespaceIdentifier, + parsedSchema.schemaName, + parsedSchema.schemaVersion + ) + const indyLedgerSeqNo = getSeqNoFromSchemaId(legacySchemaId) + + const { namespace, namespaceIdentifier: legacyIssuerId } = parseIndyDid(options.credentialDefinition.issuerId) + const didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId( + namespace, + legacyIssuerId, + indyLedgerSeqNo, + options.credentialDefinition.tag + ) + + this.credentialDefinitions[getUnQualifiedDidIndyDid(didIndyCredentialDefinitionId)] = { + ...options.credentialDefinition, + issuerId: legacyIssuerId, + schemaId: legacySchemaId, + } + credentialDefinitionId = didIndyCredentialDefinitionId + } else if (schemaId.startsWith('did:cheqd:')) { + credentialDefinitionId = options.credentialDefinition.issuerId + '/resources/' + utils.uuid() + } else { + throw new Error(`Cannot register Credential Definition. Unsupported schemaId '${schemaId}'`) + } + + this.credentialDefinitions[credentialDefinitionId] = options.credentialDefinition + return { + registrationMetadata: {}, + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + state: 'finished', + credentialDefinition: options.credentialDefinition, + credentialDefinitionId, + }, + } + } + + public async getRevocationRegistryDefinition( + _agentContext: AgentContext, + revocationRegistryDefinitionId: string + ): Promise { + const revocationRegistryDefinition = this.revocationRegistryDefinitions[revocationRegistryDefinitionId] + + if (!revocationRegistryDefinition) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Revocation registry definition with id ${revocationRegistryDefinition} not found in memory registry`, + }, + revocationRegistryDefinitionId, + revocationRegistryDefinitionMetadata: {}, + } + } + + let didIndyNamespace: string | undefined = undefined + if (isUnqualifiedCredentialDefinitionId(revocationRegistryDefinitionId)) { + const { namespaceIdentifier, schemaSeqNo, revocationRegistryTag } = + parseIndyRevocationRegistryId(revocationRegistryDefinitionId) + const qualifiedRevRegIdEnding = `:${namespaceIdentifier}/anoncreds/v0/REV_REG_DEF/${schemaSeqNo}/${revocationRegistryTag}` + const unqualifiedRevRegId = Object.keys(this.revocationRegistryDefinitions).find((revocationRegistryId) => + revocationRegistryId.endsWith(qualifiedRevRegIdEnding) + ) + didIndyNamespace = unqualifiedRevRegId ? parseIndySchemaId(unqualifiedRevRegId).namespace : undefined + } else if (isIndyDid(revocationRegistryDefinitionId)) { + didIndyNamespace = parseIndyRevocationRegistryId(revocationRegistryDefinitionId).namespace + } + + return { + resolutionMetadata: {}, + revocationRegistryDefinition, + revocationRegistryDefinitionId, + revocationRegistryDefinitionMetadata: { ...(didIndyNamespace && { didIndyNamespace }) }, + } + } + + public async registerRevocationRegistryDefinition( + _agentContext: AgentContext, + options: RegisterRevocationRegistryDefinitionOptions + ): Promise { + const parsedCredentialDefinition = parseIndyCredentialDefinitionId(options.revocationRegistryDefinition.credDefId) + const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId( + parsedCredentialDefinition.namespaceIdentifier, + parsedCredentialDefinition.schemaSeqNo, + parsedCredentialDefinition.tag + ) + const indyLedgerSeqNo = getSeqNoFromSchemaId(legacyCredentialDefinitionId) + + const { namespace, namespaceIdentifier } = parseIndyDid(options.revocationRegistryDefinition.issuerId) + const legacyIssuerId = namespaceIdentifier + const didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryDefinitionId( + namespace, + namespaceIdentifier, + indyLedgerSeqNo, + parsedCredentialDefinition.tag, + options.revocationRegistryDefinition.tag + ) + + this.revocationRegistryDefinitions[didIndyRevocationRegistryDefinitionId] = options.revocationRegistryDefinition + + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + legacyIssuerId, + indyLedgerSeqNo, + parsedCredentialDefinition.tag, + options.revocationRegistryDefinition.tag + ) + + this.revocationRegistryDefinitions[legacyRevocationRegistryDefinitionId] = { + ...options.revocationRegistryDefinition, + issuerId: legacyIssuerId, + credDefId: legacyCredentialDefinitionId, + } + + return { + registrationMetadata: {}, + revocationRegistryDefinitionMetadata: {}, + revocationRegistryDefinitionState: { + state: 'finished', + revocationRegistryDefinition: options.revocationRegistryDefinition, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + }, + } + } + + public async getRevocationStatusList( + _agentContext: AgentContext, + revocationRegistryId: string, + timestamp: number + ): Promise { + const revocationStatusLists = this.revocationStatusLists[revocationRegistryId] + + if (!revocationStatusLists || Object.entries(revocationStatusLists).length === 0) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Revocation status list for revocation registry with id ${revocationRegistryId} not found in memory registry`, + }, + revocationStatusListMetadata: {}, + } + } + + const previousTimestamps = Object.keys(revocationStatusLists) + .filter((ts) => Number(ts) <= timestamp) + .sort() + + if (!previousTimestamps) { + return { + resolutionMetadata: { + error: 'notFound', + message: `No active Revocation status list found at ${timestamp} for revocation registry with id ${revocationRegistryId}`, + }, + revocationStatusListMetadata: {}, + } + } + + return { + resolutionMetadata: {}, + revocationStatusList: revocationStatusLists[previousTimestamps[previousTimestamps.length - 1]], + revocationStatusListMetadata: {}, + } + } + + public async registerRevocationStatusList( + _agentContext: AgentContext, + options: RegisterRevocationStatusListOptions + ): Promise { + const timestamp = (options.options.timestamp as number) ?? dateToTimestamp(new Date()) + const revocationStatusList = { + ...options.revocationStatusList, + timestamp, + } satisfies AnonCredsRevocationStatusList + if (!this.revocationStatusLists[options.revocationStatusList.revRegDefId]) { + this.revocationStatusLists[options.revocationStatusList.revRegDefId] = {} + } + + this.revocationStatusLists[revocationStatusList.revRegDefId][timestamp.toString()] = revocationStatusList + return { + registrationMetadata: {}, + revocationStatusListMetadata: {}, + revocationStatusListState: { + state: 'finished', + revocationStatusList, + }, + } + } +} + +/** + * Calculates a consistent sequence number for a given schema id. + * + * Does this by hashing the schema id, transforming the hash to a number and taking the first 6 digits. + */ +function getSeqNoFromSchemaId(schemaId: string) { + const seqNo = Number(new BigNumber(Hasher.hash(schemaId, 'sha-256')).toString().slice(0, 5)) + + return seqNo +} diff --git a/packages/anoncreds/tests/InMemoryTailsFileService.ts b/packages/anoncreds/tests/InMemoryTailsFileService.ts new file mode 100644 index 0000000000..282416d882 --- /dev/null +++ b/packages/anoncreds/tests/InMemoryTailsFileService.ts @@ -0,0 +1,59 @@ +import type { AnonCredsRevocationRegistryDefinition } from '@credo-ts/anoncreds' +import type { AgentContext, FileSystem } from '@credo-ts/core' + +import { InjectionSymbols } from '@credo-ts/core' + +import { BasicTailsFileService } from '@credo-ts/anoncreds' + +export class InMemoryTailsFileService extends BasicTailsFileService { + private tailsFilePaths: Record = {} + + public async uploadTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ) { + this.tailsFilePaths[options.revocationRegistryDefinition.value.tailsHash] = + options.revocationRegistryDefinition.value.tailsLocation + + return { tailsFileUrl: options.revocationRegistryDefinition.value.tailsHash } + } + + public async getTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ) { + const { revocationRegistryDefinition } = options + const { tailsLocation, tailsHash } = revocationRegistryDefinition.value + + try { + agentContext.config.logger.debug( + `Checking to see if tails file for URL ${revocationRegistryDefinition.value.tailsLocation} has been stored in the FileSystem` + ) + + // hash is used as file identifier + const tailsExists = await this.tailsFileExists(agentContext, tailsHash) + const tailsFilePath = await this.getTailsFilePath(agentContext, tailsHash) + agentContext.config.logger.debug( + `Tails file for ${tailsLocation} ${tailsExists ? 'is stored' : 'is not stored'} at ${tailsFilePath}` + ) + + if (!tailsExists) { + agentContext.config.logger.debug(`Retrieving tails file from URL ${tailsLocation}`) + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + await fileSystem.downloadToFile(tailsLocation, tailsFilePath) + agentContext.config.logger.debug(`Saved tails file to FileSystem at path ${tailsFilePath}`) + } + + return { tailsFilePath } + } catch (error) { + agentContext.config.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { + error, + }) + throw error + } + } +} diff --git a/packages/anoncreds/tests/LocalDidResolver.ts b/packages/anoncreds/tests/LocalDidResolver.ts new file mode 100644 index 0000000000..aeb672aa79 --- /dev/null +++ b/packages/anoncreds/tests/LocalDidResolver.ts @@ -0,0 +1,31 @@ +import type { DidResolutionResult, DidResolver, AgentContext } from '@credo-ts/core' + +import { DidsApi } from '@credo-ts/core' + +export class LocalDidResolver implements DidResolver { + public readonly supportedMethods = ['sov', 'indy'] + public readonly allowsCaching = false + + public async resolve(agentContext: AgentContext, did: string): Promise { + const didDocumentMetadata = {} + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + + const didRecord = (await didsApi.getCreatedDids()).find((record) => record.did === did) + if (!didRecord) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}'`, + }, + } + } + return { + didDocument: didRecord.didDocument ?? null, + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } +} diff --git a/packages/anoncreds/tests/anoncreds-flow.test.ts b/packages/anoncreds/tests/anoncreds-flow.test.ts new file mode 100644 index 0000000000..10be999c06 --- /dev/null +++ b/packages/anoncreds/tests/anoncreds-flow.test.ts @@ -0,0 +1,474 @@ +import type { AnonCredsCredentialRequest } from '@credo-ts/anoncreds' +import type { DidRepository, Wallet } from '@credo-ts/core' + +import { + CredentialRole, + ProofRole, + InjectionSymbols, + ProofState, + ProofExchangeRecord, + CredentialExchangeRecord, + CredentialPreviewAttribute, + CredentialState, + DidResolverService, + DidsModuleConfig, + SignatureSuiteToken, + W3cCredentialsModuleConfig, +} from '@credo-ts/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' +import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' +import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { agentDependencies, getAgentConfig, getAgentContext, testLogger } from '../../core/tests' +import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from '../src/anoncreds-rs' + +import { InMemoryTailsFileService } from './InMemoryTailsFileService' +import { anoncreds } from './helpers' + +import { + AnonCredsCredentialDefinitionPrivateRecord, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsCredentialDefinitionRecord, + AnonCredsCredentialDefinitionRepository, + AnonCredsCredentialFormatService, + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsKeyCorrectnessProofRecord, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsLinkSecretRecord, + AnonCredsLinkSecretRepository, + AnonCredsModuleConfig, + AnonCredsProofFormatService, + AnonCredsRevocationRegistryDefinitionPrivateRecord, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRecord, + AnonCredsRevocationRegistryDefinitionRepository, + AnonCredsRevocationRegistryState, + AnonCredsSchemaRecord, + AnonCredsSchemaRepository, + AnonCredsVerifierServiceSymbol, +} from '@credo-ts/anoncreds' + +const registry = new InMemoryAnonCredsRegistry() +const tailsFileService = new InMemoryTailsFileService() +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + registries: [registry], + tailsFileService, + anoncreds, +}) + +const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs') +const anonCredsVerifierService = new AnonCredsRsVerifierService() +const anonCredsHolderService = new AnonCredsRsHolderService() +const anonCredsIssuerService = new AnonCredsRsIssuerService() + +const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123') } as Wallet + +const inMemoryStorageService = new InMemoryStorageService() + +const agentContext = getAgentContext({ + registerInstances: [ + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, new agentDependencies.FileSystem()], + [InjectionSymbols.StorageService, inMemoryStorageService], + [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [InjectionSymbols.Logger, testLogger], + [DidResolverService, new DidResolverService(testLogger, new DidsModuleConfig(), {} as unknown as DidRepository)], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [SignatureSuiteToken, 'default'], + ], + agentConfig, + wallet, +}) + +const anoncredsCredentialFormatService = new AnonCredsCredentialFormatService() +const anoncredsProofFormatService = new AnonCredsProofFormatService() + +const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + +describe('AnonCreds format services using anoncreds-rs', () => { + afterEach(() => { + inMemoryStorageService.contextCorrelationIdToRecords = {} + }) + + test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => { + await anonCredsFlowTest({ issuerId: indyDid, revocable: false }) + }) + + test('issuance and verification flow starting from proposal without negotiation and with revocation', async () => { + await anonCredsFlowTest({ issuerId: indyDid, revocable: true }) + }) +}) + +async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean }) { + const { issuerId, revocable } = options + + const schema = await anonCredsIssuerService.createSchema(agentContext, { + attrNames: ['name', 'age'], + issuerId, + name: 'Employee Credential', + version: '1.0.0', + }) + + const { schemaState } = await registry.registerSchema(agentContext, { + schema, + options: {}, + }) + + if (!schemaState.schema || !schemaState.schemaId) { + throw new Error('Failed to create schema') + } + + await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( + agentContext, + new AnonCredsSchemaRecord({ + schema: schemaState.schema, + schemaId: schemaState.schemaId, + methodName: 'inMemory', + }) + ) + + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await anonCredsIssuerService.createCredentialDefinition(agentContext, { + issuerId: indyDid, + schemaId: schemaState.schemaId as string, + schema, + tag: 'Employee Credential', + supportRevocation: revocable, + }) + + const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition, + options: {}, + }) + + if (!credentialDefinitionState.credentialDefinition || !credentialDefinitionState.credentialDefinitionId) { + throw new Error('Failed to create credential definition') + } + + if (!credentialDefinitionPrivate || !keyCorrectnessProof) { + throw new Error('Failed to get private part of credential definition') + } + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition: credentialDefinitionState.credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + methodName: 'inMemory', + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( + agentContext, + new AnonCredsCredentialDefinitionPrivateRecord({ + value: credentialDefinitionPrivate, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( + agentContext, + new AnonCredsKeyCorrectnessProofRecord({ + value: keyCorrectnessProof, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + let revocationRegistryDefinitionId: string | undefined + if (revocable) { + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = + await anonCredsIssuerService.createRevocationRegistryDefinition(agentContext, { + issuerId: indyDid, + credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + maximumCredentialNumber: 100, + tailsDirectoryPath: await tailsFileService.getTailsBasePath(agentContext), + tag: 'default', + }) + + // At this moment, tails file should be published and a valid public URL will be received + const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation + + const { revocationRegistryDefinitionState } = await registry.registerRevocationRegistryDefinition(agentContext, { + revocationRegistryDefinition, + options: {}, + }) + + revocationRegistryDefinitionId = revocationRegistryDefinitionState.revocationRegistryDefinitionId + + if ( + !revocationRegistryDefinitionState.revocationRegistryDefinition || + !revocationRegistryDefinitionId || + !revocationRegistryDefinitionPrivate + ) { + throw new Error('Failed to create revocation registry') + } + + await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionRepository).save( + agentContext, + new AnonCredsRevocationRegistryDefinitionRecord({ + revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, + revocationRegistryDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository).save( + agentContext, + new AnonCredsRevocationRegistryDefinitionPrivateRecord({ + state: AnonCredsRevocationRegistryState.Active, + value: revocationRegistryDefinitionPrivate, + credentialDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinition.credDefId, + revocationRegistryDefinitionId, + }) + ) + + const createdRevocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { + issuerId: indyDid, + revocationRegistryDefinition, + revocationRegistryDefinitionId, + tailsFilePath: localTailsFilePath, + }) + + const { revocationStatusListState } = await registry.registerRevocationStatusList(agentContext, { + revocationStatusList: createdRevocationStatusList, + options: {}, + }) + + if (!revocationStatusListState.revocationStatusList) { + throw new Error('Failed to create revocation status list') + } + } + + const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' }) + expect(linkSecret.linkSecretId).toBe('linkSecretId') + + await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save( + agentContext, + new AnonCredsLinkSecretRecord({ + value: linkSecret.linkSecretValue, + linkSecretId: linkSecret.linkSecretId, + }) + ) + + const holderCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + role: CredentialRole.Holder, + }) + + const issuerCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalReceived, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + role: CredentialRole.Issuer, + }) + + const credentialAttributes = [ + new CredentialPreviewAttribute({ + name: 'name', + value: 'John', + }), + new CredentialPreviewAttribute({ + name: 'age', + value: '25', + }), + ] + + // Holder creates proposal + holderCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: proposalAttachment } = await anoncredsCredentialFormatService.createProposal(agentContext, { + credentialRecord: holderCredentialRecord, + credentialFormats: { + anoncreds: { + attributes: credentialAttributes, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + }, + }) + + // Issuer processes and accepts proposal + await anoncredsCredentialFormatService.processProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: proposalAttachment, + }) + // Set attributes on the credential record, this is normally done by the protocol service + issuerCredentialRecord.credentialAttributes = credentialAttributes + + // If revocable, specify revocation registry definition id and index + const credentialFormats = revocable + ? { anoncreds: { revocationRegistryDefinitionId, revocationRegistryIndex: 1 } } + : undefined + + const { attachment: offerAttachment } = await anoncredsCredentialFormatService.acceptProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + proposalAttachment: proposalAttachment, + credentialFormats, + }) + + // Holder processes and accepts offer + await anoncredsCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment } = await anoncredsCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + credentialFormats: { + anoncreds: { + linkSecretId: linkSecret.linkSecretId, + }, + }, + }) + + // Make sure the request contains an entropy and does not contain a prover_did field + expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).entropy).toBeDefined() + expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeUndefined() + + // Issuer processes and accepts request + await anoncredsCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await anoncredsCredentialFormatService.acceptRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + }) + + // Holder processes and accepts credential + await anoncredsCredentialFormatService.processCredential(agentContext, { + offerAttachment, + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) + + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, + ]) + + const credentialId = holderCredentialRecord.credentials[0].credentialRecordId + const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { + id: credentialId, + }) + + expect(anonCredsCredential).toEqual({ + credentialId, + attributes: { + age: 25, + name: 'John', + }, + linkSecretId: 'linkSecretId', + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: revocable ? revocationRegistryDefinitionId : null, + credentialRevocationId: revocable ? '1' : null, + methodName: 'inMemory', + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + const expectedCredentialMetadata = revocable + ? { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: revocationRegistryDefinitionId, + credentialRevocationId: '1', + } + : { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + } + expect(holderCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': expectedCredentialMetadata, + '_anoncreds/credentialRequest': { + link_secret_blinding_data: expect.any(Object), + link_secret_name: expect.any(String), + nonce: expect.any(String), + }, + }) + + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': expectedCredentialMetadata, + }) + + const holderProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalSent, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + role: ProofRole.Prover, + }) + const verifierProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalReceived, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + role: ProofRole.Verifier, + }) + + const nrpRequestedTime = dateToTimestamp(new Date()) + + const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { + proofFormats: { + anoncreds: { + attributes: [ + { + name: 'name', + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + value: 'John', + referent: '1', + }, + ], + predicates: [ + { + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 18, + }, + ], + name: 'Proof Request', + version: '1.0', + nonRevokedInterval: { from: nrpRequestedTime, to: nrpRequestedTime }, + }, + }, + proofRecord: holderProofRecord, + }) + + await anoncredsProofFormatService.processProposal(agentContext, { + attachment: proofProposalAttachment, + proofRecord: verifierProofRecord, + }) + + const { attachment: proofRequestAttachment } = await anoncredsProofFormatService.acceptProposal(agentContext, { + proofRecord: verifierProofRecord, + proposalAttachment: proofProposalAttachment, + }) + + await anoncredsProofFormatService.processRequest(agentContext, { + attachment: proofRequestAttachment, + proofRecord: holderProofRecord, + }) + + const { attachment: proofAttachment } = await anoncredsProofFormatService.acceptRequest(agentContext, { + proofRecord: holderProofRecord, + requestAttachment: proofRequestAttachment, + proposalAttachment: proofProposalAttachment, + }) + + const isValid = await anoncredsProofFormatService.processPresentation(agentContext, { + attachment: proofAttachment, + proofRecord: verifierProofRecord, + requestAttachment: proofRequestAttachment, + }) + + expect(isValid).toBe(true) +} diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts new file mode 100644 index 0000000000..5ef2f84f7d --- /dev/null +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -0,0 +1,312 @@ +import { Agent, KeyType, TypedArrayEncoder } from '@credo-ts/core' + +import { getInMemoryAgentOptions } from '../../core/tests' +import { AnonCredsModule } from '../src' + +import { InMemoryAnonCredsRegistry } from './InMemoryAnonCredsRegistry' +import { anoncreds } from './helpers' + +const existingSchemas = { + '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0': { + attrNames: ['one', 'two'], + issuerId: '7Cd2Yj9yEZNcmNoH54tq9i', + name: 'Test Schema', + version: '1.0.0', + }, +} + +const existingCredentialDefinitions = { + 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG': { + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + tag: 'TAG', + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + type: 'CL', + value: { + primary: { + n: '92511867718854414868106363741369833735017762038454769060600859608405811709675033445666654908195955460485998711087020152978597220168927505650092431295783175164390266561239892662085428655566792056852960599485298025843840058914610127716620252006466964070280255168745873592143068949458568751438337748294055976926080232538440619420568859737673474560851456027625679328271511966332808025880807996449998057729417608399774744254122385012832309402226532031122728445959276178939234308090390331654445053482963947804769291501664200141562885660084823885847247231002821472258218384342423605116504024514572826071246440130942849549441', + s: '80388543865249952799447792504739237616187770512259677275061283897050980768551818104137338144380636412773836688624071360386172349725818126495487584981520630638409717065318132420766896092370913800616033623618952639023946750307405126873476182540669638841562357523429245685476919178722373320218824590869735129801004394337640642997250464303104754942997839179333543643110326022824394934965538190976474473353762308333205671176627192797138375084260446324344637548455228161138089974447059481109651156379803576163576511072261388342837813901850712083922506433336723723235701670225584863772222447543742649328218950436824219992164', + r: { + one: '676933340341980399002624386891134393471002096508227567343731826159610079436978196421307099268754545293545727546242372579987825752872485684085629459107300175443328323289748793060894500514926703654606851666031895448970879827423190730510730624784665299646624113512701254199984520803796529034094958026048762178753193812250643294518237843809104055653333871102658177900702978008644780459400512716361564897282969982554031820285585105004870317861287847206222714589633178648982299799311192432563797220854755882933052881306804544233529886513105815543097685128456041780804442879272476590077760678785460726492895806240870944398', + master_secret: + '57770757113548032970308439965749734133430520933173186296299026579579930337912607419798836831937319372744879560676750427054135869214212225572618340088847222727882935159356459822445182287686057012197046378986248048722180093079919306125315662058290895629438767985427829790980355162853804522854494960613869765167538645624719923127052541372069255024631093663068055100579264049925388231368871107383977060590248865498902704546409806115171120555709438784189721957301548212242748685629860268468247494986146122636455769804467583612610341632602695197189514316033637331733820369170763954604394734655429769801516997967996980978751', + two: '60366631925664005237432731340682977203246802182440530784833565276111958129922833461368205267143124766208499918438803966972947830682551774196763124331578934778868938718942789067536194229546670608604626738087066151521062180022991840618459591148096543440942293686250499935227881144460486543061212259250663566176469333982946568767707989969471450673037590849807300874360022327312564559087769485266016496010132793446151658150957771177955095876947792797176338483943233433284791481746843006255371654617950568875773118157773566188096075078351362095061968279597354733768049622048871890495958175847017320945873812850638157518451', + }, + rctxt: + '19574881057684356733946284215946569464410211018678168661028327420122678446653210056362495902735819742274128834330867933095119512313591151219353395069123546495720010325822330866859140765940839241212947354612836044244554152389691282543839111284006009168728161183863936810142428875817934316327118674532328892591410224676539770085459540786747902789677759379901079898127879301595929571621032704093287675668250862222728331030586585586110859977896767318814398026750215625180255041545607499673023585546720788973882263863911222208020438685873501025545464213035270207099419236974668665979962146355749687924650853489277747454993', + z: '18569464356833363098514177097771727133940629758890641648661259687745137028161881113251218061243607037717553708179509640909238773964066423807945164288256211132195919975343578956381001087353353060599758005375631247614777454313440511375923345538396573548499287265163879524050255226779884271432737062283353279122281220812931572456820130441114446870167673796490210349453498315913599982158253821945225264065364670730546176140788405935081171854642125236557475395879246419105888077042924382595999612137336915304205628167917473420377397118829734604949103124514367857266518654728464539418834291071874052392799652266418817991437', + }, + }, + }, +} as const + +const existingRevocationRegistryDefinitions = { + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG': { + credDefId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + revocDefType: 'CL_ACCUM', + value: { + publicKeys: { + accumKey: { + z: 'ab81257c-be63-4051-9e21-c7d384412f64', + }, + }, + maxCredNum: 100, + tailsHash: 'ab81257c-be63-4051-9e21-c7d384412f64', + tailsLocation: 'http://localhost:7200/tails', + }, + tag: 'TAG', + }, +} as const + +const existingRevocationStatusLists = { + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG': { + 10123: { + currentAccumulator: 'ab81257c-be63-4051-9e21-c7d384412f64', + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + revocationList: [1, 0, 1], + revRegDefId: 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG', + timestamp: 10123, + }, + }, +} + +const agent = new Agent( + getInMemoryAgentOptions( + 'credo-anoncreds-package', + {}, + { + anoncreds: new AnonCredsModule({ + autoCreateLinkSecret: false, + anoncreds, + registries: [ + new InMemoryAnonCredsRegistry({ + existingSchemas, + existingCredentialDefinitions, + existingRevocationRegistryDefinitions, + existingRevocationStatusLists, + }), + ], + }), + } + ) +) + +describe('AnonCreds API', () => { + beforeEach(async () => { + await agent.initialize() + }) + + afterEach(async () => { + await agent.wallet.delete() + await agent.shutdown() + }) + + test('create and get link secret', async () => { + await agent.modules.anoncreds.createLinkSecret({ + linkSecretId: 'anoncreds-link-secret', + }) + + const linkSecretIds = await agent.modules.anoncreds.getLinkSecretIds() + + expect(linkSecretIds).toEqual(['anoncreds-link-secret']) + }) + + test('register a schema', async () => { + const schemaResult = await agent.modules.anoncreds.registerSchema({ + options: {}, + schema: { + attrNames: ['name', 'age'], + issuerId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ', + name: 'Employee Credential', + version: '1.0.0', + }, + }) + + expect(schemaResult).toEqual({ + registrationMetadata: {}, + schemaMetadata: {}, + schemaState: { + state: 'finished', + schema: { + attrNames: ['name', 'age'], + issuerId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ', + name: 'Employee Credential', + version: '1.0.0', + }, + schemaId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', + }, + }) + + // Check if record was created + const [schemaRecord] = await agent.modules.anoncreds.getCreatedSchemas({ + schemaId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', + }) + expect(schemaRecord).toMatchObject({ + schemaId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', + methodName: 'inMemory', + schema: { + attrNames: ['name', 'age'], + issuerId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ', + name: 'Employee Credential', + version: '1.0.0', + }, + }) + + expect(schemaRecord.getTags()).toEqual({ + schemaId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', + issuerId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ', + schemaName: 'Employee Credential', + schemaVersion: '1.0.0', + methodName: 'inMemory', + unqualifiedSchemaId: '6xDN7v3AiGgusRp4bqZACZ:2:Employee Credential:1.0.0', + }) + }) + + test('resolve a schema', async () => { + const schemaResult = await agent.modules.anoncreds.getSchema('7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0') + + expect(schemaResult).toEqual({ + resolutionMetadata: {}, + schemaMetadata: {}, + schema: { + attrNames: ['one', 'two'], + issuerId: '7Cd2Yj9yEZNcmNoH54tq9i', + name: 'Test Schema', + version: '1.0.0', + }, + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + }) + }) + + test('register a credential definition', async () => { + // Create key + await agent.wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('00000000000000000000000000000My1'), + keyType: KeyType.Ed25519, + }) + + const issuerId = 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX' + + const credentialDefinitionResult = await agent.modules.anoncreds.registerCredentialDefinition({ + credentialDefinition: { + issuerId, + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + tag: 'TAG', + }, + options: { + supportRevocation: false, + }, + }) + + expect(credentialDefinitionResult).toEqual({ + registrationMetadata: {}, + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + state: 'finished', + credentialDefinition: { + issuerId: 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX', + tag: 'TAG', + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + type: 'CL', + value: { + primary: { + n: expect.any(String), + s: expect.any(String), + r: { + one: expect.any(String), + master_secret: expect.any(String), + two: expect.any(String), + }, + rctxt: expect.any(String), + z: expect.any(String), + }, + }, + }, + credentialDefinitionId: 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX/anoncreds/v0/CLAIM_DEF/75206/TAG', + }, + }) + + const [credentialDefinitionRecord] = await agent.modules.anoncreds.getCreatedCredentialDefinitions({ + credentialDefinitionId: 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX/anoncreds/v0/CLAIM_DEF/75206/TAG', + }) + expect(credentialDefinitionRecord).toMatchObject({ + credentialDefinitionId: 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX/anoncreds/v0/CLAIM_DEF/75206/TAG', + methodName: 'inMemory', + credentialDefinition: { + issuerId: 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX', + tag: 'TAG', + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + type: 'CL', + value: { + primary: { + n: expect.any(String), + s: expect.any(String), + r: { + one: expect.any(String), + master_secret: expect.any(String), + two: expect.any(String), + }, + rctxt: expect.any(String), + z: expect.any(String), + }, + }, + }, + }) + + expect(credentialDefinitionRecord.getTags()).toEqual({ + methodName: 'inMemory', + credentialDefinitionId: 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX/anoncreds/v0/CLAIM_DEF/75206/TAG', + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + issuerId: 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX', + tag: 'TAG', + unqualifiedCredentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + }) + }) + + test('resolve a credential definition', async () => { + const credentialDefinitionResult = await agent.modules.anoncreds.getCredentialDefinition( + 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG' + ) + + expect(credentialDefinitionResult).toEqual({ + resolutionMetadata: {}, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localhost', + }, + credentialDefinition: existingCredentialDefinitions['VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG'], + credentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + }) + }) + + test('resolve a revocation regsitry definition', async () => { + const revocationRegistryDefinition = await agent.modules.anoncreds.getRevocationRegistryDefinition( + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG' + ) + + expect(revocationRegistryDefinition).toEqual({ + revocationRegistryDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG', + revocationRegistryDefinition: + existingRevocationRegistryDefinitions[ + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG' + ], + resolutionMetadata: {}, + revocationRegistryDefinitionMetadata: {}, + }) + }) + + test('resolve a revocation status list', async () => { + const revocationStatusList = await agent.modules.anoncreds.getRevocationStatusList( + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG', + 10123 + ) + + expect(revocationStatusList).toEqual({ + revocationStatusList: + existingRevocationStatusLists[ + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG' + ][10123], + resolutionMetadata: {}, + revocationStatusListMetadata: {}, + }) + }) +}) diff --git a/packages/anoncreds/tests/anoncredsSetup.ts b/packages/anoncreds/tests/anoncredsSetup.ts new file mode 100644 index 0000000000..558f13b299 --- /dev/null +++ b/packages/anoncreds/tests/anoncredsSetup.ts @@ -0,0 +1,617 @@ +import type { EventReplaySubject } from '../../core/tests' +import type { + AnonCredsRegisterCredentialDefinitionOptions, + AnonCredsOfferCredentialFormat, + AnonCredsSchema, + RegisterCredentialDefinitionReturnStateFinished, + RegisterSchemaReturnStateFinished, + AnonCredsRegistry, + AnonCredsRegisterRevocationRegistryDefinitionOptions, + RegisterRevocationRegistryDefinitionReturnStateFinished, + AnonCredsRegisterRevocationStatusListOptions, + RegisterRevocationStatusListReturnStateFinished, + AnonCredsRequestedAttribute, + AnonCredsRequestedPredicate, +} from '../src' +import type { CheqdDidCreateOptions } from '@credo-ts/cheqd' +import type { AutoAcceptProof, ConnectionRecord } from '@credo-ts/core' + +import { + DidDocumentBuilder, + CacheModule, + InMemoryLruCache, + Agent, + CredoError, + AutoAcceptCredential, + CredentialEventTypes, + CredentialsModule, + CredentialState, + ProofEventTypes, + ProofsModule, + V2CredentialProtocol, + V2ProofProtocol, + DidsModule, + DifPresentationExchangeProofFormatService, + TypedArrayEncoder, + ProofState, +} from '@credo-ts/core' +import { randomUUID } from 'crypto' + +import { CheqdDidRegistrar, CheqdDidResolver, CheqdModule } from '../../cheqd/src/index' +import { getCheqdModuleConfig } from '../../cheqd/tests/setupCheqdModule' +import { sleep } from '../../core/src/utils/sleep' +import { setupSubjectTransports, setupEventReplaySubjects } from '../../core/tests' +import { + getInMemoryAgentOptions, + makeConnection, + waitForCredentialRecordSubject, + waitForProofExchangeRecordSubject, +} from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' +import { AnonCredsCredentialFormatService, AnonCredsProofFormatService, AnonCredsModule } from '../src' +import { DataIntegrityCredentialFormatService } from '../src/formats/DataIntegrityCredentialFormatService' +import { InMemoryAnonCredsRegistry } from '../tests/InMemoryAnonCredsRegistry' + +import { InMemoryTailsFileService } from './InMemoryTailsFileService' +import { LocalDidResolver } from './LocalDidResolver' +import { anoncreds } from './helpers' +import { anoncredsDefinitionFourAttributesNoRevocation } from './preCreatedAnonCredsDefinition' + +// Helper type to get the type of the agents (with the custom modules) for the credential tests +export type AnonCredsTestsAgent = Agent< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ReturnType & { mediationRecipient?: any; mediator?: any } +> + +export const getAnonCredsModules = ({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + cheqd, +}: { + autoAcceptCredentials?: AutoAcceptCredential + autoAcceptProofs?: AutoAcceptProof + registries?: [AnonCredsRegistry, ...AnonCredsRegistry[]] + cheqd?: { + rpcUrl?: string + seed?: string + } +} = {}) => { + const dataIntegrityCredentialFormatService = new DataIntegrityCredentialFormatService() + // Add support for resolving pre-created credential definitions and schemas + const inMemoryAnonCredsRegistry = new InMemoryAnonCredsRegistry({ + existingCredentialDefinitions: { + [anoncredsDefinitionFourAttributesNoRevocation.credentialDefinitionId]: + anoncredsDefinitionFourAttributesNoRevocation.credentialDefinition, + }, + existingSchemas: { + [anoncredsDefinitionFourAttributesNoRevocation.schemaId]: anoncredsDefinitionFourAttributesNoRevocation.schema, + }, + }) + + const anonCredsCredentialFormatService = new AnonCredsCredentialFormatService() + const anonCredsProofFormatService = new AnonCredsProofFormatService() + const presentationExchangeProofFormatService = new DifPresentationExchangeProofFormatService() + + const cheqdSdk = cheqd ? new CheqdModule(getCheqdModuleConfig(cheqd.seed, cheqd.rpcUrl)) : undefined + const modules = { + ...(cheqdSdk && { cheqdSdk }), + credentials: new CredentialsModule({ + autoAcceptCredentials, + credentialProtocols: [ + new V2CredentialProtocol({ + credentialFormats: [dataIntegrityCredentialFormatService, anonCredsCredentialFormatService], + }), + ], + }), + proofs: new ProofsModule({ + autoAcceptProofs, + proofProtocols: [ + new V2ProofProtocol({ + proofFormats: [anonCredsProofFormatService, presentationExchangeProofFormatService], + }), + ], + }), + anoncreds: new AnonCredsModule({ + registries: registries ?? [inMemoryAnonCredsRegistry], + tailsFileService: new InMemoryTailsFileService(), + anoncreds, + }), + dids: new DidsModule({ + resolvers: cheqd ? [new CheqdDidResolver()] : [new LocalDidResolver()], + registrars: cheqd ? [new CheqdDidRegistrar()] : undefined, + }), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 100 }), + }), + } as const + + return modules +} + +export async function issueAnonCredsCredential({ + issuerAgent, + issuerReplay, + + holderAgent, + holderReplay, + + issuerHolderConnectionId, + revocationRegistryDefinitionId, + offer, +}: { + issuerAgent: AnonCredsTestsAgent + issuerReplay: EventReplaySubject + + holderAgent: AnonCredsTestsAgent + holderReplay: EventReplaySubject + + issuerHolderConnectionId: string + revocationRegistryDefinitionId: string | null + offer: AnonCredsOfferCredentialFormat +}) { + let issuerCredentialExchangeRecord = await issuerAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: issuerHolderConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + ...offer, + revocationRegistryDefinitionId: revocationRegistryDefinitionId ?? undefined, + revocationRegistryIndex: 1, + }, + }, + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + }) + + let holderCredentialExchangeRecord = await waitForCredentialRecordSubject(holderReplay, { + threadId: issuerCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + await holderAgent.credentials.acceptOffer({ + credentialRecordId: holderCredentialExchangeRecord.id, + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + }) + + // Because we use auto-accept it can take a while to have the whole credential flow finished + // Both parties need to interact with the ledger and sign/verify the credential + holderCredentialExchangeRecord = await waitForCredentialRecordSubject(holderReplay, { + threadId: issuerCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + issuerCredentialExchangeRecord = await waitForCredentialRecordSubject(issuerReplay, { + threadId: issuerCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + + return { + issuerCredentialExchangeRecord, + holderCredentialExchangeRecord, + } +} + +interface SetupAnonCredsTestsReturn { + issuerAgent: AnonCredsTestsAgent + issuerReplay: EventReplaySubject + + issuerId: string + + holderAgent: AnonCredsTestsAgent + holderReplay: EventReplaySubject + + issuerHolderConnectionId: CreateConnections extends true ? string : undefined + holderIssuerConnectionId: CreateConnections extends true ? string : undefined + + verifierHolderConnectionId: CreateConnections extends true + ? VerifierName extends string + ? string + : undefined + : undefined + holderVerifierConnectionId: CreateConnections extends true + ? VerifierName extends string + ? string + : undefined + : undefined + + verifierAgent: VerifierName extends string ? AnonCredsTestsAgent : undefined + verifierReplay: VerifierName extends string ? EventReplaySubject : undefined + + schemaId: string + credentialDefinitionId: string + revocationRegistryDefinitionId: string | null + revocationStatusListTimestamp?: number +} + +export async function presentAnonCredsProof({ + verifierAgent, + verifierReplay, + + holderAgent, + holderReplay, + + verifierHolderConnectionId, + + request: { attributes, predicates }, +}: { + holderAgent: AnonCredsTestsAgent + holderReplay: EventReplaySubject + + verifierAgent: AnonCredsTestsAgent + verifierReplay: EventReplaySubject + + verifierHolderConnectionId: string + request: { + attributes?: Record + predicates?: Record + } +}) { + let holderProofExchangeRecordPromise = waitForProofExchangeRecordSubject(holderReplay, { + state: ProofState.RequestReceived, + }) + + let verifierProofExchangeRecord = await verifierAgent.proofs.requestProof({ + connectionId: verifierHolderConnectionId, + proofFormats: { + anoncreds: { + name: 'Test Proof Request', + requested_attributes: attributes, + requested_predicates: predicates, + version: '1.0', + }, + }, + protocolVersion: 'v2', + }) + + let holderProofExchangeRecord = await holderProofExchangeRecordPromise + + const selectedCredentials = await holderAgent.proofs.selectCredentialsForRequest({ + proofRecordId: holderProofExchangeRecord.id, + }) + + const verifierProofExchangeRecordPromise = waitForProofExchangeRecordSubject(verifierReplay, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await holderAgent.proofs.acceptRequest({ + proofRecordId: holderProofExchangeRecord.id, + proofFormats: { anoncreds: selectedCredentials.proofFormats.anoncreds }, + }) + + verifierProofExchangeRecord = await verifierProofExchangeRecordPromise + + // assert presentation is valid + expect(verifierProofExchangeRecord.isVerified).toBe(true) + + holderProofExchangeRecordPromise = waitForProofExchangeRecordSubject(holderReplay, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + verifierProofExchangeRecord = await verifierAgent.proofs.acceptPresentation({ + proofRecordId: verifierProofExchangeRecord.id, + }) + holderProofExchangeRecord = await holderProofExchangeRecordPromise + + return { + verifierProofExchangeRecord, + holderProofExchangeRecord, + } +} + +export async function setupAnonCredsTests< + VerifierName extends string | undefined = undefined, + CreateConnections extends boolean = true +>({ + issuerId, + issuerName, + holderName, + verifierName, + autoAcceptCredentials, + autoAcceptProofs, + attributeNames, + createConnections, + supportRevocation, + registries, + cheqd, +}: { + issuerId?: string + cheqd?: { + rpcUrl?: string + seed?: string + } + issuerName: string + holderName: string + verifierName?: VerifierName + autoAcceptCredentials?: AutoAcceptCredential + autoAcceptProofs?: AutoAcceptProof + attributeNames: string[] + createConnections?: CreateConnections + supportRevocation?: boolean + registries?: [AnonCredsRegistry, ...AnonCredsRegistry[]] +}): Promise> { + const issuerAgent = new Agent( + getInMemoryAgentOptions( + issuerName, + { + endpoints: ['rxjs:issuer'], + }, + getAnonCredsModules({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + cheqd, + }) + ) + ) + + const holderAgent = new Agent( + getInMemoryAgentOptions( + holderName, + { + endpoints: ['rxjs:holder'], + }, + getAnonCredsModules({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + cheqd, + }) + ) + ) + + const verifierAgent = verifierName + ? new Agent( + getInMemoryAgentOptions( + verifierName, + { + endpoints: ['rxjs:verifier'], + }, + getAnonCredsModules({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + cheqd, + }) + ) + ) + : undefined + + setupSubjectTransports(verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent]) + const [issuerReplay, holderReplay, verifierReplay] = setupEventReplaySubjects( + verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent], + [CredentialEventTypes.CredentialStateChanged, ProofEventTypes.ProofStateChanged] + ) + + await issuerAgent.initialize() + await holderAgent.initialize() + if (verifierAgent) await verifierAgent.initialize() + + // Create default link secret for holder + await holderAgent.modules.anoncreds.createLinkSecret({ + linkSecretId: 'default', + setAsDefault: true, + }) + + if (issuerId) { + const didDocument = new DidDocumentBuilder(issuerId).build() + await issuerAgent.dids.import({ did: issuerId, didDocument }) + } else if (cheqd) { + const privateKey = TypedArrayEncoder.fromString('000000000000000000000000001cheqd') + const did = await issuerAgent.dids.create({ + method: 'cheqd', + secret: { + verificationMethod: { + id: 'key-10', + type: 'Ed25519VerificationKey2020', + privateKey, + }, + }, + options: { + network: 'testnet', + methodSpecificIdAlgo: 'uuid', + }, + }) + issuerId = did.didState.did as string + } else { + throw new CredoError('issuerId is required if cheqd is not used') + } + + const { credentialDefinition, revocationRegistryDefinition, revocationStatusList, schema } = + await prepareForAnonCredsIssuance(issuerAgent, { + issuerId, + attributeNames, + supportRevocation, + }) + + let issuerHolderConnection: ConnectionRecord | undefined + let holderIssuerConnection: ConnectionRecord | undefined + let verifierHolderConnection: ConnectionRecord | undefined + let holderVerifierConnection: ConnectionRecord | undefined + + if (createConnections ?? true) { + ;[issuerHolderConnection, holderIssuerConnection] = await makeConnection(issuerAgent, holderAgent) + + if (verifierAgent) { + ;[holderVerifierConnection, verifierHolderConnection] = await makeConnection(holderAgent, verifierAgent) + } + } + + return { + issuerAgent, + issuerReplay, + + holderAgent, + holderReplay, + + issuerId, + + verifierAgent: verifierName ? verifierAgent : undefined, + verifierReplay: verifierName ? verifierReplay : undefined, + + revocationRegistryDefinitionId: revocationRegistryDefinition?.revocationRegistryDefinitionId, + revocationStatusListTimestamp: revocationStatusList.revocationStatusList?.timestamp, + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + schemaId: schema.schemaId, + + issuerHolderConnectionId: issuerHolderConnection?.id, + holderIssuerConnectionId: holderIssuerConnection?.id, + holderVerifierConnectionId: holderVerifierConnection?.id, + verifierHolderConnectionId: verifierHolderConnection?.id, + } as unknown as SetupAnonCredsTestsReturn +} + +export async function prepareForAnonCredsIssuance( + agent: Agent, + { + attributeNames, + supportRevocation, + issuerId, + }: { attributeNames: string[]; supportRevocation?: boolean; issuerId: string } +) { + const schema = await registerSchema(agent, { + // TODO: update attrNames to attributeNames + attrNames: attributeNames, + name: `Schema ${randomUUID()}`, + version: '1.0', + issuerId, + }) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + + const credentialDefinition = await registerCredentialDefinition( + agent, + { + schemaId: schema.schemaId, + issuerId, + tag: 'default', + }, + supportRevocation + ) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + + let revocationRegistryDefinition + let revocationStatusList + if (supportRevocation) { + revocationRegistryDefinition = await registerRevocationRegistryDefinition(agent, { + issuerId, + tag: 'default', + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + maximumCredentialNumber: 10, + }) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + + revocationStatusList = await registerRevocationStatusList(agent, { + revocationRegistryDefinitionId: revocationRegistryDefinition?.revocationRegistryDefinitionId, + issuerId, + }) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + } + + return { + schema: { + ...schema, + schemaId: schema.schemaId, + }, + credentialDefinition: { + ...credentialDefinition, + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + }, + revocationRegistryDefinition: { + ...revocationRegistryDefinition, + revocationRegistryDefinitionId: revocationRegistryDefinition?.revocationRegistryDefinitionId, + }, + revocationStatusList: { + ...revocationStatusList, + }, + } +} + +async function registerSchema( + agent: AnonCredsTestsAgent, + schema: AnonCredsSchema +): Promise { + const { schemaState } = await agent.modules.anoncreds.registerSchema({ + schema, + options: {}, + }) + + testLogger.test(`created schema with id ${schemaState.schemaId}`, schema) + + if (schemaState.state !== 'finished') { + throw new CredoError(`Schema not created: ${schemaState.state === 'failed' ? schemaState.reason : 'Not finished'}`) + } + + return schemaState +} + +async function registerCredentialDefinition( + agent: AnonCredsTestsAgent, + credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions, + supportRevocation?: boolean +): Promise { + const { credentialDefinitionState } = await agent.modules.anoncreds.registerCredentialDefinition({ + credentialDefinition, + options: { + supportRevocation: supportRevocation ?? false, + }, + }) + + if (credentialDefinitionState.state !== 'finished') { + throw new CredoError( + `Credential definition not created: ${ + credentialDefinitionState.state === 'failed' ? credentialDefinitionState.reason : 'Not finished' + }` + ) + } + + return credentialDefinitionState +} + +async function registerRevocationRegistryDefinition( + agent: AnonCredsTestsAgent, + revocationRegistryDefinition: AnonCredsRegisterRevocationRegistryDefinitionOptions +): Promise { + const { revocationRegistryDefinitionState } = await agent.modules.anoncreds.registerRevocationRegistryDefinition({ + revocationRegistryDefinition, + options: {}, + }) + + if (revocationRegistryDefinitionState.state !== 'finished') { + throw new CredoError( + `Revocation registry definition not created: ${ + revocationRegistryDefinitionState.state === 'failed' ? revocationRegistryDefinitionState.reason : 'Not finished' + }` + ) + } + + return revocationRegistryDefinitionState +} + +async function registerRevocationStatusList( + agent: AnonCredsTestsAgent, + revocationStatusList: AnonCredsRegisterRevocationStatusListOptions +): Promise { + const { revocationStatusListState } = await agent.modules.anoncreds.registerRevocationStatusList({ + revocationStatusList, + options: {}, + }) + + if (revocationStatusListState.state !== 'finished') { + throw new CredoError( + `Revocation status list not created: ${ + revocationStatusListState.state === 'failed' ? revocationStatusListState.reason : 'Not finished' + }` + ) + } + + return revocationStatusListState +} diff --git a/packages/anoncreds/tests/data-integrity-flow-anoncreds-pex.test.ts b/packages/anoncreds/tests/data-integrity-flow-anoncreds-pex.test.ts new file mode 100644 index 0000000000..7e70b0de5d --- /dev/null +++ b/packages/anoncreds/tests/data-integrity-flow-anoncreds-pex.test.ts @@ -0,0 +1,306 @@ +import type { AnonCredsTestsAgent } from './anoncredsSetup' +import type { EventReplaySubject } from '../../core/tests' +import type { InputDescriptorV2 } from '@sphereon/pex-models' + +import { + AutoAcceptCredential, + CredentialExchangeRecord, + CredentialState, + ProofState, + W3cCredential, + W3cCredentialService, + W3cCredentialSubject, +} from '@credo-ts/core' + +import { + createDidKidVerificationMethod, + waitForCredentialRecordSubject, + waitForProofExchangeRecord, +} from '../../core/tests' + +import { InMemoryAnonCredsRegistry } from './InMemoryAnonCredsRegistry' +import { setupAnonCredsTests } from './anoncredsSetup' +import { presentationDefinition } from './fixtures/presentation-definition' + +const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + +describe('anoncreds w3c data integrity tests', () => { + let issuerAgent: AnonCredsTestsAgent + let holderAgent: AnonCredsTestsAgent + let credentialDefinitionId: string + let issuerHolderConnectionId: string + let holderIssuerConnectionId: string + let revocationRegistryDefinitionId: string | null + + let issuerReplay: EventReplaySubject + let holderReplay: EventReplaySubject + + const inMemoryRegistry = new InMemoryAnonCredsRegistry() + + afterEach(async () => { + await issuerAgent.shutdown() + await issuerAgent.wallet.delete() + await holderAgent.shutdown() + await holderAgent.wallet.delete() + }) + + test('issuance and verification flow starting from offer with revocation', async () => { + ;({ + issuerAgent, + issuerReplay, + holderAgent, + holderReplay, + credentialDefinitionId, + issuerHolderConnectionId, + revocationRegistryDefinitionId, + holderIssuerConnectionId, + } = await setupAnonCredsTests({ + issuerId: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL', + issuerName: 'Faber Agent Credentials v2', + holderName: 'Alice Agent Credentials v2', + attributeNames: ['id', 'name', 'height', 'age'], + registries: [inMemoryRegistry], + supportRevocation: true, + })) + await anonCredsFlowTest({ + credentialDefinitionId, + issuerHolderConnectionId, + revocationRegistryDefinitionId, + holderIssuerConnectionId, + issuerReplay: issuerReplay, + holderReplay: holderReplay, + issuer: issuerAgent, + holder: holderAgent, + }) + }) + + test('issuance and verification flow starting from offer without revocation', async () => { + ;({ + issuerAgent, + issuerReplay, + holderAgent, + holderReplay, + credentialDefinitionId, + issuerHolderConnectionId, + revocationRegistryDefinitionId, + holderIssuerConnectionId, + } = await setupAnonCredsTests({ + issuerId: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL', + issuerName: 'Faber Agent Credentials v2', + holderName: 'Alice Agent Credentials v2', + attributeNames: ['id', 'name', 'height', 'age'], + registries: [inMemoryRegistry], + supportRevocation: false, + })) + await anonCredsFlowTest({ + credentialDefinitionId, + issuerHolderConnectionId, + holderIssuerConnectionId, + issuerReplay, + holderReplay, + revocationRegistryDefinitionId, + issuer: issuerAgent, + holder: holderAgent, + }) + }) +}) + +async function anonCredsFlowTest(options: { + issuer: AnonCredsTestsAgent + holder: AnonCredsTestsAgent + issuerHolderConnectionId: string + holderIssuerConnectionId: string + issuerReplay: EventReplaySubject + holderReplay: EventReplaySubject + revocationRegistryDefinitionId: string | null + credentialDefinitionId: string +}) { + const { + credentialDefinitionId, + issuerHolderConnectionId, + holderIssuerConnectionId, + issuer, + revocationRegistryDefinitionId, + holder, + issuerReplay, + holderReplay, + } = options + + const holderKdv = await createDidKidVerificationMethod(holder.context, '96213c3d7fc8d4d6754c7a0fd969598f') + const linkSecret = await holder.modules.anoncreds.createLinkSecret({ linkSecretId: 'linkSecretId' }) + expect(linkSecret).toBe('linkSecretId') + + const credential = new W3cCredential({ + context: [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2', + { + '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', + }, + ], + type: ['VerifiableCredential'], + issuer: issuerId, + issuanceDate: new Date().toISOString(), + credentialSubject: new W3cCredentialSubject({ + id: holderKdv.did, + claims: { name: 'John', age: '25', height: 173 }, + }), + }) + + // issuer offers credential + let issuerRecord = await issuer.credentials.offerCredential({ + protocolVersion: 'v2', + autoAcceptCredential: AutoAcceptCredential.Never, + connectionId: issuerHolderConnectionId, + credentialFormats: { + dataIntegrity: { + bindingRequired: true, + credential, + anonCredsLinkSecretBinding: { + credentialDefinitionId, + revocationRegistryDefinitionId: revocationRegistryDefinitionId ?? undefined, + revocationRegistryIndex: revocationRegistryDefinitionId ? 1 : undefined, + }, + didCommSignedAttachmentBinding: {}, + }, + }, + }) + + // Holder processes and accepts offer + let holderRecord = await waitForCredentialRecordSubject(holderReplay, { + state: CredentialState.OfferReceived, + threadId: issuerRecord.threadId, + }) + holderRecord = await holder.credentials.acceptOffer({ + credentialRecordId: holderRecord.id, + autoAcceptCredential: AutoAcceptCredential.Never, + credentialFormats: { + dataIntegrity: { + anonCredsLinkSecret: { + linkSecretId: 'linkSecretId', + }, + }, + }, + }) + + // issuer receives request and accepts + issuerRecord = await waitForCredentialRecordSubject(issuerReplay, { + state: CredentialState.RequestReceived, + threadId: holderRecord.threadId, + }) + issuerRecord = await issuer.credentials.acceptRequest({ + credentialRecordId: issuerRecord.id, + autoAcceptCredential: AutoAcceptCredential.Never, + credentialFormats: { + dataIntegrity: {}, + }, + }) + + holderRecord = await waitForCredentialRecordSubject(holderReplay, { + state: CredentialState.CredentialReceived, + threadId: issuerRecord.threadId, + }) + holderRecord = await holder.credentials.acceptCredential({ + credentialRecordId: holderRecord.id, + }) + + issuerRecord = await waitForCredentialRecordSubject(issuerReplay, { + state: CredentialState.Done, + threadId: holderRecord.threadId, + }) + + expect(holderRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + credentialDefinitionId, + schemaId: expect.any(String), + }, + '_anoncreds/credentialRequest': { + link_secret_blinding_data: { + v_prime: expect.any(String), + vr_prime: revocationRegistryDefinitionId ? expect.any(String) : null, + }, + nonce: expect.any(String), + link_secret_name: 'linkSecretId', + }, + }, + }, + state: CredentialState.Done, + }) + + const tags = holderRecord.getTags() + expect(tags.credentialIds).toHaveLength(1) + + await expect( + holder.dependencyManager + .resolve(W3cCredentialService) + .getCredentialRecordById(holder.context, tags.credentialIds[0]) + ).resolves + + let issuerProofExchangeRecordPromise = waitForProofExchangeRecord(issuer, { + state: ProofState.ProposalReceived, + }) + + const pdCopy = JSON.parse(JSON.stringify(presentationDefinition)) + if (!revocationRegistryDefinitionId) + pdCopy.input_descriptors.forEach((ide: InputDescriptorV2) => delete ide.constraints?.statuses) + + let holderProofExchangeRecord = await holder.proofs.proposeProof({ + protocolVersion: 'v2', + connectionId: holderIssuerConnectionId, + proofFormats: { + presentationExchange: { + presentationDefinition: pdCopy, + }, + }, + }) + + let issuerProofExchangeRecord = await issuerProofExchangeRecordPromise + + let holderProofExchangeRecordPromise = waitForProofExchangeRecord(holder, { + state: ProofState.RequestReceived, + }) + + issuerProofExchangeRecord = await issuer.proofs.acceptProposal({ + proofRecordId: issuerProofExchangeRecord.id, + }) + + holderProofExchangeRecord = await holderProofExchangeRecordPromise + + const requestedCredentials = await holder.proofs.selectCredentialsForRequest({ + proofRecordId: holderProofExchangeRecord.id, + }) + + const selectedCredentials = requestedCredentials.proofFormats.presentationExchange?.credentials + if (!selectedCredentials) { + throw new Error('No credentials found for presentation exchange') + } + + issuerProofExchangeRecordPromise = waitForProofExchangeRecord(issuer, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await holder.proofs.acceptRequest({ + proofRecordId: holderProofExchangeRecord.id, + proofFormats: { + presentationExchange: { + credentials: selectedCredentials, + }, + }, + }) + issuerProofExchangeRecord = await issuerProofExchangeRecordPromise + + holderProofExchangeRecordPromise = waitForProofExchangeRecord(holder, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + await issuer.proofs.acceptPresentation({ proofRecordId: issuerProofExchangeRecord.id }) + + holderProofExchangeRecord = await holderProofExchangeRecordPromise +} diff --git a/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts new file mode 100644 index 0000000000..733490d843 --- /dev/null +++ b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts @@ -0,0 +1,504 @@ +import type { DataIntegrityCredentialRequest, DidRepository } from '@credo-ts/core' + +import { + ProofRole, + CredentialRole, + AgentContext, + CredentialExchangeRecord, + CredentialPreviewAttribute, + CredentialState, + DidResolverService, + DidsModuleConfig, + InjectionSymbols, + KeyDidRegistrar, + KeyDidResolver, + ProofExchangeRecord, + ProofState, + SignatureSuiteToken, + W3cCredential, + W3cCredentialService, + W3cCredentialSubject, + W3cCredentialsModuleConfig, +} from '@credo-ts/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' +import { InMemoryWallet } from '../../../tests/InMemoryWallet' +import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' +import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' +import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { agentDependencies, getAgentConfig, getAgentContext, testLogger } from '../../core/tests' +import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from '../src/anoncreds-rs' + +import { InMemoryTailsFileService } from './InMemoryTailsFileService' +import { anoncreds } from './helpers' + +import { + AnonCredsCredentialDefinitionPrivateRecord, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsCredentialDefinitionRecord, + AnonCredsCredentialDefinitionRepository, + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsKeyCorrectnessProofRecord, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsLinkSecretRecord, + AnonCredsLinkSecretRepository, + AnonCredsModuleConfig, + AnonCredsProofFormatService, + AnonCredsRevocationRegistryDefinitionPrivateRecord, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRecord, + AnonCredsRevocationRegistryDefinitionRepository, + AnonCredsRevocationRegistryState, + AnonCredsSchemaRecord, + AnonCredsSchemaRepository, + AnonCredsVerifierServiceSymbol, +} from '@credo-ts/anoncreds' + +const registry = new InMemoryAnonCredsRegistry() +const tailsFileService = new InMemoryTailsFileService() +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + anoncreds, + registries: [registry], + tailsFileService, +}) + +const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs') +const anonCredsVerifierService = new AnonCredsRsVerifierService() +const anonCredsHolderService = new AnonCredsRsHolderService() +const anonCredsIssuerService = new AnonCredsRsIssuerService() + +const inMemoryStorageService = new InMemoryStorageService() + +const didsModuleConfig = new DidsModuleConfig({ + registrars: [new KeyDidRegistrar()], + resolvers: [new KeyDidResolver()], +}) +const fileSystem = new agentDependencies.FileSystem() + +const wallet = new InMemoryWallet() + +const agentContext = getAgentContext({ + registerInstances: [ + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, fileSystem], + [InjectionSymbols.StorageService, inMemoryStorageService], + [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [InjectionSymbols.Logger, testLogger], + [DidsModuleConfig, didsModuleConfig], + [DidResolverService, new DidResolverService(testLogger, didsModuleConfig, {} as unknown as DidRepository)], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [SignatureSuiteToken, 'default'], + ], + agentConfig, + wallet, +}) + +agentContext.dependencyManager.registerInstance(AgentContext, agentContext) + +const dataIntegrityCredentialFormatService = new DataIntegrityCredentialFormatService() +const anoncredsProofFormatService = new AnonCredsProofFormatService() + +const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + +describe('data integrity format service (anoncreds)', () => { + beforeAll(async () => { + await wallet.createAndOpen(agentConfig.walletConfig) + }) + + afterEach(async () => { + inMemoryStorageService.contextCorrelationIdToRecords = {} + }) + + test('issuance and verification flow anoncreds starting from offer without negotiation and without revocation', async () => { + await anonCredsFlowTest({ issuerId: indyDid, revocable: false }) + }) + + test('issuance and verification flow anoncreds starting from offer without negotiation and with revocation', async () => { + await anonCredsFlowTest({ issuerId: indyDid, revocable: true }) + }) +}) + +async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean }) { + const { issuerId, revocable } = options + + const schema = await anonCredsIssuerService.createSchema(agentContext, { + attrNames: ['name', 'age'], + issuerId, + name: 'Employee Credential', + version: '1.0.0', + }) + + const { schemaState } = await registry.registerSchema(agentContext, { + schema, + options: {}, + }) + + if (!schemaState.schema || !schemaState.schemaId) { + throw new Error('Failed to create schema') + } + + await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( + agentContext, + new AnonCredsSchemaRecord({ + schema: schemaState.schema, + schemaId: schemaState.schemaId, + methodName: 'inMemory', + }) + ) + + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await anonCredsIssuerService.createCredentialDefinition(agentContext, { + issuerId, + schemaId: schemaState.schemaId as string, + schema, + tag: 'Employee Credential', + supportRevocation: revocable, + }) + + const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition, + options: {}, + }) + + if (!credentialDefinitionState.credentialDefinition || !credentialDefinitionState.credentialDefinitionId) { + throw new Error('Failed to create credential definition') + } + + if (!credentialDefinitionPrivate || !keyCorrectnessProof) { + throw new Error('Failed to get private part of credential definition') + } + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition: credentialDefinitionState.credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + methodName: 'inMemory', + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( + agentContext, + new AnonCredsCredentialDefinitionPrivateRecord({ + value: credentialDefinitionPrivate, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( + agentContext, + new AnonCredsKeyCorrectnessProofRecord({ + value: keyCorrectnessProof, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + let revocationRegistryDefinitionId: string | undefined + if (revocable) { + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = + await anonCredsIssuerService.createRevocationRegistryDefinition(agentContext, { + issuerId: issuerId, + credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + maximumCredentialNumber: 100, + tailsDirectoryPath: await tailsFileService.getTailsBasePath(agentContext), + tag: 'default', + }) + + // At this moment, tails file should be published and a valid public URL will be received + const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation + + const { revocationRegistryDefinitionState } = await registry.registerRevocationRegistryDefinition(agentContext, { + revocationRegistryDefinition, + options: {}, + }) + + revocationRegistryDefinitionId = revocationRegistryDefinitionState.revocationRegistryDefinitionId + + if ( + !revocationRegistryDefinitionState.revocationRegistryDefinition || + !revocationRegistryDefinitionId || + !revocationRegistryDefinitionPrivate + ) { + throw new Error('Failed to create revocation registry') + } + + await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionRepository).save( + agentContext, + new AnonCredsRevocationRegistryDefinitionRecord({ + revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, + revocationRegistryDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository).save( + agentContext, + new AnonCredsRevocationRegistryDefinitionPrivateRecord({ + state: AnonCredsRevocationRegistryState.Active, + value: revocationRegistryDefinitionPrivate, + credentialDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinition.credDefId, + revocationRegistryDefinitionId, + }) + ) + + const createdRevocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { + issuerId: issuerId, + revocationRegistryDefinition, + revocationRegistryDefinitionId, + tailsFilePath: localTailsFilePath, + }) + + const { revocationStatusListState } = await registry.registerRevocationStatusList(agentContext, { + revocationStatusList: createdRevocationStatusList, + options: {}, + }) + + if (!revocationStatusListState.revocationStatusList) { + throw new Error('Failed to create revocation status list') + } + } + + const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' }) + expect(linkSecret.linkSecretId).toBe('linkSecretId') + + await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save( + agentContext, + new AnonCredsLinkSecretRecord({ + value: linkSecret.linkSecretValue, + linkSecretId: linkSecret.linkSecretId, + }) + ) + + const holderCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + role: CredentialRole.Holder, + }) + + const issuerCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalReceived, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + role: CredentialRole.Issuer, + }) + + const credentialAttributes = [ + new CredentialPreviewAttribute({ name: 'name', value: 'John' }), + new CredentialPreviewAttribute({ name: 'age', value: '25' }), + ] + + // Set attributes on the credential record, this is normally done by the protocol service + holderCredentialRecord.credentialAttributes = credentialAttributes + issuerCredentialRecord.credentialAttributes = credentialAttributes + + // -------------------------------------------------------------------------------------------------------- + + const credential = new W3cCredential({ + context: [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2', + { + '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', + }, + ], + type: ['VerifiableCredential'], + issuer: issuerId, + issuanceDate: new Date().toISOString(), + credentialSubject: new W3cCredentialSubject({ claims: { name: 'John', age: '25' } }), + }) + + const { attachment: offerAttachment } = await dataIntegrityCredentialFormatService.createOffer(agentContext, { + credentialRecord: issuerCredentialRecord, + credentialFormats: { + dataIntegrity: { + bindingRequired: true, + credential, + anonCredsLinkSecretBinding: { + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryDefinitionId, + revocationRegistryIndex: revocable ? 1 : undefined, + }, + didCommSignedAttachmentBinding: {}, + }, + }, + }) + + // Holder processes and accepts offer + await dataIntegrityCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment, appendAttachments: requestAppendAttachments } = + await dataIntegrityCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + credentialFormats: { + dataIntegrity: { + dataModelVersion: '1.1', + anonCredsLinkSecret: { + linkSecretId: linkSecret.linkSecretId, + }, + }, + }, + }) + + // Make sure the request contains an entropy and does not contain a prover_did field + expect( + (requestAttachment.getDataAsJson() as DataIntegrityCredentialRequest).binding_proof?.anoncreds_link_secret?.entropy + ).toBeDefined() + expect((requestAttachment.getDataAsJson() as Record).prover_did).toBeUndefined() + + // Issuer processes and accepts request + await dataIntegrityCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await dataIntegrityCredentialFormatService.acceptRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + requestAppendAttachments, + credentialFormats: { dataIntegrity: {} }, + }) + + // Holder processes and accepts credential + await dataIntegrityCredentialFormatService.processCredential(agentContext, { + offerAttachment, + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) + + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, + ]) + + const credentialRecordId = holderCredentialRecord.credentials[0].credentialRecordId + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const credentialRecord = await w3cCredentialService.getCredentialRecordById(agentContext, credentialRecordId) + const credentialId = credentialRecord.id + + const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { + id: credentialId, + }) + + expect(anonCredsCredential).toEqual({ + credentialId, + attributes: { + age: 25, + name: 'John', + }, + linkSecretId: 'linkSecretId', + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: revocable ? revocationRegistryDefinitionId : null, + credentialRevocationId: revocable ? '1' : null, + methodName: 'inMemory', + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + const expectedCredentialMetadata = revocable + ? { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: revocationRegistryDefinitionId, + credentialRevocationId: '1', + } + : { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + } + expect(holderCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': expectedCredentialMetadata, + '_anoncreds/credentialRequest': { + link_secret_blinding_data: expect.any(Object), + link_secret_name: expect.any(String), + nonce: expect.any(String), + }, + }) + + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': expectedCredentialMetadata, + }) + + const holderProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalSent, + role: ProofRole.Prover, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + const verifierProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + role: ProofRole.Verifier, + state: ProofState.ProposalReceived, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + + const nrpRequestedTime = dateToTimestamp(new Date()) + + const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { + proofFormats: { + anoncreds: { + attributes: [ + { + name: 'name', + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + value: 'John', + referent: '1', + }, + ], + predicates: [ + { + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 18, + }, + ], + name: 'Proof Request', + version: '1.0', + nonRevokedInterval: { from: nrpRequestedTime, to: nrpRequestedTime }, + }, + }, + proofRecord: holderProofRecord, + }) + + await anoncredsProofFormatService.processProposal(agentContext, { + attachment: proofProposalAttachment, + proofRecord: verifierProofRecord, + }) + + const { attachment: proofRequestAttachment } = await anoncredsProofFormatService.acceptProposal(agentContext, { + proofRecord: verifierProofRecord, + proposalAttachment: proofProposalAttachment, + }) + + await anoncredsProofFormatService.processRequest(agentContext, { + attachment: proofRequestAttachment, + proofRecord: holderProofRecord, + }) + + const { attachment: proofAttachment } = await anoncredsProofFormatService.acceptRequest(agentContext, { + proofRecord: holderProofRecord, + requestAttachment: proofRequestAttachment, + proposalAttachment: proofProposalAttachment, + }) + + const isValid = await anoncredsProofFormatService.processPresentation(agentContext, { + attachment: proofAttachment, + proofRecord: verifierProofRecord, + requestAttachment: proofRequestAttachment, + }) + + expect(isValid).toBe(true) +} diff --git a/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts b/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts new file mode 100644 index 0000000000..6ed03f2abe --- /dev/null +++ b/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts @@ -0,0 +1,250 @@ +import type { CreateDidKidVerificationMethodReturn } from '../../core/tests' +import type { DidRepository } from '@credo-ts/core' + +import { + AgentContext, + CredentialExchangeRecord, + CredentialPreviewAttribute, + CredentialState, + DidResolverService, + DidsModuleConfig, + Ed25519Signature2018, + InjectionSymbols, + KeyDidRegistrar, + KeyDidResolver, + KeyType, + SignatureSuiteToken, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + W3cCredential, + W3cCredentialService, + W3cCredentialSubject, + W3cCredentialsModuleConfig, + CredentialRole, +} from '@credo-ts/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' +import { InMemoryWallet } from '../../../tests/InMemoryWallet' +import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' +import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { + agentDependencies, + getAgentConfig, + getAgentContext, + testLogger, + createDidKidVerificationMethod, +} from '../../core/tests' +import { + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsModuleConfig, + AnonCredsVerifierServiceSymbol, +} from '../src' +import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from '../src/anoncreds-rs' + +import { InMemoryTailsFileService } from './InMemoryTailsFileService' +import { anoncreds } from './helpers' + +const registry = new InMemoryAnonCredsRegistry() +const tailsFileService = new InMemoryTailsFileService() +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + anoncreds, + registries: [registry], + tailsFileService, +}) + +const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs') +const anonCredsVerifierService = new AnonCredsRsVerifierService() +const anonCredsHolderService = new AnonCredsRsHolderService() +const anonCredsIssuerService = new AnonCredsRsIssuerService() + +const inMemoryStorageService = new InMemoryStorageService() + +const didsModuleConfig = new DidsModuleConfig({ + registrars: [new KeyDidRegistrar()], + resolvers: [new KeyDidResolver()], +}) +const fileSystem = new agentDependencies.FileSystem() + +const wallet = new InMemoryWallet() + +const agentContext = getAgentContext({ + registerInstances: [ + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, fileSystem], + [InjectionSymbols.StorageService, inMemoryStorageService], + [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [InjectionSymbols.Logger, testLogger], + [DidsModuleConfig, didsModuleConfig], + [DidResolverService, new DidResolverService(testLogger, didsModuleConfig, {} as unknown as DidRepository)], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [ + SignatureSuiteToken, + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + verificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ], + keyTypes: [KeyType.Ed25519], + }, + ], + ], + agentConfig, + wallet, +}) + +agentContext.dependencyManager.registerInstance(AgentContext, agentContext) + +const dataIntegrityCredentialFormatService = new DataIntegrityCredentialFormatService() + +describe('data integrity format service (w3c)', () => { + let issuerKdv: CreateDidKidVerificationMethodReturn + let holderKdv: CreateDidKidVerificationMethodReturn + + beforeAll(async () => { + await wallet.createAndOpen(agentConfig.walletConfig) + + issuerKdv = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598g') + holderKdv = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598f') + }) + + afterEach(async () => { + inMemoryStorageService.contextCorrelationIdToRecords = {} + }) + + test('issuance and verification flow w3c starting from offer without negotiation and without revocation', async () => { + const holderCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + role: CredentialRole.Holder, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const issuerCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalReceived, + role: CredentialRole.Issuer, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const credentialAttributes = [ + new CredentialPreviewAttribute({ name: 'name', value: 'John' }), + new CredentialPreviewAttribute({ name: 'age', value: '25' }), + ] + + // Set attributes on the credential record, this is normally done by the protocol service + holderCredentialRecord.credentialAttributes = credentialAttributes + issuerCredentialRecord.credentialAttributes = credentialAttributes + + // -------------------------------------------------------------------------------------------------------- + + const credential = new W3cCredential({ + context: [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2', + { + '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', + }, + ], + type: ['VerifiableCredential'], + issuer: issuerKdv.did, + issuanceDate: new Date().toISOString(), + credentialSubject: new W3cCredentialSubject({ claims: { name: 'John', age: 25 } }), + }) + + const { attachment: offerAttachment } = await dataIntegrityCredentialFormatService.createOffer(agentContext, { + credentialRecord: issuerCredentialRecord, + credentialFormats: { + dataIntegrity: { + bindingRequired: true, + credential, + didCommSignedAttachmentBinding: {}, + }, + }, + }) + + // Holder processes and accepts offer + await dataIntegrityCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment, appendAttachments: requestAppendAttachments } = + await dataIntegrityCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + credentialFormats: { + dataIntegrity: { + didCommSignedAttachment: { + kid: holderKdv.kid, + }, + }, + }, + }) + + // Issuer processes and accepts request + await dataIntegrityCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await dataIntegrityCredentialFormatService.acceptRequest( + agentContext, + { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + requestAppendAttachments, + credentialFormats: { + dataIntegrity: { + credentialSubjectId: issuerKdv.did, + issuerVerificationMethod: issuerKdv.kid, + }, + }, + } + ) + + // Holder processes and accepts credential + await dataIntegrityCredentialFormatService.processCredential(agentContext, { + offerAttachment, + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) + + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, + ]) + + await expect( + anonCredsHolderService.getCredential(agentContext, { + id: holderCredentialRecord.id, + }) + ).rejects.toThrow() + + expect(holderCredentialRecord.metadata.data).toEqual({}) + expect(issuerCredentialRecord.metadata.data).toEqual({}) + + const credentialRecordId = holderCredentialRecord.credentials[0].credentialRecordId + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const credentialRecord = await w3cCredentialService.getCredentialRecordById(agentContext, credentialRecordId) + + expect(credentialRecord.credential).toEqual({ + ...{ + ...credential, + credentialSubject: new W3cCredentialSubject({ + id: issuerKdv.did, + claims: (credential.credentialSubject as W3cCredentialSubject).claims, + }), + }, + proof: expect.any(Object), + }) + }) +}) diff --git a/packages/anoncreds/tests/data-integrity-flow.test.ts b/packages/anoncreds/tests/data-integrity-flow.test.ts new file mode 100644 index 0000000000..6e16fa5b51 --- /dev/null +++ b/packages/anoncreds/tests/data-integrity-flow.test.ts @@ -0,0 +1,255 @@ +import type { CreateDidKidVerificationMethodReturn } from '../../core/tests' +import type { DidRepository } from '@credo-ts/core' + +import { + AgentContext, + CredentialExchangeRecord, + CredentialPreviewAttribute, + CredentialState, + DidResolverService, + DidsModuleConfig, + Ed25519Signature2018, + InjectionSymbols, + KeyDidRegistrar, + KeyDidResolver, + KeyType, + SignatureSuiteToken, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + W3cCredential, + W3cCredentialService, + W3cCredentialSubject, + W3cCredentialsModuleConfig, + CredentialRole, +} from '@credo-ts/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' +import { InMemoryWallet } from '../../../tests/InMemoryWallet' +import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' +import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { + agentDependencies, + createDidKidVerificationMethod, + getAgentConfig, + getAgentContext, + testLogger, +} from '../../core/tests' +import { + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsModuleConfig, + AnonCredsVerifierServiceSymbol, +} from '../src' +import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from '../src/anoncreds-rs' + +import { InMemoryTailsFileService } from './InMemoryTailsFileService' +import { anoncreds } from './helpers' + +const registry = new InMemoryAnonCredsRegistry() +const tailsFileService = new InMemoryTailsFileService() +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + anoncreds, + registries: [registry], + tailsFileService, +}) + +const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs') +const anonCredsVerifierService = new AnonCredsRsVerifierService() +const anonCredsHolderService = new AnonCredsRsHolderService() +const anonCredsIssuerService = new AnonCredsRsIssuerService() + +const inMemoryStorageService = new InMemoryStorageService() + +const didsModuleConfig = new DidsModuleConfig({ + registrars: [new KeyDidRegistrar()], + resolvers: [new KeyDidResolver()], +}) +const fileSystem = new agentDependencies.FileSystem() + +const wallet = new InMemoryWallet() + +const agentContext = getAgentContext({ + registerInstances: [ + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, fileSystem], + [InjectionSymbols.StorageService, inMemoryStorageService], + [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [InjectionSymbols.Logger, testLogger], + [DidsModuleConfig, didsModuleConfig], + [DidResolverService, new DidResolverService(testLogger, didsModuleConfig, {} as unknown as DidRepository)], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [ + SignatureSuiteToken, + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + verificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ], + keyTypes: [KeyType.Ed25519], + }, + ], + ], + agentConfig, + wallet, +}) + +agentContext.dependencyManager.registerInstance(AgentContext, agentContext) + +const dataIntegrityCredentialFormatService = new DataIntegrityCredentialFormatService() + +const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + +describe('data integrity format service (w3c)', () => { + let issuerKdv: CreateDidKidVerificationMethodReturn + let holderKdv: CreateDidKidVerificationMethodReturn + + beforeAll(async () => { + await wallet.createAndOpen(agentConfig.walletConfig) + + issuerKdv = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598g') + holderKdv = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598f') + }) + + afterEach(async () => { + inMemoryStorageService.contextCorrelationIdToRecords = {} + }) + + test('issuance and verification flow w3c starting from offer without negotiation and without revocation', async () => { + await anonCredsFlowTest({ issuerId: indyDid, revocable: false, issuerKdv: issuerKdv, holderKdv: holderKdv }) + }) +}) + +async function anonCredsFlowTest(options: { + issuerId: string + revocable: boolean + issuerKdv: CreateDidKidVerificationMethodReturn + holderKdv: CreateDidKidVerificationMethodReturn +}) { + const { issuerKdv: issuer } = options + + const holderCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + role: CredentialRole.Holder, + }) + + const issuerCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalReceived, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + role: CredentialRole.Issuer, + }) + + const credentialAttributes = [ + new CredentialPreviewAttribute({ name: 'name', value: 'John' }), + new CredentialPreviewAttribute({ name: 'age', value: '25' }), + ] + + // Set attributes on the credential record, this is normally done by the protocol service + holderCredentialRecord.credentialAttributes = credentialAttributes + issuerCredentialRecord.credentialAttributes = credentialAttributes + + // -------------------------------------------------------------------------------------------------------- + + const credential = new W3cCredential({ + context: [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2', + { + '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', + }, + ], + type: ['VerifiableCredential'], + issuer: issuer.did, + issuanceDate: new Date().toISOString(), + credentialSubject: new W3cCredentialSubject({ claims: { name: 'John', age: '25' } }), + }) + + const { attachment: offerAttachment } = await dataIntegrityCredentialFormatService.createOffer(agentContext, { + credentialRecord: issuerCredentialRecord, + credentialFormats: { + dataIntegrity: { + bindingRequired: false, + credential, + }, + }, + }) + + // Holder processes and accepts offer + await dataIntegrityCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment, appendAttachments: requestAppendAttachments } = + await dataIntegrityCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + credentialFormats: { + dataIntegrity: {}, + }, + }) + + // Issuer processes and accepts request + await dataIntegrityCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await dataIntegrityCredentialFormatService.acceptRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + requestAppendAttachments, + credentialFormats: { + dataIntegrity: { + credentialSubjectId: issuer.did, + }, + }, + }) + + // Holder processes and accepts credential + await dataIntegrityCredentialFormatService.processCredential(agentContext, { + offerAttachment, + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) + + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, + ]) + + await expect( + anonCredsHolderService.getCredential(agentContext, { + id: holderCredentialRecord.id, + }) + ).rejects.toThrow() + + expect(holderCredentialRecord.metadata.data).toEqual({}) + + expect(issuerCredentialRecord.metadata.data).toEqual({}) + + const credentialRecordId = holderCredentialRecord.credentials[0].credentialRecordId + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const credentialRecord = await w3cCredentialService.getCredentialRecordById(agentContext, credentialRecordId) + + expect(credentialRecord.credential).toEqual({ + ...{ + ...credential, + credentialSubject: new W3cCredentialSubject({ + id: issuer.did, + claims: (credential.credentialSubject as W3cCredentialSubject).claims, + }), + }, + proof: expect.any(Object), + }) +} diff --git a/packages/anoncreds/tests/fixtures/presentation-definition.ts b/packages/anoncreds/tests/fixtures/presentation-definition.ts new file mode 100644 index 0000000000..c7cc507e63 --- /dev/null +++ b/packages/anoncreds/tests/fixtures/presentation-definition.ts @@ -0,0 +1,50 @@ +import type { DifPresentationExchangeDefinitionV1 } from '@credo-ts/core' + +export const presentationDefinition: DifPresentationExchangeDefinitionV1 = { + id: '5591656f-5b5d-40f8-ab5c-9041c8e3a6a0', + name: 'Age Verification', + purpose: 'We need to verify your age before entering a bar', + input_descriptors: [ + { + id: 'age-verification', + name: 'A specific type of VC + Issuer', + purpose: 'We want a VC of this type generated by this issuer', + schema: [ + { + uri: 'https://www.w3.org/2018/credentials/v1', + }, + ], + constraints: { + limit_disclosure: 'required' as const, + statuses: { + active: { + directive: 'required' as const, + }, + }, + fields: [ + { + path: ['$.issuer'], + filter: { + type: 'string', + const: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL', + }, + }, + { + path: ['$.credentialSubject.name'], + }, + { + path: ['$.credentialSubject.height'], + }, + { + path: ['$.credentialSubject.age'], + predicate: 'preferred' as const, + filter: { + type: 'number', + minimum: 18, + }, + }, + ], + }, + }, + ], +} diff --git a/packages/anoncreds/tests/helpers.ts b/packages/anoncreds/tests/helpers.ts new file mode 100644 index 0000000000..8ad57fcdc0 --- /dev/null +++ b/packages/anoncreds/tests/helpers.ts @@ -0,0 +1 @@ +export { anoncreds } from '@hyperledger/anoncreds-nodejs' diff --git a/packages/anoncreds/tests/indy-flow.test.ts b/packages/anoncreds/tests/indy-flow.test.ts new file mode 100644 index 0000000000..08dac4a6c0 --- /dev/null +++ b/packages/anoncreds/tests/indy-flow.test.ts @@ -0,0 +1,397 @@ +import type { AnonCredsCredentialRequest } from '@credo-ts/anoncreds' +import type { DidRepository, Wallet } from '@credo-ts/core' + +import { + CredentialRole, + ProofRole, + CredentialState, + CredentialExchangeRecord, + CredentialPreviewAttribute, + InjectionSymbols, + ProofState, + ProofExchangeRecord, + SignatureSuiteToken, + W3cCredentialsModuleConfig, + DidResolverService, + DidsModuleConfig, +} from '@credo-ts/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' +import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { agentDependencies, getAgentConfig, getAgentContext, testLogger } from '../../core/tests' +import { AnonCredsRsVerifierService, AnonCredsRsIssuerService, AnonCredsRsHolderService } from '../src/anoncreds-rs' + +import { anoncreds } from './helpers' + +import { + getUnqualifiedSchemaId, + parseIndySchemaId, + getUnqualifiedCredentialDefinitionId, + parseIndyCredentialDefinitionId, + AnonCredsModuleConfig, + LegacyIndyCredentialFormatService, + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsVerifierServiceSymbol, + AnonCredsSchemaRecord, + AnonCredsSchemaRepository, + AnonCredsCredentialDefinitionRepository, + AnonCredsCredentialDefinitionRecord, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsCredentialDefinitionPrivateRecord, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsKeyCorrectnessProofRecord, + AnonCredsLinkSecretRepository, + AnonCredsLinkSecretRecord, + LegacyIndyProofFormatService, +} from '@credo-ts/anoncreds' + +const registry = new InMemoryAnonCredsRegistry() +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + registries: [registry], + anoncreds, +}) + +const agentConfig = getAgentConfig('LegacyIndyCredentialFormatService using anoncreds-rs') +const anonCredsVerifierService = new AnonCredsRsVerifierService() +const anonCredsHolderService = new AnonCredsRsHolderService() +const anonCredsIssuerService = new AnonCredsRsIssuerService() + +const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123') } as Wallet + +const inMemoryStorageService = new InMemoryStorageService() +const agentContext = getAgentContext({ + registerInstances: [ + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.StorageService, inMemoryStorageService], + [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [DidResolverService, new DidResolverService(testLogger, new DidsModuleConfig(), {} as unknown as DidRepository)], + [InjectionSymbols.Logger, testLogger], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + [SignatureSuiteToken, 'default'], + ], + agentConfig, + wallet, +}) + +const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService() +const legacyIndyProofFormatService = new LegacyIndyProofFormatService() + +// This is just so we don't have to register an actually indy did (as we don't have the indy did registrar configured) +const indyDid = 'did:indy:bcovrin:test:LjgpST2rjsoxYegQDRm7EL' + +describe('Legacy indy format services using anoncreds-rs', () => { + test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => { + const schema = await anonCredsIssuerService.createSchema(agentContext, { + attrNames: ['name', 'age'], + issuerId: indyDid, + name: 'Employee Credential', + version: '1.0.0', + }) + + const { schemaState } = await registry.registerSchema(agentContext, { + schema, + options: {}, + }) + + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await anonCredsIssuerService.createCredentialDefinition(agentContext, { + issuerId: indyDid, + schemaId: schemaState.schemaId as string, + schema, + tag: 'Employee Credential', + supportRevocation: false, + }) + + const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition, + options: {}, + }) + + if ( + !credentialDefinitionState.credentialDefinition || + !credentialDefinitionState.credentialDefinitionId || + !schemaState.schema || + !schemaState.schemaId + ) { + throw new Error('Failed to create schema or credential definition') + } + + if ( + !credentialDefinitionState.credentialDefinition || + !credentialDefinitionState.credentialDefinitionId || + !schemaState.schema || + !schemaState.schemaId + ) { + throw new Error('Failed to create schema or credential definition') + } + + if (!credentialDefinitionPrivate || !keyCorrectnessProof) { + throw new Error('Failed to get private part of credential definition') + } + + await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( + agentContext, + new AnonCredsSchemaRecord({ + schema: schemaState.schema, + schemaId: schemaState.schemaId, + methodName: 'inMemory', + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition: credentialDefinitionState.credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + methodName: 'inMemory', + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( + agentContext, + new AnonCredsCredentialDefinitionPrivateRecord({ + value: credentialDefinitionPrivate, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( + agentContext, + new AnonCredsKeyCorrectnessProofRecord({ + value: keyCorrectnessProof, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' }) + expect(linkSecret.linkSecretId).toBe('linkSecretId') + + await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save( + agentContext, + new AnonCredsLinkSecretRecord({ + value: linkSecret.linkSecretValue, + linkSecretId: linkSecret.linkSecretId, + }) + ) + + const holderCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + role: CredentialRole.Holder, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const issuerCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalReceived, + role: CredentialRole.Issuer, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const credentialAttributes = [ + new CredentialPreviewAttribute({ + name: 'name', + value: 'John', + }), + new CredentialPreviewAttribute({ + name: 'age', + value: '25', + }), + ] + + const parsedCredentialDefinition = parseIndyCredentialDefinitionId(credentialDefinitionState.credentialDefinitionId) + const unqualifiedCredentialDefinitionId = getUnqualifiedCredentialDefinitionId( + parsedCredentialDefinition.namespaceIdentifier, + parsedCredentialDefinition.schemaSeqNo, + parsedCredentialDefinition.tag + ) + + const parsedSchemaId = parseIndySchemaId(schemaState.schemaId) + const unqualifiedSchemaId = getUnqualifiedSchemaId( + parsedSchemaId.namespaceIdentifier, + parsedSchemaId.schemaName, + parsedSchemaId.schemaVersion + ) + + // Holder creates proposal + holderCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: proposalAttachment } = await legacyIndyCredentialFormatService.createProposal(agentContext, { + credentialRecord: holderCredentialRecord, + credentialFormats: { + indy: { + attributes: credentialAttributes, + credentialDefinitionId: unqualifiedCredentialDefinitionId, + }, + }, + }) + + // Issuer processes and accepts proposal + await legacyIndyCredentialFormatService.processProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: proposalAttachment, + }) + // Set attributes on the credential record, this is normally done by the protocol service + issuerCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: offerAttachment } = await legacyIndyCredentialFormatService.acceptProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + proposalAttachment: proposalAttachment, + }) + + // Holder processes and accepts offer + await legacyIndyCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment } = await legacyIndyCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + credentialFormats: { + indy: { + linkSecretId: linkSecret.linkSecretId, + }, + }, + }) + + // Make sure the request contains a prover_did field + expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeDefined() + + // Issuer processes and accepts request + await legacyIndyCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await legacyIndyCredentialFormatService.acceptRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + }) + + // Holder processes and accepts credential + await legacyIndyCredentialFormatService.processCredential(agentContext, { + offerAttachment, + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) + + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, + ]) + + const credentialId = holderCredentialRecord.credentials[0].credentialRecordId + const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { + id: credentialId, + }) + + expect(anonCredsCredential).toEqual({ + credentialId, + attributes: { + age: 25, + name: 'John', + }, + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: null, + credentialRevocationId: null, + methodName: 'inMemory', + linkSecretId: 'linkSecretId', + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + expect(holderCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': { + schemaId: unqualifiedSchemaId, + credentialDefinitionId: unqualifiedCredentialDefinitionId, + }, + '_anoncreds/credentialRequest': { + link_secret_blinding_data: expect.any(Object), + link_secret_name: expect.any(String), + nonce: expect.any(String), + }, + }) + + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': { + schemaId: unqualifiedSchemaId, + credentialDefinitionId: unqualifiedCredentialDefinitionId, + }, + }) + + const holderProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalSent, + role: ProofRole.Prover, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + const verifierProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalReceived, + role: ProofRole.Verifier, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + + const { attachment: proofProposalAttachment } = await legacyIndyProofFormatService.createProposal(agentContext, { + proofFormats: { + indy: { + attributes: [ + { + name: 'name', + credentialDefinitionId: unqualifiedCredentialDefinitionId, + value: 'John', + referent: '1', + }, + ], + predicates: [ + { + credentialDefinitionId: unqualifiedCredentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 18, + }, + ], + name: 'Proof Request', + version: '1.0', + }, + }, + proofRecord: holderProofRecord, + }) + + await legacyIndyProofFormatService.processProposal(agentContext, { + attachment: proofProposalAttachment, + proofRecord: verifierProofRecord, + }) + + const { attachment: proofRequestAttachment } = await legacyIndyProofFormatService.acceptProposal(agentContext, { + proofRecord: verifierProofRecord, + proposalAttachment: proofProposalAttachment, + }) + + await legacyIndyProofFormatService.processRequest(agentContext, { + attachment: proofRequestAttachment, + proofRecord: holderProofRecord, + }) + + const { attachment: proofAttachment } = await legacyIndyProofFormatService.acceptRequest(agentContext, { + proofRecord: holderProofRecord, + requestAttachment: proofRequestAttachment, + proposalAttachment: proofProposalAttachment, + }) + + const isValid = await legacyIndyProofFormatService.processPresentation(agentContext, { + attachment: proofAttachment, + proofRecord: verifierProofRecord, + requestAttachment: proofRequestAttachment, + }) + + expect(isValid).toBe(true) + }) +}) diff --git a/packages/anoncreds/tests/legacyAnonCredsSetup.ts b/packages/anoncreds/tests/legacyAnonCredsSetup.ts new file mode 100644 index 0000000000..41111b14bc --- /dev/null +++ b/packages/anoncreds/tests/legacyAnonCredsSetup.ts @@ -0,0 +1,518 @@ +import type { PreCreatedAnonCredsDefinition } from './preCreatedAnonCredsDefinition' +import type { EventReplaySubject } from '../../core/tests' +import type { + AnonCredsRegisterCredentialDefinitionOptions, + AnonCredsRequestedAttribute, + AnonCredsRequestedPredicate, + AnonCredsOfferCredentialFormat, + AnonCredsSchema, + RegisterCredentialDefinitionReturnStateFinished, + RegisterSchemaReturnStateFinished, +} from '../src' +import type { AutoAcceptProof, ConnectionRecord } from '@credo-ts/core' + +import { + AgentEventTypes, + TypedArrayEncoder, + CacheModule, + InMemoryLruCache, + Agent, + CredoError, + AutoAcceptCredential, + CredentialEventTypes, + CredentialsModule, + CredentialState, + ProofEventTypes, + ProofsModule, + ProofState, + V2CredentialProtocol, + V2ProofProtocol, + DidsModule, +} from '@credo-ts/core' +import { randomUUID } from 'crypto' + +import { sleep } from '../../core/src/utils/sleep' +import { setupSubjectTransports, setupEventReplaySubjects } from '../../core/tests' +import { + getInMemoryAgentOptions, + importExistingIndyDidFromPrivateKey, + makeConnection, + publicDidSeed, + waitForCredentialRecordSubject, + waitForProofExchangeRecordSubject, +} from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' +import { + IndyVdrAnonCredsRegistry, + IndyVdrSovDidResolver, + IndyVdrModule, + IndyVdrIndyDidResolver, + IndyVdrIndyDidRegistrar, +} from '../../indy-vdr/src' +import { indyVdrModuleConfig } from '../../indy-vdr/tests/helpers' +import { + AnonCredsCredentialFormatService, + AnonCredsProofFormatService, + getUnqualifiedCredentialDefinitionId, + getUnqualifiedSchemaId, + parseIndyCredentialDefinitionId, + parseIndySchemaId, + V1CredentialProtocol, + V1ProofProtocol, + AnonCredsModule, + LegacyIndyCredentialFormatService, + LegacyIndyProofFormatService, +} from '../src' + +import { InMemoryAnonCredsRegistry } from './InMemoryAnonCredsRegistry' +import { anoncreds } from './helpers' +import { + anoncredsDefinitionFourAttributesNoRevocation, + storePreCreatedAnonCredsDefinition, +} from './preCreatedAnonCredsDefinition' + +// Helper type to get the type of the agents (with the custom modules) for the credential tests +export type AnonCredsTestsAgent = Agent< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ReturnType & { mediationRecipient?: any; mediator?: any } +> + +export const getAnonCredsIndyModules = ({ + autoAcceptCredentials, + autoAcceptProofs, +}: { + autoAcceptCredentials?: AutoAcceptCredential + autoAcceptProofs?: AutoAcceptProof +} = {}) => { + // Add support for resolving pre-created credential definitions and schemas + const inMemoryAnonCredsRegistry = new InMemoryAnonCredsRegistry({ + existingCredentialDefinitions: { + [anoncredsDefinitionFourAttributesNoRevocation.credentialDefinitionId]: + anoncredsDefinitionFourAttributesNoRevocation.credentialDefinition, + }, + existingSchemas: { + [anoncredsDefinitionFourAttributesNoRevocation.schemaId]: anoncredsDefinitionFourAttributesNoRevocation.schema, + }, + }) + + const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService() + const legacyIndyProofFormatService = new LegacyIndyProofFormatService() + + const modules = { + credentials: new CredentialsModule({ + autoAcceptCredentials, + credentialProtocols: [ + new V1CredentialProtocol({ + indyCredentialFormat: legacyIndyCredentialFormatService, + }), + new V2CredentialProtocol({ + credentialFormats: [legacyIndyCredentialFormatService, new AnonCredsCredentialFormatService()], + }), + ], + }), + proofs: new ProofsModule({ + autoAcceptProofs, + proofProtocols: [ + new V1ProofProtocol({ + indyProofFormat: legacyIndyProofFormatService, + }), + new V2ProofProtocol({ + proofFormats: [legacyIndyProofFormatService, new AnonCredsProofFormatService()], + }), + ], + }), + anoncreds: new AnonCredsModule({ + registries: [new IndyVdrAnonCredsRegistry(), inMemoryAnonCredsRegistry], + anoncreds, + }), + indyVdr: new IndyVdrModule(indyVdrModuleConfig), + dids: new DidsModule({ + resolvers: [new IndyVdrSovDidResolver(), new IndyVdrIndyDidResolver()], + registrars: [new IndyVdrIndyDidRegistrar()], + }), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 100 }), + }), + } as const + + return modules +} + +export async function presentLegacyAnonCredsProof({ + verifierAgent, + verifierReplay, + + holderAgent, + holderReplay, + + verifierHolderConnectionId, + + request: { attributes, predicates }, +}: { + holderAgent: AnonCredsTestsAgent + holderReplay: EventReplaySubject + + verifierAgent: AnonCredsTestsAgent + verifierReplay: EventReplaySubject + + verifierHolderConnectionId: string + request: { + attributes?: Record + predicates?: Record + } +}) { + let holderProofExchangeRecordPromise = waitForProofExchangeRecordSubject(holderReplay, { + state: ProofState.RequestReceived, + }) + + let verifierProofExchangeRecord = await verifierAgent.proofs.requestProof({ + connectionId: verifierHolderConnectionId, + proofFormats: { + indy: { + name: 'Test Proof Request', + requested_attributes: attributes, + requested_predicates: predicates, + version: '1.0', + }, + }, + protocolVersion: 'v2', + }) + + let holderProofExchangeRecord = await holderProofExchangeRecordPromise + + const selectedCredentials = await holderAgent.proofs.selectCredentialsForRequest({ + proofRecordId: holderProofExchangeRecord.id, + }) + + const verifierProofExchangeRecordPromise = waitForProofExchangeRecordSubject(verifierReplay, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await holderAgent.proofs.acceptRequest({ + proofRecordId: holderProofExchangeRecord.id, + proofFormats: { indy: selectedCredentials.proofFormats.indy }, + }) + + verifierProofExchangeRecord = await verifierProofExchangeRecordPromise + + // assert presentation is valid + expect(verifierProofExchangeRecord.isVerified).toBe(true) + + holderProofExchangeRecordPromise = waitForProofExchangeRecordSubject(holderReplay, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + verifierProofExchangeRecord = await verifierAgent.proofs.acceptPresentation({ + proofRecordId: verifierProofExchangeRecord.id, + }) + holderProofExchangeRecord = await holderProofExchangeRecordPromise + + return { + verifierProofExchangeRecord, + holderProofExchangeRecord, + } +} + +export async function issueLegacyAnonCredsCredential({ + issuerAgent, + issuerReplay, + + holderAgent, + holderReplay, + + issuerHolderConnectionId, + offer, +}: { + issuerAgent: AnonCredsTestsAgent + issuerReplay: EventReplaySubject + + holderAgent: AnonCredsTestsAgent + holderReplay: EventReplaySubject + + issuerHolderConnectionId: string + offer: AnonCredsOfferCredentialFormat +}) { + let issuerCredentialExchangeRecord = await issuerAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: issuerHolderConnectionId, + protocolVersion: 'v1', + credentialFormats: { + indy: offer, + }, + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + }) + + let holderCredentialExchangeRecord = await waitForCredentialRecordSubject(holderReplay, { + threadId: issuerCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + await holderAgent.credentials.acceptOffer({ + credentialRecordId: holderCredentialExchangeRecord.id, + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + }) + + // Because we use auto-accept it can take a while to have the whole credential flow finished + // Both parties need to interact with the ledger and sign/verify the credential + holderCredentialExchangeRecord = await waitForCredentialRecordSubject(holderReplay, { + threadId: issuerCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + issuerCredentialExchangeRecord = await waitForCredentialRecordSubject(issuerReplay, { + threadId: issuerCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + + return { + issuerCredentialExchangeRecord, + holderCredentialExchangeRecord, + } +} + +interface SetupAnonCredsTestsReturn { + issuerAgent: AnonCredsTestsAgent + issuerReplay: EventReplaySubject + + holderAgent: AnonCredsTestsAgent + holderReplay: EventReplaySubject + + issuerHolderConnectionId: CreateConnections extends true ? string : undefined + holderIssuerConnectionId: CreateConnections extends true ? string : undefined + + verifierHolderConnectionId: CreateConnections extends true + ? VerifierName extends string + ? string + : undefined + : undefined + holderVerifierConnectionId: CreateConnections extends true + ? VerifierName extends string + ? string + : undefined + : undefined + + verifierAgent: VerifierName extends string ? AnonCredsTestsAgent : undefined + verifierReplay: VerifierName extends string ? EventReplaySubject : undefined + + schemaId: string + credentialDefinitionId: string +} + +export async function setupAnonCredsTests< + VerifierName extends string | undefined = undefined, + CreateConnections extends boolean = true +>({ + issuerName, + holderName, + verifierName, + autoAcceptCredentials, + autoAcceptProofs, + attributeNames, + preCreatedDefinition, + createConnections, +}: { + issuerName: string + holderName: string + verifierName?: VerifierName + autoAcceptCredentials?: AutoAcceptCredential + autoAcceptProofs?: AutoAcceptProof + attributeNames?: string[] + preCreatedDefinition?: PreCreatedAnonCredsDefinition + createConnections?: CreateConnections +}): Promise> { + const issuerAgent = new Agent( + getInMemoryAgentOptions( + issuerName, + { + endpoints: ['rxjs:issuer'], + }, + getAnonCredsIndyModules({ + autoAcceptCredentials, + autoAcceptProofs, + }) + ) + ) + + const holderAgent = new Agent( + getInMemoryAgentOptions( + holderName, + { + endpoints: ['rxjs:holder'], + }, + getAnonCredsIndyModules({ + autoAcceptCredentials, + autoAcceptProofs, + }) + ) + ) + + const verifierAgent = verifierName + ? new Agent( + getInMemoryAgentOptions( + verifierName, + { + endpoints: ['rxjs:verifier'], + }, + getAnonCredsIndyModules({ + autoAcceptCredentials, + autoAcceptProofs, + }) + ) + ) + : undefined + + setupSubjectTransports(verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent]) + const [issuerReplay, holderReplay, verifierReplay] = setupEventReplaySubjects( + verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent], + [ + CredentialEventTypes.CredentialStateChanged, + ProofEventTypes.ProofStateChanged, + AgentEventTypes.AgentMessageProcessed, + ] + ) + + await issuerAgent.initialize() + await holderAgent.initialize() + if (verifierAgent) await verifierAgent.initialize() + + let credentialDefinitionId: string + let schemaId: string + + if (attributeNames) { + const result = await prepareForAnonCredsIssuance(issuerAgent, { + attributeNames, + }) + schemaId = result.schema.schemaId + credentialDefinitionId = result.credentialDefinition.credentialDefinitionId + } else if (preCreatedDefinition) { + const result = await storePreCreatedAnonCredsDefinition(issuerAgent, preCreatedDefinition) + schemaId = result.schemaId + credentialDefinitionId = result.credentialDefinitionId + } else { + throw new CredoError('Either attributeNames or preCreatedDefinition must be provided') + } + + let issuerHolderConnection: ConnectionRecord | undefined + let holderIssuerConnection: ConnectionRecord | undefined + let verifierHolderConnection: ConnectionRecord | undefined + let holderVerifierConnection: ConnectionRecord | undefined + + if (createConnections ?? true) { + ;[issuerHolderConnection, holderIssuerConnection] = await makeConnection(issuerAgent, holderAgent) + + if (verifierAgent) { + ;[holderVerifierConnection, verifierHolderConnection] = await makeConnection(holderAgent, verifierAgent) + } + } + + return { + issuerAgent, + issuerReplay, + + holderAgent, + holderReplay, + + verifierAgent: verifierName ? verifierAgent : undefined, + verifierReplay: verifierName ? verifierReplay : undefined, + + credentialDefinitionId, + schemaId, + + issuerHolderConnectionId: issuerHolderConnection?.id, + holderIssuerConnectionId: holderIssuerConnection?.id, + holderVerifierConnectionId: holderVerifierConnection?.id, + verifierHolderConnectionId: verifierHolderConnection?.id, + } as unknown as SetupAnonCredsTestsReturn +} + +export async function prepareForAnonCredsIssuance(agent: Agent, { attributeNames }: { attributeNames: string[] }) { + // Add existing endorser did to the wallet + const unqualifiedDid = await importExistingIndyDidFromPrivateKey(agent, TypedArrayEncoder.fromString(publicDidSeed)) + const didIndyDid = `did:indy:pool:localtest:${unqualifiedDid}` + + const schema = await registerSchema(agent, { + // TODO: update attrNames to attributeNames + attrNames: attributeNames, + name: `Schema ${randomUUID()}`, + version: '1.0', + issuerId: didIndyDid, + }) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + + const credentialDefinition = await registerCredentialDefinition(agent, { + schemaId: schema.schemaId, + issuerId: didIndyDid, + tag: 'default', + }) + + const s = parseIndySchemaId(schema.schemaId) + const cd = parseIndyCredentialDefinitionId(credentialDefinition.credentialDefinitionId) + + const legacySchemaId = getUnqualifiedSchemaId(s.namespaceIdentifier, s.schemaName, s.schemaVersion) + const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId( + cd.namespaceIdentifier, + cd.schemaSeqNo, + cd.tag + ) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + + // NOTE: we return the legacy schema and credential definition ids here because that's what currently expected + // in all tests. If we also support did:indy in tests we probably want to return the qualified identifiers here + // and transform them to the legacy variant in the specific tests that need it. + return { + schema: { + ...schema, + schemaId: legacySchemaId, + }, + credentialDefinition: { + ...credentialDefinition, + credentialDefinitionId: legacyCredentialDefinitionId, + }, + } +} + +async function registerSchema( + agent: AnonCredsTestsAgent, + schema: AnonCredsSchema +): Promise { + const { schemaState } = await agent.modules.anoncreds.registerSchema({ + schema, + options: {}, + }) + + testLogger.test(`created schema with id ${schemaState.schemaId}`, schema) + + if (schemaState.state !== 'finished') { + throw new CredoError(`Schema not created: ${schemaState.state === 'failed' ? schemaState.reason : 'Not finished'}`) + } + + return schemaState +} + +async function registerCredentialDefinition( + agent: AnonCredsTestsAgent, + credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions, + supportRevocation?: boolean +): Promise { + const { credentialDefinitionState } = await agent.modules.anoncreds.registerCredentialDefinition({ + credentialDefinition, + options: { + supportRevocation: supportRevocation ?? false, + }, + }) + + if (credentialDefinitionState.state !== 'finished') { + throw new CredoError( + `Credential definition not created: ${ + credentialDefinitionState.state === 'failed' ? credentialDefinitionState.reason : 'Not finished' + }` + ) + } + + return credentialDefinitionState +} diff --git a/packages/anoncreds/tests/preCreatedAnonCredsDefinition.ts b/packages/anoncreds/tests/preCreatedAnonCredsDefinition.ts new file mode 100644 index 0000000000..82a2b94da0 --- /dev/null +++ b/packages/anoncreds/tests/preCreatedAnonCredsDefinition.ts @@ -0,0 +1,151 @@ +import type { AnonCredsSchema, AnonCredsCredentialDefinition } from '../src' +import type { Agent } from '@credo-ts/core' + +import { + AnonCredsCredentialDefinitionRepository, + AnonCredsCredentialDefinitionRecord, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsCredentialDefinitionPrivateRecord, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsKeyCorrectnessProofRecord, + AnonCredsSchemaRepository, + AnonCredsSchemaRecord, +} from '../src' + +export interface PreCreatedAnonCredsDefinition { + issuerId: string + schemaId: string + credentialDefinitionId: string + schema: AnonCredsSchema + credentialDefinition: AnonCredsCredentialDefinition + credentialDefinitionPrivate: Record + keyCorrectnessProof: Record +} + +export const anoncredsDefinitionFourAttributesNoRevocation = { + issuerId: 'credo:local', + schemaId: 'credo:local:schema', + schema: { + attrNames: ['name', 'age', 'x-ray', 'profile_picture'], + issuerId: 'credo:local', + name: 'Schema Name', + version: '1.0', + }, + credentialDefinitionId: 'credo:local:credential-definition', + credentialDefinition: { + schemaId: 'credo:local:schema', + type: 'CL', + tag: 'tag', + value: { + primary: { + n: '87842344067580909966277408444363440558764291045314580602104423178157687650168574137515783083011596682498205341822521253001991710247346859334528810180859983473791435685332381644914577026991714393041281837694829666531714569304773468184162608774660276917673532378461406279371224896886846757348160147731788770644133959498002831709391509696265005727328463680120246974166261055761394275422694865569909032698710268748570183248323124218946845244915570543716801261678515813121446288449349011867607329497946065954885721231542422629361446815131914336924013877550864220205030969961519116440923889700246952520655194088422125641621', + s: '69585933262049989531552712771544223829616331466295327156038080298722180189374070456516482840961622801020714723494443598718745756648244383057833250571946737722223613081188606410505601567970530798276213073638873887801576633737868713019853656225867097144558635377369757055840931920709157858725646296685147267826571256007722846364081499274250433775662711717110199314409042413031855249695928954762979834910796160814589353096600506122969291286757421268618838169683166928720905974358968632051252055910993324549284729085615194481093897573541250546454949119655123676361798100569282690584814756985213731833425369900477564979010', + r: { + profile_picture: + '56575128683798674694438915817479505443743253337801397920821780178659644249475525978455616538902613196717576377419185983361806558271595964937588266097198383747557742626643122298452847892849778439106198459852214334460776178227901022491273387950441891641330412827697345769419469610734610488537867845288260941307437489599714625775586367861738296058442092320855137842657268859685095728158784715360154463805698324194129102154806751808438104129971793369492026667323536357562703685229629441284202554058484480214222025106615592167554775568575807508278963155108517731786963397858030485522424658829639677976590390820169869073200', + 'x-ray': + '4429491840836206101003994233508864012187564382413928818895671069835065680533992160781470074219852773674696334347362459463897817219052750340897702193572499182977701668625398683658991805121424674136311299283016970316544187267054289569198546773215031479482260322544654957142884459184428027854940027752155612590587642516903083262229065472491814384702133496471330486467581778716334335275346258314295575233074657145562994842354594364047727685371274464137633746596641017341209491890389971323547549608198696951588751716542219618047045046922380841088581474440360383774049991712109718443615259751685964351471346156235272121244', + age: '34464133915990848076172956986942790781564627267163857153755401938865593706180491775035527094432152851977858974574438100253851735462548728758284653574676507290567672357719893117401804649043379260178019838001420596961699582539180381743668120346145550456444068023413485537682825946192917596284529912243176212005565510377698806948449760886743104231020718561242445300457942540103693783088262177855227891113931334056884150062880312049705115476533946093557482787375937331361689485176624207175023697386475938329263586935943665574075798962270178437766379821708316285530080529980490389839947669325266452879229582371214499012612', + master_secret: + '38372429610741422406199714249213246366906343751294294829694391582953760720225588703520388344373098717107657040147604613864354784177681288995876048599409914278200777777200222902865497493798844582456490640345000578595600902579455307860530099030170134673330079320158678476576861732706236304285512340355557179781084190373672642598913227538397256170869018401601754146174357843016398178664550573525049099758448789892809123809991955608230329192029689201174442017530466627285120998377323070166778742674557849952740275274425090754321956808458556133902289044636934452616072457362174599696240244163958071489757167161583287933012', + name: '41014013584868046940700297249654754572274594835779951534978780509396977042415950946284051956455879784546208251194317042990838479458078414271066475371475719632527405306934369332919756609109089802754604614447544303040670620162431770312811111420364343596350095891193740183438610947019059605080151572169544146741323014803750372894372300201338270963588484184290460332439690109198731291013339195198123927997192050560232040532857696388283943458200564285681697989059451816012798203849458951987721802358355697798416266835904531418404697914286845661698786368333652847985082209238280143342495672395631692765298272162995797569740', + }, + rctxt: + '16464722301641371206795881649865961791391119369238144262351376759998547325193125958178365261014077529290877201871073338238175718751512634970198926263007056342978990689969456392692111606890857863410151739614242584205255933593176032937508159365740280803585540161311765279412187853100312368091670201051445665979580414179722155511904119291267556670898349452934114060252844746709342947092347639967380788646634895927915396884037865675737326202375569733497988678199054469925503315464827635110290483873502384431508621395360462662144739042917561805592212382743183474314648949079506764741523628476697466886348814972165047593874', + z: '5086587237066022122592240466639759759197483459008844919556388558833089663388745591506920438478975788937430639765987249898508923678351803973966395842087396772473869807319134567545547452734083776130923170509623388183986706092631690714781941282838024654249896893122657288906270700076463278050469772885634503867932830785454843583497077679797341553636350691461767337121931803370579640775369014088566460120702115889992200717701648077014995999075705835588872372779039259166655013491756409300211842297643755762635305221446843135791869943423621935351723204484939009848795467272824067020396930592203947811218809406908148715719', + }, + }, + issuerId: 'credo:local', + } as const, + credentialDefinitionPrivate: { + value: { + p_key: { + p: '153106379864000571148244129393644359652915648228922865704034717848201257573711975410842249776774340282707070699188357941704419065506879869153368776685089864574213027860744665885495451570312654666553400664817638055640959493090117044573909597544451007424783554709810971978462054687564122167163008486609395189323', + q: '143433513589715225570424868730460814186295248488418046236140906261945486526592961949317089992204995919813207126151675742651793874217437404477734553450968322381792085701168997166274362068275271480837971411849776482330563165130630493727303434368852578442453996929162141918876892637411876828819200099016338341721', + }, + r_key: null, + }, + }, + keyCorrectnessProof: { + c: '72621053745018163420031916023482785072816409059198217131015916736131767413657', + xz_cap: + '850274881578391934591661770499447911869154800148299500263874916050779445823447102668092153433728656375015765761354432219755520089911283859886666553084120210133905470067151388083301645251915026254235638163102175440836675166365116199189847928985785462952520996043755101505464140075542527760998161806736989296142694393573067235176383286326575951725644122034993819063094875012621492390763412422817125174356572029013077801862332805795215683753519579718920477624121202644930350367215214542463800793998640126556548189444688653202497435188567796702113879020747139076602410012948098824390435197929923803354277522109510375297135339425621753996919972568739435217128936644723583682301802635324212980800874', + xr_cap: [ + [ + 'x-ray', + '1100296278367416126114512007086000487210173672004285027745100265192134765694126348119800071804546191417833683034536597650783498444560501462357858121914203811865896262443584339684237285664028704415483426358202274120021100901360331693230326292769872930453947863205588147511391283413052111942623639754543386209030635141994688312217536019020434243095546229287795946750908911413788650350211962712639212413953123460971905722732358302610201494880492534115487591159817033168864766781293911971682776048128895972688381669965782573233335977210686193312708855557396035067558877812116840533952924116218659976940056925006065917183351521932413688739706531845759800108145273991896265259789151250986616381633223', + ], + [ + 'name', + '1151677146096660866229552030352179694035972595978702233335395687975006128403978939413169006246467335399458408482394131802327576122988237637405268441421343190774792801291831329097751565170336068794696772883820209892850219009734815871907725112585278083984281867652458685739586906528980871128998071164801243747269857073422085222151542159127826388237354141299032527656470932267274251023405017003550519552456930893356865784696302301996513660338414169071292078781459119334933746561120664611210295497231397948470931390986914679772196687087522240646219460292074148552770482278355388395737796681110209931199929480806706476777308556698464749631817995690906500982756261833089324624250299274269203350576182', + ], + [ + 'age', + '490485849555111356820439691896438489296519861720204752410186712625678203967649200109967435279584870327955651453267624288059390155543267829501248331008678585263675306357830624934178898869634777512470682895534391362729336070049260786094475609562591798306834498553387461357110318139093950991659181990010523915675533751860187808749896829933819117909279010917924695409599480557618560496347703841620915234353355901053204801409130456492624298763824434885338283717904287280072713671539844965263596814324792626092256082253290381738006043522929067906193260006532376530286161685137964855828105499752570878571791182869523102057556030682287740260687838931733614377639874207845451747064495433207201653436715', + ], + [ + 'master_secret', + '1318990044998363713649979253597193456453512785324186865141988049557895679782044929876662884599903069485547717612091908694136951560866463941672407934301199186282931811123963068701617563429544022224055681047519232144418208799948967337304668359638698743414280448724794600856698091905123534218737499559671809012990700276270677528238440369911518651808138160431877863981128424537730654234049052437112784605883147588845015206335914422630545340954573816850852796391448853146860525197976441360984340677756102030083690859325863848596316176884255652393746688428251045270549704171014813881851678909934504072584622734014858589268155080014295398398457298272419060536229446589951707101217585014491428076518636', + ], + [ + 'profile_picture', + '626128909514031861048611274299543488396178512804450237734576430023887274743970983888968738560062281447232501644235732190657881416295947662076218443595968416492300135536820604684825587002869836933465348240667269380721505682854695936658536043347142984963045658919215661548340875420653437468343448949143330848700282622905251500903165366616989093528387548421898172630311164792908296732119373995492469657592915994374916970385323733879692921481867888839992919173543562249790190392930924209699652327647797204082384416540630932657082008347095070625889215957989868906105233598674999715503418316871508439518805591147670972038744125173028393048388065281173594232476773594862090167445272672533232951411418', + ], + ], + }, +} satisfies PreCreatedAnonCredsDefinition + +export async function storePreCreatedAnonCredsDefinition( + agent: Agent, + preCreatedDefinition: PreCreatedAnonCredsDefinition +) { + const anoncredsCredentialDefinitionRepository = agent.context.dependencyManager.resolve( + AnonCredsCredentialDefinitionRepository + ) + await anoncredsCredentialDefinitionRepository.save( + agent.context, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinitionId: preCreatedDefinition.credentialDefinitionId, + credentialDefinition: preCreatedDefinition.credentialDefinition, + methodName: 'local', + }) + ) + + const anoncredsCredentialDefinitionPrivateRepository = agent.context.dependencyManager.resolve( + AnonCredsCredentialDefinitionPrivateRepository + ) + await anoncredsCredentialDefinitionPrivateRepository.save( + agent.context, + new AnonCredsCredentialDefinitionPrivateRecord({ + credentialDefinitionId: preCreatedDefinition.credentialDefinitionId, + value: preCreatedDefinition.credentialDefinitionPrivate, + }) + ) + + const anonCredsKeyCorrectnessProofRepository = agent.context.dependencyManager.resolve( + AnonCredsKeyCorrectnessProofRepository + ) + await anonCredsKeyCorrectnessProofRepository.save( + agent.context, + new AnonCredsKeyCorrectnessProofRecord({ + credentialDefinitionId: preCreatedDefinition.credentialDefinitionId, + value: preCreatedDefinition.keyCorrectnessProof, + }) + ) + + const anonCredsSchemaRepository = agent.context.dependencyManager.resolve(AnonCredsSchemaRepository) + await anonCredsSchemaRepository.save( + agent.context, + new AnonCredsSchemaRecord({ + methodName: 'local', + schema: preCreatedDefinition.schema, + schemaId: preCreatedDefinition.schemaId, + }) + ) + + return { + issuerId: preCreatedDefinition.issuerId, + schemaId: preCreatedDefinition.schemaId, + credentialDefinitionId: preCreatedDefinition.credentialDefinitionId, + } +} diff --git a/packages/anoncreds/tests/setup.ts b/packages/anoncreds/tests/setup.ts new file mode 100644 index 0000000000..0254a395ff --- /dev/null +++ b/packages/anoncreds/tests/setup.ts @@ -0,0 +1,3 @@ +import '@hyperledger/anoncreds-nodejs' + +jest.setTimeout(120000) diff --git a/packages/anoncreds/tests/v2-credential-revocation.test.ts b/packages/anoncreds/tests/v2-credential-revocation.test.ts new file mode 100644 index 0000000000..d4de4a22c6 --- /dev/null +++ b/packages/anoncreds/tests/v2-credential-revocation.test.ts @@ -0,0 +1,242 @@ +import type { AnonCredsTestsAgent } from './anoncredsSetup' +import type { EventReplaySubject } from '../../core/tests' + +import { + DidCommMessageRepository, + JsonTransformer, + CredentialState, + CredentialExchangeRecord, + V2CredentialPreview, + V2OfferCredentialMessage, + CredentialRole, +} from '@credo-ts/core' + +import { waitForCredentialRecordSubject } from '../../core/tests' +import { waitForRevocationNotification } from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' + +import { InMemoryAnonCredsRegistry } from './InMemoryAnonCredsRegistry' +import { setupAnonCredsTests } from './anoncredsSetup' + +const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', +}) + +describe('IC v2 credential revocation', () => { + let faberAgent: AnonCredsTestsAgent + let aliceAgent: AnonCredsTestsAgent + let credentialDefinitionId: string + let revocationRegistryDefinitionId: string | null + let aliceConnectionId: string + + let faberReplay: EventReplaySubject + let aliceReplay: EventReplaySubject + + const inMemoryRegistry = new InMemoryAnonCredsRegistry() + + const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + revocationRegistryDefinitionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerId, + issuerName: 'Faber Agent Credentials v2', + holderName: 'Alice Agent Credentials v2', + attributeNames: ['name', 'age', 'x-ray', 'profile_picture'], + supportRevocation: true, + registries: [inMemoryRegistry], + })) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with V2 credential proposal to Faber', async () => { + testLogger.test('Alice sends (v2) credential proposal to Faber') + + const credentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', + }, + }, + comment: 'v2 propose credential test', + }) + + expect(credentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.ProposalSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Proposal', + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + revocationRegistryDefinitionId: revocationRegistryDefinitionId ?? undefined, + revocationRegistryIndex: 1, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) + const offerMessage = await didCommMessageRepository.findAgentMessage(faberAgent.context, { + associatedRecordId: faberCredentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + + expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + comment: 'V2 AnonCreds Proposal', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/2.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + { + name: 'x-ray', + 'mime-type': 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + 'mime-type': 'text/plain', + value: 'profile picture', + }, + ], + }, + 'offers~attach': expect.any(Array), + }) + + expect(aliceCredentialRecord).toMatchObject({ + id: expect.any(String), + connectionId: expect.any(String), + type: CredentialExchangeRecord.type, + }) + + // below values are not in json object + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: faberCredentialRecord.threadId, + connectionId: aliceCredentialRecord.connectionId, + role: CredentialRole.Holder, + state: aliceCredentialRecord.state, + credentialIds: [], + }) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.RequestSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + await aliceAgent.credentials.acceptCredential({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Faber waits for state done') + const doneCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + // Now revoke the credential + const credentialRevocationRegistryDefinitionId = doneCredentialRecord.getTag( + 'anonCredsRevocationRegistryId' + ) as string + const credentialRevocationIndex = doneCredentialRecord.getTag('anonCredsCredentialRevocationId') as string + + expect(credentialRevocationRegistryDefinitionId).toBeDefined() + expect(credentialRevocationIndex).toBeDefined() + expect(credentialRevocationRegistryDefinitionId).toEqual(revocationRegistryDefinitionId) + + await faberAgent.modules.anoncreds.updateRevocationStatusList({ + revocationStatusList: { + revocationRegistryDefinitionId: credentialRevocationRegistryDefinitionId, + revokedCredentialIndexes: [Number(credentialRevocationIndex)], + }, + options: {}, + }) + + await faberAgent.credentials.sendRevocationNotification({ + credentialRecordId: doneCredentialRecord.id, + revocationFormat: 'anoncreds', + revocationId: `${credentialRevocationRegistryDefinitionId}::${credentialRevocationIndex}`, + }) + + testLogger.test('Alice waits for credential revocation notification from Faber') + await waitForRevocationNotification(aliceAgent, { + threadId: faberCredentialRecord.threadId, + }) + }) +}) diff --git a/packages/anoncreds/tests/v2-credentials.test.ts b/packages/anoncreds/tests/v2-credentials.test.ts new file mode 100644 index 0000000000..d39d039579 --- /dev/null +++ b/packages/anoncreds/tests/v2-credentials.test.ts @@ -0,0 +1,678 @@ +import type { AnonCredsTestsAgent } from './anoncredsSetup' +import type { EventReplaySubject } from '../../core/tests' +import type { AnonCredsHolderService, AnonCredsProposeCredentialFormat } from '@credo-ts/anoncreds' + +import { + DidCommMessageRepository, + JsonTransformer, + CredentialState, + CredentialExchangeRecord, + V2CredentialPreview, + V2IssueCredentialMessage, + V2OfferCredentialMessage, + V2ProposeCredentialMessage, + V2RequestCredentialMessage, + CredentialRole, +} from '@credo-ts/core' + +import { waitForCredentialRecord, waitForCredentialRecordSubject } from '../../core/tests' +import testLogger from '../../core/tests/logger' + +import { InMemoryAnonCredsRegistry } from './InMemoryAnonCredsRegistry' +import { issueAnonCredsCredential, setupAnonCredsTests } from './anoncredsSetup' + +import { AnonCredsHolderServiceSymbol } from '@credo-ts/anoncreds' + +const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', +}) + +describe('IC V2 AnonCreds credentials', () => { + let faberAgent: AnonCredsTestsAgent + let aliceAgent: AnonCredsTestsAgent + let credentialDefinitionId: string + let faberConnectionId: string + let aliceConnectionId: string + + let faberReplay: EventReplaySubject + let aliceReplay: EventReplaySubject + + let anonCredsCredentialProposal: AnonCredsProposeCredentialFormat + + const inMemoryRegistry = new InMemoryAnonCredsRegistry() + + const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + + const newCredentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'another x-ray value', + profile_picture: 'another profile picture', + }) + + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerId, + issuerName: 'Faber Agent Credentials v2', + holderName: 'Alice Agent Credentials v2', + attributeNames: ['name', 'age', 'x-ray', 'profile_picture'], + registries: [inMemoryRegistry], + })) + + anonCredsCredentialProposal = { + credentialDefinitionId: credentialDefinitionId, + schemaIssuerDid: issuerId, + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: `${issuerId}/q7ATwTYbQDgiigVijUAej:2:test:1.0`, + issuerDid: issuerId, + } + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with V2 credential proposal to Faber', async () => { + testLogger.test('Alice sends (v2) credential proposal to Faber') + + const credentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + schemaIssuerDid: issuerId, + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: `${issuerId}/q7ATwTYbQDgiigVijUAej:2:test:1.0`, + issuerDid: issuerId, + credentialDefinitionId: `${issuerId}/:3:CL:12:tag`, + }, + }, + comment: 'v2 propose credential test', + }) + + expect(credentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.ProposalSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Proposal', + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) + const offerMessage = await didCommMessageRepository.findAgentMessage(faberAgent.context, { + associatedRecordId: faberCredentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + + expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + comment: 'V2 AnonCreds Proposal', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/2.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + { + name: 'x-ray', + 'mime-type': 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + 'mime-type': 'text/plain', + value: 'profile picture', + }, + ], + }, + 'offers~attach': expect.any(Array), + }) + + expect(aliceCredentialRecord).toMatchObject({ + id: expect.any(String), + connectionId: expect.any(String), + type: CredentialExchangeRecord.type, + }) + + // below values are not in json object + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: faberCredentialRecord.threadId, + connectionId: aliceCredentialRecord.connectionId, + state: aliceCredentialRecord.state, + role: CredentialRole.Holder, + credentialIds: [], + }) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.RequestSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + await aliceAgent.credentials.acceptCredential({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Faber waits for state done') + await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + }) + + test('Faber issues credential which is then deleted from Alice`s wallet', async () => { + const { holderCredentialExchangeRecord } = await issueAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnectionId, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + offer: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + revocationRegistryDefinitionId: null, + }) + + // test that delete credential removes from both repository and wallet + // latter is tested by spying on holder service to + // see if deleteCredential is called + const holderService = aliceAgent.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const deleteCredentialSpy = jest.spyOn(holderService, 'deleteCredential') + await aliceAgent.credentials.deleteById(holderCredentialExchangeRecord.id, { + deleteAssociatedCredentials: true, + deleteAssociatedDidCommMessages: true, + }) + expect(deleteCredentialSpy).toHaveBeenNthCalledWith( + 1, + aliceAgent.context, + holderCredentialExchangeRecord.credentials[0].credentialRecordId + ) + + return expect(aliceAgent.credentials.getById(holderCredentialExchangeRecord.id)).rejects.toThrowError( + `CredentialRecord: record with id ${holderCredentialExchangeRecord.id} not found.` + ) + }) + + test('Alice starts with proposal, faber sends a counter offer, alice sends second proposal, faber sends second offer', async () => { + // proposeCredential -> negotiateProposal -> negotiateOffer -> negotiateProposal -> acceptOffer -> acceptRequest -> DONE (credential issued) + + let faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Alice sends credential proposal to Faber') + let aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + ...anonCredsCredentialProposal, + attributes: credentialPreview.attributes, + }, + }, + comment: 'v2 propose credential test', + }) + expect(aliceCredentialExchangeRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await faberCredentialRecordPromise + + let aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + faberCredentialRecord = await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await aliceCredentialRecordPromise + + // Check if the state of the credential records did not change + faberCredentialRecord = await faberAgent.credentials.getById(faberCredentialRecord.id) + faberCredentialRecord.assertState(CredentialState.OfferSent) + + aliceCredentialRecord = await aliceAgent.credentials.getById(aliceCredentialRecord.id) + aliceCredentialRecord.assertState(CredentialState.OfferReceived) + + faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + // second proposal + aliceCredentialExchangeRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + anoncreds: { + ...anonCredsCredentialProposal, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + expect(aliceCredentialExchangeRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + faberCredentialRecord = await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + + aliceCredentialRecord = await aliceCredentialRecordPromise + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialExchangeRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + state: CredentialState.RequestSent, + protocolVersion: 'v2', + threadId: aliceCredentialExchangeRecord.threadId, + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.RequestReceived, + }) + testLogger.test('Faber sends credential to Alice') + + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + // testLogger.test('Alice sends credential ack to Faber') + await aliceAgent.credentials.acceptCredential({ credentialRecordId: aliceCredentialRecord.id }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: expect.any(String), + connectionId: expect.any(String), + state: CredentialState.CredentialReceived, + }) + }) + + test('Faber starts with offer, alice sends counter proposal, faber sends second offer, alice sends second proposal', async () => { + let aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + let faberCredentialRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await aliceCredentialRecordPromise + + let faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + aliceCredentialRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + anoncreds: { + ...anonCredsCredentialProposal, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + expect(aliceCredentialRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + faberCredentialRecord = await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + + aliceCredentialRecord = await aliceCredentialRecordPromise + + faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + aliceCredentialRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + anoncreds: { + ...anonCredsCredentialProposal, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + expect(aliceCredentialRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Proposal', + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialRecord = await aliceCredentialRecordPromise + + faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + state: CredentialState.RequestSent, + protocolVersion: 'v2', + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await aliceCredentialRecordPromise + + const proposalMessage = await aliceAgent.credentials.findProposalMessage(aliceCredentialRecord.id) + const offerMessage = await aliceAgent.credentials.findOfferMessage(aliceCredentialRecord.id) + const requestMessage = await aliceAgent.credentials.findRequestMessage(aliceCredentialRecord.id) + const credentialMessage = await aliceAgent.credentials.findCredentialMessage(aliceCredentialRecord.id) + + expect(proposalMessage).toBeInstanceOf(V2ProposeCredentialMessage) + expect(offerMessage).toBeInstanceOf(V2OfferCredentialMessage) + expect(requestMessage).toBeInstanceOf(V2RequestCredentialMessage) + expect(credentialMessage).toBeInstanceOf(V2IssueCredentialMessage) + + const formatData = await aliceAgent.credentials.getFormatData(aliceCredentialRecord.id) + expect(formatData).toMatchObject({ + proposalAttributes: [ + { + name: 'name', + mimeType: 'text/plain', + value: 'John', + }, + { + name: 'age', + mimeType: 'text/plain', + value: '99', + }, + { + name: 'x-ray', + mimeType: 'text/plain', + value: 'another x-ray value', + }, + { + name: 'profile_picture', + mimeType: 'text/plain', + value: 'another profile picture', + }, + ], + proposal: { + anoncreds: { + schema_issuer_did: expect.any(String), + schema_id: expect.any(String), + schema_name: expect.any(String), + schema_version: expect.any(String), + cred_def_id: expect.any(String), + issuer_did: expect.any(String), + }, + }, + offer: { + anoncreds: { + schema_id: expect.any(String), + cred_def_id: expect.any(String), + key_correctness_proof: expect.any(Object), + nonce: expect.any(String), + }, + }, + offerAttributes: [ + { + name: 'name', + mimeType: 'text/plain', + value: 'John', + }, + { + name: 'age', + mimeType: 'text/plain', + value: '99', + }, + { + name: 'x-ray', + mimeType: 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + mimeType: 'text/plain', + value: 'profile picture', + }, + ], + request: { + anoncreds: { + entropy: expect.any(String), + cred_def_id: expect.any(String), + blinded_ms: expect.any(Object), + blinded_ms_correctness_proof: expect.any(Object), + nonce: expect.any(String), + }, + }, + credential: { + anoncreds: { + schema_id: expect.any(String), + cred_def_id: expect.any(String), + rev_reg_id: null, + values: { + age: { raw: '99', encoded: '99' }, + profile_picture: { + raw: 'profile picture', + encoded: '28661874965215723474150257281172102867522547934697168414362313592277831163345', + }, + name: { + raw: 'John', + encoded: '76355713903561865866741292988746191972523015098789458240077478826513114743258', + }, + 'x-ray': { + raw: 'some x-ray', + encoded: '43715611391396952879378357808399363551139229809726238083934532929974486114650', + }, + }, + signature: expect.any(Object), + signature_correctness_proof: expect.any(Object), + rev_reg: null, + witness: null, + }, + }, + }) + }) + + test('Faber starts with V2 offer, alice declines the offer', async () => { + testLogger.test('Faber sends credential offer to Alice') + const faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + expect(aliceCredentialRecord).toMatchObject({ + id: expect.any(String), + type: CredentialExchangeRecord.type, + }) + + testLogger.test('Alice declines offer') + aliceCredentialRecord = await aliceAgent.credentials.declineOffer(aliceCredentialRecord.id) + + expect(aliceCredentialRecord.state).toBe(CredentialState.Declined) + }) +}) diff --git a/packages/anoncreds/tests/v2-proofs.test.ts b/packages/anoncreds/tests/v2-proofs.test.ts new file mode 100644 index 0000000000..7622c34126 --- /dev/null +++ b/packages/anoncreds/tests/v2-proofs.test.ts @@ -0,0 +1,1011 @@ +import type { AnonCredsTestsAgent } from './anoncredsSetup' +import type { EventReplaySubject } from '../../core/tests' +import type { AnonCredsRequestProofFormat } from '@credo-ts/anoncreds' +import type { CredentialExchangeRecord } from '@credo-ts/core' + +import { + Attachment, + AttachmentData, + LinkedAttachment, + ProofState, + ProofExchangeRecord, + V2ProposePresentationMessage, + V2RequestPresentationMessage, + V2PresentationMessage, +} from '@credo-ts/core' + +import { sleep } from '../../core/src/utils/sleep' +import { waitForProofExchangeRecord } from '../../core/tests' +import testLogger from '../../core/tests/logger' +import { dateToTimestamp } from '../src/utils/timestamp' + +import { InMemoryAnonCredsRegistry } from './InMemoryAnonCredsRegistry' +import { issueAnonCredsCredential, setupAnonCredsTests } from './anoncredsSetup' + +describe('PP V2 AnonCreds Proofs', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let credentialDefinitionId: string + let revocationRegistryDefinitionId: string | null + let aliceConnectionId: string + let faberConnectionId: string + let faberProofExchangeRecord: ProofExchangeRecord + let aliceProofExchangeRecord: ProofExchangeRecord + let faberCredentialExchangeRecord: CredentialExchangeRecord + + const inMemoryRegistry = new InMemoryAnonCredsRegistry() + + const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + revocationRegistryDefinitionId, + //revocationStatusListTimestamp, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerId, + issuerName: 'Faber agent AnonCreds proofs', + holderName: 'Alice agent AnonCreds proofs', + attributeNames: ['name', 'age', 'image_0', 'image_1'], + registries: [inMemoryRegistry], + supportRevocation: true, + })) + ;({ issuerCredentialExchangeRecord: faberCredentialExchangeRecord } = await issueAnonCredsCredential({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnectionId, + revocationRegistryDefinitionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'John', + }, + { + name: 'age', + value: '99', + }, + ], + linkedAttachments: [ + new LinkedAttachment({ + name: 'image_0', + attachment: new Attachment({ + filename: 'picture-of-a-cat.png', + data: new AttachmentData({ base64: 'cGljdHVyZSBvZiBhIGNhdA==' }), + }), + }), + new LinkedAttachment({ + name: 'image_1', + attachment: new Attachment({ + filename: 'picture-of-a-dog.png', + data: new AttachmentData({ base64: 'UGljdHVyZSBvZiBhIGRvZw==' }), + }), + }), + ], + }, + })) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with proof proposal to Faber', async () => { + // Alice sends a presentation proposal to Faber + testLogger.test('Alice sends a presentation proposal to Faber') + + let faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + state: ProofState.ProposalReceived, + }) + + aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + proofFormats: { + anoncreds: { + name: 'abc', + version: '1.0', + attributes: [ + { + name: 'name', + value: 'Alice', + credentialDefinitionId, + }, + ], + predicates: [ + { + name: 'age', + predicate: '>=', + threshold: 50, + credentialDefinitionId, + }, + ], + }, + }, + }) + + // Faber waits for a presentation proposal from Alice + testLogger.test('Faber waits for a presentation proposal from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + }) + + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber accepts the presentation proposal from Alice + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits for the presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof@v1.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation provided by Alice + testLogger.test('Faber accepts the presentation provided by Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + + const proposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id) + const requestMessage = await aliceAgent.proofs.findRequestMessage(aliceProofExchangeRecord.id) + const presentationMessage = await aliceAgent.proofs.findPresentationMessage(aliceProofExchangeRecord.id) + + expect(proposalMessage).toBeInstanceOf(V2ProposePresentationMessage) + expect(requestMessage).toBeInstanceOf(V2RequestPresentationMessage) + expect(presentationMessage).toBeInstanceOf(V2PresentationMessage) + + const formatData = await aliceAgent.proofs.getFormatData(aliceProofExchangeRecord.id) + + expect(formatData).toMatchObject({ + proposal: { + anoncreds: { + name: 'abc', + version: '1.0', + nonce: expect.any(String), + requested_attributes: { + [Object.keys(formatData.proposal?.anoncreds?.requested_attributes ?? {})[0]]: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + [Object.keys(formatData.proposal?.anoncreds?.requested_predicates ?? {})[0]]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + request: { + anoncreds: { + name: 'abc', + version: '1.0', + nonce: expect.any(String), + requested_attributes: { + [Object.keys(formatData.request?.anoncreds?.requested_attributes ?? {})[0]]: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + [Object.keys(formatData.request?.anoncreds?.requested_predicates ?? {})[0]]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + presentation: { + anoncreds: { + proof: { + // Only one proof means the predicate and attribute was combined into one proof (and thus we can + // know that it was the same cred) + proofs: [ + { + primary_proof: expect.any(Object), + non_revoc_proof: null, + }, + ], + aggregated_proof: { + c_hash: expect.any(String), + c_list: expect.any(Array), + }, + }, + requested_proof: expect.any(Object), + identifiers: expect.any(Array), + }, + }, + }) + }) + + test('Faber starts with proof request to Alice', async () => { + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + + expect(aliceProofExchangeRecord.id).not.toBeNull() + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof@v1.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation + testLogger.test('Faber accept the presentation from Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she receives a presentation acknowledgement + testLogger.test('Alice waits for acceptance by Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) + + test('Alice provides credentials via call to getRequestedCredentials', async () => { + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const retrievedCredentials = await aliceAgent.proofs.getCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + expect(retrievedCredentials).toMatchObject({ + proofFormats: { + anoncreds: { + attributes: { + name: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + credentialId: expect.any(String), + attributes: { + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + age: 99, + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + credentialRevocationId: '1', + revocationRegistryId: revocationRegistryDefinitionId, + }, + }, + ], + image_0: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + credentialId: expect.any(String), + attributes: { + age: 99, + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + credentialRevocationId: '1', + revocationRegistryId: revocationRegistryDefinitionId, + }, + }, + ], + }, + predicates: { + age: [ + { + credentialId: expect.any(String), + credentialInfo: { + credentialId: expect.any(String), + attributes: { + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + name: 'John', + age: 99, + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + credentialRevocationId: '1', + revocationRegistryId: revocationRegistryDefinitionId, + }, + }, + ], + }, + }, + }, + }) + }) + + test('Faber starts with proof request to Alice but gets Problem Reported', async () => { + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + + expect(aliceProofExchangeRecord.id).not.toBeNull() + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + }) + + aliceProofExchangeRecord = await aliceAgent.proofs.sendProblemReport({ + description: 'Problem inside proof request', + proofRecordId: aliceProofExchangeRecord.id, + }) + + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + protocolVersion: 'v2', + }) + }) + + test('Credential is revoked after proof request and before presentation', async () => { + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + const nrpRequestedTime = dateToTimestamp(new Date()) + 1 + + const requestProofFormat: AnonCredsRequestProofFormat = { + non_revoked: { from: nrpRequestedTime, to: nrpRequestedTime }, + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + } + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: requestProofFormat, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + // Revoke the credential + const credentialRevocationRegistryDefinitionId = faberCredentialExchangeRecord.getTag( + 'anonCredsRevocationRegistryId' + ) as string + const credentialRevocationIndex = faberCredentialExchangeRecord.getTag('anonCredsCredentialRevocationId') as string + + expect(credentialRevocationRegistryDefinitionId).toBeDefined() + expect(credentialRevocationIndex).toBeDefined() + + // FIXME: do not use delays. Maybe we can add the timestamp to parameters? + // InMemoryAnonCredsRegistry would respect what we ask while actual VDRs will use their own + await sleep(2000) + await faberAgent.modules.anoncreds.updateRevocationStatusList({ + revocationStatusList: { + revocationRegistryDefinitionId: credentialRevocationRegistryDefinitionId, + revokedCredentialIndexes: [Number(credentialRevocationIndex)], + }, + options: {}, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation + testLogger.test('Faber accept the presentation from Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she receives a presentation acknowledgement + testLogger.test('Alice waits for acceptance by Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.Done, + }) + }) + + test('Credential is revoked before proof request', async () => { + // Revoke the credential + const credentialRevocationRegistryDefinitionId = faberCredentialExchangeRecord.getTag( + 'anonCredsRevocationRegistryId' + ) as string + const credentialRevocationIndex = faberCredentialExchangeRecord.getTag('anonCredsCredentialRevocationId') as string + + expect(credentialRevocationRegistryDefinitionId).toBeDefined() + expect(credentialRevocationIndex).toBeDefined() + + const { revocationStatusListState } = await faberAgent.modules.anoncreds.updateRevocationStatusList({ + revocationStatusList: { + revocationRegistryDefinitionId: credentialRevocationRegistryDefinitionId, + revokedCredentialIndexes: [Number(credentialRevocationIndex)], + }, + options: {}, + }) + + expect(revocationStatusListState.revocationStatusList).toBeDefined() + const revokedTimestamp = revocationStatusListState.revocationStatusList?.timestamp + + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + const nrpRequestedTime = (revokedTimestamp ?? dateToTimestamp(new Date())) + 1 + + const requestProofFormat: AnonCredsRequestProofFormat = { + non_revoked: { from: nrpRequestedTime, to: nrpRequestedTime }, + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + } + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: requestProofFormat, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: { filterByNonRevocationRequirements: false } }, + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + // Faber receives presentation and checks that it is not valid + expect(faberProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + isVerified: false, + state: ProofState.Abandoned, + }) + + // Faber will send a problem report, meaning for Alice that the proof state is abandoned + // as well + await waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + }) + }) +}) diff --git a/packages/anoncreds/tsconfig.build.json b/packages/anoncreds/tsconfig.build.json new file mode 100644 index 0000000000..2b75d0adab --- /dev/null +++ b/packages/anoncreds/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build" + }, + "include": ["src/**/*"] +} diff --git a/packages/anoncreds/tsconfig.json b/packages/anoncreds/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/anoncreds/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/packages/askar/CHANGELOG.md b/packages/askar/CHANGELOG.md new file mode 100644 index 0000000000..bf8bd0d2d0 --- /dev/null +++ b/packages/askar/CHANGELOG.md @@ -0,0 +1,110 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release +- Updated dependencies [66e696d] + - @credo-ts/core@0.5.6 + +## 0.5.5 + +### Patch Changes + +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) +- Updated dependencies [3239ef3] +- Updated dependencies [d548fa4] +- Updated dependencies [482a630] + - @credo-ts/core@0.5.5 + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +**Note:** Version bump only for package @credo-ts/askar + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +### Bug Fixes + +- node-ffi-napi compatibility ([#1821](https://github.com/openwallet-foundation/credo-ts/issues/1821)) ([81d351b](https://github.com/openwallet-foundation/credo-ts/commit/81d351bc9d4d508ebfac9e7f2b2f10276ab1404a)) + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +**Note:** Version bump only for package @credo-ts/askar + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +### Features + +- add support for key type k256 ([#1722](https://github.com/openwallet-foundation/credo-ts/issues/1722)) ([22d5bff](https://github.com/openwallet-foundation/credo-ts/commit/22d5bffc939f6644f324f6ddba4c8269212e9dc4)) +- **openid4vc:** persistance and events ([#1793](https://github.com/openwallet-foundation/credo-ts/issues/1793)) ([f4c386a](https://github.com/openwallet-foundation/credo-ts/commit/f4c386a6ccf8adb829cad30b81d524e6ffddb029)) +- optional backup on storage migration ([#1745](https://github.com/openwallet-foundation/credo-ts/issues/1745)) ([81ff63c](https://github.com/openwallet-foundation/credo-ts/commit/81ff63ccf7c71eccf342899d298a780d66045534)) +- **tenants:** support for tenant storage migration ([#1747](https://github.com/openwallet-foundation/credo-ts/issues/1747)) ([12c617e](https://github.com/openwallet-foundation/credo-ts/commit/12c617efb45d20fda8965b9b4da24c92e975c9a2)) + +## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) + +### Bug Fixes + +- **askar:** throw error if imported wallet exists ([#1593](https://github.com/hyperledger/aries-framework-javascript/issues/1593)) ([c2bb2a5](https://github.com/hyperledger/aries-framework-javascript/commit/c2bb2a52f10add35de883c9a27716db01b9028df)) +- update tsyringe for ts 5 support ([#1588](https://github.com/hyperledger/aries-framework-javascript/issues/1588)) ([296955b](https://github.com/hyperledger/aries-framework-javascript/commit/296955b3a648416ac6b502da05a10001920af222)) + +## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) + +### Bug Fixes + +- **askar:** in memory wallet creation ([#1498](https://github.com/hyperledger/aries-framework-javascript/issues/1498)) ([4a158e6](https://github.com/hyperledger/aries-framework-javascript/commit/4a158e64b97595be0733d4277c28c462bd47c908)) + +### Features + +- support askar profiles for multi-tenancy ([#1538](https://github.com/hyperledger/aries-framework-javascript/issues/1538)) ([e448a2a](https://github.com/hyperledger/aries-framework-javascript/commit/e448a2a58dddff2cdf80c4549ea2d842a54b43d1)) + +# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) + +### Bug Fixes + +- **askar:** anoncrypt messages unpacking ([#1332](https://github.com/hyperledger/aries-framework-javascript/issues/1332)) ([1c6aeae](https://github.com/hyperledger/aries-framework-javascript/commit/1c6aeae31ac57e83f4059f3dba35ccb1ca36926e)) +- **askar:** custom error handling ([#1372](https://github.com/hyperledger/aries-framework-javascript/issues/1372)) ([c72ba14](https://github.com/hyperledger/aries-framework-javascript/commit/c72ba149bad3a4596f5818b28516f6286b9088bf)) +- **askar:** default key derivation method ([#1420](https://github.com/hyperledger/aries-framework-javascript/issues/1420)) ([7b59629](https://github.com/hyperledger/aries-framework-javascript/commit/7b5962917488cfd0c5adc170d3c3fc64aa82ef2c)) +- **askar:** generate nonce suitable for anoncreds ([#1295](https://github.com/hyperledger/aries-framework-javascript/issues/1295)) ([ecce0a7](https://github.com/hyperledger/aries-framework-javascript/commit/ecce0a71578f45f55743198a1f3699bd257dc74b)) +- imports from core ([#1303](https://github.com/hyperledger/aries-framework-javascript/issues/1303)) ([3e02227](https://github.com/hyperledger/aries-framework-javascript/commit/3e02227a7b23677e9886eb1c03d1a3ec154947a9)) +- issuance with unqualified identifiers ([#1431](https://github.com/hyperledger/aries-framework-javascript/issues/1431)) ([de90caf](https://github.com/hyperledger/aries-framework-javascript/commit/de90cafb8d12b7a940f881184cd745c4b5043cbc)) +- seed and private key validation and return type in registrars ([#1324](https://github.com/hyperledger/aries-framework-javascript/issues/1324)) ([c0e5339](https://github.com/hyperledger/aries-framework-javascript/commit/c0e5339edfa32df92f23fb9c920796b4b59adf52)) +- set updateAt on records when updating a record ([#1272](https://github.com/hyperledger/aries-framework-javascript/issues/1272)) ([2669d7d](https://github.com/hyperledger/aries-framework-javascript/commit/2669d7dd3d7c0ddfd1108dfd65e6115dd3418500)) +- small issues with migration and WAL files ([#1443](https://github.com/hyperledger/aries-framework-javascript/issues/1443)) ([83cf387](https://github.com/hyperledger/aries-framework-javascript/commit/83cf387fa52bb51d8adb2d5fedc5111994d4dde1)) +- small updates to cheqd module and demo ([#1439](https://github.com/hyperledger/aries-framework-javascript/issues/1439)) ([61daf0c](https://github.com/hyperledger/aries-framework-javascript/commit/61daf0cb27de80a5e728e2e9dad13d729baf476c)) + +- feat!: add data, cache and temp dirs to FileSystem (#1306) ([ff5596d](https://github.com/hyperledger/aries-framework-javascript/commit/ff5596d0631e93746494c017797d0191b6bdb0b1)), closes [#1306](https://github.com/hyperledger/aries-framework-javascript/issues/1306) + +### Features + +- 0.4.0 migration script ([#1392](https://github.com/hyperledger/aries-framework-javascript/issues/1392)) ([bc5455f](https://github.com/hyperledger/aries-framework-javascript/commit/bc5455f7b42612a2b85e504bc6ddd36283a42bfa)) +- add initial askar package ([#1211](https://github.com/hyperledger/aries-framework-javascript/issues/1211)) ([f18d189](https://github.com/hyperledger/aries-framework-javascript/commit/f18d1890546f7d66571fe80f2f3fc1fead1cd4c3)) +- **anoncreds:** legacy indy proof format service ([#1283](https://github.com/hyperledger/aries-framework-javascript/issues/1283)) ([c72fd74](https://github.com/hyperledger/aries-framework-javascript/commit/c72fd7416f2c1bc0497a84036e16adfa80585e49)) +- **askar:** import/export wallet support for SQLite ([#1377](https://github.com/hyperledger/aries-framework-javascript/issues/1377)) ([19cefa5](https://github.com/hyperledger/aries-framework-javascript/commit/19cefa54596a4e4848bdbe89306a884a5ce2e991)) +- basic message pthid/thid support ([#1381](https://github.com/hyperledger/aries-framework-javascript/issues/1381)) ([f27fb99](https://github.com/hyperledger/aries-framework-javascript/commit/f27fb9921e11e5bcd654611d97d9fa1c446bc2d5)) +- indy sdk aries askar migration script ([#1289](https://github.com/hyperledger/aries-framework-javascript/issues/1289)) ([4a6b99c](https://github.com/hyperledger/aries-framework-javascript/commit/4a6b99c617de06edbaf1cb07c8adfa8de9b3ec15)) +- **openid4vc:** jwt format and more crypto ([#1472](https://github.com/hyperledger/aries-framework-javascript/issues/1472)) ([bd4932d](https://github.com/hyperledger/aries-framework-javascript/commit/bd4932d34f7314a6d49097b6460c7570e1ebc7a8)) +- support for did:jwk and p-256, p-384, p-512 ([#1446](https://github.com/hyperledger/aries-framework-javascript/issues/1446)) ([700d3f8](https://github.com/hyperledger/aries-framework-javascript/commit/700d3f89728ce9d35e22519e505d8203a4c9031e)) + +### BREAKING CHANGES + +- Agent-produced files will now be divided in different system paths depending on their nature: data, temp and cache. Previously, they were located at a single location, defaulting to a temporary directory. + +If you specified a custom path in `FileSystem` object constructor, you now must provide an object containing `baseDataPath`, `baseTempPath` and `baseCachePath`. They can point to the same path, although it's recommended to specify different path to avoid future file clashes. diff --git a/packages/askar/README.md b/packages/askar/README.md new file mode 100644 index 0000000000..b8a5aa3447 --- /dev/null +++ b/packages/askar/README.md @@ -0,0 +1,31 @@ +

+
+ Credo Logo +

+

Credo Askar Module

+

+ License + typescript + @credo-ts/askar version + +

+
+ +Credo Askar provides secure storage and crypto capabilities of Credo. See the [Aries Askar Setup](https://credo.js.org/guides/getting-started/set-up/aries-askar) for installation instructions. diff --git a/packages/askar/jest.config.ts b/packages/askar/jest.config.ts new file mode 100644 index 0000000000..93c0197296 --- /dev/null +++ b/packages/askar/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/askar/package.json b/packages/askar/package.json new file mode 100644 index 0000000000..b0a05c1666 --- /dev/null +++ b/packages/askar/package.json @@ -0,0 +1,49 @@ +{ + "name": "@credo-ts/askar", + "main": "src/index", + "types": "src/index", + "version": "0.5.6", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/askar", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/askar" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build", + "test": "jest" + }, + "dependencies": { + "@credo-ts/core": "workspace:*", + "bn.js": "^5.2.1", + "class-transformer": "0.5.1", + "class-validator": "0.14.1", + "rxjs": "^7.8.0", + "tsyringe": "^4.8.0" + }, + "devDependencies": { + "@hyperledger/aries-askar-nodejs": "^0.2.1", + "@hyperledger/aries-askar-shared": "^0.2.1", + "@types/bn.js": "^5.1.0", + "@types/ref-array-di": "^1.2.6", + "@types/ref-struct-di": "^1.1.10", + "reflect-metadata": "^0.1.13", + "rimraf": "^4.4.0", + "typescript": "~5.5.2" + }, + "peerDependencies": { + "@hyperledger/aries-askar-shared": "^0.2.1" + } +} diff --git a/packages/askar/src/AskarModule.ts b/packages/askar/src/AskarModule.ts new file mode 100644 index 0000000000..a1e1762f40 --- /dev/null +++ b/packages/askar/src/AskarModule.ts @@ -0,0 +1,67 @@ +import type { AskarModuleConfigOptions } from './AskarModuleConfig' +import type { AgentContext, DependencyManager, Module } from '@credo-ts/core' + +import { CredoError, InjectionSymbols } from '@credo-ts/core' +import { Store } from '@hyperledger/aries-askar-shared' + +import { AskarMultiWalletDatabaseScheme, AskarModuleConfig } from './AskarModuleConfig' +import { AskarStorageService } from './storage' +import { assertAskarWallet } from './utils/assertAskarWallet' +import { AskarProfileWallet, AskarWallet } from './wallet' + +export class AskarModule implements Module { + public readonly config: AskarModuleConfig + + public constructor(config: AskarModuleConfigOptions) { + this.config = new AskarModuleConfig(config) + } + + public register(dependencyManager: DependencyManager) { + dependencyManager.registerInstance(AskarModuleConfig, this.config) + + if (dependencyManager.isRegistered(InjectionSymbols.Wallet)) { + throw new CredoError('There is an instance of Wallet already registered') + } else { + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) + + // If the multiWalletDatabaseScheme is set to ProfilePerWallet, we want to register the AskarProfileWallet + if (this.config.multiWalletDatabaseScheme === AskarMultiWalletDatabaseScheme.ProfilePerWallet) { + dependencyManager.registerContextScoped(AskarProfileWallet) + } + } + + if (dependencyManager.isRegistered(InjectionSymbols.StorageService)) { + throw new CredoError('There is an instance of StorageService already registered') + } else { + dependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) + } + } + + public async initialize(agentContext: AgentContext): Promise { + // We MUST use an askar wallet here + assertAskarWallet(agentContext.wallet) + + const wallet = agentContext.wallet + + // Register the Askar store instance on the dependency manager + // This allows it to be re-used for tenants + agentContext.dependencyManager.registerInstance(Store, agentContext.wallet.store) + + // If the multiWalletDatabaseScheme is set to ProfilePerWallet, we want to register the AskarProfileWallet + // and return that as the wallet for all tenants, but not for the main agent, that should use the AskarWallet + if (this.config.multiWalletDatabaseScheme === AskarMultiWalletDatabaseScheme.ProfilePerWallet) { + agentContext.dependencyManager.container.register(InjectionSymbols.Wallet, { + useFactory: (container) => { + // If the container is the same as the root dependency manager container + // it means we are in the main agent, and we should use the root wallet + if (container === agentContext.dependencyManager.container) { + return wallet + } + + // Otherwise we want to return the AskarProfileWallet + return container.resolve(AskarProfileWallet) + }, + }) + } + } +} diff --git a/packages/askar/src/AskarModuleConfig.ts b/packages/askar/src/AskarModuleConfig.ts new file mode 100644 index 0000000000..91ec72ed5b --- /dev/null +++ b/packages/askar/src/AskarModuleConfig.ts @@ -0,0 +1,82 @@ +import type { AriesAskar } from '@hyperledger/aries-askar-shared' + +export enum AskarMultiWalletDatabaseScheme { + /** + * Each wallet get its own database and uses a separate store. + */ + DatabasePerWallet = 'DatabasePerWallet', + + /** + * All wallets are stored in a single database, but each wallet uses a separate profile. + */ + ProfilePerWallet = 'ProfilePerWallet', +} + +export interface AskarModuleConfigOptions { + /** + * + * ## Node.JS + * + * ```ts + * import { ariesAskar } from '@hyperledger/aries-askar-nodejs' + * + * const agent = new Agent({ + * config: {}, + * dependencies: agentDependencies, + * modules: { + * ariesAskar: new AskarModule({ + * ariesAskar, + * }) + * } + * }) + * ``` + * + * ## React Native + * + * ```ts + * import { ariesAskar } from '@hyperledger/aries-askar-react-native' + * + * const agent = new Agent({ + * config: {}, + * dependencies: agentDependencies, + * modules: { + * ariesAskar: new AskarModule({ + * ariesAskar, + * }) + * } + * }) + * ``` + */ + ariesAskar: AriesAskar + + /** + * Determine the strategy for storing wallets if multiple wallets are used in a single agent. + * This is mostly the case in multi-tenancy, and determines whether each tenant will get a separate + * database, or whether all wallets will be stored in a single database, using a different profile + * for each wallet. + * + * @default {@link AskarMultiWalletDatabaseScheme.DatabasePerWallet} (for backwards compatibility) + */ + multiWalletDatabaseScheme?: AskarMultiWalletDatabaseScheme +} + +/** + * @public + */ +export class AskarModuleConfig { + private options: AskarModuleConfigOptions + + public constructor(options: AskarModuleConfigOptions) { + this.options = options + } + + /** See {@link AskarModuleConfigOptions.ariesAskar} */ + public get ariesAskar() { + return this.options.ariesAskar + } + + /** See {@link AskarModuleConfigOptions.multiWalletDatabaseScheme} */ + public get multiWalletDatabaseScheme() { + return this.options.multiWalletDatabaseScheme ?? AskarMultiWalletDatabaseScheme.DatabasePerWallet + } +} diff --git a/packages/askar/src/__tests__/migration-postgres.e2e.test.ts b/packages/askar/src/__tests__/migration-postgres.e2e.test.ts new file mode 100644 index 0000000000..6eeeb3769c --- /dev/null +++ b/packages/askar/src/__tests__/migration-postgres.e2e.test.ts @@ -0,0 +1,44 @@ +import { StorageUpdateService } from '@credo-ts/core' + +import { Agent } from '../../../core/src/agent/Agent' +import { CURRENT_FRAMEWORK_STORAGE_VERSION } from '../../../core/src/storage/migration/updates' +import { askarPostgresStorageConfig, getAskarPostgresAgentOptions } from '../../tests/helpers' + +const agentOptions = getAskarPostgresAgentOptions('Migration', askarPostgresStorageConfig, {}) + +describe('migration with postgres backend', () => { + test('Automatic update on agent startup', async () => { + // Initialize agent and set its storage version to 0.1 in order to force automatic update in the next startup + let agent = new Agent(agentOptions) + await agent.initialize() + + let storageUpdateService = agent.dependencyManager.resolve(StorageUpdateService) + await storageUpdateService.setCurrentStorageVersion(agent.context, '0.1') + await agent.shutdown() + + // Now start agent with auto update storage + agent = new Agent({ ...agentOptions, config: { ...agentOptions.config, autoUpdateStorageOnStartup: true } }) + storageUpdateService = agent.dependencyManager.resolve(StorageUpdateService) + + // Should fail because export is not supported when using postgres + await expect(agent.initialize()).rejects.toThrow(/backend does not support export/) + + expect(await storageUpdateService.getCurrentStorageVersion(agent.context)).toEqual('0.1') + await agent.shutdown() + + // Now start agent with auto update storage, but this time disable backup + agent = new Agent({ + ...agentOptions, + config: { ...agentOptions.config, autoUpdateStorageOnStartup: true, backupBeforeStorageUpdate: false }, + }) + + // Should work OK + await agent.initialize() + expect(await storageUpdateService.getCurrentStorageVersion(agent.context)).toEqual( + CURRENT_FRAMEWORK_STORAGE_VERSION + ) + await agent.shutdown() + + await agent.wallet.delete() + }) +}) diff --git a/packages/askar/src/index.ts b/packages/askar/src/index.ts new file mode 100644 index 0000000000..532ef7c842 --- /dev/null +++ b/packages/askar/src/index.ts @@ -0,0 +1,15 @@ +// Wallet +export { + AskarWallet, + AskarWalletPostgresStorageConfig, + AskarWalletPostgresConfig, + AskarWalletPostgresCredentials, + AskarProfileWallet, +} from './wallet' + +// Storage +export { AskarStorageService } from './storage' + +// Module +export { AskarModule } from './AskarModule' +export { AskarModuleConfigOptions, AskarMultiWalletDatabaseScheme } from './AskarModuleConfig' diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts new file mode 100644 index 0000000000..a8ea386f60 --- /dev/null +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -0,0 +1,170 @@ +import type { + BaseRecordConstructor, + AgentContext, + BaseRecord, + Query, + QueryOptions, + StorageService, +} from '@credo-ts/core' + +import { RecordDuplicateError, WalletError, RecordNotFoundError, injectable, JsonTransformer } from '@credo-ts/core' +import { Scan } from '@hyperledger/aries-askar-shared' + +import { AskarErrorCode, isAskarError } from '../utils/askarError' +import { assertAskarWallet } from '../utils/assertAskarWallet' + +import { askarQueryFromSearchQuery, recordToInstance, transformFromRecordTagValues } from './utils' + +@injectable() +export class AskarStorageService implements StorageService { + /** @inheritDoc */ + public async save(agentContext: AgentContext, record: T) { + assertAskarWallet(agentContext.wallet) + + record.updatedAt = new Date() + + const value = JsonTransformer.serialize(record) + const tags = transformFromRecordTagValues(record.getTags()) as Record + + try { + await agentContext.wallet.withSession((session) => + session.insert({ category: record.type, name: record.id, value, tags }) + ) + } catch (error) { + if (isAskarError(error, AskarErrorCode.Duplicate)) { + throw new RecordDuplicateError(`Record with id ${record.id} already exists`, { recordType: record.type }) + } + + throw new WalletError('Error saving record', { cause: error }) + } + } + + /** @inheritDoc */ + public async update(agentContext: AgentContext, record: T): Promise { + assertAskarWallet(agentContext.wallet) + + record.updatedAt = new Date() + + const value = JsonTransformer.serialize(record) + const tags = transformFromRecordTagValues(record.getTags()) as Record + + try { + await agentContext.wallet.withSession((session) => + session.replace({ category: record.type, name: record.id, value, tags }) + ) + } catch (error) { + if (isAskarError(error, AskarErrorCode.NotFound)) { + throw new RecordNotFoundError(`record with id ${record.id} not found.`, { + recordType: record.type, + cause: error, + }) + } + + throw new WalletError('Error updating record', { cause: error }) + } + } + + /** @inheritDoc */ + public async delete(agentContext: AgentContext, record: T) { + assertAskarWallet(agentContext.wallet) + + try { + await agentContext.wallet.withSession((session) => session.remove({ category: record.type, name: record.id })) + } catch (error) { + if (isAskarError(error, AskarErrorCode.NotFound)) { + throw new RecordNotFoundError(`record with id ${record.id} not found.`, { + recordType: record.type, + cause: error, + }) + } + throw new WalletError('Error deleting record', { cause: error }) + } + } + + /** @inheritDoc */ + public async deleteById( + agentContext: AgentContext, + recordClass: BaseRecordConstructor, + id: string + ): Promise { + assertAskarWallet(agentContext.wallet) + + try { + await agentContext.wallet.withSession((session) => session.remove({ category: recordClass.type, name: id })) + } catch (error) { + if (isAskarError(error, AskarErrorCode.NotFound)) { + throw new RecordNotFoundError(`record with id ${id} not found.`, { + recordType: recordClass.type, + cause: error, + }) + } + throw new WalletError('Error deleting record', { cause: error }) + } + } + + /** @inheritDoc */ + public async getById(agentContext: AgentContext, recordClass: BaseRecordConstructor, id: string): Promise { + assertAskarWallet(agentContext.wallet) + + try { + const record = await agentContext.wallet.withSession((session) => + session.fetch({ category: recordClass.type, name: id }) + ) + if (!record) { + throw new RecordNotFoundError(`record with id ${id} not found.`, { + recordType: recordClass.type, + }) + } + return recordToInstance(record, recordClass) + } catch (error) { + if (error instanceof RecordNotFoundError) throw error + throw new WalletError(`Error getting record ${recordClass.name}`, { cause: error }) + } + } + + /** @inheritDoc */ + public async getAll(agentContext: AgentContext, recordClass: BaseRecordConstructor): Promise { + assertAskarWallet(agentContext.wallet) + + const records = await agentContext.wallet.withSession((session) => session.fetchAll({ category: recordClass.type })) + + const instances = [] + for (const record of records) { + instances.push(recordToInstance(record, recordClass)) + } + return instances + } + + /** @inheritDoc */ + public async findByQuery( + agentContext: AgentContext, + recordClass: BaseRecordConstructor, + query: Query, + queryOptions?: QueryOptions + ): Promise { + const wallet = agentContext.wallet + assertAskarWallet(wallet) + + const askarQuery = askarQueryFromSearchQuery(query) + + const scan = new Scan({ + category: recordClass.type, + store: wallet.store, + tagFilter: askarQuery, + profile: wallet.profile, + offset: queryOptions?.offset, + limit: queryOptions?.limit, + }) + + const instances = [] + try { + const records = await scan.fetchAll() + for (const record of records) { + instances.push(recordToInstance(record, recordClass)) + } + return instances + } catch (error) { + throw new WalletError(`Error executing query. ${error.message}`, { cause: error }) + } + } +} diff --git a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts new file mode 100644 index 0000000000..a16777a853 --- /dev/null +++ b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts @@ -0,0 +1,359 @@ +import type { AgentContext, TagsBase } from '@credo-ts/core' + +import { TypedArrayEncoder, SigningProviderRegistry, RecordDuplicateError, RecordNotFoundError } from '@credo-ts/core' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' + +import { TestRecord } from '../../../../core/src/storage/__tests__/TestRecord' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' +import { AskarWallet } from '../../wallet/AskarWallet' +import { AskarStorageService } from '../AskarStorageService' +import { askarQueryFromSearchQuery } from '../utils' + +const startDate = Date.now() + +describe('AskarStorageService', () => { + let wallet: AskarWallet + let storageService: AskarStorageService + let agentContext: AgentContext + + beforeEach(async () => { + const agentConfig = getAgentConfig('AskarStorageServiceTest') + + wallet = new AskarWallet(agentConfig.logger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + agentContext = getAgentContext({ + wallet, + agentConfig, + }) + await wallet.createAndOpen(agentConfig.walletConfig) + storageService = new AskarStorageService() + }) + + afterEach(async () => { + await wallet.delete() + }) + + const insertRecord = async ({ id, tags }: { id?: string; tags?: TagsBase }) => { + const props = { + id, + foo: 'bar', + tags: tags ?? { myTag: 'foobar' }, + } + const record = new TestRecord(props) + await storageService.save(agentContext, record) + return record + } + + describe('tag transformation', () => { + it('should correctly transform tag values to string before storing', async () => { + const record = await insertRecord({ + id: 'test-id', + tags: { + someBoolean: true, + someOtherBoolean: false, + someStringValue: 'string', + anArrayValue: ['foo', 'bar'], + anArrayValueWhereValuesContainColon: ['foo:bar:test', 'https://google.com'], + // booleans are stored as '1' and '0' so we store the string values '1' and '0' as 'n__1' and 'n__0' + someStringNumberValue: '1', + anotherStringNumberValue: '0', + }, + }) + + const retrieveRecord = await wallet.withSession((session) => + ariesAskar.sessionFetch({ + category: record.type, + name: record.id, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sessionHandle: session.handle!, + forUpdate: false, + }) + ) + + expect(JSON.parse(retrieveRecord?.getTags(0) ?? '{}')).toEqual({ + someBoolean: '1', + someOtherBoolean: '0', + someStringValue: 'string', + 'anArrayValue:foo': '1', + 'anArrayValue:bar': '1', + 'anArrayValueWhereValuesContainColon:foo:bar:test': '1', + 'anArrayValueWhereValuesContainColon:https://google.com': '1', + someStringNumberValue: 'n__1', + anotherStringNumberValue: 'n__0', + }) + }) + + it('should correctly transform tag values from string after retrieving', async () => { + await wallet.withSession( + async (session) => + await ariesAskar.sessionUpdate({ + category: TestRecord.type, + name: 'some-id', + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sessionHandle: session.handle!, + value: TypedArrayEncoder.fromString('{}'), + tags: { + someBoolean: '1', + someOtherBoolean: '0', + someStringValue: 'string', + // Before 0.5.0, there was a bug where array values that contained a : would be incorrectly + // parsed back into a record as we would split on ':' and thus only the first part would be included + // in the record as the tag value. If the record was never re-saved it would work well, as well as if the + // record tag was generated dynamically before save (as then the incorrectly transformed back value would be + // overwritten again on save). + 'anArrayValueWhereValuesContainColon:foo:bar:test': '1', + 'anArrayValueWhereValuesContainColon:https://google.com': '1', + 'anArrayValue:foo': '1', + 'anArrayValue:bar': '1', + // booleans are stored as '1' and '0' so we store the string values '1' and '0' as 'n__1' and 'n__0' + someStringNumberValue: 'n__1', + anotherStringNumberValue: 'n__0', + }, + operation: 0, // EntryOperation.Insert + }) + ) + + const record = await storageService.getById(agentContext, TestRecord, 'some-id') + + expect(record.getTags()).toEqual({ + someBoolean: true, + someOtherBoolean: false, + someStringValue: 'string', + anArrayValue: expect.arrayContaining(['bar', 'foo']), + anArrayValueWhereValuesContainColon: expect.arrayContaining(['foo:bar:test', 'https://google.com']), + someStringNumberValue: '1', + anotherStringNumberValue: '0', + }) + }) + }) + + describe('save()', () => { + it('should throw RecordDuplicateError if a record with the id already exists', async () => { + const record = await insertRecord({ id: 'test-id' }) + + return expect(() => storageService.save(agentContext, record)).rejects.toThrowError(RecordDuplicateError) + }) + + it('should save the record', async () => { + const record = await insertRecord({ id: 'test-id' }) + const found = await storageService.getById(agentContext, TestRecord, 'test-id') + + expect(record).toEqual(found) + }) + + it('updatedAt should have a new value after a save', async () => { + const record = await insertRecord({ id: 'test-id' }) + expect(record.updatedAt?.getTime()).toBeGreaterThan(startDate) + }) + }) + + describe('getById()', () => { + it('should throw RecordNotFoundError if the record does not exist', async () => { + return expect(() => storageService.getById(agentContext, TestRecord, 'does-not-exist')).rejects.toThrowError( + RecordNotFoundError + ) + }) + + it('should return the record by id', async () => { + const record = await insertRecord({ id: 'test-id' }) + const found = await storageService.getById(agentContext, TestRecord, 'test-id') + + expect(found).toEqual(record) + }) + }) + + describe('update()', () => { + it('should throw RecordNotFoundError if the record does not exist', async () => { + const record = new TestRecord({ + id: 'test-id', + foo: 'test', + tags: { some: 'tag' }, + }) + + return expect(() => storageService.update(agentContext, record)).rejects.toThrowError(RecordNotFoundError) + }) + + it('should update the record', async () => { + const record = await insertRecord({ id: 'test-id' }) + + record.replaceTags({ ...record.getTags(), foo: 'bar' }) + record.foo = 'foobaz' + await storageService.update(agentContext, record) + + const retrievedRecord = await storageService.getById(agentContext, TestRecord, record.id) + expect(retrievedRecord).toEqual(record) + }) + + it('updatedAt should have a new value after an update', async () => { + const record = await insertRecord({ id: 'test-id' }) + expect(record.updatedAt?.getTime()).toBeGreaterThan(startDate) + }) + }) + + describe('delete()', () => { + it('should throw RecordNotFoundError if the record does not exist', async () => { + const record = new TestRecord({ + id: 'test-id', + foo: 'test', + tags: { some: 'tag' }, + }) + + return expect(() => storageService.delete(agentContext, record)).rejects.toThrowError(RecordNotFoundError) + }) + + it('should delete the record', async () => { + const record = await insertRecord({ id: 'test-id' }) + await storageService.delete(agentContext, record) + + return expect(() => storageService.getById(agentContext, TestRecord, record.id)).rejects.toThrowError( + RecordNotFoundError + ) + }) + }) + + describe('getAll()', () => { + it('should retrieve all records', async () => { + const createdRecords = await Promise.all( + Array(5) + .fill(undefined) + .map((_, index) => insertRecord({ id: `record-${index}` })) + ) + + const records = await storageService.getAll(agentContext, TestRecord) + + expect(records).toEqual(expect.arrayContaining(createdRecords)) + }) + }) + + describe('findByQuery()', () => { + it('should retrieve all records that match the query', async () => { + const expectedRecord = await insertRecord({ tags: { myTag: 'foobar' } }) + const expectedRecord2 = await insertRecord({ tags: { myTag: 'foobar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery(agentContext, TestRecord, { myTag: 'foobar' }) + + expect(records.length).toBe(2) + expect(records).toEqual(expect.arrayContaining([expectedRecord, expectedRecord2])) + }) + + it('finds records using $and statements', async () => { + const expectedRecord = await insertRecord({ tags: { myTag: 'foo', anotherTag: 'bar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery(agentContext, TestRecord, { + $and: [{ myTag: 'foo' }, { anotherTag: 'bar' }], + }) + + expect(records.length).toBe(1) + expect(records[0]).toEqual(expectedRecord) + }) + + it('finds records using $or statements', async () => { + const expectedRecord = await insertRecord({ tags: { myTag: 'foo' } }) + const expectedRecord2 = await insertRecord({ tags: { anotherTag: 'bar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery(agentContext, TestRecord, { + $or: [{ myTag: 'foo' }, { anotherTag: 'bar' }], + }) + + expect(records.length).toBe(2) + expect(records).toEqual(expect.arrayContaining([expectedRecord, expectedRecord2])) + }) + + it('finds records using $not statements', async () => { + const expectedRecord = await insertRecord({ tags: { myTag: 'foo' } }) + const expectedRecord2 = await insertRecord({ tags: { anotherTag: 'bar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery(agentContext, TestRecord, { + $not: { myTag: 'notfoobar' }, + }) + + expect(records.length).toBe(2) + expect(records).toEqual(expect.arrayContaining([expectedRecord, expectedRecord2])) + }) + + it('correctly transforms an advanced query into a valid WQL query', async () => { + const expectedQuery = { + $and: [ + { + $and: undefined, + $not: undefined, + $or: [ + { myTag: '1', $and: undefined, $or: undefined, $not: undefined }, + { myTag: '0', $and: undefined, $or: undefined, $not: undefined }, + ], + }, + { + $or: undefined, + $not: undefined, + $and: [ + { theNumber: 'n__0', $and: undefined, $or: undefined, $not: undefined }, + { theNumber: 'n__1', $and: undefined, $or: undefined, $not: undefined }, + ], + }, + ], + $or: [ + { + 'aValue:foo': '1', + 'aValue:bar': '1', + $and: undefined, + $or: undefined, + $not: undefined, + }, + ], + $not: { myTag: 'notfoobar', $and: undefined, $or: undefined, $not: undefined }, + } + + expect( + askarQueryFromSearchQuery({ + $and: [ + { + $or: [{ myTag: true }, { myTag: false }], + }, + { + $and: [{ theNumber: '0' }, { theNumber: '1' }], + }, + ], + $or: [ + { + aValue: ['foo', 'bar'], + }, + ], + $not: { myTag: 'notfoobar' }, + }) + ).toEqual(expectedQuery) + }) + + it('should retrieve correct paginated records', async () => { + await insertRecord({ tags: { myTag: 'notfoobar' } }) + await insertRecord({ tags: { myTag: 'foobar' } }) + await insertRecord({ tags: { myTag: 'foobar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery(agentContext, TestRecord, {}, { offset: 3, limit: 2 }) + + expect(records.length).toBe(1) + }) + + it('should retrieve correct paginated records that match the query', async () => { + await insertRecord({ tags: { myTag: 'notfoobar' } }) + const expectedRecord1 = await insertRecord({ tags: { myTag: 'foobar' } }) + const expectedRecord2 = await insertRecord({ tags: { myTag: 'foobar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery( + agentContext, + TestRecord, + { + myTag: 'foobar', + }, + { offset: 0, limit: 2 } + ) + + expect(records.length).toBe(2) + expect(records).toEqual(expect.arrayContaining([expectedRecord1, expectedRecord2])) + }) + }) +}) diff --git a/packages/askar/src/storage/index.ts b/packages/askar/src/storage/index.ts new file mode 100644 index 0000000000..ac0265f1ea --- /dev/null +++ b/packages/askar/src/storage/index.ts @@ -0,0 +1 @@ +export * from './AskarStorageService' diff --git a/packages/askar/src/storage/utils.ts b/packages/askar/src/storage/utils.ts new file mode 100644 index 0000000000..0a9cd001a0 --- /dev/null +++ b/packages/askar/src/storage/utils.ts @@ -0,0 +1,111 @@ +import type { BaseRecord, BaseRecordConstructor, Query, TagsBase } from '@credo-ts/core' +import type { EntryObject } from '@hyperledger/aries-askar-shared' + +import { JsonTransformer } from '@credo-ts/core' + +export function recordToInstance(record: EntryObject, recordClass: BaseRecordConstructor): T { + const instance = JsonTransformer.deserialize(record.value as string, recordClass) + instance.id = record.name + + const tags = record.tags ? transformToRecordTagValues(record.tags) : {} + instance.replaceTags(tags) + + return instance +} + +export function transformToRecordTagValues(tags: Record): TagsBase { + const transformedTags: TagsBase = {} + + for (const [key, value] of Object.entries(tags)) { + // If the value is a boolean string ('1' or '0') + // use the boolean val + if (value === '1' && key?.includes(':')) { + const [tagName, ...tagValues] = key.split(':') + const tagValue = tagValues.join(':') + + const transformedValue = transformedTags[tagName] + + if (Array.isArray(transformedValue)) { + transformedTags[tagName] = [...transformedValue, tagValue] + } else { + transformedTags[tagName] = [tagValue] + } + } + // Transform '1' and '0' to boolean + else if (value === '1' || value === '0') { + transformedTags[key] = value === '1' + } + // If 1 or 0 is prefixed with 'n__' we need to remove it. This is to prevent + // casting the value to a boolean + else if (value === 'n__1' || value === 'n__0') { + transformedTags[key] = value === 'n__1' ? '1' : '0' + } + // Otherwise just use the value + else { + transformedTags[key] = value as string + } + } + + return transformedTags +} + +export function transformFromRecordTagValues(tags: TagsBase): { [key: string]: string | undefined } { + const transformedTags: { [key: string]: string | undefined } = {} + + for (const [key, value] of Object.entries(tags)) { + // If the value is of type null we use the value undefined + // Askar doesn't support null as a value + if (value === null) { + transformedTags[key] = undefined + } + // If the value is a boolean use the Askar + // '1' or '0' syntax + else if (typeof value === 'boolean') { + transformedTags[key] = value ? '1' : '0' + } + // If the value is 1 or 0, we need to add something to the value, otherwise + // the next time we deserialize the tag values it will be converted to boolean + else if (value === '1' || value === '0') { + transformedTags[key] = `n__${value}` + } + // If the value is an array we create a tag for each array + // item ("tagName:arrayItem" = "1") + else if (Array.isArray(value)) { + value.forEach((item) => { + const tagName = `${key}:${item}` + transformedTags[tagName] = '1' + }) + } + // Otherwise just use the value + else { + transformedTags[key] = value + } + } + + return transformedTags +} + +/** + * Transforms the search query into a wallet query compatible with Askar WQL. + * + * The format used by Credo is almost the same as the WQL query, with the exception of + * the encoding of values, however this is handled by the {@link AskarStorageServiceUtil.transformToRecordTagValues} + * method. + */ +export function askarQueryFromSearchQuery(query: Query): Record { + // eslint-disable-next-line prefer-const + let { $and, $or, $not, ...tags } = query + + $and = ($and as Query[] | undefined)?.map((q) => askarQueryFromSearchQuery(q)) + $or = ($or as Query[] | undefined)?.map((q) => askarQueryFromSearchQuery(q)) + $not = $not ? askarQueryFromSearchQuery($not as Query) : undefined + + const askarQuery = { + ...transformFromRecordTagValues(tags as unknown as TagsBase), + $and, + $or, + $not, + } + + return askarQuery +} diff --git a/packages/askar/src/utils/askarError.ts b/packages/askar/src/utils/askarError.ts new file mode 100644 index 0000000000..632733413a --- /dev/null +++ b/packages/askar/src/utils/askarError.ts @@ -0,0 +1,17 @@ +import { AriesAskarError } from '@hyperledger/aries-askar-shared' + +export enum AskarErrorCode { + Success = 0, + Backend = 1, + Busy = 2, + Duplicate = 3, + Encryption = 4, + Input = 5, + NotFound = 6, + Unexpected = 7, + Unsupported = 8, + Custom = 100, +} + +export const isAskarError = (error: Error, askarErrorCode?: AskarErrorCode): error is AriesAskarError => + error instanceof AriesAskarError && (askarErrorCode === undefined || error.code === askarErrorCode) diff --git a/packages/askar/src/utils/askarKeyTypes.ts b/packages/askar/src/utils/askarKeyTypes.ts new file mode 100644 index 0000000000..5288ccc565 --- /dev/null +++ b/packages/askar/src/utils/askarKeyTypes.ts @@ -0,0 +1,44 @@ +import { KeyType } from '@credo-ts/core' +import { KeyAlgs } from '@hyperledger/aries-askar-shared' + +export enum AskarKeyTypePurpose { + KeyManagement = 'KeyManagement', + Signing = 'Signing', +} + +const keyTypeToAskarAlg = { + [KeyType.Ed25519]: { + keyAlg: KeyAlgs.Ed25519, + purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing], + }, + [KeyType.X25519]: { + keyAlg: KeyAlgs.X25519, + purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing], + }, + [KeyType.Bls12381g1]: { + keyAlg: KeyAlgs.Bls12381G1, + purposes: [AskarKeyTypePurpose.KeyManagement], + }, + [KeyType.Bls12381g2]: { + keyAlg: KeyAlgs.Bls12381G2, + purposes: [AskarKeyTypePurpose.KeyManagement], + }, + [KeyType.Bls12381g1g2]: { + keyAlg: KeyAlgs.Bls12381G1, + purposes: [AskarKeyTypePurpose.KeyManagement], + }, + [KeyType.P256]: { + keyAlg: KeyAlgs.EcSecp256r1, + purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing], + }, + [KeyType.K256]: { + keyAlg: KeyAlgs.EcSecp256k1, + purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing], + }, +} + +export const isKeyTypeSupportedByAskarForPurpose = (keyType: KeyType, purpose: AskarKeyTypePurpose) => + keyType in keyTypeToAskarAlg && + keyTypeToAskarAlg[keyType as keyof typeof keyTypeToAskarAlg].purposes.includes(purpose) + +export const keyTypesSupportedByAskar = Object.keys(keyTypeToAskarAlg) as KeyType[] diff --git a/packages/askar/src/utils/askarWalletConfig.ts b/packages/askar/src/utils/askarWalletConfig.ts new file mode 100644 index 0000000000..1819222e2a --- /dev/null +++ b/packages/askar/src/utils/askarWalletConfig.ts @@ -0,0 +1,87 @@ +import type { WalletConfig } from '@credo-ts/core' + +import { KeyDerivationMethod, WalletError } from '@credo-ts/core' +import { KdfMethod, StoreKeyMethod } from '@hyperledger/aries-askar-shared' + +import { + isAskarWalletPostgresStorageConfig, + isAskarWalletSqliteStorageConfig, +} from '../wallet/AskarWalletStorageConfig' + +export const keyDerivationMethodToStoreKeyMethod = (keyDerivationMethod: KeyDerivationMethod) => { + const correspondenceTable = { + [KeyDerivationMethod.Raw]: KdfMethod.Raw, + [KeyDerivationMethod.Argon2IInt]: KdfMethod.Argon2IInt, + [KeyDerivationMethod.Argon2IMod]: KdfMethod.Argon2IMod, + } + + return new StoreKeyMethod(correspondenceTable[keyDerivationMethod]) +} + +/** + * Creates a proper askar wallet URI value based on walletConfig + * @param walletConfig WalletConfig object + * @param credoDataPath framework data path (used in case walletConfig.storage.path is undefined) + * @returns string containing the askar wallet URI + */ +export const uriFromWalletConfig = ( + walletConfig: WalletConfig, + credoDataPath: string +): { uri: string; path?: string } => { + let uri = '' + let path + + // By default use sqlite as database backend + if (!walletConfig.storage) { + walletConfig.storage = { type: 'sqlite' } + } + + const urlParams = [] + + const storageConfig = walletConfig.storage + if (isAskarWalletSqliteStorageConfig(storageConfig)) { + if (storageConfig.config?.inMemory) { + uri = 'sqlite://:memory:' + } else { + path = storageConfig.config?.path ?? `${credoDataPath}/wallet/${walletConfig.id}/sqlite.db` + uri = `sqlite://${path}` + } + } else if (isAskarWalletPostgresStorageConfig(storageConfig)) { + if (!storageConfig.config || !storageConfig.credentials) { + throw new WalletError('Invalid storage configuration for postgres wallet') + } + + if (storageConfig.config.connectTimeout !== undefined) { + urlParams.push(`connect_timeout=${encodeURIComponent(storageConfig.config.connectTimeout)}`) + } + if (storageConfig.config.idleTimeout !== undefined) { + urlParams.push(`idle_timeout=${encodeURIComponent(storageConfig.config.idleTimeout)}`) + } + if (storageConfig.credentials.adminAccount !== undefined) { + urlParams.push(`admin_account=${encodeURIComponent(storageConfig.credentials.adminAccount)}`) + } + if (storageConfig.credentials.adminPassword !== undefined) { + urlParams.push(`admin_password=${encodeURIComponent(storageConfig.credentials.adminPassword)}`) + } + + uri = `postgres://${encodeURIComponent(storageConfig.credentials.account)}:${encodeURIComponent( + storageConfig.credentials.password + )}@${storageConfig.config.host}/${encodeURIComponent(walletConfig.id)}` + } else { + throw new WalletError(`Storage type not supported: ${storageConfig.type}`) + } + + // Common config options + if (storageConfig.config?.maxConnections !== undefined) { + urlParams.push(`max_connections=${encodeURIComponent(storageConfig.config.maxConnections)}`) + } + if (storageConfig.config?.minConnections !== undefined) { + urlParams.push(`min_connections=${encodeURIComponent(storageConfig.config.minConnections)}`) + } + + if (urlParams.length > 0) { + uri = `${uri}?${urlParams.join('&')}` + } + + return { uri, path } +} diff --git a/packages/askar/src/utils/assertAskarWallet.ts b/packages/askar/src/utils/assertAskarWallet.ts new file mode 100644 index 0000000000..40879c5468 --- /dev/null +++ b/packages/askar/src/utils/assertAskarWallet.ts @@ -0,0 +1,15 @@ +import type { Wallet } from '@credo-ts/core' + +import { CredoError } from '@credo-ts/core' + +import { AskarWallet, AskarProfileWallet } from '../wallet' + +export function assertAskarWallet(wallet: Wallet): asserts wallet is AskarProfileWallet | AskarWallet { + if (!(wallet instanceof AskarProfileWallet) && !(wallet instanceof AskarWallet)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const walletClassName = (wallet as any).constructor?.name ?? 'unknown' + throw new CredoError( + `Expected wallet to be instance of AskarProfileWallet or AskarWallet, found ${walletClassName}` + ) + } +} diff --git a/packages/askar/src/utils/index.ts b/packages/askar/src/utils/index.ts new file mode 100644 index 0000000000..b9f658de82 --- /dev/null +++ b/packages/askar/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './askarError' +export * from './askarKeyTypes' +export * from './askarWalletConfig' diff --git a/packages/askar/src/wallet/AskarBaseWallet.ts b/packages/askar/src/wallet/AskarBaseWallet.ts new file mode 100644 index 0000000000..5d0415387b --- /dev/null +++ b/packages/askar/src/wallet/AskarBaseWallet.ts @@ -0,0 +1,483 @@ +import type { + EncryptedMessage, + WalletConfig, + WalletCreateKeyOptions, + WalletSignOptions, + UnpackedMessageContext, + WalletVerifyOptions, + Wallet, + WalletConfigRekey, + KeyPair, + WalletExportImportConfig, + Logger, + SigningProviderRegistry, +} from '@credo-ts/core' +import type { Session } from '@hyperledger/aries-askar-shared' + +import { + WalletKeyExistsError, + isValidSeed, + isValidPrivateKey, + JsonEncoder, + Buffer, + CredoError, + WalletError, + Key, + TypedArrayEncoder, +} from '@credo-ts/core' +import { CryptoBox, Store, Key as AskarKey, keyAlgFromString } from '@hyperledger/aries-askar-shared' +import BigNumber from 'bn.js' + +import { + AskarErrorCode, + AskarKeyTypePurpose, + isAskarError, + isKeyTypeSupportedByAskarForPurpose, + keyTypesSupportedByAskar, +} from '../utils' + +import { didcommV1Pack, didcommV1Unpack } from './didcommV1' + +const isError = (error: unknown): error is Error => error instanceof Error + +export abstract class AskarBaseWallet implements Wallet { + protected logger: Logger + protected signingKeyProviderRegistry: SigningProviderRegistry + + public constructor(logger: Logger, signingKeyProviderRegistry: SigningProviderRegistry) { + this.logger = logger + this.signingKeyProviderRegistry = signingKeyProviderRegistry + } + + /** + * Abstract methods that need to be implemented by subclasses + */ + public abstract isInitialized: boolean + public abstract isProvisioned: boolean + public abstract create(walletConfig: WalletConfig): Promise + public abstract createAndOpen(walletConfig: WalletConfig): Promise + public abstract open(walletConfig: WalletConfig): Promise + public abstract rotateKey(walletConfig: WalletConfigRekey): Promise + public abstract close(): Promise + public abstract delete(): Promise + public abstract export(exportConfig: WalletExportImportConfig): Promise + public abstract import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig): Promise + public abstract dispose(): void | Promise + public abstract profile: string + + protected abstract store: Store + + /** + * Run callback with the session provided, the session will + * be closed once the callback resolves or rejects if it is not closed yet. + * + * TODO: update to new `using` syntax so we don't have to use a callback + */ + public async withSession(callback: (session: Session) => Return): Promise> { + let session: Session | undefined = undefined + try { + session = await this.store.session(this.profile).open() + + const result = await callback(session) + + return result + } finally { + if (session?.handle) { + await session.close() + } + } + } + + /** + * Run callback with a transaction. If the callback resolves the transaction + * will be committed if the transaction is not closed yet. If the callback rejects + * the transaction will be rolled back if the transaction is not closed yet. + * + * TODO: update to new `using` syntax so we don't have to use a callback + */ + public async withTransaction(callback: (transaction: Session) => Return): Promise> { + let session: Session | undefined = undefined + try { + session = await this.store.transaction(this.profile).open() + + const result = await callback(session) + + if (session.handle) { + await session.commit() + } + return result + } catch (error) { + if (session?.handle) { + await session?.rollback() + } + + throw error + } + } + + public get supportedKeyTypes() { + const signingKeyProviderSupportedKeyTypes = this.signingKeyProviderRegistry.supportedKeyTypes + + return Array.from(new Set([...keyTypesSupportedByAskar, ...signingKeyProviderSupportedKeyTypes])) + } + + /** + * Create a key with an optional seed and keyType. + * The keypair is also automatically stored in the wallet afterwards + */ + public async createKey({ seed, privateKey, keyType }: WalletCreateKeyOptions): Promise { + try { + if (seed && privateKey) { + throw new WalletError('Only one of seed and privateKey can be set') + } + + if (seed && !isValidSeed(seed, keyType)) { + throw new WalletError('Invalid seed provided') + } + + if (privateKey && !isValidPrivateKey(privateKey, keyType)) { + throw new WalletError('Invalid private key provided') + } + + if (isKeyTypeSupportedByAskarForPurpose(keyType, AskarKeyTypePurpose.KeyManagement)) { + const algorithm = keyAlgFromString(keyType) + + // Create key + let key: AskarKey | undefined + try { + const _key = privateKey + ? AskarKey.fromSecretBytes({ secretKey: privateKey, algorithm }) + : seed + ? AskarKey.fromSeed({ seed, algorithm }) + : AskarKey.generate(algorithm) + + // FIXME: we need to create a separate const '_key' so TS definitely knows _key is defined in the session callback. + // This will be fixed once we use the new 'using' syntax + key = _key + + const keyPublicBytes = key.publicBytes + + // Store key + await this.withSession((session) => + session.insertKey({ key: _key, name: TypedArrayEncoder.toBase58(keyPublicBytes) }) + ) + + key.handle.free() + return Key.fromPublicKey(keyPublicBytes, keyType) + } catch (error) { + key?.handle.free() + // Handle case where key already exists + if (isAskarError(error, AskarErrorCode.Duplicate)) { + throw new WalletKeyExistsError('Key already exists') + } + + // Otherwise re-throw error + throw error + } + } else { + // Check if there is a signing key provider for the specified key type. + if (this.signingKeyProviderRegistry.hasProviderForKeyType(keyType)) { + const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(keyType) + + const keyPair = await signingKeyProvider.createKeyPair({ seed, privateKey }) + await this.storeKeyPair(keyPair) + return Key.fromPublicKeyBase58(keyPair.publicKeyBase58, keyType) + } + throw new WalletError(`Unsupported key type: '${keyType}'`) + } + } catch (error) { + // If already instance of `WalletError`, re-throw + if (error instanceof WalletError) throw error + + if (!isError(error)) { + throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError(`Error creating key with key type '${keyType}': ${error.message}`, { cause: error }) + } + } + + /** + * sign a Buffer with an instance of a Key class + * + * @param data Buffer The data that needs to be signed + * @param key Key The key that is used to sign the data + * + * @returns A signature for the data + */ + public async sign({ data, key }: WalletSignOptions): Promise { + let askarKey: AskarKey | null | undefined + let keyPair: KeyPair | null | undefined + + try { + if (isKeyTypeSupportedByAskarForPurpose(key.keyType, AskarKeyTypePurpose.KeyManagement)) { + askarKey = await this.withSession( + async (session) => (await session.fetchKey({ name: key.publicKeyBase58 }))?.key + ) + } + + // FIXME: remove the custom KeyPair record now that we deprecate Indy SDK. + // We can do this in a migration script + + // Fallback to fetching key from the non-askar storage, this is to handle the case + // where a key wasn't supported at first by the wallet, but now is + if (!askarKey) { + // TODO: we should probably make retrieveKeyPair + insertKey + deleteKeyPair a transaction + keyPair = await this.retrieveKeyPair(key.publicKeyBase58) + + // If we have the key stored in a custom record, but it is now supported by Askar, + // we 'import' the key into askar storage and remove the custom key record + if (keyPair && isKeyTypeSupportedByAskarForPurpose(keyPair.keyType, AskarKeyTypePurpose.KeyManagement)) { + const _askarKey = AskarKey.fromSecretBytes({ + secretKey: TypedArrayEncoder.fromBase58(keyPair.privateKeyBase58), + algorithm: keyAlgFromString(keyPair.keyType), + }) + askarKey = _askarKey + + await this.withSession((session) => + session.insertKey({ + name: key.publicKeyBase58, + key: _askarKey, + }) + ) + + // Now we can remove it from the custom record as we have imported it into Askar + await this.deleteKeyPair(key.publicKeyBase58) + keyPair = undefined + } + } + + if (!askarKey && !keyPair) { + throw new WalletError('Key entry not found') + } + + // Not all keys are supported for signing + if (isKeyTypeSupportedByAskarForPurpose(key.keyType, AskarKeyTypePurpose.Signing)) { + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`Currently not supporting signing of multiple messages`) + } + + askarKey = + askarKey ?? + (keyPair + ? AskarKey.fromSecretBytes({ + secretKey: TypedArrayEncoder.fromBase58(keyPair.privateKeyBase58), + algorithm: keyAlgFromString(keyPair.keyType), + }) + : undefined) + + if (!askarKey) { + throw new WalletError('Key entry not found') + } + + const signed = askarKey.signMessage({ message: data as Buffer }) + return Buffer.from(signed) + } else { + // Check if there is a signing key provider for the specified key type. + if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { + const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) + + // It could be that askar supports storing the key, but can't sign with it + // (in case of bls) + const privateKeyBase58 = + keyPair?.privateKeyBase58 ?? + (askarKey?.secretBytes ? TypedArrayEncoder.toBase58(askarKey.secretBytes) : undefined) + + if (!privateKeyBase58) { + throw new WalletError('Key entry not found') + } + const signed = await signingKeyProvider.sign({ + data, + privateKeyBase58: privateKeyBase58, + publicKeyBase58: key.publicKeyBase58, + }) + + return signed + } + throw new WalletError(`Unsupported keyType: ${key.keyType}`) + } + } catch (error) { + if (!isError(error)) { + throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError(`Error signing data with verkey ${key.publicKeyBase58}. ${error.message}`, { cause: error }) + } finally { + askarKey?.handle.free() + } + } + + /** + * Verify the signature with the data and the used key + * + * @param data Buffer The data that has to be confirmed to be signed + * @param key Key The key that was used in the signing process + * @param signature Buffer The signature that was created by the signing process + * + * @returns A boolean whether the signature was created with the supplied data and key + * + * @throws {WalletError} When it could not do the verification + * @throws {WalletError} When an unsupported keytype is used + */ + public async verify({ data, key, signature }: WalletVerifyOptions): Promise { + let askarKey: AskarKey | undefined + try { + if (isKeyTypeSupportedByAskarForPurpose(key.keyType, AskarKeyTypePurpose.Signing)) { + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`Currently not supporting verification of multiple messages`) + } + + askarKey = AskarKey.fromPublicBytes({ + algorithm: keyAlgFromString(key.keyType), + publicKey: key.publicKey, + }) + const verified = askarKey.verifySignature({ message: data as Buffer, signature }) + askarKey.handle.free() + return verified + } else if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { + // Check if there is a signing key provider for the specified key type. + const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) + const signed = await signingKeyProvider.verify({ + data, + signature, + publicKeyBase58: key.publicKeyBase58, + }) + + return signed + } else { + throw new WalletError(`Unsupported keyType: ${key.keyType}`) + } + } catch (error) { + askarKey?.handle.free() + if (!isError(error)) { + throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError(`Error verifying signature of data signed with verkey ${key.publicKeyBase58}`, { + cause: error, + }) + } + } + + /** + * Pack a message using DIDComm V1 algorithm + * + * @param payload message to send + * @param recipientKeys array containing recipient keys in base58 + * @param senderVerkey sender key in base58 + * @returns JWE Envelope to send + */ + public async pack( + payload: Record, + recipientKeys: string[], + senderVerkey?: string // in base58 + ): Promise { + const senderKey = senderVerkey + ? await this.withSession((session) => session.fetchKey({ name: senderVerkey })) + : undefined + + try { + if (senderVerkey && !senderKey) { + throw new WalletError(`Sender key not found`) + } + + const envelope = didcommV1Pack(payload, recipientKeys, senderKey?.key) + + return envelope + } finally { + senderKey?.key.handle.free() + } + } + + /** + * Unpacks a JWE Envelope coded using DIDComm V1 algorithm + * + * @param messagePackage JWE Envelope + * @returns UnpackedMessageContext with plain text message, sender key and recipient key + */ + public async unpack(messagePackage: EncryptedMessage): Promise { + const protectedJson = JsonEncoder.fromBase64(messagePackage.protected) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recipientKids: string[] = protectedJson.recipients.map((r: any) => r.header.kid) + + // TODO: how long should sessions last? Just for the duration of the unpack? Or should each item in the recipientKids get a separate session? + const returnValue = await this.withSession(async (session) => { + for (const recipientKid of recipientKids) { + const recipientKeyEntry = await session.fetchKey({ name: recipientKid }) + try { + if (recipientKeyEntry) { + return didcommV1Unpack(messagePackage, recipientKeyEntry.key) + } + } finally { + recipientKeyEntry?.key.handle.free() + } + } + }) + + if (!returnValue) { + throw new WalletError('No corresponding recipient key found') + } + + return returnValue + } + + public async generateNonce(): Promise { + try { + // generate an 80-bit nonce suitable for AnonCreds proofs + const nonce = CryptoBox.randomNonce().slice(0, 10) + return new BigNumber(nonce).toString() + } catch (error) { + if (!isError(error)) { + throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError('Error generating nonce', { cause: error }) + } + } + + public async generateWalletKey() { + try { + return Store.generateRawKey() + } catch (error) { + throw new WalletError('Error generating wallet key', { cause: error }) + } + } + + private async retrieveKeyPair(publicKeyBase58: string): Promise { + try { + const entryObject = await this.withSession((session) => + session.fetch({ category: 'KeyPairRecord', name: `key-${publicKeyBase58}` }) + ) + + if (!entryObject) return null + + return JsonEncoder.fromString(entryObject?.value as string) as KeyPair + } catch (error) { + throw new WalletError('Error retrieving KeyPair record', { cause: error }) + } + } + + private async deleteKeyPair(publicKeyBase58: string): Promise { + try { + await this.withSession((session) => session.remove({ category: 'KeyPairRecord', name: `key-${publicKeyBase58}` })) + } catch (error) { + throw new WalletError('Error removing KeyPair record', { cause: error }) + } + } + + private async storeKeyPair(keyPair: KeyPair): Promise { + try { + await this.withSession((session) => + session.insert({ + category: 'KeyPairRecord', + name: `key-${keyPair.publicKeyBase58}`, + value: JSON.stringify(keyPair), + tags: { + keyType: keyPair.keyType, + }, + }) + ) + } catch (error) { + if (isAskarError(error, AskarErrorCode.Duplicate)) { + throw new WalletKeyExistsError('Key already exists') + } + throw new WalletError('Error saving KeyPair record', { cause: error }) + } + } +} diff --git a/packages/askar/src/wallet/AskarProfileWallet.ts b/packages/askar/src/wallet/AskarProfileWallet.ts new file mode 100644 index 0000000000..c3586404de --- /dev/null +++ b/packages/askar/src/wallet/AskarProfileWallet.ts @@ -0,0 +1,178 @@ +import type { WalletConfig } from '@credo-ts/core' + +import { + WalletExportUnsupportedError, + WalletDuplicateError, + WalletNotFoundError, + InjectionSymbols, + Logger, + SigningProviderRegistry, + WalletError, +} from '@credo-ts/core' +import { Store } from '@hyperledger/aries-askar-shared' +import { inject, injectable } from 'tsyringe' + +import { AskarErrorCode, isAskarError } from '../utils' + +import { AskarBaseWallet } from './AskarBaseWallet' + +@injectable() +export class AskarProfileWallet extends AskarBaseWallet { + private walletConfig?: WalletConfig + public readonly store: Store + public isInitialized = false + + public constructor( + store: Store, + @inject(InjectionSymbols.Logger) logger: Logger, + signingKeyProviderRegistry: SigningProviderRegistry + ) { + super(logger, signingKeyProviderRegistry) + + this.store = store + } + + public get isProvisioned() { + return this.walletConfig !== undefined + } + + public get profile() { + if (!this.walletConfig) { + throw new WalletError('No profile configured.') + } + + return this.walletConfig.id + } + + /** + * Dispose method is called when an agent context is disposed. + */ + public async dispose() { + if (this.isInitialized) { + await this.close() + } + } + + public async create(walletConfig: WalletConfig): Promise { + this.logger.debug(`Creating wallet for profile '${walletConfig.id}'`) + + try { + await this.store.createProfile(walletConfig.id) + } catch (error) { + if (isAskarError(error, AskarErrorCode.Duplicate)) { + const errorMessage = `Wallet for profile '${walletConfig.id}' already exists` + this.logger.debug(errorMessage) + + throw new WalletDuplicateError(errorMessage, { + walletType: 'AskarProfileWallet', + cause: error, + }) + } + + const errorMessage = `Error creating wallet for profile '${walletConfig.id}'` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + + this.logger.debug(`Successfully created wallet for profile '${walletConfig.id}'`) + } + + public async open(walletConfig: WalletConfig): Promise { + this.logger.debug(`Opening wallet for profile '${walletConfig.id}'`) + + try { + this.walletConfig = walletConfig + + // TODO: what is faster? listProfiles or open and close session? + // I think open/close is more scalable (what if profiles is 10.000.000?) + // We just want to check if the profile exists. Because the wallet initialization logic + // first tries to open, and if it doesn't exist it will create it. So we must check here + // if the profile exists + await this.withSession(() => { + /* no-op */ + }) + this.isInitialized = true + } catch (error) { + // Profile does not exist + if (isAskarError(error, AskarErrorCode.NotFound)) { + const errorMessage = `Wallet for profile '${walletConfig.id}' not found` + this.logger.debug(errorMessage) + + throw new WalletNotFoundError(errorMessage, { + walletType: 'AskarProfileWallet', + cause: error, + }) + } + + const errorMessage = `Error opening wallet for profile '${walletConfig.id}'` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + + this.logger.debug(`Successfully opened wallet for profile '${walletConfig.id}'`) + } + + public async createAndOpen(walletConfig: WalletConfig): Promise { + await this.create(walletConfig) + await this.open(walletConfig) + } + + public async delete() { + if (!this.walletConfig) { + throw new WalletError( + 'Can not delete wallet that does not have wallet config set. Make sure to call create wallet before deleting the wallet' + ) + } + + this.logger.info(`Deleting profile '${this.profile}'`) + if (this.isInitialized) { + await this.close() + } + + try { + await this.store.removeProfile(this.profile) + } catch (error) { + const errorMessage = `Error deleting wallet for profile '${this.profile}': ${error.message}` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + } + + public async export() { + // This PR should help with this: https://github.com/hyperledger/aries-askar/pull/159 + throw new WalletExportUnsupportedError('Exporting a profile is not supported.') + } + + public async import() { + // This PR should help with this: https://github.com/hyperledger/aries-askar/pull/159 + throw new WalletError('Importing a profile is not supported.') + } + + public async rotateKey(): Promise { + throw new WalletError( + 'Rotating a key is not supported for a profile. You can rotate the key on the main askar wallet.' + ) + } + + public async close() { + this.logger.debug(`Closing wallet for profile ${this.walletConfig?.id}`) + + if (!this.isInitialized) { + throw new WalletError('Wallet is in invalid state, you are trying to close wallet that is not initialized.') + } + + this.isInitialized = false + } +} diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts new file mode 100644 index 0000000000..34be645d9c --- /dev/null +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -0,0 +1,422 @@ +import type { WalletConfig, WalletConfigRekey, WalletExportImportConfig } from '@credo-ts/core' + +import { + WalletExportPathExistsError, + WalletInvalidKeyError, + WalletDuplicateError, + CredoError, + Logger, + WalletError, + InjectionSymbols, + SigningProviderRegistry, + FileSystem, + WalletNotFoundError, + KeyDerivationMethod, + WalletImportPathExistsError, + WalletExportUnsupportedError, +} from '@credo-ts/core' +import { Store } from '@hyperledger/aries-askar-shared' +import { inject, injectable } from 'tsyringe' + +import { AskarErrorCode, isAskarError, keyDerivationMethodToStoreKeyMethod, uriFromWalletConfig } from '../utils' + +import { AskarBaseWallet } from './AskarBaseWallet' +import { isAskarWalletSqliteStorageConfig } from './AskarWalletStorageConfig' + +/** + * @todo: rename after 0.5.0, as we now have multiple types of AskarWallet + */ +@injectable() +export class AskarWallet extends AskarBaseWallet { + private fileSystem: FileSystem + + private walletConfig?: WalletConfig + private _store?: Store + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + @inject(InjectionSymbols.FileSystem) fileSystem: FileSystem, + signingKeyProviderRegistry: SigningProviderRegistry + ) { + super(logger, signingKeyProviderRegistry) + this.fileSystem = fileSystem + } + + public get isProvisioned() { + return this.walletConfig !== undefined + } + + public get isInitialized() { + return this._store !== undefined + } + + public get store() { + if (!this._store) { + throw new CredoError( + 'Wallet has not been initialized yet. Make sure to await agent.initialize() before using the agent.' + ) + } + + return this._store + } + + public get profile() { + if (!this.walletConfig) { + throw new WalletError('No profile configured.') + } + + return this.walletConfig.id + } + + /** + * Dispose method is called when an agent context is disposed. + */ + public async dispose() { + if (this.isInitialized) { + await this.close() + } + } + + /** + * @throws {WalletDuplicateError} if the wallet already exists + * @throws {WalletError} if another error occurs + */ + public async create(walletConfig: WalletConfig): Promise { + await this.createAndOpen(walletConfig) + await this.close() + } + + /** + * @throws {WalletDuplicateError} if the wallet already exists + * @throws {WalletError} if another error occurs + */ + public async createAndOpen(walletConfig: WalletConfig): Promise { + this.logger.debug(`Creating wallet '${walletConfig.id}`) + + const askarWalletConfig = await this.getAskarWalletConfig(walletConfig) + + // Check if database exists + const { path: filePath } = uriFromWalletConfig(walletConfig, this.fileSystem.dataPath) + if (filePath && (await this.fileSystem.exists(filePath))) { + throw new WalletDuplicateError(`Wallet '${walletConfig.id}' already exists.`, { + walletType: 'AskarWallet', + }) + } + try { + // Make sure path exists before creating the wallet + if (filePath) { + await this.fileSystem.createDirectory(filePath) + } + + this._store = await Store.provision({ + recreate: false, + uri: askarWalletConfig.uri, + profile: askarWalletConfig.profile, + keyMethod: askarWalletConfig.keyMethod, + passKey: askarWalletConfig.passKey, + }) + + // TODO: Should we do something to check if it exists? + // Like this.withSession()? + + this.walletConfig = walletConfig + } catch (error) { + // FIXME: Askar should throw a Duplicate error code, but is currently returning Encryption + // And if we provide the very same wallet key, it will open it without any error + if ( + isAskarError(error) && + (error.code === AskarErrorCode.Encryption || error.code === AskarErrorCode.Duplicate) + ) { + const errorMessage = `Wallet '${walletConfig.id}' already exists` + this.logger.debug(errorMessage) + + throw new WalletDuplicateError(errorMessage, { + walletType: 'AskarWallet', + cause: error, + }) + } + + const errorMessage = `Error creating wallet '${walletConfig.id}'` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + + this.logger.debug(`Successfully created wallet '${walletConfig.id}'`) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + public async open(walletConfig: WalletConfig): Promise { + await this._open(walletConfig) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + public async rotateKey(walletConfig: WalletConfigRekey): Promise { + if (!walletConfig.rekey) { + throw new WalletError('Wallet rekey undefined!. Please specify the new wallet key') + } + await this._open( + { + id: walletConfig.id, + key: walletConfig.key, + keyDerivationMethod: walletConfig.keyDerivationMethod, + }, + walletConfig.rekey, + walletConfig.rekeyDerivationMethod + ) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + private async _open( + walletConfig: WalletConfig, + rekey?: string, + rekeyDerivation?: KeyDerivationMethod + ): Promise { + if (this._store) { + throw new WalletError( + 'Wallet instance already opened. Close the currently opened wallet before re-opening the wallet' + ) + } + + const askarWalletConfig = await this.getAskarWalletConfig(walletConfig) + + try { + this._store = await Store.open({ + uri: askarWalletConfig.uri, + keyMethod: askarWalletConfig.keyMethod, + passKey: askarWalletConfig.passKey, + }) + + if (rekey) { + await this._store.rekey({ + passKey: rekey, + keyMethod: keyDerivationMethodToStoreKeyMethod(rekeyDerivation ?? KeyDerivationMethod.Argon2IMod), + }) + } + + // TODO: Should we do something to check if it exists? + // Like this.withSession()? + + this.walletConfig = walletConfig + } catch (error) { + if ( + isAskarError(error) && + (error.code === AskarErrorCode.NotFound || + (error.code === AskarErrorCode.Backend && + isAskarWalletSqliteStorageConfig(walletConfig.storage) && + walletConfig.storage.config?.inMemory)) + ) { + const errorMessage = `Wallet '${walletConfig.id}' not found` + this.logger.debug(errorMessage) + + throw new WalletNotFoundError(errorMessage, { + walletType: 'AskarWallet', + cause: error, + }) + } else if (isAskarError(error) && error.code === AskarErrorCode.Encryption) { + const errorMessage = `Incorrect key for wallet '${walletConfig.id}'` + this.logger.debug(errorMessage) + throw new WalletInvalidKeyError(errorMessage, { + walletType: 'AskarWallet', + cause: error, + }) + } + throw new WalletError(`Error opening wallet ${walletConfig.id}: ${error.message}`, { cause: error }) + } + + this.logger.debug(`Wallet '${walletConfig.id}' opened with handle '${this._store.handle.handle}'`) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + public async delete(): Promise { + if (!this.walletConfig) { + throw new WalletError( + 'Can not delete wallet that does not have wallet config set. Make sure to call create wallet before deleting the wallet' + ) + } + + this.logger.info(`Deleting wallet '${this.walletConfig.id}'`) + if (this._store) { + await this.close() + } + + try { + const { uri } = uriFromWalletConfig(this.walletConfig, this.fileSystem.dataPath) + await Store.remove(uri) + } catch (error) { + const errorMessage = `Error deleting wallet '${this.walletConfig.id}': ${error.message}` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + } + + public async export(exportConfig: WalletExportImportConfig) { + if (!this.walletConfig) { + throw new WalletError( + 'Can not export wallet that does not have wallet config set. Make sure to open it before exporting' + ) + } + + const { path: destinationPath, key: exportKey } = exportConfig + + const { path: sourcePath } = uriFromWalletConfig(this.walletConfig, this.fileSystem.dataPath) + + if (isAskarWalletSqliteStorageConfig(this.walletConfig.storage) && this.walletConfig.storage?.inMemory) { + throw new WalletExportUnsupportedError('Export is not supported for in memory wallet') + } + if (!sourcePath) { + throw new WalletExportUnsupportedError('Export is only supported for SQLite backend') + } + + try { + // Export path already exists + if (await this.fileSystem.exists(destinationPath)) { + throw new WalletExportPathExistsError( + `Unable to create export, wallet export at path '${exportConfig.path}' already exists` + ) + } + const exportedWalletConfig = await this.getAskarWalletConfig({ + ...this.walletConfig, + key: exportKey, + storage: { type: 'sqlite', config: { path: destinationPath } }, + }) + + // Make sure destination path exists + await this.fileSystem.createDirectory(destinationPath) + + await this.store.copyTo({ + recreate: false, + uri: exportedWalletConfig.uri, + keyMethod: exportedWalletConfig.keyMethod, + passKey: exportedWalletConfig.passKey, + }) + } catch (error) { + const errorMessage = `Error exporting wallet '${this.walletConfig.id}': ${error.message}` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + if (error instanceof WalletExportPathExistsError) throw error + + throw new WalletError(errorMessage, { cause: error }) + } + } + + public async import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig) { + const { path: sourcePath, key: importKey } = importConfig + const { path: destinationPath } = uriFromWalletConfig(walletConfig, this.fileSystem.dataPath) + + if (!destinationPath) { + throw new WalletError('Import is only supported for SQLite backend') + } + + let sourceWalletStore: Store | undefined = undefined + try { + const importWalletConfig = await this.getAskarWalletConfig(walletConfig) + + // Import path already exists + if (await this.fileSystem.exists(destinationPath)) { + throw new WalletExportPathExistsError(`Unable to import wallet. Path '${destinationPath}' already exists`) + } + + // Make sure destination path exists + await this.fileSystem.createDirectory(destinationPath) + // Open imported wallet and copy to destination + sourceWalletStore = await Store.open({ + uri: `sqlite://${sourcePath}`, + keyMethod: importWalletConfig.keyMethod, + passKey: importKey, + }) + + const defaultProfile = await sourceWalletStore.getDefaultProfile() + if (defaultProfile !== importWalletConfig.profile) { + throw new WalletError( + `Trying to import wallet with walletConfig.id ${importWalletConfig.profile}, however the wallet contains a default profile with id ${defaultProfile}. The walletConfig.id MUST match with the default profile. In the future this behavior may be changed. See https://github.com/hyperledger/aries-askar/issues/221 for more information.` + ) + } + + await sourceWalletStore.copyTo({ + recreate: false, + uri: importWalletConfig.uri, + keyMethod: importWalletConfig.keyMethod, + passKey: importWalletConfig.passKey, + }) + + await sourceWalletStore.close() + } catch (error) { + await sourceWalletStore?.close() + const errorMessage = `Error importing wallet '${walletConfig.id}': ${error.message}` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + if (error instanceof WalletImportPathExistsError) throw error + + // Cleanup any wallet file we could have created + if (await this.fileSystem.exists(destinationPath)) { + await this.fileSystem.delete(destinationPath) + } + + throw new WalletError(errorMessage, { cause: error }) + } + } + + /** + * @throws {WalletError} if the wallet is already closed or another error occurs + */ + public async close(): Promise { + this.logger.debug(`Closing wallet ${this.walletConfig?.id}`) + if (!this._store) { + throw new WalletError('Wallet is in invalid state, you are trying to close wallet that has no handle.') + } + + try { + await this.store.close() + this._store = undefined + } catch (error) { + const errorMessage = `Error closing wallet': ${error.message}` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + } + + private async getAskarWalletConfig(walletConfig: WalletConfig) { + const { uri, path } = uriFromWalletConfig(walletConfig, this.fileSystem.dataPath) + + return { + uri, + path, + profile: walletConfig.id, + // FIXME: Default derivation method should be set somewhere in either agent config or some constants + keyMethod: keyDerivationMethodToStoreKeyMethod( + walletConfig.keyDerivationMethod ?? KeyDerivationMethod.Argon2IMod + ), + passKey: walletConfig.key, + } + } +} diff --git a/packages/askar/src/wallet/AskarWalletStorageConfig.ts b/packages/askar/src/wallet/AskarWalletStorageConfig.ts new file mode 100644 index 0000000000..be73af4546 --- /dev/null +++ b/packages/askar/src/wallet/AskarWalletStorageConfig.ts @@ -0,0 +1,47 @@ +import type { WalletStorageConfig } from '@credo-ts/core' + +export interface AskarWalletPostgresConfig { + host: string + connectTimeout?: number + idleTimeout?: number + maxConnections?: number + minConnections?: number +} + +export interface AskarWalletSqliteConfig { + // TODO: add other sqlite config options + maxConnections?: number + minConnections?: number + inMemory?: boolean + path?: string +} + +export interface AskarWalletPostgresCredentials { + account: string + password: string + adminAccount?: string + adminPassword?: string +} + +export interface AskarWalletPostgresStorageConfig extends WalletStorageConfig { + type: 'postgres' + config: AskarWalletPostgresConfig + credentials: AskarWalletPostgresCredentials +} + +export interface AskarWalletSqliteStorageConfig extends WalletStorageConfig { + type: 'sqlite' + config?: AskarWalletSqliteConfig +} + +export function isAskarWalletSqliteStorageConfig( + config?: WalletStorageConfig +): config is AskarWalletSqliteStorageConfig { + return config?.type === 'sqlite' +} + +export function isAskarWalletPostgresStorageConfig( + config?: WalletStorageConfig +): config is AskarWalletPostgresStorageConfig { + return config?.type === 'postgres' +} diff --git a/packages/askar/src/wallet/JweEnvelope.ts b/packages/askar/src/wallet/JweEnvelope.ts new file mode 100644 index 0000000000..96561e9479 --- /dev/null +++ b/packages/askar/src/wallet/JweEnvelope.ts @@ -0,0 +1,62 @@ +import { JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' +import { Expose, Type } from 'class-transformer' + +export class JweRecipient { + @Expose({ name: 'encrypted_key' }) + public encryptedKey!: string + public header?: Record + + public constructor(options: { encryptedKey: Uint8Array; header?: Record }) { + if (options) { + this.encryptedKey = TypedArrayEncoder.toBase64URL(options.encryptedKey) + + this.header = options.header + } + } +} + +export interface JweEnvelopeOptions { + protected: string + unprotected?: string + recipients?: JweRecipient[] + ciphertext: string + iv: string + tag: string + aad?: string + header?: string[] + encryptedKey?: string +} + +export class JweEnvelope { + public protected!: string + public unprotected?: string + + @Type(() => JweRecipient) + public recipients?: JweRecipient[] + public ciphertext!: string + public iv!: string + public tag!: string + public aad?: string + public header?: string[] + + @Expose({ name: 'encrypted_key' }) + public encryptedKey?: string + + public constructor(options: JweEnvelopeOptions) { + if (options) { + this.protected = options.protected + this.unprotected = options.unprotected + this.recipients = options.recipients + this.ciphertext = options.ciphertext + this.iv = options.iv + this.tag = options.tag + this.aad = options.aad + this.header = options.header + this.encryptedKey = options.encryptedKey + } + } + + public toJson() { + return JsonTransformer.toJSON(this) + } +} diff --git a/packages/askar/src/wallet/__tests__/AskarProfileWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarProfileWallet.test.ts new file mode 100644 index 0000000000..edac285724 --- /dev/null +++ b/packages/askar/src/wallet/__tests__/AskarProfileWallet.test.ts @@ -0,0 +1,58 @@ +import type { WalletConfig } from '@credo-ts/core' + +import { SigningProviderRegistry, WalletDuplicateError, WalletNotFoundError, KeyDerivationMethod } from '@credo-ts/core' + +import { testLogger, agentDependencies } from '../../../../core/tests' +import { AskarProfileWallet } from '../AskarProfileWallet' +import { AskarWallet } from '../AskarWallet' + +// use raw key derivation method to speed up wallet creating / opening / closing between tests +const rootWalletConfig: WalletConfig = { + id: 'Wallet: AskarProfileWalletTest', + // generated using indy.generateWalletKey + key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', + keyDerivationMethod: KeyDerivationMethod.Raw, +} + +describe('AskarWallet management', () => { + let rootAskarWallet: AskarWallet + let profileAskarWallet: AskarProfileWallet + + afterEach(async () => { + if (profileAskarWallet) { + await profileAskarWallet.delete() + } + + if (rootAskarWallet) { + await rootAskarWallet.delete() + } + }) + + test('Create, open, close, delete', async () => { + const signingProviderRegistry = new SigningProviderRegistry([]) + rootAskarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), signingProviderRegistry) + + // Create and open wallet + await rootAskarWallet.createAndOpen(rootWalletConfig) + + profileAskarWallet = new AskarProfileWallet(rootAskarWallet.store, testLogger, signingProviderRegistry) + + // Create, open and close profile + await profileAskarWallet.create({ ...rootWalletConfig, id: 'profile-id' }) + await profileAskarWallet.open({ ...rootWalletConfig, id: 'profile-id' }) + await profileAskarWallet.close() + + // try to re-create it + await expect(profileAskarWallet.createAndOpen({ ...rootWalletConfig, id: 'profile-id' })).rejects.toThrowError( + WalletDuplicateError + ) + + // Re-open profile + await profileAskarWallet.open({ ...rootWalletConfig, id: 'profile-id' }) + + // try to open non-existent wallet + await expect(profileAskarWallet.open({ ...rootWalletConfig, id: 'non-existent-profile-id' })).rejects.toThrowError( + WalletNotFoundError + ) + }) +}) diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts new file mode 100644 index 0000000000..98c370f393 --- /dev/null +++ b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts @@ -0,0 +1,322 @@ +import type { + SigningProvider, + WalletConfig, + CreateKeyPairOptions, + KeyPair, + SignOptions, + VerifyOptions, +} from '@credo-ts/core' + +import { + WalletKeyExistsError, + Key, + WalletError, + WalletDuplicateError, + WalletNotFoundError, + WalletInvalidKeyError, + KeyType, + SigningProviderRegistry, + TypedArrayEncoder, + KeyDerivationMethod, + Buffer, +} from '@credo-ts/core' +import { Store } from '@hyperledger/aries-askar-shared' + +import { encodeToBase58 } from '../../../../core/src/utils/base58' +import { agentDependencies } from '../../../../core/tests/helpers' +import testLogger from '../../../../core/tests/logger' +import { AskarWallet } from '../AskarWallet' + +// use raw key derivation method to speed up wallet creating / opening / closing between tests +const walletConfig: WalletConfig = { + id: 'Wallet: AskarWalletTest', + // generated using indy.generateWalletKey + key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', + keyDerivationMethod: KeyDerivationMethod.Raw, +} + +describe('AskarWallet basic operations', () => { + let askarWallet: AskarWallet + + const seed = TypedArrayEncoder.fromString('sample-seed-min-of-32-bytes-long') + const privateKey = TypedArrayEncoder.fromString('2103de41b4ae37e8e28586d84a342b67') + const message = TypedArrayEncoder.fromString('sample-message') + + beforeEach(async () => { + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + await askarWallet.createAndOpen(walletConfig) + }) + + afterEach(async () => { + await askarWallet.delete() + }) + + test('supportedKeyTypes', () => { + expect(askarWallet.supportedKeyTypes).toEqual([ + KeyType.Ed25519, + KeyType.X25519, + KeyType.Bls12381g1, + KeyType.Bls12381g2, + KeyType.Bls12381g1g2, + KeyType.P256, + KeyType.K256, + ]) + }) + + test('Get the wallet store', () => { + expect(askarWallet.store).toEqual(expect.any(Store)) + }) + + test('Generate Nonce', async () => { + const nonce = await askarWallet.generateNonce() + + expect(nonce).toMatch(/[0-9]+/) + }) + + test('Create ed25519 keypair from seed', async () => { + const key = await askarWallet.createKey({ + seed, + keyType: KeyType.Ed25519, + }) + + expect(key).toMatchObject({ + keyType: KeyType.Ed25519, + }) + }) + + test('Create ed25519 keypair from private key', async () => { + const key = await askarWallet.createKey({ + privateKey, + keyType: KeyType.Ed25519, + }) + + expect(key).toMatchObject({ + keyType: KeyType.Ed25519, + }) + }) + + test('Attempt to create ed25519 keypair from both seed and private key', async () => { + await expect( + askarWallet.createKey({ + privateKey, + seed, + keyType: KeyType.Ed25519, + }) + ).rejects.toThrowError() + }) + + test('Create x25519 keypair', async () => { + await expect(askarWallet.createKey({ seed, keyType: KeyType.X25519 })).resolves.toMatchObject({ + keyType: KeyType.X25519, + }) + }) + + test('Create P-256 keypair', async () => { + await expect( + askarWallet.createKey({ seed: Buffer.concat([seed, seed]), keyType: KeyType.P256 }) + ).resolves.toMatchObject({ + keyType: KeyType.P256, + }) + }) + + test('throws WalletKeyExistsError when a key already exists', async () => { + const privateKey = TypedArrayEncoder.fromString('2103de41b4ae37e8e28586d84a342b68') + await expect(askarWallet.createKey({ privateKey, keyType: KeyType.Ed25519 })).resolves.toEqual(expect.any(Key)) + await expect(askarWallet.createKey({ privateKey, keyType: KeyType.Ed25519 })).rejects.toThrowError( + WalletKeyExistsError + ) + }) + + test('Fail to create a P384 keypair', async () => { + await expect(askarWallet.createKey({ seed, keyType: KeyType.P384 })).rejects.toThrowError(WalletError) + }) + + test('Create a signature with a ed25519 keypair', async () => { + const ed25519Key = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) + const signature = await askarWallet.sign({ + data: message, + key: ed25519Key, + }) + expect(signature.length).toStrictEqual(64) + }) + + test('Verify a signed message with a ed25519 publicKey', async () => { + const ed25519Key = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) + const signature = await askarWallet.sign({ + data: message, + key: ed25519Key, + }) + await expect(askarWallet.verify({ key: ed25519Key, data: message, signature })).resolves.toStrictEqual(true) + }) + + test('Create K-256 keypair', async () => { + await expect( + askarWallet.createKey({ seed: Buffer.concat([seed, seed]), keyType: KeyType.K256 }) + ).resolves.toMatchObject({ + keyType: KeyType.K256, + }) + }) + + test('Verify a signed message with a k256 publicKey', async () => { + const k256Key = await askarWallet.createKey({ keyType: KeyType.K256 }) + const signature = await askarWallet.sign({ + data: message, + key: k256Key, + }) + await expect(askarWallet.verify({ key: k256Key, data: message, signature })).resolves.toStrictEqual(true) + }) +}) + +describe.skip('Currently, all KeyTypes are supported by Askar natively', () => { + describe('AskarWallet with custom signing provider', () => { + let askarWallet: AskarWallet + + const seed = TypedArrayEncoder.fromString('sample-seed') + const message = TypedArrayEncoder.fromString('sample-message') + + class DummySigningProvider implements SigningProvider { + public keyType: KeyType = KeyType.Bls12381g1g2 + + public async createKeyPair(options: CreateKeyPairOptions): Promise { + return { + publicKeyBase58: encodeToBase58(Buffer.from(options.seed || TypedArrayEncoder.fromString('publicKeyBase58'))), + privateKeyBase58: 'privateKeyBase58', + keyType: KeyType.Bls12381g1g2, + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async sign(options: SignOptions): Promise { + return new Buffer('signed') + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async verify(options: VerifyOptions): Promise { + return true + } + } + + beforeEach(async () => { + askarWallet = new AskarWallet( + testLogger, + new agentDependencies.FileSystem(), + new SigningProviderRegistry([new DummySigningProvider()]) + ) + await askarWallet.createAndOpen(walletConfig) + }) + + afterEach(async () => { + await askarWallet.delete() + }) + + test('Create custom keypair and use it for signing', async () => { + const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) + expect(key.keyType).toBe(KeyType.Bls12381g1g2) + expect(key.publicKeyBase58).toBe(encodeToBase58(Buffer.from(seed))) + + const signature = await askarWallet.sign({ + data: message, + key, + }) + + expect(signature).toBeInstanceOf(Buffer) + }) + + test('Create custom keypair and use it for verifying', async () => { + const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) + expect(key.keyType).toBe(KeyType.Bls12381g1g2) + expect(key.publicKeyBase58).toBe(encodeToBase58(Buffer.from(seed))) + + const signature = await askarWallet.verify({ + data: message, + signature: new Buffer('signature'), + key, + }) + + expect(signature).toBeTruthy() + }) + + test('Attempt to create the same custom keypair twice', async () => { + await askarWallet.createKey({ seed: TypedArrayEncoder.fromString('keybase58'), keyType: KeyType.Bls12381g1g2 }) + + await expect( + askarWallet.createKey({ seed: TypedArrayEncoder.fromString('keybase58'), keyType: KeyType.Bls12381g1g2 }) + ).rejects.toThrow(WalletError) + }) + }) +}) + +describe('AskarWallet management', () => { + let askarWallet: AskarWallet + + afterEach(async () => { + if (askarWallet) { + await askarWallet.delete() + } + }) + + test('Create', async () => { + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + + const initialKey = Store.generateRawKey() + const anotherKey = Store.generateRawKey() + + // Create and open wallet + await askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Create', key: initialKey }) + + // Close and try to re-create it + await askarWallet.close() + await expect( + askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Create', key: anotherKey }) + ).rejects.toThrowError(WalletDuplicateError) + }) + + test('Open', async () => { + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + + const initialKey = Store.generateRawKey() + const wrongKey = Store.generateRawKey() + + // Create and open wallet + await askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Open', key: initialKey }) + + // Close and try to re-opening it with a wrong key + await askarWallet.close() + await expect(askarWallet.open({ ...walletConfig, id: 'AskarWallet Open', key: wrongKey })).rejects.toThrowError( + WalletInvalidKeyError + ) + + // Try to open a non existent wallet + await expect( + askarWallet.open({ ...walletConfig, id: 'AskarWallet Open - Non existent', key: initialKey }) + ).rejects.toThrowError(WalletNotFoundError) + }) + + test('Rotate key', async () => { + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + + const initialKey = Store.generateRawKey() + await askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Key Rotation', key: initialKey }) + + await askarWallet.close() + + const newKey = Store.generateRawKey() + await askarWallet.rotateKey({ + ...walletConfig, + id: 'AskarWallet Key Rotation', + key: initialKey, + rekey: newKey, + rekeyDerivationMethod: KeyDerivationMethod.Raw, + }) + + await askarWallet.close() + + await expect( + askarWallet.open({ ...walletConfig, id: 'AskarWallet Key Rotation', key: initialKey }) + ).rejects.toThrowError(WalletInvalidKeyError) + + await askarWallet.open({ ...walletConfig, id: 'AskarWallet Key Rotation', key: newKey }) + + await askarWallet.close() + }) +}) diff --git a/packages/askar/src/wallet/__tests__/packing.test.ts b/packages/askar/src/wallet/__tests__/packing.test.ts new file mode 100644 index 0000000000..c75e4d9f8c --- /dev/null +++ b/packages/askar/src/wallet/__tests__/packing.test.ts @@ -0,0 +1,46 @@ +import type { WalletConfig } from '@credo-ts/core' + +import { JsonTransformer, BasicMessage, KeyType, SigningProviderRegistry, KeyDerivationMethod } from '@credo-ts/core' + +import { agentDependencies } from '../../../../core/tests/helpers' +import testLogger from '../../../../core/tests/logger' +import { AskarWallet } from '../AskarWallet' + +// use raw key derivation method to speed up wallet creating / opening / closing between tests +const walletConfig: WalletConfig = { + id: 'Askar Wallet Packing', + // generated using indy.generateWalletKey + key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', + keyDerivationMethod: KeyDerivationMethod.Raw, +} + +describe('askarWallet packing', () => { + let askarWallet: AskarWallet + + beforeEach(async () => { + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + await askarWallet.createAndOpen(walletConfig) + }) + + afterEach(async () => { + await askarWallet.delete() + }) + + test('DIDComm V1 packing and unpacking', async () => { + // Create both sender and recipient keys + const senderKey = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) + const recipientKey = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) + + const message = new BasicMessage({ content: 'hello' }) + + const encryptedMessage = await askarWallet.pack( + message.toJSON(), + [recipientKey.publicKeyBase58], + senderKey.publicKeyBase58 + ) + + const plainTextMessage = await askarWallet.unpack(encryptedMessage) + + expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, BasicMessage)).toEqual(message) + }) +}) diff --git a/packages/askar/src/wallet/didcommV1.ts b/packages/askar/src/wallet/didcommV1.ts new file mode 100644 index 0000000000..6ab9a7f76d --- /dev/null +++ b/packages/askar/src/wallet/didcommV1.ts @@ -0,0 +1,177 @@ +import type { EncryptedMessage } from '@credo-ts/core' + +import { WalletError, JsonEncoder, JsonTransformer, Key, KeyType, TypedArrayEncoder, Buffer } from '@credo-ts/core' +import { CryptoBox, Key as AskarKey, KeyAlgs } from '@hyperledger/aries-askar-shared' + +import { JweEnvelope, JweRecipient } from './JweEnvelope' + +export function didcommV1Pack(payload: Record, recipientKeys: string[], senderKey?: AskarKey) { + let cek: AskarKey | undefined + let senderExchangeKey: AskarKey | undefined + + try { + cek = AskarKey.generate(KeyAlgs.Chacha20C20P) + + senderExchangeKey = senderKey ? senderKey.convertkey({ algorithm: KeyAlgs.X25519 }) : undefined + + const recipients: JweRecipient[] = [] + + for (const recipientKey of recipientKeys) { + let targetExchangeKey: AskarKey | undefined + try { + targetExchangeKey = AskarKey.fromPublicBytes({ + publicKey: Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519).publicKey, + algorithm: KeyAlgs.Ed25519, + }).convertkey({ algorithm: KeyAlgs.X25519 }) + + if (senderKey && senderExchangeKey) { + const encryptedSender = CryptoBox.seal({ + recipientKey: targetExchangeKey, + message: TypedArrayEncoder.fromString(TypedArrayEncoder.toBase58(senderKey.publicBytes)), + }) + const nonce = CryptoBox.randomNonce() + const encryptedCek = CryptoBox.cryptoBox({ + recipientKey: targetExchangeKey, + senderKey: senderExchangeKey, + message: cek.secretBytes, + nonce, + }) + + recipients.push( + new JweRecipient({ + encryptedKey: encryptedCek, + header: { + kid: recipientKey, + sender: TypedArrayEncoder.toBase64URL(encryptedSender), + iv: TypedArrayEncoder.toBase64URL(nonce), + }, + }) + ) + } else { + const encryptedCek = CryptoBox.seal({ + recipientKey: targetExchangeKey, + message: cek.secretBytes, + }) + recipients.push( + new JweRecipient({ + encryptedKey: encryptedCek, + header: { + kid: recipientKey, + }, + }) + ) + } + } finally { + targetExchangeKey?.handle.free() + } + } + + const protectedJson = { + enc: 'xchacha20poly1305_ietf', + typ: 'JWM/1.0', + alg: senderKey ? 'Authcrypt' : 'Anoncrypt', + recipients: recipients.map((item) => JsonTransformer.toJSON(item)), + } + + const { ciphertext, tag, nonce } = cek.aeadEncrypt({ + message: Buffer.from(JSON.stringify(payload)), + aad: Buffer.from(JsonEncoder.toBase64URL(protectedJson)), + }).parts + + const envelope = new JweEnvelope({ + ciphertext: TypedArrayEncoder.toBase64URL(ciphertext), + iv: TypedArrayEncoder.toBase64URL(nonce), + protected: JsonEncoder.toBase64URL(protectedJson), + tag: TypedArrayEncoder.toBase64URL(tag), + }).toJson() + + return envelope as EncryptedMessage + } finally { + cek?.handle.free() + senderExchangeKey?.handle.free() + } +} + +export function didcommV1Unpack(messagePackage: EncryptedMessage, recipientKey: AskarKey) { + const protectedJson = JsonEncoder.fromBase64(messagePackage.protected) + + const alg = protectedJson.alg + if (!['Anoncrypt', 'Authcrypt'].includes(alg)) { + throw new WalletError(`Unsupported pack algorithm: ${alg}`) + } + + const recipient = protectedJson.recipients.find( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (r: any) => r.header.kid === TypedArrayEncoder.toBase58(recipientKey.publicBytes) + ) + + if (!recipient) { + throw new WalletError('No corresponding recipient key found') + } + + const sender = recipient?.header.sender ? TypedArrayEncoder.fromBase64(recipient.header.sender) : undefined + const iv = recipient?.header.iv ? TypedArrayEncoder.fromBase64(recipient.header.iv) : undefined + const encrypted_key = TypedArrayEncoder.fromBase64(recipient.encrypted_key) + + if (sender && !iv) { + throw new WalletError('Missing IV') + } else if (!sender && iv) { + throw new WalletError('Unexpected IV') + } + + let payloadKey, senderKey + + let sender_x: AskarKey | undefined + let recip_x: AskarKey | undefined + + try { + recip_x = recipientKey.convertkey({ algorithm: KeyAlgs.X25519 }) + + if (sender && iv) { + senderKey = TypedArrayEncoder.toUtf8String( + CryptoBox.sealOpen({ + recipientKey: recip_x, + ciphertext: sender, + }) + ) + sender_x = AskarKey.fromPublicBytes({ + algorithm: KeyAlgs.Ed25519, + publicKey: TypedArrayEncoder.fromBase58(senderKey), + }).convertkey({ algorithm: KeyAlgs.X25519 }) + + payloadKey = CryptoBox.open({ + recipientKey: recip_x, + senderKey: sender_x, + message: encrypted_key, + nonce: iv, + }) + } else { + payloadKey = CryptoBox.sealOpen({ ciphertext: encrypted_key, recipientKey: recip_x }) + } + } finally { + sender_x?.handle.free() + recip_x?.handle.free() + } + + if (!senderKey && alg === 'Authcrypt') { + throw new WalletError('Sender public key not provided for Authcrypt') + } + + let cek: AskarKey | undefined + try { + cek = AskarKey.fromSecretBytes({ algorithm: KeyAlgs.Chacha20C20P, secretKey: payloadKey }) + const message = cek.aeadDecrypt({ + ciphertext: TypedArrayEncoder.fromBase64(messagePackage.ciphertext), + nonce: TypedArrayEncoder.fromBase64(messagePackage.iv), + tag: TypedArrayEncoder.fromBase64(messagePackage.tag), + aad: TypedArrayEncoder.fromString(messagePackage.protected), + }) + return { + plaintextMessage: JsonEncoder.fromBuffer(message), + senderKey, + recipientKey: TypedArrayEncoder.toBase58(recipientKey.publicBytes), + } + } finally { + cek?.handle.free() + } +} diff --git a/packages/askar/src/wallet/index.ts b/packages/askar/src/wallet/index.ts new file mode 100644 index 0000000000..49e1da0a79 --- /dev/null +++ b/packages/askar/src/wallet/index.ts @@ -0,0 +1,3 @@ +export { AskarWallet } from './AskarWallet' +export { AskarProfileWallet } from './AskarProfileWallet' +export * from './AskarWalletStorageConfig' diff --git a/packages/askar/tests/askar-inmemory.test.ts b/packages/askar/tests/askar-inmemory.test.ts new file mode 100644 index 0000000000..d98e762fa5 --- /dev/null +++ b/packages/askar/tests/askar-inmemory.test.ts @@ -0,0 +1,64 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' + +import { Agent } from '@credo-ts/core' +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' + +import { e2eTest, getAskarSqliteAgentOptions } from './helpers' + +const aliceInMemoryAgentOptions = getAskarSqliteAgentOptions( + 'AgentsAlice', + { + endpoints: ['rxjs:alice'], + }, + true +) +const bobInMemoryAgentOptions = getAskarSqliteAgentOptions( + 'AgentsBob', + { + endpoints: ['rxjs:bob'], + }, + true +) + +describe('Askar In Memory agents', () => { + let aliceAgent: Agent + let bobAgent: Agent + + afterAll(async () => { + if (bobAgent) { + await bobAgent.shutdown() + await bobAgent.wallet.delete() + } + + if (aliceAgent) { + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + } + }) + + test('In memory Askar wallets E2E test', async () => { + const aliceMessages = new Subject() + const bobMessages = new Subject() + + const subjectMap = { + 'rxjs:alice': aliceMessages, + 'rxjs:bob': bobMessages, + } + + aliceAgent = new Agent(aliceInMemoryAgentOptions) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + bobAgent = new Agent(bobInMemoryAgentOptions) + bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages)) + bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await bobAgent.initialize() + + await e2eTest(aliceAgent, bobAgent) + }) +}) diff --git a/packages/askar/tests/askar-postgres.e2e.test.ts b/packages/askar/tests/askar-postgres.e2e.test.ts new file mode 100644 index 0000000000..92a7168b5f --- /dev/null +++ b/packages/askar/tests/askar-postgres.e2e.test.ts @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' + +import { Agent } from '@credo-ts/core' +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' + +import { askarPostgresStorageConfig, e2eTest, getAskarPostgresAgentOptions } from './helpers' + +const alicePostgresAgentOptions = getAskarPostgresAgentOptions('AgentsAlice', askarPostgresStorageConfig, { + endpoints: ['rxjs:alice'], +}) +const bobPostgresAgentOptions = getAskarPostgresAgentOptions('AgentsBob', askarPostgresStorageConfig, { + endpoints: ['rxjs:bob'], +}) + +describe('Askar Postgres agents', () => { + let aliceAgent: Agent + let bobAgent: Agent + + afterAll(async () => { + if (bobAgent) { + await bobAgent.shutdown() + await bobAgent.wallet.delete() + } + + if (aliceAgent) { + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + } + }) + + test('Postgres Askar wallets E2E test', async () => { + const aliceMessages = new Subject() + const bobMessages = new Subject() + + const subjectMap = { + 'rxjs:alice': aliceMessages, + 'rxjs:bob': bobMessages, + } + + aliceAgent = new Agent(alicePostgresAgentOptions) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + bobAgent = new Agent(bobPostgresAgentOptions) + bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages)) + bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await bobAgent.initialize() + + await e2eTest(aliceAgent, bobAgent) + }) +}) diff --git a/packages/askar/tests/askar-sqlite.test.ts b/packages/askar/tests/askar-sqlite.test.ts new file mode 100644 index 0000000000..2f13b84b23 --- /dev/null +++ b/packages/askar/tests/askar-sqlite.test.ts @@ -0,0 +1,269 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { + Agent, + BasicMessageRecord, + BasicMessageRepository, + BasicMessageRole, + KeyDerivationMethod, + TypedArrayEncoder, + utils, + WalletDuplicateError, + WalletInvalidKeyError, + WalletNotFoundError, +} from '@credo-ts/core' +import { Store } from '@hyperledger/aries-askar-shared' +import { tmpdir } from 'os' +import path from 'path' + +import { getAskarSqliteAgentOptions } from './helpers' + +const aliceAgentOptions = getAskarSqliteAgentOptions('AgentsAlice') +const bobAgentOptions = getAskarSqliteAgentOptions('AgentsBob') + +describe('Askar SQLite agents', () => { + let aliceAgent: Agent + let bobAgent: Agent + + beforeEach(async () => { + aliceAgent = new Agent(aliceAgentOptions) + bobAgent = new Agent(bobAgentOptions) + }) + + afterEach(async () => { + await aliceAgent.shutdown() + await bobAgent.shutdown() + + if (aliceAgent.wallet.isProvisioned) { + await aliceAgent.wallet.delete() + } + if (bobAgent.wallet.isProvisioned) { + await bobAgent.wallet.delete() + } + }) + + test('open, create and open wallet with different wallet key that it is in agent config', async () => { + const walletConfig = { + id: 'mywallet', + key: 'mysecretwalletkey-0', + } + + try { + await aliceAgent.wallet.open(walletConfig) + } catch (error) { + if (error instanceof WalletNotFoundError) { + await aliceAgent.wallet.create(walletConfig) + await aliceAgent.wallet.open(walletConfig) + } + } + + await aliceAgent.initialize() + + expect(aliceAgent.isInitialized).toBe(true) + }) + + test('when opening non-existing wallet throw WalletNotFoundError', async () => { + const walletConfig = { + id: 'mywallet', + key: 'mysecretwalletkey-1', + } + + await expect(aliceAgent.wallet.open(walletConfig)).rejects.toThrowError(WalletNotFoundError) + }) + + test('when create wallet and shutdown, wallet is closed', async () => { + const walletConfig = { + id: 'mywallet', + key: 'mysecretwalletkey-2', + } + + await aliceAgent.wallet.create(walletConfig) + + await aliceAgent.shutdown() + + await expect(aliceAgent.wallet.open(walletConfig)).resolves.toBeUndefined() + }) + + test('create wallet with custom key derivation method', async () => { + const walletConfig = { + id: 'mywallet', + key: Store.generateRawKey(TypedArrayEncoder.fromString('mysecretwalletkey')), + keyDerivationMethod: KeyDerivationMethod.Raw, + } + + await aliceAgent.wallet.createAndOpen(walletConfig) + + expect(aliceAgent.wallet.isInitialized).toBe(true) + }) + + test('when exporting and importing a wallet, content is copied', async () => { + await bobAgent.initialize() + const bobBasicMessageRepository = bobAgent.dependencyManager.resolve(BasicMessageRepository) + + const basicMessageRecord = new BasicMessageRecord({ + id: 'some-id', + connectionId: 'connId', + content: 'hello', + role: BasicMessageRole.Receiver, + sentTime: 'sentIt', + }) + + // Save in wallet + await bobBasicMessageRepository.save(bobAgent.context, basicMessageRecord) + + if (!bobAgent.config.walletConfig) { + throw new Error('No wallet config on bobAgent') + } + + const backupKey = 'someBackupKey' + const backupWalletName = `backup-${utils.uuid()}` + const backupPath = path.join(tmpdir(), backupWalletName) + + // Create backup and delete wallet + await bobAgent.wallet.export({ path: backupPath, key: backupKey }) + await bobAgent.wallet.delete() + + // Initialize the wallet again and assert record does not exist + // This should create a new wallet + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await bobAgent.wallet.initialize(bobAgent.config.walletConfig!) + expect(await bobBasicMessageRepository.findById(bobAgent.context, basicMessageRecord.id)).toBeNull() + await bobAgent.wallet.delete() + + // Import backup with SAME wallet id and initialize + await bobAgent.wallet.import(bobAgent.config.walletConfig, { path: backupPath, key: backupKey }) + await bobAgent.wallet.initialize(bobAgent.config.walletConfig) + + // Expect same basic message record to exist in new wallet + expect(await bobBasicMessageRepository.getById(bobAgent.context, basicMessageRecord.id)).toMatchObject({ + id: basicMessageRecord.id, + connectionId: basicMessageRecord.connectionId, + content: basicMessageRecord.content, + createdAt: basicMessageRecord.createdAt, + updatedAt: basicMessageRecord.updatedAt, + type: basicMessageRecord.type, + }) + }) + + test('throws error when exporting a wallet and importing it with a different walletConfig.id', async () => { + await bobAgent.initialize() + + if (!bobAgent.config.walletConfig) { + throw new Error('No wallet config on bobAgent') + } + + const backupKey = 'someBackupKey' + const backupWalletName = `backup-${utils.uuid()}` + const backupPath = path.join(tmpdir(), backupWalletName) + + // Create backup and delete wallet + await bobAgent.wallet.export({ path: backupPath, key: backupKey }) + await bobAgent.wallet.delete() + + // Import backup with different wallet id and initialize + await expect( + bobAgent.wallet.import({ id: backupWalletName, key: backupWalletName }, { path: backupPath, key: backupKey }) + ).rejects.toThrow( + `Error importing wallet '${backupWalletName}': Trying to import wallet with walletConfig.id ${backupWalletName}, however the wallet contains a default profile with id ${bobAgent.config.walletConfig.id}. The walletConfig.id MUST match with the default profile. In the future this behavior may be changed. See https://github.com/hyperledger/aries-askar/issues/221 for more information.` + ) + }) + + test('throws error when attempting to export and import to existing paths', async () => { + await bobAgent.initialize() + + if (!bobAgent.config.walletConfig) { + throw new Error('No wallet config on bobAgent') + } + + const backupKey = 'someBackupKey' + const backupWalletName = `backup-${utils.uuid()}` + const backupPath = path.join(tmpdir(), backupWalletName) + + // Create backup and try to export it again to the same path + await bobAgent.wallet.export({ path: backupPath, key: backupKey }) + await expect(bobAgent.wallet.export({ path: backupPath, key: backupKey })).rejects.toThrow( + /Unable to create export/ + ) + + await bobAgent.wallet.delete() + + // Import backup with different wallet id and initialize + await bobAgent.wallet.import(bobAgent.config.walletConfig, { path: backupPath, key: backupKey }) + await bobAgent.wallet.initialize(bobAgent.config.walletConfig) + await bobAgent.wallet.close() + + // Try to import again an existing wallet + await expect( + bobAgent.wallet.import(bobAgent.config.walletConfig, { path: backupPath, key: backupKey }) + ).rejects.toThrow(/Unable to import wallet/) + }) + + test('throws error when attempting to import using wrong key', async () => { + await bobAgent.initialize() + + if (!bobAgent.config.walletConfig) { + throw new Error('No wallet config on bobAgent') + } + + const backupKey = 'someBackupKey' + const wrongBackupKey = 'wrongBackupKey' + const backupWalletName = `backup-${utils.uuid()}` + const backupPath = path.join(tmpdir(), backupWalletName) + + // Create backup and try to export it again to the same path + await bobAgent.wallet.export({ path: backupPath, key: backupKey }) + await bobAgent.wallet.delete() + + // Try to import backup with wrong key + await expect( + bobAgent.wallet.import(bobAgent.config.walletConfig, { path: backupPath, key: wrongBackupKey }) + ).rejects.toThrow() + + // Try to import again using the correct key + await bobAgent.wallet.import(bobAgent.config.walletConfig, { path: backupPath, key: backupKey }) + await bobAgent.wallet.initialize(bobAgent.config.walletConfig) + await bobAgent.wallet.close() + }) + + test('changing wallet key', async () => { + const walletConfig = { + id: 'mywallet', + key: 'mysecretwalletkey', + } + + await aliceAgent.wallet.createAndOpen(walletConfig) + await aliceAgent.initialize() + + //Close agent + const walletConfigRekey = { + id: 'mywallet', + key: 'mysecretwalletkey', + rekey: '123', + } + + await aliceAgent.shutdown() + await aliceAgent.wallet.rotateKey(walletConfigRekey) + await aliceAgent.initialize() + + expect(aliceAgent.isInitialized).toBe(true) + }) + + test('when creating already existing wallet throw WalletDuplicateError', async () => { + const walletConfig = { + id: 'mywallet', + key: 'mysecretwalletkey-2', + } + + await aliceAgent.wallet.create(walletConfig) + await expect(aliceAgent.wallet.create(walletConfig)).rejects.toThrowError(WalletDuplicateError) + }) + + test('when opening wallet with invalid key throw WalletInvalidKeyError', async () => { + const walletConfig = { + id: 'mywallet', + key: 'mysecretwalletkey-3', + } + + await aliceAgent.wallet.create(walletConfig) + await expect(aliceAgent.wallet.open({ ...walletConfig, key: 'abcd' })).rejects.toThrowError(WalletInvalidKeyError) + }) +}) diff --git a/packages/askar/tests/helpers.ts b/packages/askar/tests/helpers.ts new file mode 100644 index 0000000000..af6fddea15 --- /dev/null +++ b/packages/askar/tests/helpers.ts @@ -0,0 +1,132 @@ +import type { AskarWalletPostgresStorageConfig } from '../src/wallet' +import type { Agent, InitConfig } from '@credo-ts/core' + +import { ConnectionsModule, HandshakeProtocol, LogLevel, utils } from '@credo-ts/core' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import { registerAriesAskar } from '@hyperledger/aries-askar-shared' +import path from 'path' + +import { waitForBasicMessage } from '../../core/tests/helpers' +import { TestLogger } from '../../core/tests/logger' +import { agentDependencies } from '../../node/src' +import { AskarModule } from '../src/AskarModule' +import { AskarModuleConfig } from '../src/AskarModuleConfig' +import { AskarWallet } from '../src/wallet' + +export const askarModuleConfig = new AskarModuleConfig({ ariesAskar }) +registerAriesAskar({ askar: askarModuleConfig.ariesAskar }) +export const askarModule = new AskarModule(askarModuleConfig) +export { ariesAskar } + +// When using the AskarWallet directly, the native dependency won't be loaded by default. +// So in tests depending on Askar, we import this wallet so we're sure the native dependency is loaded. +export const RegisteredAskarTestWallet = AskarWallet + +export const genesisPath = process.env.GENESIS_TXN_PATH + ? path.resolve(process.env.GENESIS_TXN_PATH) + : path.join(__dirname, '../../../../network/genesis/local-genesis.txn') + +export const publicDidSeed = process.env.TEST_AGENT_PUBLIC_DID_SEED ?? '000000000000000000000000Trustee9' + +export const askarPostgresStorageConfig: AskarWalletPostgresStorageConfig = { + type: 'postgres', + config: { + host: 'localhost:5432', + }, + credentials: { + account: 'postgres', + password: 'postgres', + }, +} + +export function getAskarPostgresAgentOptions( + name: string, + storageConfig: AskarWalletPostgresStorageConfig, + extraConfig: Partial = {} +) { + const random = utils.uuid().slice(0, 4) + const config: InitConfig = { + label: `PostgresAgent: ${name} - ${random}`, + walletConfig: { + id: `PostgresWallet${name}${random}`, + key: `Key${name}`, + storage: storageConfig, + }, + autoUpdateStorageOnStartup: false, + logger: new TestLogger(LogLevel.off, name), + ...extraConfig, + } + return { + config, + dependencies: agentDependencies, + modules: { + askar: new AskarModule(askarModuleConfig), + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + }, + } as const +} + +export function getAskarSqliteAgentOptions(name: string, extraConfig: Partial = {}, inMemory?: boolean) { + const random = utils.uuid().slice(0, 4) + const config: InitConfig = { + label: `SQLiteAgent: ${name} - ${random}`, + walletConfig: { + id: `SQLiteWallet${name} - ${random}`, + key: `Key${name}`, + storage: { type: 'sqlite', inMemory }, + }, + autoUpdateStorageOnStartup: false, + logger: new TestLogger(LogLevel.off, name), + ...extraConfig, + } + return { + config, + dependencies: agentDependencies, + modules: { + askar: new AskarModule(askarModuleConfig), + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + }, + } as const +} + +/** + * Basic E2E test: connect two agents, send a basic message and verify it they can be re initialized + * @param senderAgent + * @param receiverAgent + */ +export async function e2eTest(senderAgent: Agent, receiverAgent: Agent) { + const senderReceiverOutOfBandRecord = await senderAgent.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + const { connectionRecord: bobConnectionAtReceiversender } = await receiverAgent.oob.receiveInvitation( + senderReceiverOutOfBandRecord.outOfBandInvitation + ) + if (!bobConnectionAtReceiversender) throw new Error('Connection not created') + + await receiverAgent.connections.returnWhenIsConnected(bobConnectionAtReceiversender.id) + + const [senderConnectionAtReceiver] = await senderAgent.connections.findAllByOutOfBandId( + senderReceiverOutOfBandRecord.id + ) + const senderConnection = await senderAgent.connections.returnWhenIsConnected(senderConnectionAtReceiver.id) + + const message = 'hello, world' + await senderAgent.basicMessages.sendMessage(senderConnection.id, message) + + const basicMessage = await waitForBasicMessage(receiverAgent, { + content: message, + }) + + expect(basicMessage.content).toBe(message) + + expect(senderAgent.isInitialized).toBe(true) + await senderAgent.shutdown() + expect(senderAgent.isInitialized).toBe(false) + await senderAgent.initialize() + expect(senderAgent.isInitialized).toBe(true) +} diff --git a/packages/askar/tests/setup.ts b/packages/askar/tests/setup.ts new file mode 100644 index 0000000000..97eeec3b92 --- /dev/null +++ b/packages/askar/tests/setup.ts @@ -0,0 +1,4 @@ +import 'reflect-metadata' +import '@hyperledger/aries-askar-nodejs' + +jest.setTimeout(180000) diff --git a/packages/askar/tsconfig.build.json b/packages/askar/tsconfig.build.json new file mode 100644 index 0000000000..2b75d0adab --- /dev/null +++ b/packages/askar/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build" + }, + "include": ["src/**/*"] +} diff --git a/packages/askar/tsconfig.json b/packages/askar/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/askar/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/packages/bbs-signatures/CHANGELOG.md b/packages/bbs-signatures/CHANGELOG.md new file mode 100644 index 0000000000..385ddd7628 --- /dev/null +++ b/packages/bbs-signatures/CHANGELOG.md @@ -0,0 +1,75 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release +- Updated dependencies [66e696d] + - @credo-ts/core@0.5.6 + +## 0.5.5 + +### Patch Changes + +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) +- Updated dependencies [3239ef3] +- Updated dependencies [d548fa4] +- Updated dependencies [482a630] + - @credo-ts/core@0.5.5 + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +**Note:** Version bump only for package @credo-ts/bbs-signatures + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +**Note:** Version bump only for package @credo-ts/bbs-signatures + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +**Note:** Version bump only for package @credo-ts/bbs-signatures + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +### Bug Fixes + +- jsonld document loader node 18 ([#1454](https://github.com/openwallet-foundation/credo-ts/issues/1454)) ([3656d49](https://github.com/openwallet-foundation/credo-ts/commit/3656d4902fb832e5e75142b1846074d4f39c11a2)) +- unused imports ([#1733](https://github.com/openwallet-foundation/credo-ts/issues/1733)) ([e0b971e](https://github.com/openwallet-foundation/credo-ts/commit/e0b971e86b506bb78dafa21f76ae3b193abe9a9d)) + +## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) + +**Note:** Version bump only for package @credo-ts/bbs-signatures + +## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) + +**Note:** Version bump only for package @credo-ts/bbs-signatures + +# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) + +### Bug Fixes + +- jsonld credential format identifier version ([#1412](https://github.com/hyperledger/aries-framework-javascript/issues/1412)) ([c46a6b8](https://github.com/hyperledger/aries-framework-javascript/commit/c46a6b81b8a1e28e05013c27ffe2eeaee4724130)) +- seed and private key validation and return type in registrars ([#1324](https://github.com/hyperledger/aries-framework-javascript/issues/1324)) ([c0e5339](https://github.com/hyperledger/aries-framework-javascript/commit/c0e5339edfa32df92f23fb9c920796b4b59adf52)) + +### Features + +- **core:** add W3cCredentialsApi ([c888736](https://github.com/hyperledger/aries-framework-javascript/commit/c888736cb6b51014e23f5520fbc4074cf0e49e15)) +- **openid4vc:** jwt format and more crypto ([#1472](https://github.com/hyperledger/aries-framework-javascript/issues/1472)) ([bd4932d](https://github.com/hyperledger/aries-framework-javascript/commit/bd4932d34f7314a6d49097b6460c7570e1ebc7a8)) diff --git a/packages/bbs-signatures/README.md b/packages/bbs-signatures/README.md new file mode 100644 index 0000000000..8aa81032d8 --- /dev/null +++ b/packages/bbs-signatures/README.md @@ -0,0 +1,90 @@ +

+
+ Credo Logo +

+

Credo BBS+ Module

+

+ License + typescript + @credo-ts/bbs-signatures version + +

+
+ +Credo BBS Module provides an optional addon to Credo to use BBS signatures in W3C VC exchange. + +## Installation + +```sh +# or npm/yarn +pnpm add @credo-ts/bbs-signatures +``` + +### React Native + +When using Credo inside the React Native environment, temporarily, a dependency for creating keys, signing and verifying, with bbs keys must be swapped. Inside your `package.json` the following must be added. This is only needed for React Native environments + +#### yarn + +```diff ++ "resolutions": { ++ "@mattrglobal/bbs-signatures": "@animo-id/react-native-bbs-signatures@^0.1.0", ++ }, + "dependencies": { + ... ++ "@animo-id/react-native-bbs-signatures": "^0.1.0", + } +``` + +#### npm + +```diff ++ "overrides": { ++ "@mattrglobal/bbs-signatures": "@animo-id/react-native-bbs-signatures@^0.1.0", ++ }, + "dependencies": { + ... ++ "@animo-id/react-native-bbs-signatures": "^0.1.0", + } +``` + +#### pnpm + +```diff ++ "pnpm": { ++ overrides": { ++ "@mattrglobal/bbs-signatures": "npm:@animo-id/react-native-bbs-signatures@^0.1.0", ++ } ++ }, + "dependencies": { + ... ++ "@animo-id/react-native-bbs-signatures": "^0.1.0", + } +``` + +The resolution field says that any instance of `@mattrglobal/bbs-signatures` in any child dependency must be swapped with `@animo-id/react-native-bbs-signatures`. + +The added dependency is required for autolinking and should be the same as the one used in the resolution. + +[React Native Bbs Signature](https://github.com/animo/react-native-bbs-signatures) has some quirks with setting it up correctly. If any errors occur while using this library, please refer to their README for the installation guide. + +### Issue with `node-bbs-signatures` + +Right now some platforms will see an "error" when installing the `@credo-ts/bbs-signatures` package. This is because the BBS signatures library that we use under the hood is built for Linux x86 and MacOS x86 (and not Windows and MacOS arm). This means that it will show that it could not download the binary. This is not an error for developers, the library that fails is `node-bbs-signatures` and is an optional dependency for performance improvements. It will fallback to a (slower) wasm build. diff --git a/packages/bbs-signatures/jest.config.ts b/packages/bbs-signatures/jest.config.ts new file mode 100644 index 0000000000..8641cf4d67 --- /dev/null +++ b/packages/bbs-signatures/jest.config.ts @@ -0,0 +1,14 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/bbs-signatures/package.json b/packages/bbs-signatures/package.json new file mode 100644 index 0000000000..f11c7e7e1a --- /dev/null +++ b/packages/bbs-signatures/package.json @@ -0,0 +1,48 @@ +{ + "name": "@credo-ts/bbs-signatures", + "main": "src/index", + "types": "src/index", + "version": "0.5.6", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/bbs-signatures", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/bbs-signatures" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build", + "test": "jest" + }, + "dependencies": { + "@credo-ts/core": "workspace:*", + "@mattrglobal/bbs-signatures": "^1.0.0", + "@mattrglobal/bls12381-key-pair": "^1.0.0", + "@stablelib/random": "^1.0.2" + }, + "peerDependencies": { + "@animo-id/react-native-bbs-signatures": "^0.1.0" + }, + "devDependencies": { + "@credo-ts/node": "workspace:*", + "reflect-metadata": "^0.1.13", + "rimraf": "^4.4.0", + "typescript": "~5.5.2" + }, + "peerDependenciesMeta": { + "@animo-id/react-native-bbs-signatures": { + "optional": true + } + } +} diff --git a/packages/bbs-signatures/src/BbsModule.ts b/packages/bbs-signatures/src/BbsModule.ts new file mode 100644 index 0000000000..5cd1c4206d --- /dev/null +++ b/packages/bbs-signatures/src/BbsModule.ts @@ -0,0 +1,43 @@ +import type { DependencyManager, Module } from '@credo-ts/core' + +import { + AgentConfig, + KeyType, + SigningProviderToken, + VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020, + SignatureSuiteToken, +} from '@credo-ts/core' + +import { Bls12381g2SigningProvider } from './Bls12381g2SigningProvider' +import { BbsBlsSignature2020, BbsBlsSignatureProof2020 } from './signature-suites' + +export class BbsModule implements Module { + /** + * Registers the dependencies of the bbs module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The '@credo-ts/bbs-signatures' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." + ) + + // Signing providers. + dependencyManager.registerSingleton(SigningProviderToken, Bls12381g2SigningProvider) + + // Signature suites. + dependencyManager.registerInstance(SignatureSuiteToken, { + suiteClass: BbsBlsSignature2020, + proofType: 'BbsBlsSignature2020', + verificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], + keyTypes: [KeyType.Bls12381g2], + }) + dependencyManager.registerInstance(SignatureSuiteToken, { + suiteClass: BbsBlsSignatureProof2020, + proofType: 'BbsBlsSignatureProof2020', + verificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], + keyTypes: [KeyType.Bls12381g2], + }) + } +} diff --git a/packages/bbs-signatures/src/Bls12381g2SigningProvider.ts b/packages/bbs-signatures/src/Bls12381g2SigningProvider.ts new file mode 100644 index 0000000000..2739546155 --- /dev/null +++ b/packages/bbs-signatures/src/Bls12381g2SigningProvider.ts @@ -0,0 +1,106 @@ +import type { SigningProvider, CreateKeyPairOptions, KeyPair, SignOptions, VerifyOptions } from '@credo-ts/core' + +import { KeyType, injectable, TypedArrayEncoder, SigningProviderError, Buffer } from '@credo-ts/core' +import { bls12381toBbs, verify, sign, generateBls12381G2KeyPair } from '@mattrglobal/bbs-signatures' + +/** + * This will be extracted to the bbs package. + */ +@injectable() +export class Bls12381g2SigningProvider implements SigningProvider { + public readonly keyType = KeyType.Bls12381g2 + + /** + * Create a KeyPair with type Bls12381g2 + * + * @throws {SigningProviderError} When a key could not be created + */ + public async createKeyPair({ seed, privateKey }: CreateKeyPairOptions): Promise { + if (privateKey) { + throw new SigningProviderError('Cannot create keypair from private key') + } + + const blsKeyPair = await generateBls12381G2KeyPair(seed) + + return { + keyType: KeyType.Bls12381g2, + publicKeyBase58: TypedArrayEncoder.toBase58(blsKeyPair.publicKey), + privateKeyBase58: TypedArrayEncoder.toBase58(blsKeyPair.secretKey), + } + } + + /** + * Sign an arbitrary amount of messages, in byte form, with a keypair + * + * @param messages Buffer[] List of messages in Buffer form + * @param publicKey Buffer Publickey required for the signing process + * @param privateKey Buffer PrivateKey required for the signing process + * + * @returns A Buffer containing the signature of the messages + * + * @throws {SigningProviderError} When there are no supplied messages + */ + public async sign({ data, publicKeyBase58, privateKeyBase58 }: SignOptions): Promise { + if (data.length === 0) throw new SigningProviderError('Unable to create a signature without any messages') + // Check if it is a single message or list and if it is a single message convert it to a list + const normalizedMessages = (TypedArrayEncoder.isTypedArray(data) ? [data as Buffer] : data) as Buffer[] + + // Get the Uint8Array variant of all the messages + const messageBuffers = normalizedMessages.map((m) => Uint8Array.from(m)) + + const publicKey = TypedArrayEncoder.fromBase58(publicKeyBase58) + const privateKey = TypedArrayEncoder.fromBase58(privateKeyBase58) + + const bbsKeyPair = await bls12381toBbs({ + keyPair: { publicKey: Uint8Array.from(publicKey), secretKey: Uint8Array.from(privateKey) }, + messageCount: normalizedMessages.length, + }) + + // Sign the messages via the keyPair + const signature = await sign({ + keyPair: bbsKeyPair, + messages: messageBuffers, + }) + + // Convert the Uint8Array signature to a Buffer type + return Buffer.from(signature) + } + + /** + * Verify an arbitrary amount of messages with their signature created with their key pair + * + * @param publicKey Buffer The public key used to sign the messages + * @param messages Buffer[] The messages that have to be verified if they are signed + * @param signature Buffer The signature that has to be verified if it was created with the messages and public key + * + * @returns A boolean whether the signature is create with the public key over the messages + * + * @throws {SigningProviderError} When the message list is empty + * @throws {SigningProviderError} When the verification process failed + */ + public async verify({ data, publicKeyBase58, signature }: VerifyOptions): Promise { + if (data.length === 0) throw new SigningProviderError('Unable to create a signature without any messages') + // Check if it is a single message or list and if it is a single message convert it to a list + const normalizedMessages = (TypedArrayEncoder.isTypedArray(data) ? [data as Buffer] : data) as Buffer[] + + const publicKey = TypedArrayEncoder.fromBase58(publicKeyBase58) + + // Get the Uint8Array variant of all the messages + const messageBuffers = normalizedMessages.map((m) => Uint8Array.from(m)) + + const bbsKeyPair = await bls12381toBbs({ + keyPair: { publicKey: Uint8Array.from(publicKey) }, + messageCount: normalizedMessages.length, + }) + + // Verify the signature against the messages with their public key + const { verified, error } = await verify({ signature, messages: messageBuffers, publicKey: bbsKeyPair.publicKey }) + + // If the messages could not be verified and an error occurred + if (!verified && error) { + throw new SigningProviderError(`Could not verify the signature against the messages: ${error}`) + } + + return verified + } +} diff --git a/packages/bbs-signatures/src/__tests__/BbsModule.test.ts b/packages/bbs-signatures/src/__tests__/BbsModule.test.ts new file mode 100644 index 0000000000..ca92c1bb9e --- /dev/null +++ b/packages/bbs-signatures/src/__tests__/BbsModule.test.ts @@ -0,0 +1,42 @@ +import type { DependencyManager } from '@credo-ts/core' + +import { + KeyType, + SigningProviderToken, + VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020, + SignatureSuiteToken, +} from '@credo-ts/core' + +import { BbsModule } from '../BbsModule' +import { Bls12381g2SigningProvider } from '../Bls12381g2SigningProvider' +import { BbsBlsSignature2020, BbsBlsSignatureProof2020 } from '../signature-suites' + +const dependencyManager = { + registerInstance: jest.fn(), + registerSingleton: jest.fn(), + resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), +} as unknown as DependencyManager + +describe('BbsModule', () => { + test('registers dependencies on the dependency manager', () => { + const bbsModule = new BbsModule() + bbsModule.register(dependencyManager) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(SigningProviderToken, Bls12381g2SigningProvider) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(SignatureSuiteToken, { + suiteClass: BbsBlsSignature2020, + proofType: 'BbsBlsSignature2020', + verificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], + keyTypes: [KeyType.Bls12381g2], + }) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(SignatureSuiteToken, { + suiteClass: BbsBlsSignatureProof2020, + proofType: 'BbsBlsSignatureProof2020', + verificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], + keyTypes: [KeyType.Bls12381g2], + }) + }) +}) diff --git a/packages/bbs-signatures/src/index.ts b/packages/bbs-signatures/src/index.ts new file mode 100644 index 0000000000..0b218fb3d0 --- /dev/null +++ b/packages/bbs-signatures/src/index.ts @@ -0,0 +1,4 @@ +export * from './signature-suites' +export * from './BbsModule' +export * from './Bls12381g2SigningProvider' +export * from './types' diff --git a/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts b/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts new file mode 100644 index 0000000000..6722824f64 --- /dev/null +++ b/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts @@ -0,0 +1,402 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { + SignatureSuiteOptions, + CreateProofOptions, + VerifyProofOptions, + CanonizeOptions, + CreateVerifyDataOptions, + SuiteSignOptions, + VerifySignatureOptions, +} from '../types' +import type { VerificationMethod, JsonObject, DocumentLoader, Proof } from '@credo-ts/core' + +import { + CredoError, + TypedArrayEncoder, + SECURITY_CONTEXT_BBS_URL, + SECURITY_CONTEXT_URL, + w3cDate, + vcLibraries, +} from '@credo-ts/core' + +const { jsonld, jsonldSignatures } = vcLibraries +const LinkedDataProof = jsonldSignatures.suites.LinkedDataProof + +/** + * A BBS+ signature suite for use with BLS12-381 key pairs + */ +export class BbsBlsSignature2020 extends LinkedDataProof { + private proof: Record + /** + * Default constructor + * @param options {SignatureSuiteOptions} options for constructing the signature suite + */ + public constructor(options: SignatureSuiteOptions = {}) { + const { verificationMethod, signer, key, date, useNativeCanonize, LDKeyClass } = options + // validate common options + if (verificationMethod !== undefined && typeof verificationMethod !== 'string') { + throw new TypeError('"verificationMethod" must be a URL string.') + } + super({ + type: 'BbsBlsSignature2020', + }) + + this.proof = { + '@context': [ + { + sec: 'https://w3id.org/security#', + proof: { + '@id': 'sec:proof', + '@type': '@id', + '@container': '@graph', + }, + }, + SECURITY_CONTEXT_BBS_URL, + ], + type: 'BbsBlsSignature2020', + } + + this.LDKeyClass = LDKeyClass + this.signer = signer + this.verificationMethod = verificationMethod + this.proofSignatureKey = 'proofValue' + if (key) { + if (verificationMethod === undefined) { + this.verificationMethod = key.id + } + this.key = key + if (typeof key.signer === 'function') { + this.signer = key.signer() + } + if (typeof key.verifier === 'function') { + this.verifier = key.verifier() + } + } + if (date) { + this.date = new Date(date) + + if (isNaN(this.date)) { + throw TypeError(`"date" "${date}" is not a valid date.`) + } + } + this.useNativeCanonize = useNativeCanonize + } + + public ensureSuiteContext({ document }: { document: Record }) { + if ( + document['@context'] === SECURITY_CONTEXT_BBS_URL || + (Array.isArray(document['@context']) && document['@context'].includes(SECURITY_CONTEXT_BBS_URL)) + ) { + // document already includes the required context + return + } + throw new TypeError( + `The document to be signed must contain this suite's @context, ` + `"${SECURITY_CONTEXT_BBS_URL}".` + ) + } + + /** + * @param options {CreateProofOptions} options for creating the proof + * + * @returns {Promise} Resolves with the created proof object. + */ + public async createProof(options: CreateProofOptions): Promise> { + const { document, purpose, documentLoader, compactProof } = options + + let proof: JsonObject + + // use proof JSON-LD document passed to API + if (this.proof) { + proof = await jsonld.compact(this.proof, SECURITY_CONTEXT_URL, { + documentLoader, + compactToRelative: true, + }) + } else { + // create proof JSON-LD document + proof = { '@context': SECURITY_CONTEXT_URL } + } + + // ensure proof type is set + proof.type = this.type + + // set default `now` date if not given in `proof` or `options` + let date = this.date + if (proof.created === undefined && date === undefined) { + date = new Date() + } + + // ensure date is in string format + if (date !== undefined && typeof date !== 'string') { + date = w3cDate(date) + } + + // add API overrides + if (date !== undefined) { + proof.created = date + } + + if (this.verificationMethod !== undefined) { + proof.verificationMethod = this.verificationMethod + } + + // allow purpose to update the proof; the `proof` is in the + // SECURITY_CONTEXT_URL `@context` -- therefore the `purpose` must + // ensure any added fields are also represented in that same `@context` + proof = await purpose.update(proof, { + document, + suite: this, + documentLoader, + }) + + // create data to sign + const verifyData = ( + await this.createVerifyData({ + document, + proof, + documentLoader, + + compactProof, + }) + ).map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) + + // sign data + proof = await this.sign({ + verifyData, + document, + proof, + documentLoader, + }) + delete proof['@context'] + + return proof + } + + /** + * @param options {object} options for verifying the proof. + * + * @returns {Promise<{object}>} Resolves with the verification result. + */ + public async verifyProof(options: VerifyProofOptions): Promise> { + const { proof, document, documentLoader, purpose } = options + + try { + // create data to verify + const verifyData = ( + await this.createVerifyData({ + document, + proof, + documentLoader, + compactProof: false, + }) + ).map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) + + // fetch verification method + const verificationMethod = await this.getVerificationMethod({ + proof, + documentLoader, + }) + + // verify signature on data + const verified = await this.verifySignature({ + verifyData, + verificationMethod, + document, + proof, + documentLoader, + }) + if (!verified) { + throw new Error('Invalid signature.') + } + + // ensure proof was performed for a valid purpose + const { valid, error } = await purpose.validate(proof, { + document, + suite: this, + verificationMethod, + documentLoader, + }) + if (!valid) { + throw error + } + + return { verified: true } + } catch (error) { + return { verified: false, error } + } + } + + public async canonize(input: Record, options: CanonizeOptions): Promise { + const { documentLoader, skipExpansion } = options + return jsonld.canonize(input, { + algorithm: 'URDNA2015', + format: 'application/n-quads', + documentLoader, + skipExpansion, + useNative: this.useNativeCanonize, + }) + } + + public async canonizeProof(proof: Record, options: CanonizeOptions): Promise { + const { documentLoader } = options + proof = { ...proof } + delete proof[this.proofSignatureKey] + return this.canonize(proof, { + documentLoader, + skipExpansion: false, + }) + } + + /** + * @param document {CreateVerifyDataOptions} options to create verify data + * + * @returns {Promise<{string[]>}. + */ + public async createVerifyData(options: CreateVerifyDataOptions): Promise { + const { proof, document, documentLoader } = options + + const proof2 = { ...proof, '@context': document['@context'] } + + const proofStatements = await this.createVerifyProofData(proof2, { + documentLoader, + }) + const documentStatements = await this.createVerifyDocumentData(document, { + documentLoader, + }) + + // concatenate c14n proof options and c14n document + return proofStatements.concat(documentStatements) + } + + /** + * @param proof to canonicalize + * @param options to create verify data + * + * @returns {Promise<{string[]>}. + */ + public async createVerifyProofData( + proof: Record, + { documentLoader }: { documentLoader?: DocumentLoader } + ): Promise { + const c14nProofOptions = await this.canonizeProof(proof, { + documentLoader, + }) + + return c14nProofOptions.split('\n').filter((_) => _.length > 0) + } + + /** + * @param document to canonicalize + * @param options to create verify data + * + * @returns {Promise<{string[]>}. + */ + public async createVerifyDocumentData( + document: Record, + { documentLoader }: { documentLoader?: DocumentLoader } + ): Promise { + const c14nDocument = await this.canonize(document, { + documentLoader, + }) + + return c14nDocument.split('\n').filter((_) => _.length > 0) + } + + /** + * @param document {object} to be signed. + * @param proof {object} + * @param documentLoader {function} + */ + public async getVerificationMethod({ + proof, + documentLoader, + }: { + proof: Proof + documentLoader?: DocumentLoader + }): Promise { + let { verificationMethod } = proof + + if (typeof verificationMethod === 'object' && verificationMethod !== null) { + verificationMethod = verificationMethod.id + } + + if (!verificationMethod) { + throw new Error('No "verificationMethod" found in proof.') + } + + if (!documentLoader) { + throw new CredoError('Missing custom document loader. This is required for resolving verification methods.') + } + + const { document } = await documentLoader(verificationMethod) + + if (!document) { + throw new Error(`Verification method ${verificationMethod} not found.`) + } + + // ensure verification method has not been revoked + if (document.revoked !== undefined) { + throw new Error('The verification method has been revoked.') + } + + return document as unknown as VerificationMethod + } + + /** + * @param options {SuiteSignOptions} Options for signing. + * + * @returns {Promise<{object}>} the proof containing the signature value. + */ + public async sign(options: SuiteSignOptions): Promise { + const { verifyData, proof } = options + + if (!(this.signer && typeof this.signer.sign === 'function')) { + throw new Error('A signer API with sign function has not been specified.') + } + + const proofValue: Uint8Array = await this.signer.sign({ + data: verifyData, + }) + + proof[this.proofSignatureKey] = TypedArrayEncoder.toBase64(proofValue) + + return proof as Proof + } + + /** + * @param verifyData {VerifySignatureOptions} Options to verify the signature. + * + * @returns {Promise} + */ + public async verifySignature(options: VerifySignatureOptions): Promise { + const { verificationMethod, verifyData, proof } = options + let { verifier } = this + + if (!verifier) { + const key = await this.LDKeyClass.from(verificationMethod) + verifier = key.verifier(key, this.alg, this.type) + } + + return await verifier.verify({ + data: verifyData, + signature: new Uint8Array(TypedArrayEncoder.fromBase64(proof[this.proofSignatureKey] as string)), + }) + } + + public static proofType = [ + 'BbsBlsSignature2020', + 'sec:BbsBlsSignature2020', + 'https://w3id.org/security#BbsBlsSignature2020', + ] +} diff --git a/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts b/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts new file mode 100644 index 0000000000..779762b5d5 --- /dev/null +++ b/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts @@ -0,0 +1,404 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { DeriveProofOptions, VerifyProofOptions, CreateVerifyDataOptions, CanonizeOptions } from '../types' +import type { VerifyProofResult } from '../types/VerifyProofResult' +import type { JsonObject, DocumentLoader, Proof } from '@credo-ts/core' + +import { CredoError, TypedArrayEncoder, SECURITY_CONTEXT_URL, vcLibraries } from '@credo-ts/core' +import { blsCreateProof, blsVerifyProof } from '@mattrglobal/bbs-signatures' +import { Bls12381G2KeyPair } from '@mattrglobal/bls12381-key-pair' +import { randomBytes } from '@stablelib/random' + +import { BbsBlsSignature2020 } from './BbsBlsSignature2020' + +const { jsonld, jsonldSignatures } = vcLibraries +const LinkedDataProof = jsonldSignatures.suites.LinkedDataProof + +export class BbsBlsSignatureProof2020 extends LinkedDataProof { + public constructor({ useNativeCanonize, key, LDKeyClass }: Record = {}) { + super({ + type: 'BbsBlsSignatureProof2020', + }) + + this.proof = { + '@context': [ + { + sec: 'https://w3id.org/security#', + proof: { + '@id': 'sec:proof', + '@type': '@id', + '@container': '@graph', + }, + }, + 'https://w3id.org/security/bbs/v1', + ], + type: 'BbsBlsSignatureProof2020', + } + this.mappedDerivedProofType = 'BbsBlsSignature2020' + this.supportedDeriveProofType = BbsBlsSignatureProof2020.supportedDerivedProofType + + this.LDKeyClass = LDKeyClass ?? Bls12381G2KeyPair + this.proofSignatureKey = 'proofValue' + this.key = key + this.useNativeCanonize = useNativeCanonize + } + + /** + * Derive a proof from a proof and reveal document + * + * @param options {object} options for deriving a proof. + * + * @returns {Promise} Resolves with the derived proof object. + */ + public async deriveProof(options: DeriveProofOptions): Promise> { + const { document, proof, revealDocument, documentLoader } = options + let { nonce } = options + + const proofType = proof.type + + if (typeof proofType !== 'string') { + throw new TypeError(`Expected proof.type to be of type 'string', got ${typeof proofType} instead.`) + } + + // Validate that the input proof document has a proof compatible with this suite + if (!BbsBlsSignatureProof2020.supportedDerivedProofType.includes(proofType)) { + throw new TypeError( + `proof document proof incompatible, expected proof types of ${JSON.stringify( + BbsBlsSignatureProof2020.supportedDerivedProofType + )} received ${proof.type}` + ) + } + + const signatureBase58 = proof[this.proofSignatureKey] + + if (typeof signatureBase58 !== 'string') { + throw new TypeError(`Expected signature to be a base58 encoded string, got ${typeof signatureBase58} instead.`) + } + + //Extract the BBS signature from the input proof + const signature = TypedArrayEncoder.fromBase64(signatureBase58) + + //Initialize the BBS signature suite + const suite = new BbsBlsSignature2020() + + //Initialize the derived proof + let derivedProof + if (this.proof) { + // use proof JSON-LD document passed to API + derivedProof = await jsonld.compact(this.proof, SECURITY_CONTEXT_URL, { + documentLoader, + compactToRelative: false, + }) + } else { + // create proof JSON-LD document + derivedProof = { '@context': SECURITY_CONTEXT_URL } + } + + // ensure proof type is set + derivedProof.type = this.type + + // Get the input document statements + const documentStatements = await suite.createVerifyDocumentData(document, { + documentLoader, + }) + + // Get the proof statements + const proofStatements = await suite.createVerifyProofData(proof, { + documentLoader, + }) + + // Transform any blank node identifiers for the input + // document statements into actual node identifiers + // e.g _:c14n0 => urn:bnid:_:c14n0 + const transformedInputDocumentStatements = documentStatements.map((element) => + element.replace(/(_:c14n[0-9]+)/g, '') + ) + + //Transform the resulting RDF statements back into JSON-LD + const compactInputProofDocument = await jsonld.fromRDF(transformedInputDocumentStatements.join('\n')) + + // Frame the result to create the reveal document result + const revealDocumentResult = await jsonld.frame(compactInputProofDocument, revealDocument, { documentLoader }) + + // Canonicalize the resulting reveal document + const revealDocumentStatements = await suite.createVerifyDocumentData(revealDocumentResult, { + documentLoader, + }) + + //Get the indicies of the revealed statements from the transformed input document offset + //by the number of proof statements + const numberOfProofStatements = proofStatements.length + + //Always reveal all the statements associated to the original proof + //these are always the first statements in the normalized form + const proofRevealIndicies = Array.from(Array(numberOfProofStatements).keys()) + + //Reveal the statements indicated from the reveal document + const documentRevealIndicies = revealDocumentStatements.map( + (key) => transformedInputDocumentStatements.indexOf(key) + numberOfProofStatements + ) + + // Check there is not a mismatch + if (documentRevealIndicies.length !== revealDocumentStatements.length) { + throw new Error('Some statements in the reveal document not found in original proof') + } + + // Combine all indicies to get the resulting list of revealed indicies + const revealIndicies = proofRevealIndicies.concat(documentRevealIndicies) + + // Create a nonce if one is not supplied + if (!nonce) { + nonce = randomBytes(50) + } + + // Set the nonce on the derived proof + // derivedProof.nonce = Buffer.from(nonce).toString('base64') + derivedProof.nonce = TypedArrayEncoder.toBase64(nonce) + + //Combine all the input statements that + //were originally signed to generate the proof + const allInputStatements: Uint8Array[] = proofStatements + .concat(documentStatements) + .map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) + + // Fetch the verification method + const verificationMethod = await this.getVerificationMethod({ + proof, + documentLoader, + }) + + // Construct a key pair class from the returned verification method + const key = verificationMethod.publicKeyJwk + ? await this.LDKeyClass.fromJwk(verificationMethod) + : await this.LDKeyClass.from(verificationMethod) + + // Compute the proof + const outputProof = await blsCreateProof({ + signature, + publicKey: Uint8Array.from(key.publicKeyBuffer), + messages: allInputStatements, + nonce, + revealed: revealIndicies, + }) + + // Set the proof value on the derived proof + derivedProof.proofValue = TypedArrayEncoder.toBase64(outputProof) + + // Set the relevant proof elements on the derived proof from the input proof + derivedProof.verificationMethod = proof.verificationMethod + derivedProof.proofPurpose = proof.proofPurpose + derivedProof.created = proof.created + + return { + document: { ...revealDocumentResult }, + proof: derivedProof, + } + } + + /** + * @param options {object} options for verifying the proof. + * + * @returns {Promise<{object}>} Resolves with the verification result. + */ + public async verifyProof(options: VerifyProofOptions): Promise { + const { document, documentLoader, purpose } = options + const { proof } = options + + try { + proof.type = this.mappedDerivedProofType + + const proofIncludingDocumentContext = { ...proof, '@context': document['@context'] } + + // Get the proof statements + const proofStatements = await this.createVerifyProofData(proofIncludingDocumentContext, { + documentLoader, + }) + + // Get the document statements + const documentStatements = await this.createVerifyProofData(document, { + documentLoader, + }) + + // Transform the blank node identifier placeholders for the document statements + // back into actual blank node identifiers + const transformedDocumentStatements = documentStatements.map((element) => + element.replace(//g, '$1') + ) + + // Combine all the statements to be verified + const statementsToVerify: Uint8Array[] = proofStatements + .concat(transformedDocumentStatements) + .map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) + + // Fetch the verification method + const verificationMethod = await this.getVerificationMethod({ + proof, + documentLoader, + }) + + // Construct a key pair class from the returned verification method + const key = verificationMethod.publicKeyJwk + ? await this.LDKeyClass.fromJwk(verificationMethod) + : await this.LDKeyClass.from(verificationMethod) + + const proofValue = proof.proofValue + + if (typeof proofValue !== 'string') { + throw new CredoError(`Expected proof.proofValue to be of type 'string', got ${typeof proof}`) + } + + // Verify the proof + const verified = await blsVerifyProof({ + proof: TypedArrayEncoder.fromBase64(proofValue), + publicKey: key.publicKeyBuffer, + messages: statementsToVerify, + nonce: TypedArrayEncoder.fromBase64(proof.nonce as string), + }) + + // Ensure proof was performed for a valid purpose + const { valid, error } = await purpose.validate(proof, { + document, + suite: this, + verificationMethod, + documentLoader, + }) + if (!valid) { + throw error + } + + return verified + } catch (error) { + return { verified: false, error } + } + } + + public async canonize(input: JsonObject, options: CanonizeOptions): Promise { + const { documentLoader, skipExpansion } = options + return jsonld.canonize(input, { + algorithm: 'URDNA2015', + format: 'application/n-quads', + documentLoader, + skipExpansion, + useNative: this.useNativeCanonize, + }) + } + + public async canonizeProof(proof: JsonObject, options: CanonizeOptions): Promise { + const { documentLoader } = options + proof = { ...proof } + + delete proof.nonce + delete proof.proofValue + + return this.canonize(proof, { + documentLoader, + skipExpansion: false, + }) + } + + /** + * @param document {CreateVerifyDataOptions} options to create verify data + * + * @returns {Promise<{string[]>}. + */ + public async createVerifyData(options: CreateVerifyDataOptions): Promise { + const { proof, document, documentLoader } = options + + const proofStatements = await this.createVerifyProofData(proof, { + documentLoader, + }) + const documentStatements = await this.createVerifyDocumentData(document, { + documentLoader, + }) + + // concatenate c14n proof options and c14n document + return proofStatements.concat(documentStatements) + } + + /** + * @param proof to canonicalize + * @param options to create verify data + * + * @returns {Promise<{string[]>}. + */ + public async createVerifyProofData( + proof: JsonObject, + { documentLoader }: { documentLoader?: DocumentLoader } + ): Promise { + const c14nProofOptions = await this.canonizeProof(proof, { + documentLoader, + }) + + return c14nProofOptions.split('\n').filter((_) => _.length > 0) + } + + /** + * @param document to canonicalize + * @param options to create verify data + * + * @returns {Promise<{string[]>}. + */ + public async createVerifyDocumentData( + document: JsonObject, + { documentLoader }: { documentLoader?: DocumentLoader } + ): Promise { + const c14nDocument = await this.canonize(document, { + documentLoader, + }) + + return c14nDocument.split('\n').filter((_) => _.length > 0) + } + + public async getVerificationMethod(options: { proof: Proof; documentLoader?: DocumentLoader }) { + if (this.key) { + // This happens most often during sign() operations. For verify(), + // the expectation is that the verification method will be fetched + // by the documentLoader (below), not provided as a `key` parameter. + return this.key.export({ publicKey: true }) + } + + let { verificationMethod } = options.proof + + if (typeof verificationMethod === 'object' && verificationMethod !== null) { + verificationMethod = verificationMethod.id + } + + if (!verificationMethod) { + throw new Error('No "verificationMethod" found in proof.') + } + + if (!options.documentLoader) { + throw new CredoError('Missing custom document loader. This is required for resolving verification methods.') + } + + const { document } = await options.documentLoader(verificationMethod) + + verificationMethod = typeof document === 'string' ? JSON.parse(document) : document + + // await this.assertVerificationMethod(verificationMethod) + return verificationMethod + } + + public static proofType = [ + 'BbsBlsSignatureProof2020', + 'sec:BbsBlsSignatureProof2020', + 'https://w3id.org/security#BbsBlsSignatureProof2020', + ] + + public static supportedDerivedProofType = [ + 'BbsBlsSignature2020', + 'sec:BbsBlsSignature2020', + 'https://w3id.org/security#BbsBlsSignature2020', + ] +} diff --git a/packages/bbs-signatures/src/signature-suites/index.ts b/packages/bbs-signatures/src/signature-suites/index.ts new file mode 100644 index 0000000000..932af48e2f --- /dev/null +++ b/packages/bbs-signatures/src/signature-suites/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +export { Bls12381G2KeyPair } from '@mattrglobal/bls12381-key-pair' +export { BbsBlsSignature2020 } from './BbsBlsSignature2020' +export { BbsBlsSignatureProof2020 } from './BbsBlsSignatureProof2020' diff --git a/packages/bbs-signatures/src/types/CanonizeOptions.ts b/packages/bbs-signatures/src/types/CanonizeOptions.ts new file mode 100644 index 0000000000..f03a2a9a20 --- /dev/null +++ b/packages/bbs-signatures/src/types/CanonizeOptions.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { DocumentLoader } from '@credo-ts/core' + +/** + * Options for canonizing a document + */ +export interface CanonizeOptions { + /** + * Optional custom document loader + */ + documentLoader?: DocumentLoader + + /** + * Indicates whether to skip expansion during canonization + */ + readonly skipExpansion?: boolean +} diff --git a/packages/bbs-signatures/src/types/CreateProofOptions.ts b/packages/bbs-signatures/src/types/CreateProofOptions.ts new file mode 100644 index 0000000000..3f61766a6d --- /dev/null +++ b/packages/bbs-signatures/src/types/CreateProofOptions.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { DocumentLoader, ProofPurpose, JsonObject } from '@credo-ts/core' + +/** + * Options for creating a proof + */ +export interface CreateProofOptions { + /** + * Document to create the proof for + */ + readonly document: JsonObject + /** + * The proof purpose to specify for the generated proof + */ + readonly purpose: ProofPurpose + /** + * Optional custom document loader + */ + documentLoader?: DocumentLoader + /** + * Indicates whether to compact the resulting proof + */ + readonly compactProof: boolean +} diff --git a/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts b/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts new file mode 100644 index 0000000000..8e8545da61 --- /dev/null +++ b/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { JsonObject, DocumentLoader } from '@credo-ts/core' + +/** + * Options for creating a proof + */ +export interface CreateVerifyDataOptions { + /** + * Document to create the proof for + */ + readonly document: JsonObject + + /** + * The proof + */ + readonly proof: JsonObject + + /** + * Optional custom document loader + */ + documentLoader?: DocumentLoader + + /** + * Indicates whether to compact the proof + */ + readonly compactProof: boolean +} diff --git a/packages/bbs-signatures/src/types/DeriveProofOptions.ts b/packages/bbs-signatures/src/types/DeriveProofOptions.ts new file mode 100644 index 0000000000..487495c327 --- /dev/null +++ b/packages/bbs-signatures/src/types/DeriveProofOptions.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { JsonObject, DocumentLoader, Proof } from '@credo-ts/core' + +/** + * Options for creating a proof + */ +export interface DeriveProofOptions { + /** + * Document outlining what statements to reveal + */ + readonly revealDocument: JsonObject + /** + * The document featuring the proof to derive from + */ + readonly document: JsonObject + /** + * The proof for the document + */ + readonly proof: Proof + /** + * Optional custom document loader + */ + // eslint-disable-next-line + documentLoader?: DocumentLoader + + /** + * Nonce to include in the derived proof + */ + readonly nonce?: Uint8Array + /** + * Indicates whether to compact the resulting proof + */ + readonly skipProofCompaction?: boolean +} diff --git a/packages/bbs-signatures/src/types/DidDocumentPublicKey.ts b/packages/bbs-signatures/src/types/DidDocumentPublicKey.ts new file mode 100644 index 0000000000..d8a7476e1f --- /dev/null +++ b/packages/bbs-signatures/src/types/DidDocumentPublicKey.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { PublicJsonWebKey } from './JsonWebKey' + +/** + * Interface for the public key definition entry in a DID Document. + * @see https://w3c-ccg.github.io/did-spec/#public-keys + */ +export interface DidDocumentPublicKey { + /** + * Fully qualified identifier of this public key, e.g. did:example:entity.id#keys-1 + */ + readonly id: string + + /** + * The type of this public key, as defined in: https://w3c-ccg.github.io/ld-cryptosuite-registry/ + */ + readonly type: string + + /** + * The DID of the controller of this key. + */ + readonly controller?: string + + /** + * The value of the public key in Base58 format. Only one value field will be present. + */ + readonly publicKeyBase58?: string + + /** + * Public key in JWK format. + * @see https://w3c-ccg.github.io/did-spec/#public-keys + */ + readonly publicKeyJwk?: PublicJsonWebKey + + /** + * Public key in HEX format. + * @see https://w3c-ccg.github.io/did-spec/#public-keys + */ + readonly publicKeyHex?: string +} diff --git a/packages/bbs-signatures/src/types/JsonWebKey.ts b/packages/bbs-signatures/src/types/JsonWebKey.ts new file mode 100644 index 0000000000..a027778879 --- /dev/null +++ b/packages/bbs-signatures/src/types/JsonWebKey.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +export enum JwkKty { + OctetKeyPair = 'OKP', + EC = 'EC', + RSA = 'RSA', +} + +export interface JwkEc { + readonly kty: JwkKty.EC + readonly crv: string + readonly d?: string + readonly x?: string + readonly y?: string + readonly kid?: string +} + +export interface JwkOctetKeyPair { + readonly kty: JwkKty.OctetKeyPair + readonly crv: string + readonly d?: string + readonly x?: string + readonly y?: string + readonly kid?: string +} + +export interface JwkRsa { + readonly kty: JwkKty.RSA + readonly e: string + readonly n: string +} + +export interface JwkRsaPrivate extends JwkRsa { + readonly d: string + readonly p: string + readonly q: string + readonly dp: string + readonly dq: string + readonly qi: string +} +export type JsonWebKey = JwkOctetKeyPair | JwkEc | JwkRsa | JwkRsaPrivate +export type PublicJsonWebKey = JwkOctetKeyPair | JwkEc | JwkRsa diff --git a/packages/bbs-signatures/src/types/KeyPairOptions.ts b/packages/bbs-signatures/src/types/KeyPairOptions.ts new file mode 100644 index 0000000000..624029cd9c --- /dev/null +++ b/packages/bbs-signatures/src/types/KeyPairOptions.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +/** + * Options for constructing a key pair + */ +export interface KeyPairOptions { + /** + * The key id + */ + readonly id?: string + /** + * The key controller + */ + readonly controller?: string + /** + * Base58 encoding of the private key + */ + readonly privateKeyBase58?: string + /** + * Base58 encoding of the public key + */ + readonly publicKeyBase58: string +} diff --git a/packages/bbs-signatures/src/types/KeyPairSigner.ts b/packages/bbs-signatures/src/types/KeyPairSigner.ts new file mode 100644 index 0000000000..2aaa37f7cf --- /dev/null +++ b/packages/bbs-signatures/src/types/KeyPairSigner.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +/** + * Key pair signer + */ +export interface KeyPairSigner { + /** + * Signer function + */ + readonly sign: (options: KeyPairSignerOptions) => Promise +} + +/** + * Key pair signer options + */ +export interface KeyPairSignerOptions { + readonly data: Uint8Array | Uint8Array[] +} diff --git a/packages/bbs-signatures/src/types/KeyPairVerifier.ts b/packages/bbs-signatures/src/types/KeyPairVerifier.ts new file mode 100644 index 0000000000..ed89f3bffe --- /dev/null +++ b/packages/bbs-signatures/src/types/KeyPairVerifier.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +/** + * Key pair verifier + */ +export interface KeyPairVerifier { + /** + * Key pair verify function + */ + readonly verify: (options: KeyPairVerifierOptions) => Promise +} + +/** + * Key pair verifier options + */ +export interface KeyPairVerifierOptions { + readonly data: Uint8Array | Uint8Array[] + readonly signature: Uint8Array +} diff --git a/packages/bbs-signatures/src/types/SignatureSuiteOptions.ts b/packages/bbs-signatures/src/types/SignatureSuiteOptions.ts new file mode 100644 index 0000000000..a667d1a8cb --- /dev/null +++ b/packages/bbs-signatures/src/types/SignatureSuiteOptions.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { KeyPairSigner } from './KeyPairSigner' +import type { JsonArray, LdKeyPair } from '@credo-ts/core' +import type { Bls12381G2KeyPair } from '@mattrglobal/bls12381-key-pair' + +/** + * Options for constructing a signature suite + */ +export interface SignatureSuiteOptions { + /** + * An optional signer interface for handling the sign operation + */ + readonly signer?: KeyPairSigner + /** + * The key pair used to generate the proof + */ + readonly key?: Bls12381G2KeyPair + /** + * A key id URL to the paired public key used for verifying the proof + */ + readonly verificationMethod?: string + /** + * The `created` date to report in generated proofs + */ + readonly date?: string | Date + /** + * Indicates whether to use the native implementation + * of RDF Dataset Normalization + */ + readonly useNativeCanonize?: boolean + /** + * Additional proof elements + */ + readonly proof?: JsonArray + /** + * Linked Data Key class implementation + */ + readonly LDKeyClass?: LdKeyPair +} diff --git a/packages/bbs-signatures/src/types/SuiteSignOptions.ts b/packages/bbs-signatures/src/types/SuiteSignOptions.ts new file mode 100644 index 0000000000..51e9de6402 --- /dev/null +++ b/packages/bbs-signatures/src/types/SuiteSignOptions.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { JsonObject, DocumentLoader } from '@credo-ts/core' + +/** + * Options for signing using a signature suite + */ +export interface SuiteSignOptions { + /** + * Input document to sign + */ + readonly document: JsonObject + /** + * Optional custom document loader + */ + documentLoader?: DocumentLoader + + /** + * The array of statements to sign + */ + readonly verifyData: readonly Uint8Array[] + /** + * The proof + */ + readonly proof: JsonObject +} diff --git a/packages/bbs-signatures/src/types/VerifyProofOptions.ts b/packages/bbs-signatures/src/types/VerifyProofOptions.ts new file mode 100644 index 0000000000..1f48ffe859 --- /dev/null +++ b/packages/bbs-signatures/src/types/VerifyProofOptions.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { Proof, JsonObject, ProofPurpose, DocumentLoader } from '@credo-ts/core' + +/** + * Options for verifying a proof + */ +export interface VerifyProofOptions { + /** + * The proof + */ + readonly proof: Proof + /** + * The document + */ + readonly document: JsonObject + /** + * The proof purpose to specify for the generated proof + */ + readonly purpose: ProofPurpose + /** + * Optional custom document loader + */ + documentLoader?: DocumentLoader +} diff --git a/packages/bbs-signatures/src/types/VerifyProofResult.ts b/packages/bbs-signatures/src/types/VerifyProofResult.ts new file mode 100644 index 0000000000..96996d006d --- /dev/null +++ b/packages/bbs-signatures/src/types/VerifyProofResult.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +/** + * Result of calling verify proof + */ +export interface VerifyProofResult { + /** + * A boolean indicating if the verification was successful + */ + readonly verified: boolean + /** + * A string representing the error if the verification failed + */ + readonly error?: unknown +} diff --git a/packages/bbs-signatures/src/types/VerifySignatureOptions.ts b/packages/bbs-signatures/src/types/VerifySignatureOptions.ts new file mode 100644 index 0000000000..a283c805a2 --- /dev/null +++ b/packages/bbs-signatures/src/types/VerifySignatureOptions.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { VerificationMethod, JsonObject, Proof, DocumentLoader } from '@credo-ts/core' + +/** + * Options for verifying a signature + */ +export interface VerifySignatureOptions { + /** + * Document to verify + */ + readonly document: JsonObject + /** + * Array of statements to verify + */ + readonly verifyData: Uint8Array[] + /** + * Verification method to verify the signature against + */ + readonly verificationMethod: VerificationMethod + /** + * Proof to verify + */ + readonly proof: Proof + /** + * Optional custom document loader + */ + documentLoader?: DocumentLoader +} diff --git a/packages/bbs-signatures/src/types/index.ts b/packages/bbs-signatures/src/types/index.ts new file mode 100644 index 0000000000..60575814bb --- /dev/null +++ b/packages/bbs-signatures/src/types/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +export { KeyPairOptions } from './KeyPairOptions' +export { KeyPairSigner } from './KeyPairSigner' +export { KeyPairVerifier } from './KeyPairVerifier' +export { SignatureSuiteOptions } from './SignatureSuiteOptions' +export { CreateProofOptions } from './CreateProofOptions' +export { VerifyProofOptions } from './VerifyProofOptions' +export { CanonizeOptions } from './CanonizeOptions' +export { CreateVerifyDataOptions } from './CreateVerifyDataOptions' +export { VerifySignatureOptions } from './VerifySignatureOptions' +export { SuiteSignOptions } from './SuiteSignOptions' +export { DeriveProofOptions } from './DeriveProofOptions' +export { DidDocumentPublicKey } from './DidDocumentPublicKey' diff --git a/packages/bbs-signatures/tests/bbs-signatures.test.ts b/packages/bbs-signatures/tests/bbs-signatures.test.ts new file mode 100644 index 0000000000..0a6c098624 --- /dev/null +++ b/packages/bbs-signatures/tests/bbs-signatures.test.ts @@ -0,0 +1,292 @@ +import type { W3cCredentialRepository } from '../../core/src/modules/vc/repository/W3cCredentialRepository' +import type { AgentContext, W3cJwtCredentialService, Wallet } from '@credo-ts/core' + +import { + ClaimFormat, + W3cCredentialService, + W3cJsonLdVerifiablePresentation, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + KeyType, + JsonTransformer, + DidKey, + SigningProviderRegistry, + W3cCredential, + CredentialIssuancePurpose, + VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020, + vcLibraries, + W3cPresentation, + Ed25519Signature2018, + TypedArrayEncoder, + W3cJsonLdVerifiableCredential, +} from '@credo-ts/core' + +import { RegisteredAskarTestWallet } from '../../askar/tests/helpers' +import { W3cCredentialsModuleConfig } from '../../core/src/modules/vc/W3cCredentialsModuleConfig' +import { SignatureSuiteRegistry } from '../../core/src/modules/vc/data-integrity/SignatureSuiteRegistry' +import { W3cJsonLdCredentialService } from '../../core/src/modules/vc/data-integrity/W3cJsonLdCredentialService' +import { customDocumentLoader } from '../../core/src/modules/vc/data-integrity/__tests__/documentLoader' +import { LinkedDataProof } from '../../core/src/modules/vc/data-integrity/models/LinkedDataProof' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import { BbsBlsSignature2020, BbsBlsSignatureProof2020, Bls12381g2SigningProvider } from '../src' + +import { BbsBlsSignature2020Fixtures } from './fixtures' +import { describeSkipNode18 } from './util' + +const { jsonldSignatures } = vcLibraries +const { purposes } = jsonldSignatures + +const signatureSuiteRegistry = new SignatureSuiteRegistry([ + { + suiteClass: BbsBlsSignature2020, + proofType: 'BbsBlsSignature2020', + verificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], + keyTypes: [KeyType.Bls12381g2], + }, + { + suiteClass: BbsBlsSignatureProof2020, + proofType: 'BbsBlsSignatureProof2020', + verificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], + keyTypes: [KeyType.Bls12381g2], + }, + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + verificationMethodTypes: [VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018], + keyTypes: [KeyType.Ed25519], + }, +]) + +const signingProviderRegistry = new SigningProviderRegistry([new Bls12381g2SigningProvider()]) + +const agentConfig = getAgentConfig('BbsSignaturesE2eTest') + +describeSkipNode18('BBS W3cCredentialService', () => { + let wallet: Wallet + let agentContext: AgentContext + let w3cJsonLdCredentialService: W3cJsonLdCredentialService + let w3cCredentialService: W3cCredentialService + const privateKey = TypedArrayEncoder.fromString('testseed000000000000000000000001') + + beforeAll(async () => { + // Use askar wallet so we can use the signing provider registry + // TODO: support signing provider registry in memory wallet + // so we don't have to use askar here + wallet = new RegisteredAskarTestWallet( + agentConfig.logger, + new agentDependencies.FileSystem(), + signingProviderRegistry + ) + await wallet.createAndOpen(agentConfig.walletConfig) + agentContext = getAgentContext({ + agentConfig, + wallet, + }) + w3cJsonLdCredentialService = new W3cJsonLdCredentialService( + signatureSuiteRegistry, + new W3cCredentialsModuleConfig({ + documentLoader: customDocumentLoader, + }) + ) + w3cCredentialService = new W3cCredentialService( + {} as unknown as W3cCredentialRepository, + w3cJsonLdCredentialService, + {} as unknown as W3cJwtCredentialService + ) + }) + + afterAll(async () => { + await wallet.delete() + }) + + describe('Utility methods', () => { + describe('getKeyTypesByProofType', () => { + it('should return the correct key types for BbsBlsSignature2020 proof type', async () => { + const keyTypes = w3cJsonLdCredentialService.getKeyTypesByProofType('BbsBlsSignature2020') + expect(keyTypes).toEqual([KeyType.Bls12381g2]) + }) + it('should return the correct key types for BbsBlsSignatureProof2020 proof type', async () => { + const keyTypes = w3cJsonLdCredentialService.getKeyTypesByProofType('BbsBlsSignatureProof2020') + expect(keyTypes).toEqual([KeyType.Bls12381g2]) + }) + }) + + describe('getVerificationMethodTypesByProofType', () => { + it('should return the correct key types for BbsBlsSignature2020 proof type', async () => { + const verificationMethodTypes = + w3cJsonLdCredentialService.getVerificationMethodTypesByProofType('BbsBlsSignature2020') + expect(verificationMethodTypes).toEqual([VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020]) + }) + it('should return the correct key types for BbsBlsSignatureProof2020 proof type', async () => { + const verificationMethodTypes = + w3cJsonLdCredentialService.getVerificationMethodTypesByProofType('BbsBlsSignatureProof2020') + expect(verificationMethodTypes).toEqual([VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020]) + }) + }) + }) + + describe('BbsBlsSignature2020', () => { + let issuerDidKey: DidKey + let verificationMethod: string + + beforeAll(async () => { + // FIXME: askar doesn't create the same privateKey based on the same seed as when generated used askar BBS library... + // See https://github.com/hyperledger/aries-askar/issues/219 + const key = await wallet.createKey({ + keyType: KeyType.Bls12381g2, + privateKey: TypedArrayEncoder.fromBase58('2szQ7zB4tKLJPsGK3YTp9SNQ6hoWYFG5rGhmgfQM4nb7'), + }) + + issuerDidKey = new DidKey(key) + verificationMethod = `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}` + }) + + describe('signCredential', () => { + it('should return a successfully signed credential bbs', async () => { + const credentialJson = BbsBlsSignature2020Fixtures.TEST_LD_DOCUMENT + credentialJson.issuer = issuerDidKey.did + + const credential = JsonTransformer.fromJSON(credentialJson, W3cCredential) + + const vc = await w3cJsonLdCredentialService.signCredential(agentContext, { + format: ClaimFormat.LdpVc, + credential, + proofType: 'BbsBlsSignature2020', + verificationMethod, + }) + + expect(vc).toBeInstanceOf(W3cJsonLdVerifiableCredential) + expect(vc.issuer).toEqual(issuerDidKey.did) + expect(Array.isArray(vc.proof)).toBe(false) + expect(vc.proof).toBeInstanceOf(LinkedDataProof) + + vc.proof = vc.proof as LinkedDataProof + expect(vc.proof.verificationMethod).toEqual(verificationMethod) + }) + }) + + describe('verifyCredential', () => { + it('should verify the credential successfully', async () => { + const result = await w3cJsonLdCredentialService.verifyCredential(agentContext, { + credential: JsonTransformer.fromJSON( + BbsBlsSignature2020Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ), + proofPurpose: new purposes.AssertionProofPurpose(), + }) + + expect(result.isValid).toEqual(true) + }) + }) + + describe('deriveProof', () => { + it('should derive proof successfully', async () => { + const credentialJson = BbsBlsSignature2020Fixtures.TEST_LD_DOCUMENT_SIGNED + + const vc = JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential) + + const revealDocument = { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/citizenship/v1', + 'https://w3id.org/security/bbs/v1', + ], + type: ['VerifiableCredential', 'PermanentResidentCard'], + credentialSubject: { + '@explicit': true, + type: ['PermanentResident', 'Person'], + givenName: {}, + familyName: {}, + gender: {}, + }, + } + + const result = await w3cJsonLdCredentialService.deriveProof(agentContext, { + credential: vc, + revealDocument: revealDocument, + verificationMethod: verificationMethod, + }) + + result.proof = result.proof as LinkedDataProof + expect(result.proof.verificationMethod).toBe( + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN' + ) + }) + }) + + describe('verifyDerived', () => { + it('should verify the derived proof successfully', async () => { + const result = await w3cJsonLdCredentialService.verifyCredential(agentContext, { + credential: JsonTransformer.fromJSON( + BbsBlsSignature2020Fixtures.TEST_VALID_DERIVED, + W3cJsonLdVerifiableCredential + ), + proofPurpose: new purposes.AssertionProofPurpose(), + }) + expect(result.isValid).toEqual(true) + }) + }) + + describe('createPresentation', () => { + it('should create a presentation successfully', async () => { + const vc = JsonTransformer.fromJSON( + BbsBlsSignature2020Fixtures.TEST_VALID_DERIVED, + W3cJsonLdVerifiableCredential + ) + const result = await w3cCredentialService.createPresentation({ credentials: [vc] }) + + expect(result).toBeInstanceOf(W3cPresentation) + + expect(result.type).toEqual(expect.arrayContaining(['VerifiablePresentation'])) + + expect(result.verifiableCredential).toHaveLength(1) + expect(result.verifiableCredential).toEqual(expect.arrayContaining([vc])) + }) + }) + + describe('signPresentation', () => { + it('should sign the presentation successfully', async () => { + const signingKey = await wallet.createKey({ + privateKey, + keyType: KeyType.Ed25519, + }) + const signingDidKey = new DidKey(signingKey) + const verificationMethod = `${signingDidKey.did}#${signingDidKey.key.fingerprint}` + const presentation = JsonTransformer.fromJSON(BbsBlsSignature2020Fixtures.TEST_VP_DOCUMENT, W3cPresentation) + + const purpose = new CredentialIssuancePurpose({ + controller: { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + }, + date: new Date().toISOString(), + }) + + const verifiablePresentation = await w3cJsonLdCredentialService.signPresentation(agentContext, { + format: ClaimFormat.LdpVp, + presentation: presentation, + proofPurpose: purpose, + proofType: 'Ed25519Signature2018', + challenge: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + verificationMethod: verificationMethod, + }) + + expect(verifiablePresentation).toBeInstanceOf(W3cJsonLdVerifiablePresentation) + }) + }) + + describe('verifyPresentation', () => { + it('should successfully verify a presentation containing a single verifiable credential bbs', async () => { + const vp = JsonTransformer.fromJSON( + BbsBlsSignature2020Fixtures.TEST_VP_DOCUMENT_SIGNED, + W3cJsonLdVerifiablePresentation + ) + + const result = await w3cJsonLdCredentialService.verifyPresentation(agentContext, { + presentation: vp, + challenge: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + }) + + expect(result.isValid).toBe(true) + }) + }) + }) +}) diff --git a/packages/bbs-signatures/tests/bbs-signing-provider.test.ts b/packages/bbs-signatures/tests/bbs-signing-provider.test.ts new file mode 100644 index 0000000000..21acb9988f --- /dev/null +++ b/packages/bbs-signatures/tests/bbs-signing-provider.test.ts @@ -0,0 +1,76 @@ +import type { Wallet, WalletConfig } from '@credo-ts/core' + +import { KeyDerivationMethod, KeyType, TypedArrayEncoder, SigningProviderRegistry } from '@credo-ts/core' +import { BBS_SIGNATURE_LENGTH } from '@mattrglobal/bbs-signatures' + +import { RegisteredAskarTestWallet } from '../../askar/tests/helpers' +import { testLogger, agentDependencies } from '../../core/tests' +import { Bls12381g2SigningProvider } from '../src' + +import { describeSkipNode18 } from './util' + +// use raw key derivation method to speed up wallet creating / opening / closing between tests +const walletConfig: WalletConfig = { + id: 'Wallet: BBS Signing Provider', + // generated using indy.generateWalletKey + key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', + keyDerivationMethod: KeyDerivationMethod.Raw, +} + +describeSkipNode18('BBS Signing Provider', () => { + let wallet: Wallet + const seed = TypedArrayEncoder.fromString('sample-seed-min-of-32-bytes-long') + const message = TypedArrayEncoder.fromString('sample-message') + + beforeEach(async () => { + wallet = new RegisteredAskarTestWallet( + testLogger, + new agentDependencies.FileSystem(), + new SigningProviderRegistry([new Bls12381g2SigningProvider()]) + ) + await wallet.createAndOpen(walletConfig) + }) + + afterEach(async () => { + await wallet.delete() + }) + + test('Create bls12381g2 keypair', async () => { + const key = await wallet.createKey({ seed, keyType: KeyType.Bls12381g2 }) + expect(key.keyType).toStrictEqual(KeyType.Bls12381g2) + expect(key.publicKeyBase58).toStrictEqual( + 'yVLZ92FeZ3AYco43LXtJgtM8kUD1WPUyQPw4VwxZ1iYSak85GYGSJwURhVJM4R6ASRGuM9vjjSU91pKbaqTWQgLjPJjFuK8HdDmAHi3thYun9QUGjarrK7BzC11LurcpYqD' + ) + }) + + test('Fail to sign with bls12381g1g2 keypair', async () => { + const key = await wallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) + + await expect( + wallet.sign({ + data: message, + key, + }) + ).rejects.toThrow( + 'Error signing data with verkey AeAihfn5UFf7y9oesemKE1oLmTwKMRv7fafTepespr3qceF4RUMggAbogkoC8n6rXgtJytq4oGy59DsVHxmNj9WGWwkiRnP3Sz2r924RLVbc2NdP4T7yEPsSFZPsWmLjgnP1vXHpj4bVXNcTmkUmF6mSXinF3HehnQVip14vRFuMzYVxMUh28ofTJzbtUqxMWZQRu. Unsupported keyType: bls12381g1g2' + ) + }) + + test('Create a signature with a bls12381g2 keypair', async () => { + const bls12381g2Key = await wallet.createKey({ seed, keyType: KeyType.Bls12381g2 }) + const signature = await wallet.sign({ + data: message, + key: bls12381g2Key, + }) + expect(signature.length).toStrictEqual(BBS_SIGNATURE_LENGTH) + }) + + test('Verify a signed message with a bls12381g2 publicKey', async () => { + const bls12381g2Key = await wallet.createKey({ seed, keyType: KeyType.Bls12381g2 }) + const signature = await wallet.sign({ + data: message, + key: bls12381g2Key, + }) + await expect(wallet.verify({ key: bls12381g2Key, data: message, signature })).resolves.toStrictEqual(true) + }) +}) diff --git a/packages/bbs-signatures/tests/fixtures.ts b/packages/bbs-signatures/tests/fixtures.ts new file mode 100644 index 0000000000..18430eb592 --- /dev/null +++ b/packages/bbs-signatures/tests/fixtures.ts @@ -0,0 +1,210 @@ +import { CREDENTIALS_CONTEXT_V1_URL, SECURITY_CONTEXT_BBS_URL } from '@credo-ts/core' + +export const BbsBlsSignature2020Fixtures = { + TEST_LD_DOCUMENT: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: '', + identifier: '83627465', + name: 'Permanent Resident Card', + description: 'Government of Example Permanent Resident Card.', + issuanceDate: '2019-12-03T12:19:52Z', + expirationDate: '2029-12-03T12:19:52Z', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['PermanentResident', 'Person'], + givenName: 'JOHN', + familyName: 'SMITH', + gender: 'Male', + image: 'data:image/png;base64,iVBORw0KGgokJggg==', + residentSince: '2015-01-01', + lprCategory: 'C09', + lprNumber: '999-999-999', + commuterClassification: 'C1', + birthCountry: 'Bahamas', + birthDate: '1958-07-17', + }, + }, + + TEST_LD_DOCUMENT_SIGNED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + identifier: '83627465', + name: 'Permanent Resident Card', + description: 'Government of Example Permanent Resident Card.', + issuanceDate: '2019-12-03T12:19:52Z', + expirationDate: '2029-12-03T12:19:52Z', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['PermanentResident', 'Person'], + givenName: 'JOHN', + familyName: 'SMITH', + gender: 'Male', + image: 'data:image/png;base64,iVBORw0KGgokJggg==', + residentSince: '2015-01-01', + lprCategory: 'C09', + lprNumber: '999-999-999', + commuterClassification: 'C1', + birthCountry: 'Bahamas', + birthDate: '1958-07-17', + }, + proof: { + type: 'BbsBlsSignature2020', + created: '2022-04-13T13:47:47Z', + verificationMethod: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + proofPurpose: 'assertionMethod', + proofValue: + 'hoNNnnRIoEoaY9Fvg3pGVG2eWTAHnR1kIM01nObEL2FdI2IkkpM3246jn3VBD8KBYUHlKfzccE4m7waZyoLEkBLFiK2g54Q2i+CdtYBgDdkUDsoULSBMcH1MwGHwdjfXpldFNFrHFx/IAvLVniyeMQ==', + }, + }, + TEST_LD_DOCUMENT_BAD_SIGNED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + identifier: '83627465', + name: 'Permanent Resident Card', + description: 'Government of Example Permanent Resident Card.', + issuanceDate: '2019-12-03T12:19:52Z', + expirationDate: '2029-12-03T12:19:52Z', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['PermanentResident', 'Person'], + givenName: 'JOHN', + familyName: 'SMITH', + gender: 'Male', + image: 'data:image/png;base64,iVBORw0KGgokJggg==', + residentSince: '2015-01-01', + lprCategory: 'C09', + lprNumber: '999-999-999', + commuterClassification: 'C1', + birthCountry: 'Bahamas', + birthDate: '1958-07-17', + }, + proof: { + type: 'BbsBlsSignature2020', + created: '2022-04-13T13:47:47Z', + verificationMethod: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + proofPurpose: 'assertionMethod', + proofValue: + 'gU44r/fmvGpkOyMRZX4nwRB6IsbrL7zbVTs+yu6bZGeCNJuiJqS5U6fCPuvGQ+iNYUHlKfzccE4m7waZyoLEkBLFiK2g54Q2i+CdtYBgDdkUDsoULSBMcH1MwGHwdjfXpldFNFrHFx/IAvLVniyeMQ==', + }, + }, + + TEST_VALID_DERIVED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['PermanentResidentCard', 'VerifiableCredential'], + description: 'Government of Example Permanent Resident Card.', + identifier: '83627465', + name: 'Permanent Resident Card', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['Person', 'PermanentResident'], + familyName: 'SMITH', + gender: 'Male', + givenName: 'JOHN', + }, + expirationDate: '2029-12-03T12:19:52Z', + issuanceDate: '2019-12-03T12:19:52Z', + issuer: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + proof: { + type: 'BbsBlsSignatureProof2020', + created: '2022-04-13T13:47:47Z', + nonce: 'GfuRhH8hSAcWm5RWgUQYNQNWjQBsWuVgMCJrhTCD3kSpnHmQOkHcnNAoBsgyMAT4UUI=', + proofPurpose: 'assertionMethod', + proofValue: + 'ABkB/wbvkcCcbPRE5vrXc++orru4MsgrS4ESsZ30RNCs3noqLwm94/RZNp62I6Hyf0Kmht0Vog70HDtnNzbnMAj/zD9oT/N53pOADrtn5v+xZgP3cK4N2d6amg6h3LXem29gidW9hMrROPLit5cWEIL4/HOzxPxQQGYiwEXdW++Aja5ZuwJoMsIx7ysn4C4ekN7JXZtnAAAAdJR/oeDShxRdSBlnCSUHkE4Ol+Z3AhXBKkxb4AxiMKOiNmBreMTjJUGwNAPNU2aKnAAAAAIBUuKV0W0YBQZY/mwLmwCcyOWMiaEpjnVhYip4jhBBZw1aPBe8GzsG9zv3Sf9XAyGEAvVFe3OvwvMwYY5nZYdYoLSR4TLl1aLw0oChiPm2zb6ApXypCEEVd8KhJMATyssTlY48bEljDNixAD2rVDaoAAAACWjyrWp3b62M5Onuwo9EItCrBjPD68xC12q1agqgwFTnOI0+MfEwVGNZsA0IqkCGrZmo3AyRpcRm51IYDWYorM4hued5EcVHeCGd6NrnLSxTFPEu8lnmCoMXcxBWDCZFRGb//M5WlncbsYiz01itHbSs1nmpj3o+DYlF2ZyOYphvLo5A9T4rWVwHRK1+LeCDEawOnI03DWLyN8U4ZpbpcdZNK421IwNjseYY+ptvvL3juZ2uQR84maAZYy/OjMuHNyzqHPXNgsLLqtrvPo0kncefp+x1jgA0J/b5xfT72+vhKZAN1R48/uPf+DySC3avwD3T+YHjePn1bBOidhCWMjwzI9LYO8VvhcWXzH7nBWh5MeUch+Wkl777KrsLhrXnCg==', + verificationMethod: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + }, + }, + + TEST_VP_DOCUMENT: { + '@context': [CREDENTIALS_CONTEXT_V1_URL], + type: ['VerifiablePresentation'], + verifiableCredential: [ + { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['PermanentResidentCard', 'VerifiableCredential'], + description: 'Government of Example Permanent Resident Card.', + identifier: '83627465', + name: 'Permanent Resident Card', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['Person', 'PermanentResident'], + familyName: 'SMITH', + gender: 'Male', + givenName: 'JOHN', + }, + expirationDate: '2029-12-03T12:19:52Z', + issuanceDate: '2019-12-03T12:19:52Z', + issuer: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + proof: { + type: 'BbsBlsSignatureProof2020', + created: '2022-04-13T13:47:47Z', + nonce: 'GfuRhH8hSAcWm5RWgUQYNQNWjQBsWuVgMCJrhTCD3kSpnHmQOkHcnNAoBsgyMAT4UUI=', + proofPurpose: 'assertionMethod', + proofValue: + 'ABkB/wbvkcCcbPRE5vrXc++orru4MsgrS4ESsZ30RNCs3noqLwm94/RZNp62I6Hyf0Kmht0Vog70HDtnNzbnMAj/zD9oT/N53pOADrtn5v+xZgP3cK4N2d6amg6h3LXem29gidW9hMrROPLit5cWEIL4/HOzxPxQQGYiwEXdW++Aja5ZuwJoMsIx7ysn4C4ekN7JXZtnAAAAdJR/oeDShxRdSBlnCSUHkE4Ol+Z3AhXBKkxb4AxiMKOiNmBreMTjJUGwNAPNU2aKnAAAAAIBUuKV0W0YBQZY/mwLmwCcyOWMiaEpjnVhYip4jhBBZw1aPBe8GzsG9zv3Sf9XAyGEAvVFe3OvwvMwYY5nZYdYoLSR4TLl1aLw0oChiPm2zb6ApXypCEEVd8KhJMATyssTlY48bEljDNixAD2rVDaoAAAACWjyrWp3b62M5Onuwo9EItCrBjPD68xC12q1agqgwFTnOI0+MfEwVGNZsA0IqkCGrZmo3AyRpcRm51IYDWYorM4hued5EcVHeCGd6NrnLSxTFPEu8lnmCoMXcxBWDCZFRGb//M5WlncbsYiz01itHbSs1nmpj3o+DYlF2ZyOYphvLo5A9T4rWVwHRK1+LeCDEawOnI03DWLyN8U4ZpbpcdZNK421IwNjseYY+ptvvL3juZ2uQR84maAZYy/OjMuHNyzqHPXNgsLLqtrvPo0kncefp+x1jgA0J/b5xfT72+vhKZAN1R48/uPf+DySC3avwD3T+YHjePn1bBOidhCWMjwzI9LYO8VvhcWXzH7nBWh5MeUch+Wkl777KrsLhrXnCg==', + verificationMethod: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + }, + }, + ], + }, + TEST_VP_DOCUMENT_SIGNED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL], + type: ['VerifiablePresentation'], + verifiableCredential: [ + { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['PermanentResidentCard', 'VerifiableCredential'], + description: 'Government of Example Permanent Resident Card.', + identifier: '83627465', + name: 'Permanent Resident Card', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['Person', 'PermanentResident'], + familyName: 'SMITH', + gender: 'Male', + givenName: 'JOHN', + }, + expirationDate: '2029-12-03T12:19:52Z', + issuanceDate: '2019-12-03T12:19:52Z', + issuer: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + proof: { + type: 'BbsBlsSignatureProof2020', + created: '2022-04-13T13:47:47Z', + nonce: 'GfuRhH8hSAcWm5RWgUQYNQNWjQBsWuVgMCJrhTCD3kSpnHmQOkHcnNAoBsgyMAT4UUI=', + proofPurpose: 'assertionMethod', + proofValue: + 'ABkB/wbvkcCcbPRE5vrXc++orru4MsgrS4ESsZ30RNCs3noqLwm94/RZNp62I6Hyf0Kmht0Vog70HDtnNzbnMAj/zD9oT/N53pOADrtn5v+xZgP3cK4N2d6amg6h3LXem29gidW9hMrROPLit5cWEIL4/HOzxPxQQGYiwEXdW++Aja5ZuwJoMsIx7ysn4C4ekN7JXZtnAAAAdJR/oeDShxRdSBlnCSUHkE4Ol+Z3AhXBKkxb4AxiMKOiNmBreMTjJUGwNAPNU2aKnAAAAAIBUuKV0W0YBQZY/mwLmwCcyOWMiaEpjnVhYip4jhBBZw1aPBe8GzsG9zv3Sf9XAyGEAvVFe3OvwvMwYY5nZYdYoLSR4TLl1aLw0oChiPm2zb6ApXypCEEVd8KhJMATyssTlY48bEljDNixAD2rVDaoAAAACWjyrWp3b62M5Onuwo9EItCrBjPD68xC12q1agqgwFTnOI0+MfEwVGNZsA0IqkCGrZmo3AyRpcRm51IYDWYorM4hued5EcVHeCGd6NrnLSxTFPEu8lnmCoMXcxBWDCZFRGb//M5WlncbsYiz01itHbSs1nmpj3o+DYlF2ZyOYphvLo5A9T4rWVwHRK1+LeCDEawOnI03DWLyN8U4ZpbpcdZNK421IwNjseYY+ptvvL3juZ2uQR84maAZYy/OjMuHNyzqHPXNgsLLqtrvPo0kncefp+x1jgA0J/b5xfT72+vhKZAN1R48/uPf+DySC3avwD3T+YHjePn1bBOidhCWMjwzI9LYO8VvhcWXzH7nBWh5MeUch+Wkl777KrsLhrXnCg==', + verificationMethod: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + }, + }, + ], + proof: { + verificationMethod: + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519Signature2018', + created: '2022-04-21T10:15:38Z', + proofPurpose: 'authentication', + challenge: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..wGtR9yuTRfhrsvCthUOn-fg_lK0mZIe2IOO2Lv21aOXo5YUAbk50qMBLk4C1iqoOx-Jz6R0g4aa4cuqpdXzkBw', + }, + }, +} diff --git a/packages/bbs-signatures/tests/setup.ts b/packages/bbs-signatures/tests/setup.ts new file mode 100644 index 0000000000..78143033f2 --- /dev/null +++ b/packages/bbs-signatures/tests/setup.ts @@ -0,0 +1,3 @@ +import 'reflect-metadata' + +jest.setTimeout(120000) diff --git a/packages/bbs-signatures/tests/util.ts b/packages/bbs-signatures/tests/util.ts new file mode 100644 index 0000000000..efe9f799bd --- /dev/null +++ b/packages/bbs-signatures/tests/util.ts @@ -0,0 +1,9 @@ +export function describeSkipNode18(...parameters: Parameters) { + const version = process.version + + if (version.startsWith('v18.')) { + describe.skip(...parameters) + } else { + describe(...parameters) + } +} diff --git a/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts b/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts new file mode 100644 index 0000000000..f6b1983c15 --- /dev/null +++ b/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts @@ -0,0 +1,282 @@ +import type { V2IssueCredentialMessage } from '../../core/src/modules/credentials/protocol/v2/messages/V2IssueCredentialMessage' +import type { EventReplaySubject, JsonLdTestsAgent } from '../../core/tests' + +import { TypedArrayEncoder } from '../../core/src' +import { KeyType } from '../../core/src/crypto' +import { CredentialState } from '../../core/src/modules/credentials/models' +import { CredentialExchangeRecord } from '../../core/src/modules/credentials/repository/CredentialExchangeRecord' +import { CREDENTIALS_CONTEXT_V1_URL, SECURITY_CONTEXT_BBS_URL } from '../../core/src/modules/vc' +import { JsonTransformer } from '../../core/src/utils/JsonTransformer' +import { waitForCredentialRecordSubject, setupJsonLdTests, testLogger } from '../../core/tests' + +import { describeSkipNode18 } from './util' + +let faberAgent: JsonLdTestsAgent +let faberReplay: EventReplaySubject +let aliceAgent: JsonLdTestsAgent +let aliceReplay: EventReplaySubject +let aliceConnectionId: string +let aliceCredentialRecord: CredentialExchangeRecord +let faberCredentialRecord: CredentialExchangeRecord + +const signCredentialOptions = { + credential: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + issuanceDate: '2019-12-03T12:19:52Z', + expirationDate: '2029-12-03T12:19:52Z', + identifier: '83627465', + name: 'Permanent Resident Card', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['PermanentResident', 'Person'], + givenName: 'JOHN', + familyName: 'SMITH', + gender: 'Male', + image: 'data:image/png;base64,iVBORw0KGgokJggg==', + residentSince: '2015-01-01', + description: 'Government of Example Permanent Resident Card.', + lprCategory: 'C09', + lprNumber: '999-999-999', + commuterClassification: 'C1', + birthCountry: 'Bahamas', + birthDate: '1958-07-17', + }, + }, + options: { + proofType: 'BbsBlsSignature2020', + proofPurpose: 'assertionMethod', + }, +} + +describeSkipNode18('credentials, BBS+ signature', () => { + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + holderIssuerConnectionId: aliceConnectionId, + } = await setupJsonLdTests({ + issuerName: 'Faber Agent Credentials LD BBS+', + holderName: 'Alice Agent Credentials LD BBS+', + useBbs: true, + })) + + await faberAgent.context.wallet.createKey({ + keyType: KeyType.Ed25519, + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + }) + // FIXME: askar doesn't create the same privateKey based on the same seed as when generated used askar BBS library... + // See https://github.com/hyperledger/aries-askar/issues/219 + await faberAgent.context.wallet.createKey({ + keyType: KeyType.Bls12381g2, + privateKey: TypedArrayEncoder.fromBase58('2szQ7zB4tKLJPsGK3YTp9SNQ6hoWYFG5rGhmgfQM4nb7'), + }) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with V2 (ld format, BbsBlsSignature2020 signature) credential proposal to Faber', async () => { + testLogger.test('Alice sends (v2 jsonld) credential proposal to Faber') + const credentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + jsonld: signCredentialOptions, + }, + comment: 'v2 propose credential test for W3C Credentials', + }) + + expect(credentialExchangeRecord.connectionId).toEqual(aliceConnectionId) + expect(credentialExchangeRecord.protocolVersion).toEqual('v2') + expect(credentialExchangeRecord.state).toEqual(CredentialState.ProposalSent) + expect(credentialExchangeRecord.threadId).not.toBeNull() + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 W3C Offer', + }) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const offerMessage = await faberAgent.credentials.findOfferMessage(faberCredentialRecord.id) + expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({ + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + '@id': expect.any(String), + comment: 'V2 W3C Offer', + formats: [ + { + attach_id: expect.any(String), + format: 'aries/ld-proof-vc-detail@v1.0', + }, + ], + 'offers~attach': [ + { + '@id': expect.any(String), + 'mime-type': 'application/json', + data: expect.any(Object), + lastmod_time: undefined, + byte_count: undefined, + }, + ], + '~thread': { + thid: expect.any(String), + pthid: undefined, + sender_order: undefined, + received_orders: undefined, + }, + '~service': undefined, + '~attach': undefined, + '~please_ack': undefined, + '~timing': undefined, + '~transport': undefined, + '~l10n': undefined, + credential_preview: expect.any(Object), + replacement_id: undefined, + }) + expect(aliceCredentialRecord.id).not.toBeNull() + expect(aliceCredentialRecord.type).toBe(CredentialExchangeRecord.type) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + jsonld: undefined, + }, + }) + + expect(offerCredentialExchangeRecord.connectionId).toEqual(aliceConnectionId) + expect(offerCredentialExchangeRecord.protocolVersion).toEqual('v2') + expect(offerCredentialExchangeRecord.state).toEqual(CredentialState.RequestSent) + expect(offerCredentialExchangeRecord.threadId).not.toBeNull() + + testLogger.test('Faber waits for credential request from Alice') + await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 W3C Offer', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Alice sends credential ack to Faber') + await aliceAgent.credentials.acceptCredential({ credentialRecordId: aliceCredentialRecord.id }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: expect.any(String), + connectionId: expect.any(String), + state: CredentialState.CredentialReceived, + }) + + const credentialMessage = await faberAgent.credentials.findCredentialMessage(faberCredentialRecord.id) + const w3cCredential = (credentialMessage as V2IssueCredentialMessage).credentialAttachments[0].getDataAsJson() + + expect(w3cCredential).toMatchObject({ + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/citizenship/v1', + 'https://w3id.org/security/bbs/v1', + ], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + issuanceDate: '2019-12-03T12:19:52Z', + expirationDate: '2029-12-03T12:19:52Z', + identifier: '83627465', + name: 'Permanent Resident Card', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['PermanentResident', 'Person'], + givenName: 'JOHN', + familyName: 'SMITH', + gender: 'Male', + image: 'data:image/png;base64,iVBORw0KGgokJggg==', + residentSince: '2015-01-01', + description: 'Government of Example Permanent Resident Card.', + lprCategory: 'C09', + lprNumber: '999-999-999', + commuterClassification: 'C1', + birthCountry: 'Bahamas', + birthDate: '1958-07-17', + }, + proof: { + type: 'BbsBlsSignature2020', + created: expect.any(String), + verificationMethod: + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + proofPurpose: 'assertionMethod', + proofValue: expect.any(String), + }, + }) + + expect(JsonTransformer.toJSON(credentialMessage)).toMatchObject({ + '@type': 'https://didcomm.org/issue-credential/2.0/issue-credential', + '@id': expect.any(String), + comment: 'V2 W3C Offer', + formats: [ + { + attach_id: expect.any(String), + format: 'aries/ld-proof-vc@v1.0', + }, + ], + 'credentials~attach': [ + { + '@id': expect.any(String), + 'mime-type': 'application/json', + data: expect.any(Object), + lastmod_time: undefined, + byte_count: undefined, + }, + ], + '~thread': { + thid: expect.any(String), + pthid: undefined, + sender_order: undefined, + received_orders: undefined, + }, + '~please_ack': { on: ['RECEIPT'] }, + '~service': undefined, + '~attach': undefined, + '~timing': undefined, + '~transport': undefined, + '~l10n': undefined, + }) + }) +}) diff --git a/packages/bbs-signatures/tsconfig.build.json b/packages/bbs-signatures/tsconfig.build.json new file mode 100644 index 0000000000..9c30e30bd2 --- /dev/null +++ b/packages/bbs-signatures/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + + "compilerOptions": { + "outDir": "./build" + }, + + "include": ["src/**/*"] +} diff --git a/packages/bbs-signatures/tsconfig.json b/packages/bbs-signatures/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/bbs-signatures/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/packages/cheqd/CHANGELOG.md b/packages/cheqd/CHANGELOG.md new file mode 100644 index 0000000000..9f7f4123e0 --- /dev/null +++ b/packages/cheqd/CHANGELOG.md @@ -0,0 +1,92 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release +- Updated dependencies [66e696d] + - @credo-ts/anoncreds@0.5.6 + - @credo-ts/core@0.5.6 + +## 0.5.5 + +### Patch Changes + +- d548fa4: feat: support new 'DIDCommMessaging' didcomm v2 service type (in addition to older 'DIDComm' service type) +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) +- Updated dependencies [3239ef3] +- Updated dependencies [d548fa4] +- Updated dependencies [482a630] + - @credo-ts/core@0.5.5 + - @credo-ts/anoncreds@0.5.5 + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +### Bug Fixes + +- cheqd create from did document ([#1850](https://github.com/openwallet-foundation/credo-ts/issues/1850)) ([dcd028e](https://github.com/openwallet-foundation/credo-ts/commit/dcd028ea04863bf9bc93e6bd2f73c6d2a70f274b)) + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +### Bug Fixes + +- udpate cheqd deps ([#1830](https://github.com/openwallet-foundation/credo-ts/issues/1830)) ([6b4b71b](https://github.com/openwallet-foundation/credo-ts/commit/6b4b71bf365262e8c2c9718547b60c44f2afc920)) +- update cheqd to 2.4.2 ([#1817](https://github.com/openwallet-foundation/credo-ts/issues/1817)) ([8154df4](https://github.com/openwallet-foundation/credo-ts/commit/8154df45f45bd9da0c60abe3792ff0f081e81818)) + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +### Bug Fixes + +- **cheqd:** do not crash agent if cheqd down ([#1808](https://github.com/openwallet-foundation/credo-ts/issues/1808)) ([842efd4](https://github.com/openwallet-foundation/credo-ts/commit/842efd4512748a0787ce331add394426b3b07943)) + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +### Features + +- anoncreds w3c migration ([#1744](https://github.com/openwallet-foundation/credo-ts/issues/1744)) ([d7c2bbb](https://github.com/openwallet-foundation/credo-ts/commit/d7c2bbb4fde57cdacbbf1ed40c6bd1423f7ab015)) +- **anoncreds:** issue revocable credentials ([#1427](https://github.com/openwallet-foundation/credo-ts/issues/1427)) ([c59ad59](https://github.com/openwallet-foundation/credo-ts/commit/c59ad59fbe63b6d3760d19030e0f95fb2ea8488a)) +- **openid4vc:** persistance and events ([#1793](https://github.com/openwallet-foundation/credo-ts/issues/1793)) ([f4c386a](https://github.com/openwallet-foundation/credo-ts/commit/f4c386a6ccf8adb829cad30b81d524e6ffddb029)) + +## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) + +### Bug Fixes + +- **cheqd:** changed the name formatting to a encoded hex value ([#1574](https://github.com/hyperledger/aries-framework-javascript/issues/1574)) ([d299f55](https://github.com/hyperledger/aries-framework-javascript/commit/d299f55113cb4c59273ae9fbbb8773b6f0009192)) +- update tsyringe for ts 5 support ([#1588](https://github.com/hyperledger/aries-framework-javascript/issues/1588)) ([296955b](https://github.com/hyperledger/aries-framework-javascript/commit/296955b3a648416ac6b502da05a10001920af222)) + +## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) + +### Bug Fixes + +- **cheqd:** make cosmos payer seed optional ([#1547](https://github.com/hyperledger/aries-framework-javascript/issues/1547)) ([9377378](https://github.com/hyperledger/aries-framework-javascript/commit/9377378b0124bf2f593342dba95a13ea5d8944c8)) +- force did:key resolver/registrar presence ([#1535](https://github.com/hyperledger/aries-framework-javascript/issues/1535)) ([aaa13dc](https://github.com/hyperledger/aries-framework-javascript/commit/aaa13dc77d6d5133cd02e768e4173462fa65064a)) + +# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) + +### Bug Fixes + +- small updates to cheqd module and demo ([#1439](https://github.com/hyperledger/aries-framework-javascript/issues/1439)) ([61daf0c](https://github.com/hyperledger/aries-framework-javascript/commit/61daf0cb27de80a5e728e2e9dad13d729baf476c)) + +### Features + +- Add cheqd demo and localnet for tests ([#1435](https://github.com/hyperledger/aries-framework-javascript/issues/1435)) ([1ffb011](https://github.com/hyperledger/aries-framework-javascript/commit/1ffb0111fc3db170e5623d350cb912b22027387a)) +- Add cheqd-sdk module ([#1334](https://github.com/hyperledger/aries-framework-javascript/issues/1334)) ([b38525f](https://github.com/hyperledger/aries-framework-javascript/commit/b38525f3433e50418ea149949108b4218ac9ba2a)) +- support for did:jwk and p-256, p-384, p-512 ([#1446](https://github.com/hyperledger/aries-framework-javascript/issues/1446)) ([700d3f8](https://github.com/hyperledger/aries-framework-javascript/commit/700d3f89728ce9d35e22519e505d8203a4c9031e)) diff --git a/packages/cheqd/README.md b/packages/cheqd/README.md new file mode 100644 index 0000000000..7ee2c81875 --- /dev/null +++ b/packages/cheqd/README.md @@ -0,0 +1,31 @@ +

+
+ Credo Logo +

+

Credo Cheqd Module

+

+ License + typescript + @credo-ts/cheqd version + +

+
+ +Credo cheqd provides integration of the cheqd network into Credo. See the [Cheqd Setup](https://credo.js.org/guides/getting-started/set-up/cheqd) for installation instructions. diff --git a/packages/cheqd/jest.config.ts b/packages/cheqd/jest.config.ts new file mode 100644 index 0000000000..93c0197296 --- /dev/null +++ b/packages/cheqd/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/cheqd/package.json b/packages/cheqd/package.json new file mode 100644 index 0000000000..91293174c0 --- /dev/null +++ b/packages/cheqd/package.json @@ -0,0 +1,46 @@ +{ + "name": "@credo-ts/cheqd", + "main": "src/index", + "types": "src/index", + "version": "0.5.6", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/cheqd", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/cheqd" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build", + "test": "jest" + }, + "dependencies": { + "@cheqd/sdk": "^2.4.4", + "@cheqd/ts-proto": "~2.2.0", + "@cosmjs/crypto": "~0.30.0", + "@cosmjs/proto-signing": "~0.30.0", + "@cosmjs/stargate": "~0.30.0", + "@credo-ts/anoncreds": "workspace:*", + "@credo-ts/core": "workspace:*", + "@stablelib/ed25519": "^1.0.3", + "class-transformer": "^0.5.1", + "class-validator": "0.14.1", + "rxjs": "^7.8.0", + "tsyringe": "^4.8.0" + }, + "devDependencies": { + "rimraf": "^4.0.7", + "typescript": "~5.5.2" + } +} diff --git a/packages/cheqd/src/CheqdModule.ts b/packages/cheqd/src/CheqdModule.ts new file mode 100644 index 0000000000..af739a194e --- /dev/null +++ b/packages/cheqd/src/CheqdModule.ts @@ -0,0 +1,40 @@ +import type { CheqdModuleConfigOptions } from './CheqdModuleConfig' +import type { AgentContext, DependencyManager, Module } from '@credo-ts/core' + +import { AgentConfig, Buffer } from '@credo-ts/core' + +import { CheqdModuleConfig } from './CheqdModuleConfig' +import { CheqdLedgerService } from './ledger' + +export class CheqdModule implements Module { + public readonly config: CheqdModuleConfig + + public constructor(config: CheqdModuleConfigOptions) { + this.config = new CheqdModuleConfig(config) + } + + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The '@credo-ts/cheqd' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." + ) + + // Register config + dependencyManager.registerInstance(CheqdModuleConfig, this.config) + + dependencyManager.registerSingleton(CheqdLedgerService) + + // Cheqd module needs Buffer to be available globally + // If it is not available yet, we overwrite it with the + // Buffer implementation from Credo + global.Buffer = global.Buffer || Buffer + } + + public async initialize(agentContext: AgentContext): Promise { + // not required + const cheqdLedgerService = agentContext.dependencyManager.resolve(CheqdLedgerService) + await cheqdLedgerService.connect() + } +} diff --git a/packages/cheqd/src/CheqdModuleConfig.ts b/packages/cheqd/src/CheqdModuleConfig.ts new file mode 100644 index 0000000000..0dcfd31f20 --- /dev/null +++ b/packages/cheqd/src/CheqdModuleConfig.ts @@ -0,0 +1,25 @@ +/** + * CheqdModuleConfigOptions defines the interface for the options of the CheqdModuleConfig class. + */ +export interface CheqdModuleConfigOptions { + networks: NetworkConfig[] +} + +export interface NetworkConfig { + rpcUrl?: string + cosmosPayerSeed?: string + network: string +} + +export class CheqdModuleConfig { + private options: CheqdModuleConfigOptions + + public constructor(options: CheqdModuleConfigOptions) { + this.options = options + } + + /** See {@link CheqdModuleConfigOptions.networks} */ + public get networks() { + return this.options.networks + } +} diff --git a/packages/cheqd/src/anoncreds/index.ts b/packages/cheqd/src/anoncreds/index.ts new file mode 100644 index 0000000000..6a8bd47548 --- /dev/null +++ b/packages/cheqd/src/anoncreds/index.ts @@ -0,0 +1 @@ +export { CheqdAnonCredsRegistry } from './services/CheqdAnonCredsRegistry' diff --git a/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts b/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts new file mode 100644 index 0000000000..0d2c41bb73 --- /dev/null +++ b/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts @@ -0,0 +1,355 @@ +import type { CheqdCreateResourceOptions } from '../../dids' +import type { + AnonCredsRegistry, + GetCredentialDefinitionReturn, + GetRevocationStatusListReturn, + GetRevocationRegistryDefinitionReturn, + GetSchemaReturn, + RegisterCredentialDefinitionOptions, + RegisterCredentialDefinitionReturn, + RegisterSchemaReturn, + RegisterSchemaOptions, + RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationStatusListReturn, +} from '@credo-ts/anoncreds' +import type { AgentContext } from '@credo-ts/core' + +import { CredoError, Hasher, JsonTransformer, TypedArrayEncoder, utils } from '@credo-ts/core' + +import { CheqdDidResolver, CheqdDidRegistrar } from '../../dids' +import { cheqdSdkAnonCredsRegistryIdentifierRegex, parseCheqdDid } from '../utils/identifiers' +import { + CheqdCredentialDefinition, + CheqdRevocationRegistryDefinition, + CheqdRevocationStatusList, + CheqdSchema, +} from '../utils/transform' + +export class CheqdAnonCredsRegistry implements AnonCredsRegistry { + public methodName = 'cheqd' + + /** + * This class supports resolving and registering objects with cheqd identifiers. + * It needs to include support for the schema, credential definition, revocation registry as well + * as the issuer id (which is needed when registering objects). + */ + public readonly supportedIdentifier = cheqdSdkAnonCredsRegistryIdentifierRegex + + public async getSchema(agentContext: AgentContext, schemaId: string): Promise { + try { + const cheqdDidResolver = agentContext.dependencyManager.resolve(CheqdDidResolver) + const parsedDid = parseCheqdDid(schemaId) + if (!parsedDid) { + throw new Error(`Invalid schemaId: ${schemaId}`) + } + + agentContext.config.logger.trace(`Submitting get schema request for schema '${schemaId}' to ledger`) + + const response = await cheqdDidResolver.resolveResource(agentContext, schemaId) + const schema = JsonTransformer.fromJSON(response.resource, CheqdSchema) + + return { + schema: { + attrNames: schema.attrNames, + name: schema.name, + version: schema.version, + issuerId: parsedDid.did, + }, + schemaId, + resolutionMetadata: {}, + schemaMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error(`Error retrieving schema '${schemaId}'`, { + error, + schemaId, + }) + + return { + schemaId, + resolutionMetadata: { + error: 'notFound', + message: `unable to resolve schema: ${error.message}`, + }, + schemaMetadata: {}, + } + } + } + + public async registerSchema( + agentContext: AgentContext, + options: RegisterSchemaOptions + ): Promise { + try { + const cheqdDidRegistrar = agentContext.dependencyManager.resolve(CheqdDidRegistrar) + + const schema = options.schema + const schemaResource = { + id: utils.uuid(), + name: `${schema.name}-Schema`, + resourceType: 'anonCredsSchema', + data: { + name: schema.name, + version: schema.version, + attrNames: schema.attrNames, + }, + version: schema.version, + } satisfies CheqdCreateResourceOptions + + const response = await cheqdDidRegistrar.createResource(agentContext, schema.issuerId, schemaResource) + if (response.resourceState.state !== 'finished') { + throw new Error(response.resourceState.reason) + } + + return { + schemaState: { + state: 'finished', + schema, + schemaId: `${schema.issuerId}/resources/${schemaResource.id}`, + }, + registrationMetadata: {}, + schemaMetadata: {}, + } + } catch (error) { + agentContext.config.logger.debug(`Error registering schema for did '${options.schema.issuerId}'`, { + error, + did: options.schema.issuerId, + schema: options, + }) + + return { + schemaMetadata: {}, + registrationMetadata: {}, + schemaState: { + state: 'failed', + schema: options.schema, + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async registerCredentialDefinition( + agentContext: AgentContext, + options: RegisterCredentialDefinitionOptions + ): Promise { + try { + const cheqdDidRegistrar = agentContext.dependencyManager.resolve(CheqdDidRegistrar) + const { credentialDefinition } = options + const schema = await this.getSchema(agentContext, credentialDefinition.schemaId) + if (!schema.schema) { + throw new Error(`Schema not found for schemaId: ${credentialDefinition.schemaId}`) + } + + const credDefName = `${schema.schema.name}-${credentialDefinition.tag}` + const credDefNameHashBuffer = Hasher.hash(credDefName, 'sha-256') + + const credDefResource = { + id: utils.uuid(), + name: TypedArrayEncoder.toHex(credDefNameHashBuffer), + resourceType: 'anonCredsCredDef', + data: { + type: credentialDefinition.type, + tag: credentialDefinition.tag, + value: credentialDefinition.value, + schemaId: credentialDefinition.schemaId, + }, + version: utils.uuid(), + } satisfies CheqdCreateResourceOptions + + const response = await cheqdDidRegistrar.createResource( + agentContext, + credentialDefinition.issuerId, + credDefResource + ) + if (response.resourceState.state !== 'finished') { + throw new Error(response.resourceState.reason) + } + + return { + credentialDefinitionState: { + state: 'finished', + credentialDefinition, + credentialDefinitionId: `${credentialDefinition.issuerId}/resources/${credDefResource.id}`, + }, + registrationMetadata: {}, + credentialDefinitionMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error( + `Error registering credential definition for did '${options.credentialDefinition.issuerId}'`, + { + error, + did: options.credentialDefinition.issuerId, + schema: options, + } + ) + + return { + credentialDefinitionMetadata: {}, + registrationMetadata: {}, + credentialDefinitionState: { + state: 'failed', + credentialDefinition: options.credentialDefinition, + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async getCredentialDefinition( + agentContext: AgentContext, + credentialDefinitionId: string + ): Promise { + try { + const cheqdDidResolver = agentContext.dependencyManager.resolve(CheqdDidResolver) + const parsedDid = parseCheqdDid(credentialDefinitionId) + if (!parsedDid) { + throw new Error(`Invalid credentialDefinitionId: ${credentialDefinitionId}`) + } + + agentContext.config.logger.trace( + `Submitting get credential definition request for '${credentialDefinitionId}' to ledger` + ) + + const response = await cheqdDidResolver.resolveResource(agentContext, credentialDefinitionId) + const credentialDefinition = JsonTransformer.fromJSON(response.resource, CheqdCredentialDefinition) + return { + credentialDefinition: { + ...credentialDefinition, + issuerId: parsedDid.did, + }, + credentialDefinitionId, + resolutionMetadata: {}, + credentialDefinitionMetadata: (response.resourceMetadata ?? {}) as Record, + } + } catch (error) { + agentContext.config.logger.error(`Error retrieving credential definition '${credentialDefinitionId}'`, { + error, + credentialDefinitionId, + }) + + return { + credentialDefinitionId, + resolutionMetadata: { + error: 'notFound', + message: `unable to resolve credential definition: ${error.message}`, + }, + credentialDefinitionMetadata: {}, + } + } + } + + public async getRevocationRegistryDefinition( + agentContext: AgentContext, + revocationRegistryDefinitionId: string + ): Promise { + try { + const cheqdDidResolver = agentContext.dependencyManager.resolve(CheqdDidResolver) + const parsedDid = parseCheqdDid(revocationRegistryDefinitionId) + if (!parsedDid) { + throw new Error(`Invalid revocationRegistryDefinitionId: ${revocationRegistryDefinitionId}`) + } + + agentContext.config.logger.trace( + `Submitting get revocation registry definition request for '${revocationRegistryDefinitionId}' to ledger` + ) + + const response = await cheqdDidResolver.resolveResource( + agentContext, + `${revocationRegistryDefinitionId}&resourceType=anonCredsRevocRegDef` + ) + const revocationRegistryDefinition = JsonTransformer.fromJSON( + response.resource, + CheqdRevocationRegistryDefinition + ) + return { + revocationRegistryDefinition: { + ...revocationRegistryDefinition, + issuerId: parsedDid.did, + }, + revocationRegistryDefinitionId, + resolutionMetadata: {}, + revocationRegistryDefinitionMetadata: (response.resourceMetadata ?? {}) as Record, + } + } catch (error) { + agentContext.config.logger.error( + `Error retrieving revocation registry definition '${revocationRegistryDefinitionId}'`, + { + error, + revocationRegistryDefinitionId, + } + ) + + return { + revocationRegistryDefinitionId, + resolutionMetadata: { + error: 'notFound', + message: `unable to resolve revocation registry definition: ${error.message}`, + }, + revocationRegistryDefinitionMetadata: {}, + } + } + } + + public async registerRevocationRegistryDefinition(): Promise { + throw new Error('Not implemented!') + } + + // FIXME: this method doesn't retrieve the revocation status list at a specified time, it just resolves the revocation registry definition + public async getRevocationStatusList( + agentContext: AgentContext, + revocationRegistryId: string, + timestamp: number + ): Promise { + try { + const cheqdDidResolver = agentContext.dependencyManager.resolve(CheqdDidResolver) + const parsedDid = parseCheqdDid(revocationRegistryId) + if (!parsedDid) { + throw new Error(`Invalid revocationRegistryId: ${revocationRegistryId}`) + } + + agentContext.config.logger.trace( + `Submitting get revocation status request for '${revocationRegistryId}' to ledger` + ) + + const response = await cheqdDidResolver.resolveResource( + agentContext, + `${revocationRegistryId}&resourceType=anonCredsStatusList&resourceVersionTime=${timestamp}` + ) + const revocationStatusList = JsonTransformer.fromJSON(response.resource, CheqdRevocationStatusList) + + const statusListTimestamp = response.resourceMetadata?.created?.getUTCSeconds() + if (!statusListTimestamp) { + throw new CredoError(`Unable to extract revocation status list timestamp from resource ${revocationRegistryId}`) + } + + return { + revocationStatusList: { + ...revocationStatusList, + issuerId: parsedDid.did, + timestamp: statusListTimestamp, + }, + resolutionMetadata: {}, + revocationStatusListMetadata: (response.resourceMetadata ?? {}) as Record, + } + } catch (error) { + agentContext.config.logger.error(`Error retrieving revocation registry status list '${revocationRegistryId}'`, { + error, + revocationRegistryId, + }) + + return { + resolutionMetadata: { + error: 'notFound', + message: `unable to resolve revocation registry status list: ${error.message}`, + }, + revocationStatusListMetadata: {}, + } + } + } + + public async registerRevocationStatusList(): Promise { + throw new Error('Not implemented!') + } +} diff --git a/packages/cheqd/src/anoncreds/utils/identifiers.ts b/packages/cheqd/src/anoncreds/utils/identifiers.ts new file mode 100644 index 0000000000..ac4b58170c --- /dev/null +++ b/packages/cheqd/src/anoncreds/utils/identifiers.ts @@ -0,0 +1,65 @@ +import type { CheqdNetwork } from '@cheqd/sdk' +import type { ParsedDid } from '@credo-ts/core' + +import { TypedArrayEncoder, utils } from '@credo-ts/core' +import { isBase58 } from 'class-validator' + +const ID_CHAR = '([a-z,A-Z,0-9,-])' +const NETWORK = '(testnet|mainnet)' +const IDENTIFIER = `((?:${ID_CHAR}*:)*(${ID_CHAR}+))` +const PATH = `(/[^#?]*)?` +const QUERY = `([?][^#]*)?` +const VERSION_ID = `(.*?)` +const FRAGMENT = `([#].*)?` + +export const cheqdSdkAnonCredsRegistryIdentifierRegex = new RegExp( + `^did:cheqd:${NETWORK}:${IDENTIFIER}${PATH}${QUERY}${FRAGMENT}$` +) + +export const cheqdDidRegex = new RegExp(`^did:cheqd:${NETWORK}:${IDENTIFIER}${QUERY}${FRAGMENT}$`) +export const cheqdDidVersionRegex = new RegExp( + `^did:cheqd:${NETWORK}:${IDENTIFIER}/version/${VERSION_ID}${QUERY}${FRAGMENT}$` +) +export const cheqdDidVersionsRegex = new RegExp(`^did:cheqd:${NETWORK}:${IDENTIFIER}/versions${QUERY}${FRAGMENT}$`) +export const cheqdDidMetadataRegex = new RegExp(`^did:cheqd:${NETWORK}:${IDENTIFIER}/metadata${QUERY}${FRAGMENT}$`) +export const cheqdResourceRegex = new RegExp( + `^did:cheqd:${NETWORK}:${IDENTIFIER}/resources/${IDENTIFIER}${QUERY}${FRAGMENT}$` +) +export const cheqdResourceMetadataRegex = new RegExp( + `^did:cheqd:${NETWORK}:${IDENTIFIER}/resources/${IDENTIFIER}/metadata${QUERY}${FRAGMENT}` +) + +export type ParsedCheqdDid = ParsedDid & { network: `${CheqdNetwork}` } +export function parseCheqdDid(didUrl: string): ParsedCheqdDid | null { + if (didUrl === '' || !didUrl) return null + const sections = didUrl.match(cheqdSdkAnonCredsRegistryIdentifierRegex) + if (sections) { + if ( + !( + utils.isValidUuid(sections[2]) || + (isBase58(sections[2]) && TypedArrayEncoder.fromBase58(sections[2]).length == 16) + ) + ) { + return null + } + const parts: ParsedCheqdDid = { + did: `did:cheqd:${sections[1]}:${sections[2]}`, + method: 'cheqd', + network: sections[1] as `${CheqdNetwork}`, + id: sections[2], + didUrl, + } + if (sections[7]) { + const params = sections[7].slice(1).split('&') + parts.params = {} + for (const p of params) { + const kv = p.split('=') + parts.params[kv[0]] = kv[1] + } + } + if (sections[6]) parts.path = sections[6] + if (sections[8]) parts.fragment = sections[8].slice(1) + return parts + } + return null +} diff --git a/packages/cheqd/src/anoncreds/utils/transform.ts b/packages/cheqd/src/anoncreds/utils/transform.ts new file mode 100644 index 0000000000..5309223b9c --- /dev/null +++ b/packages/cheqd/src/anoncreds/utils/transform.ts @@ -0,0 +1,149 @@ +// These functions will throw an error if the JSON doesn't +// match the expected interface, even if the JSON is valid. + +import type { + AnonCredsCredentialDefinition, + AnonCredsRevocationRegistryDefinition, + AnonCredsRevocationStatusList, + AnonCredsSchema, +} from '@credo-ts/anoncreds' + +import { Type } from 'class-transformer' +import { + ArrayMinSize, + Contains, + IsArray, + IsInstance, + IsNumber, + IsObject, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator' + +export class CheqdSchema { + public constructor(options: Omit) { + if (options) { + this.name = options.name + this.attrNames = options.attrNames + this.version = options.version + } + } + + @IsString() + public name!: string + + @IsArray() + @IsString({ each: true }) + @ArrayMinSize(1) + public attrNames!: string[] + + @IsString() + public version!: string +} + +export class CheqdCredentialDefinitionValue { + @IsObject() + public primary!: Record + + @IsObject() + @IsOptional() + public revocation?: unknown +} + +export class CheqdCredentialDefinition { + public constructor(options: Omit) { + if (options) { + this.schemaId = options.schemaId + this.type = options.type + this.tag = options.tag + this.value = options.value + } + } + + @IsString() + public schemaId!: string + + @Contains('CL') + public type!: 'CL' + + @IsString() + public tag!: string + + @ValidateNested() + @IsInstance(CheqdCredentialDefinitionValue) + @Type(() => CheqdCredentialDefinitionValue) + public value!: CheqdCredentialDefinitionValue +} + +export class AccumKey { + @IsString() + public z!: string +} + +export class PublicKeys { + @ValidateNested() + @IsInstance(AccumKey) + @Type(() => AccumKey) + public accumKey!: AccumKey +} + +export class CheqdRevocationRegistryDefinitionValue { + @ValidateNested() + @IsInstance(PublicKeys) + @Type(() => PublicKeys) + public publicKeys!: PublicKeys + + @IsNumber() + public maxCredNum!: number + + @IsString() + public tailsLocation!: string + + @IsString() + public tailsHash!: string +} + +export class CheqdRevocationRegistryDefinition { + public constructor(options: Omit) { + if (options) { + this.revocDefType = options.revocDefType + this.credDefId = options.credDefId + this.tag = options.tag + this.value = options.value + } + } + + @Contains('CL_ACCUM') + public revocDefType!: 'CL_ACCUM' + + @IsString() + public credDefId!: string + + @IsString() + public tag!: string + + @ValidateNested() + @IsInstance(CheqdRevocationRegistryDefinitionValue) + @Type(() => CheqdRevocationRegistryDefinitionValue) + public value!: CheqdRevocationRegistryDefinitionValue +} + +export class CheqdRevocationStatusList { + public constructor(options: Omit) { + if (options) { + this.revRegDefId = options.revRegDefId + this.revocationList = options.revocationList + this.currentAccumulator = options.currentAccumulator + } + } + + @IsString() + public revRegDefId!: string + + @IsNumber({}, { each: true }) + public revocationList!: number[] + + @IsString() + public currentAccumulator!: string +} diff --git a/packages/cheqd/src/dids/CheqdDidRegistrar.ts b/packages/cheqd/src/dids/CheqdDidRegistrar.ts new file mode 100644 index 0000000000..8c0afd5c12 --- /dev/null +++ b/packages/cheqd/src/dids/CheqdDidRegistrar.ts @@ -0,0 +1,458 @@ +import type { CheqdNetwork, DIDDocument, DidStdFee, TVerificationKey, VerificationMethods } from '@cheqd/sdk' +import type { SignInfo } from '@cheqd/ts-proto/cheqd/did/v2' +import type { + AgentContext, + DidRegistrar, + DidCreateOptions, + DidCreateResult, + DidDeactivateResult, + DidUpdateResult, + DidUpdateOptions, +} from '@credo-ts/core' + +import { MethodSpecificIdAlgo, createDidVerificationMethod } from '@cheqd/sdk' +import { MsgCreateResourcePayload } from '@cheqd/ts-proto/cheqd/resource/v2' +import { + DidDocument, + DidDocumentRole, + DidRecord, + DidRepository, + KeyType, + Buffer, + isValidPrivateKey, + utils, + TypedArrayEncoder, + getKeyFromVerificationMethod, + JsonTransformer, + VerificationMethod, +} from '@credo-ts/core' + +import { parseCheqdDid } from '../anoncreds/utils/identifiers' +import { CheqdLedgerService } from '../ledger' + +import { + createMsgCreateDidDocPayloadToSign, + generateDidDoc, + validateSpecCompliantPayload, + createMsgDeactivateDidDocPayloadToSign, +} from './didCheqdUtil' + +export class CheqdDidRegistrar implements DidRegistrar { + public readonly supportedMethods = ['cheqd'] + + public async create(agentContext: AgentContext, options: CheqdDidCreateOptions): Promise { + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + const cheqdLedgerService = agentContext.dependencyManager.resolve(CheqdLedgerService) + + let didDocument: DidDocument + const versionId = options.options?.versionId ?? utils.uuid() + + try { + if (options.didDocument && validateSpecCompliantPayload(options.didDocument)) { + didDocument = options.didDocument + + const cheqdDid = parseCheqdDid(options.didDocument.id) + if (!cheqdDid) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `Unable to parse cheqd did ${options.didDocument.id}`, + }, + } + } + } else if (options.secret?.verificationMethod) { + const withoutDidDocumentOptions = options as CheqdDidCreateWithoutDidDocumentOptions + const verificationMethod = withoutDidDocumentOptions.secret.verificationMethod + const methodSpecificIdAlgo = withoutDidDocumentOptions.options.methodSpecificIdAlgo + const privateKey = verificationMethod.privateKey + if (privateKey && !isValidPrivateKey(privateKey, KeyType.Ed25519)) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Invalid private key provided', + }, + } + } + + const key = await agentContext.wallet.createKey({ + keyType: KeyType.Ed25519, + privateKey: privateKey, + }) + + didDocument = generateDidDoc({ + verificationMethod: verificationMethod.type as VerificationMethods, + verificationMethodId: verificationMethod.id || 'key-1', + methodSpecificIdAlgo: (methodSpecificIdAlgo as MethodSpecificIdAlgo) || MethodSpecificIdAlgo.Uuid, + network: withoutDidDocumentOptions.options.network as CheqdNetwork, + publicKey: TypedArrayEncoder.toHex(key.publicKey), + }) + + const contextMapping = { + Ed25519VerificationKey2018: 'https://w3id.org/security/suites/ed25519-2018/v1', + Ed25519VerificationKey2020: 'https://w3id.org/security/suites/ed25519-2020/v1', + JsonWebKey2020: 'https://w3id.org/security/suites/jws-2020/v1', + } + const contextUrl = contextMapping[verificationMethod.type] + + // Add the context to the did document + // NOTE: cheqd sdk uses https://www.w3.org/ns/did/v1 while Credo did doc uses https://w3id.org/did/v1 + // We should align these at some point. For now we just return a consistent value. + didDocument.context = ['https://www.w3.org/ns/did/v1', contextUrl] + } else { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Provide a didDocument or at least one verificationMethod with seed in secret', + }, + } + } + + const didDocumentJson = didDocument.toJSON() as DIDDocument + + const payloadToSign = await createMsgCreateDidDocPayloadToSign(didDocumentJson, versionId) + const signInputs = await this.signPayload(agentContext, payloadToSign, didDocument.verificationMethod) + + const response = await cheqdLedgerService.create(didDocumentJson, signInputs, versionId) + if (response.code !== 0) { + throw new Error(`${response.rawLog}`) + } + + // Save the did so we know we created it and can issue with it + const didRecord = new DidRecord({ + did: didDocument.id, + role: DidDocumentRole.Created, + didDocument, + }) + await didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: didDocument.id, + didDocument, + secret: options.secret, + }, + } + } catch (error) { + agentContext.config.logger.error(`Error registering DID`, error) + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async update(agentContext: AgentContext, options: CheqdDidUpdateOptions): Promise { + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + const cheqdLedgerService = agentContext.dependencyManager.resolve(CheqdLedgerService) + + const versionId = options.options?.versionId || utils.uuid() + const verificationMethod = options.secret?.verificationMethod + let didDocument: DidDocument + let didRecord: DidRecord | null + + try { + if (options.didDocument && validateSpecCompliantPayload(options.didDocument)) { + didDocument = options.didDocument + const resolvedDocument = await cheqdLedgerService.resolve(didDocument.id) + didRecord = await didRepository.findCreatedDid(agentContext, didDocument.id) + if (!resolvedDocument.didDocument || resolvedDocument.didDocumentMetadata.deactivated || !didRecord) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Did not found', + }, + } + } + + if (verificationMethod) { + const privateKey = verificationMethod.privateKey + if (privateKey && !isValidPrivateKey(privateKey, KeyType.Ed25519)) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Invalid private key provided', + }, + } + } + + const key = await agentContext.wallet.createKey({ + keyType: KeyType.Ed25519, + privateKey: privateKey, + }) + + didDocument.verificationMethod?.concat( + JsonTransformer.fromJSON( + createDidVerificationMethod( + [verificationMethod.type as VerificationMethods], + [ + { + methodSpecificId: didDocument.id.split(':')[3], + didUrl: didDocument.id, + keyId: `${didDocument.id}#${verificationMethod.id}`, + publicKey: TypedArrayEncoder.toHex(key.publicKey), + }, + ] + ), + VerificationMethod + ) + ) + } + } else { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Provide a valid didDocument', + }, + } + } + + const payloadToSign = await createMsgCreateDidDocPayloadToSign(didDocument as DIDDocument, versionId) + const signInputs = await this.signPayload(agentContext, payloadToSign, didDocument.verificationMethod) + + const response = await cheqdLedgerService.update(didDocument as DIDDocument, signInputs, versionId) + if (response.code !== 0) { + throw new Error(`${response.rawLog}`) + } + + // Save the did so we know we created it and can issue with it + didRecord.didDocument = didDocument + await didRepository.update(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: didDocument.id, + didDocument, + secret: options.secret, + }, + } + } catch (error) { + agentContext.config.logger.error(`Error updating DID`, error) + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async deactivate( + agentContext: AgentContext, + options: CheqdDidDeactivateOptions + ): Promise { + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + const cheqdLedgerService = agentContext.dependencyManager.resolve(CheqdLedgerService) + + const did = options.did + const versionId = options.options?.versionId || utils.uuid() + + try { + const { didDocument, didDocumentMetadata } = await cheqdLedgerService.resolve(did) + + const didRecord = await didRepository.findCreatedDid(agentContext, did) + if (!didDocument || didDocumentMetadata.deactivated || !didRecord) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Did not found', + }, + } + } + const payloadToSign = createMsgDeactivateDidDocPayloadToSign(didDocument, versionId) + const didDocumentInstance = JsonTransformer.fromJSON(didDocument, DidDocument) + const signInputs = await this.signPayload(agentContext, payloadToSign, didDocumentInstance.verificationMethod) + const response = await cheqdLedgerService.deactivate(didDocument, signInputs, versionId) + if (response.code !== 0) { + throw new Error(`${response.rawLog}`) + } + + await didRepository.update(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: didDocument.id, + didDocument: JsonTransformer.fromJSON(didDocument, DidDocument), + secret: options.secret, + }, + } + } catch (error) { + agentContext.config.logger.error(`Error deactivating DID`, error) + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async createResource(agentContext: AgentContext, did: string, resource: CheqdCreateResourceOptions) { + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + const cheqdLedgerService = agentContext.dependencyManager.resolve(CheqdLedgerService) + const { didDocument, didDocumentMetadata } = await cheqdLedgerService.resolve(did) + const didRecord = await didRepository.findCreatedDid(agentContext, did) + if (!didDocument || didDocumentMetadata.deactivated || !didRecord) { + return { + resourceMetadata: {}, + resourceRegistrationMetadata: {}, + resourceState: { + state: 'failed', + reason: `DID: ${did} not found`, + }, + } + } + + try { + let data: Uint8Array + if (typeof resource.data === 'string') { + data = TypedArrayEncoder.fromBase64(resource.data) + } else if (typeof resource.data == 'object') { + data = TypedArrayEncoder.fromString(JSON.stringify(resource.data)) + } else { + data = resource.data + } + + const resourcePayload = MsgCreateResourcePayload.fromPartial({ + collectionId: did.split(':')[3], + id: resource.id, + resourceType: resource.resourceType, + name: resource.name, + version: resource.version, + alsoKnownAs: resource.alsoKnownAs, + data, + }) + const payloadToSign = MsgCreateResourcePayload.encode(resourcePayload).finish() + + const didDocumentInstance = JsonTransformer.fromJSON(didDocument, DidDocument) + const signInputs = await this.signPayload(agentContext, payloadToSign, didDocumentInstance.verificationMethod) + const response = await cheqdLedgerService.createResource(did, resourcePayload, signInputs) + if (response.code !== 0) { + throw new Error(`${response.rawLog}`) + } + + return { + resourceMetadata: {}, + resourceRegistrationMetadata: {}, + resourceState: { + state: 'finished', + resourceId: resourcePayload.id, + resource: resourcePayload, + }, + } + } catch (error) { + return { + resourceMetadata: {}, + resourceRegistrationMetadata: {}, + resourceState: { + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + private async signPayload( + agentContext: AgentContext, + payload: Uint8Array, + verificationMethod: VerificationMethod[] = [] + ) { + return await Promise.all( + verificationMethod.map(async (method) => { + const key = getKeyFromVerificationMethod(method) + return { + verificationMethodId: method.id, + signature: await agentContext.wallet.sign({ data: Buffer.from(payload), key }), + } satisfies SignInfo + }) + ) + } +} + +export interface CheqdDidCreateWithoutDidDocumentOptions extends DidCreateOptions { + method: 'cheqd' + did?: undefined + didDocument?: undefined + options: { + network: `${CheqdNetwork}` + fee?: DidStdFee + versionId?: string + methodSpecificIdAlgo?: `${MethodSpecificIdAlgo}` + } + secret: { + verificationMethod: IVerificationMethod + } +} + +export interface CheqdDidCreateFromDidDocumentOptions extends DidCreateOptions { + method: 'cheqd' + did?: undefined + didDocument: DidDocument + options?: { + fee?: DidStdFee + versionId?: string + } +} + +export type CheqdDidCreateOptions = CheqdDidCreateFromDidDocumentOptions | CheqdDidCreateWithoutDidDocumentOptions + +export interface CheqdDidUpdateOptions extends DidUpdateOptions { + did: string + didDocument: DidDocument + options: { + fee?: DidStdFee + versionId?: string + } + secret?: { + verificationMethod: IVerificationMethod + } +} + +export interface CheqdDidDeactivateOptions extends DidCreateOptions { + method: 'cheqd' + did: string + options: { + fee?: DidStdFee + versionId?: string + } +} + +export interface CheqdCreateResourceOptions extends Omit, 'data'> { + data: string | Uint8Array | object +} + +interface IVerificationMethod { + type: `${VerificationMethods}` + id: TVerificationKey + privateKey?: Buffer +} diff --git a/packages/cheqd/src/dids/CheqdDidResolver.ts b/packages/cheqd/src/dids/CheqdDidResolver.ts new file mode 100644 index 0000000000..24126d73de --- /dev/null +++ b/packages/cheqd/src/dids/CheqdDidResolver.ts @@ -0,0 +1,194 @@ +import type { ParsedCheqdDid } from '../anoncreds/utils/identifiers' +import type { Metadata } from '@cheqd/ts-proto/cheqd/resource/v2' +import type { AgentContext, DidResolutionResult, DidResolver, ParsedDid } from '@credo-ts/core' + +import { DidDocument, CredoError, utils, JsonTransformer } from '@credo-ts/core' + +import { + cheqdDidMetadataRegex, + cheqdDidRegex, + cheqdDidVersionRegex, + cheqdDidVersionsRegex, + cheqdResourceMetadataRegex, + cheqdResourceRegex, + parseCheqdDid, +} from '../anoncreds/utils/identifiers' +import { CheqdLedgerService } from '../ledger' + +import { filterResourcesByNameAndType, getClosestResourceVersion, renderResourceData } from './didCheqdUtil' + +export class CheqdDidResolver implements DidResolver { + public readonly supportedMethods = ['cheqd'] + public readonly allowsCaching = true + public readonly allowsLocalDidRecord = true + + public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { + const didDocumentMetadata = {} + + try { + const parsedDid = parseCheqdDid(parsed.didUrl) + if (!parsedDid) { + throw new Error('Invalid DID') + } + + switch (did) { + case did.match(cheqdDidRegex)?.input: + return await this.resolveDidDoc(agentContext, parsedDid.did) + case did.match(cheqdDidVersionRegex)?.input: { + const version = did.split('/')[2] + return await this.resolveDidDoc(agentContext, parsedDid.did, version) + } + case did.match(cheqdDidVersionsRegex)?.input: + return await this.resolveAllDidDocVersions(agentContext, parsedDid) + case did.match(cheqdDidMetadataRegex)?.input: + return await this.dereferenceCollectionResources(agentContext, parsedDid) + case did.match(cheqdResourceMetadataRegex)?.input: + return await this.dereferenceResourceMetadata(agentContext, parsedDid) + default: + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'Invalid request', + message: `Unsupported did Url: '${did}'`, + }, + } + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } + + public async resolveResource(agentContext: AgentContext, did: string) { + const cheqdLedgerService = agentContext.dependencyManager.resolve(CheqdLedgerService) + try { + const parsedDid = parseCheqdDid(did) + if (!parsedDid) { + throw new Error('Invalid DID') + } + + const { params, id } = parsedDid + let resourceId: string + if (did.match(cheqdResourceRegex)?.input) { + resourceId = did.split('/')[2] + } else if (params && params.resourceName && params.resourceType) { + let resources = (await cheqdLedgerService.resolveCollectionResources(parsedDid.did, id)).resources + resources = filterResourcesByNameAndType(resources, params.resourceName, params.resourceType) + if (!resources.length) { + throw new Error(`No resources found`) + } + + let resource: Metadata | undefined + if (params.version) { + resource = resources.find((resource) => resource.version == params.version) + } else { + const date = params.resourceVersionTime ? new Date(Number(params.resourceVersionTime) * 1000) : new Date() + // find the resourceId closest to the created time + resource = getClosestResourceVersion(resources, date) + } + + if (!resource) { + throw new Error(`No resources found`) + } + + resourceId = resource.id + } else { + return { + error: 'notFound', + message: `resolver_error: Invalid did url '${did}'`, + } + } + if (!utils.isValidUuid(resourceId)) { + throw new Error('Invalid resource Id') + } + + const { resource, metadata } = await cheqdLedgerService.resolveResource(parsedDid.did, id, resourceId) + if (!resource || !metadata) { + throw new Error('resolver_error: Unable to resolve resource, Please try again') + } + + const result = await renderResourceData(resource.data, metadata.mediaType) + return { + resource: result, + resourceMetadata: metadata, + resourceResolutionMetadata: {}, + } + } catch (error) { + return { + error: 'notFound', + message: `resolver_error: Unable to resolve resource '${did}': ${error}`, + } + } + } + + private async resolveAllDidDocVersions(agentContext: AgentContext, parsedDid: ParsedCheqdDid) { + const cheqdLedgerService = agentContext.dependencyManager.resolve(CheqdLedgerService) + const { did } = parsedDid + + const { didDocumentVersionsMetadata } = await cheqdLedgerService.resolveMetadata(did) + return { + didDocument: new DidDocument({ id: did }), + didDocumentMetadata: didDocumentVersionsMetadata, + didResolutionMetadata: {}, + } + } + + private async dereferenceCollectionResources(agentContext: AgentContext, parsedDid: ParsedCheqdDid) { + const cheqdLedgerService = agentContext.dependencyManager.resolve(CheqdLedgerService) + const { did, id } = parsedDid + + const metadata = await cheqdLedgerService.resolveCollectionResources(did, id) + return { + didDocument: new DidDocument({ id: did }), + didDocumentMetadata: { + linkedResourceMetadata: metadata, + }, + didResolutionMetadata: {}, + } + } + + private async dereferenceResourceMetadata(agentContext: AgentContext, parsedDid: ParsedCheqdDid) { + const cheqdLedgerService = agentContext.dependencyManager.resolve(CheqdLedgerService) + const { did, id } = parsedDid + + if (!parsedDid.path) { + throw new CredoError(`Missing path in did ${parsedDid.did}`) + } + + const [, , resourceId] = parsedDid.path.split('/') + + if (!resourceId) { + throw new CredoError(`Missing resource id in didUrl ${parsedDid.didUrl}`) + } + + const metadata = await cheqdLedgerService.resolveResourceMetadata(did, id, resourceId) + return { + didDocument: new DidDocument({ id: did }), + didDocumentMetadata: { + linkedResourceMetadata: metadata, + }, + didResolutionMetadata: {}, + } + } + + private async resolveDidDoc(agentContext: AgentContext, did: string, version?: string): Promise { + const cheqdLedgerService = agentContext.dependencyManager.resolve(CheqdLedgerService) + + const { didDocument, didDocumentMetadata } = await cheqdLedgerService.resolve(did, version) + const { resources } = await cheqdLedgerService.resolveCollectionResources(did, did.split(':')[3]) + didDocumentMetadata.linkedResourceMetadata = resources + + return { + didDocument: JsonTransformer.fromJSON(didDocument, DidDocument), + didDocumentMetadata, + didResolutionMetadata: {}, + } + } +} diff --git a/packages/cheqd/src/dids/didCheqdUtil.ts b/packages/cheqd/src/dids/didCheqdUtil.ts new file mode 100644 index 0000000000..beca3428c1 --- /dev/null +++ b/packages/cheqd/src/dids/didCheqdUtil.ts @@ -0,0 +1,152 @@ +import type { CheqdNetwork, DIDDocument, MethodSpecificIdAlgo, TVerificationKey } from '@cheqd/sdk' +import type { Metadata } from '@cheqd/ts-proto/cheqd/resource/v2' + +import { + createDidPayload, + createDidVerificationMethod, + createVerificationKeys, + DIDModule, + VerificationMethods, +} from '@cheqd/sdk' +import { MsgCreateDidDocPayload, MsgDeactivateDidDocPayload } from '@cheqd/ts-proto/cheqd/did/v2' +import { EnglishMnemonic as _ } from '@cosmjs/crypto' +import { DirectSecp256k1HdWallet, DirectSecp256k1Wallet } from '@cosmjs/proto-signing' +import { DidDocument, CredoError, JsonEncoder, TypedArrayEncoder, JsonTransformer } from '@credo-ts/core' + +export function validateSpecCompliantPayload(didDocument: DidDocument): SpecValidationResult { + // id is required, validated on both compile and runtime + if (!didDocument.id && !didDocument.id.startsWith('did:cheqd:')) return { valid: false, error: 'id is required' } + + // verificationMethod is required + if (!didDocument.verificationMethod) return { valid: false, error: 'verificationMethod is required' } + + // verificationMethod must be an array + if (!Array.isArray(didDocument.verificationMethod)) + return { valid: false, error: 'verificationMethod must be an array' } + + // verificationMethod must be not be empty + if (!didDocument.verificationMethod.length) return { valid: false, error: 'verificationMethod must be not be empty' } + + // verificationMethod types must be supported + const isValidVerificationMethod = didDocument.verificationMethod.every((vm) => { + switch (vm.type) { + case VerificationMethods.Ed255192020: + return vm.publicKeyMultibase != null + case VerificationMethods.JWK: + return vm.publicKeyJwk != null + case VerificationMethods.Ed255192018: + return vm.publicKeyBase58 != null + default: + return false + } + }) + + if (!isValidVerificationMethod) return { valid: false, error: 'verificationMethod publicKey is Invalid' } + + const isValidService = didDocument.service + ? didDocument?.service?.every((s) => { + return s?.serviceEndpoint && s?.id && s?.type + }) + : true + + if (!isValidService) return { valid: false, error: 'Service is Invalid' } + return { valid: true } as SpecValidationResult +} + +// Create helpers in sdk like MsgCreateDidDocPayload.fromDIDDocument to replace the below +export async function createMsgCreateDidDocPayloadToSign(didPayload: DIDDocument, versionId: string) { + didPayload.service = didPayload.service?.map((e) => { + return { + ...e, + serviceEndpoint: Array.isArray(e.serviceEndpoint) ? e.serviceEndpoint : [e.serviceEndpoint], + } + }) + const { protobufVerificationMethod, protobufService } = await DIDModule.validateSpecCompliantPayload(didPayload) + return MsgCreateDidDocPayload.encode( + MsgCreateDidDocPayload.fromPartial({ + context: didPayload?.['@context'], + id: didPayload.id, + controller: didPayload.controller, + verificationMethod: protobufVerificationMethod, + authentication: didPayload.authentication, + assertionMethod: didPayload.assertionMethod, + capabilityInvocation: didPayload.capabilityInvocation, + capabilityDelegation: didPayload.capabilityDelegation, + keyAgreement: didPayload.keyAgreement, + service: protobufService, + alsoKnownAs: didPayload.alsoKnownAs, + versionId, + }) + ).finish() +} + +export function createMsgDeactivateDidDocPayloadToSign(didPayload: DIDDocument, versionId?: string) { + return MsgDeactivateDidDocPayload.encode( + MsgDeactivateDidDocPayload.fromPartial({ + id: didPayload.id, + versionId, + }) + ).finish() +} + +export type SpecValidationResult = { + valid: boolean + error?: string +} + +export function generateDidDoc(options: IDidDocOptions) { + const { verificationMethod, methodSpecificIdAlgo, verificationMethodId, network, publicKey } = options + const verificationKeys = createVerificationKeys(publicKey, methodSpecificIdAlgo, verificationMethodId, network) + if (!verificationKeys) { + throw new Error('Invalid DID options') + } + const verificationMethods = createDidVerificationMethod([verificationMethod], [verificationKeys]) + const didPayload = createDidPayload(verificationMethods, [verificationKeys]) + return JsonTransformer.fromJSON(didPayload, DidDocument) +} + +export interface IDidDocOptions { + verificationMethod: VerificationMethods + verificationMethodId: TVerificationKey + methodSpecificIdAlgo: MethodSpecificIdAlgo + network: CheqdNetwork + publicKey: string +} + +export function getClosestResourceVersion(resources: Metadata[], date: Date) { + const result = [...resources].sort(function (a, b) { + if (!a.created || !b.created) throw new CredoError("Missing required property 'created' on resource") + const distancea = Math.abs(date.getTime() - a.created.getTime()) + const distanceb = Math.abs(date.getTime() - b.created.getTime()) + return distancea - distanceb + }) + + return result[0] +} + +export function filterResourcesByNameAndType(resources: Metadata[], name: string, type: string) { + return resources.filter((resource) => resource.name == name && resource.resourceType == type) +} + +export async function renderResourceData(data: Uint8Array, mimeType: string) { + if (mimeType == 'application/json') { + return await JsonEncoder.fromBuffer(data) + } else if (mimeType == 'text/plain') { + return TypedArrayEncoder.toUtf8String(data) + } else { + return TypedArrayEncoder.toBase64URL(data) + } +} + +export class EnglishMnemonic extends _ { + public static readonly _mnemonicMatcher = /^[a-z]+( [a-z]+)*$/ +} + +export function getCosmosPayerWallet(cosmosPayerSeed?: string) { + if (!cosmosPayerSeed || cosmosPayerSeed === '') { + return DirectSecp256k1HdWallet.generate() + } + return EnglishMnemonic._mnemonicMatcher.test(cosmosPayerSeed) + ? DirectSecp256k1HdWallet.fromMnemonic(cosmosPayerSeed, { prefix: 'cheqd' }) + : DirectSecp256k1Wallet.fromKey(TypedArrayEncoder.fromString(cosmosPayerSeed.replace(/^0x/, '')), 'cheqd') +} diff --git a/packages/cheqd/src/dids/index.ts b/packages/cheqd/src/dids/index.ts new file mode 100644 index 0000000000..315b1c0982 --- /dev/null +++ b/packages/cheqd/src/dids/index.ts @@ -0,0 +1,8 @@ +export { + CheqdDidRegistrar, + CheqdDidCreateOptions, + CheqdDidDeactivateOptions, + CheqdDidUpdateOptions, + CheqdCreateResourceOptions, +} from './CheqdDidRegistrar' +export { CheqdDidResolver } from './CheqdDidResolver' diff --git a/packages/cheqd/src/index.ts b/packages/cheqd/src/index.ts new file mode 100644 index 0000000000..4270e5c072 --- /dev/null +++ b/packages/cheqd/src/index.ts @@ -0,0 +1,17 @@ +// Dids +export { + CheqdDidRegistrar, + CheqdDidCreateOptions, + CheqdDidDeactivateOptions, + CheqdDidUpdateOptions, + CheqdDidResolver, +} from './dids' + +// AnonCreds +export { CheqdAnonCredsRegistry } from './anoncreds' + +export { CheqdLedgerService } from './ledger' + +export { CheqdModule } from './CheqdModule' + +export { CheqdModuleConfig, CheqdModuleConfigOptions } from './CheqdModuleConfig' diff --git a/packages/cheqd/src/ledger/CheqdLedgerService.ts b/packages/cheqd/src/ledger/CheqdLedgerService.ts new file mode 100644 index 0000000000..384a12d9d5 --- /dev/null +++ b/packages/cheqd/src/ledger/CheqdLedgerService.ts @@ -0,0 +1,169 @@ +import type { AbstractCheqdSDKModule, CheqdSDK, DidStdFee, DIDDocument } from '@cheqd/sdk' +import type { QueryAllDidDocVersionsMetadataResponse, SignInfo } from '@cheqd/ts-proto/cheqd/did/v2' +import type { MsgCreateResourcePayload } from '@cheqd/ts-proto/cheqd/resource/v2' +import type { DirectSecp256k1HdWallet, DirectSecp256k1Wallet } from '@cosmjs/proto-signing' +import type { DidDocumentMetadata } from '@credo-ts/core' + +import { createCheqdSDK, DIDModule, ResourceModule, CheqdNetwork } from '@cheqd/sdk' +import { CredoError, inject, injectable, InjectionSymbols, Logger } from '@credo-ts/core' + +import { CheqdModuleConfig } from '../CheqdModuleConfig' +import { parseCheqdDid } from '../anoncreds/utils/identifiers' +import { getCosmosPayerWallet } from '../dids/didCheqdUtil' + +export interface ICheqdLedgerConfig { + network: string + rpcUrl: string + readonly cosmosPayerWallet: Promise + sdk?: Promise +} + +export enum DefaultRPCUrl { + Mainnet = 'https://rpc.cheqd.net', + Testnet = 'https://rpc.cheqd.network', +} + +@injectable() +export class CheqdLedgerService { + private networks: ICheqdLedgerConfig[] + private logger: Logger + + public constructor(cheqdSdkModuleConfig: CheqdModuleConfig, @inject(InjectionSymbols.Logger) logger: Logger) { + this.logger = logger + this.networks = cheqdSdkModuleConfig.networks.map((config) => { + const { network, rpcUrl, cosmosPayerSeed } = config + return { + network, + rpcUrl: rpcUrl ? rpcUrl : network === CheqdNetwork.Mainnet ? DefaultRPCUrl.Mainnet : DefaultRPCUrl.Testnet, + cosmosPayerWallet: getCosmosPayerWallet(cosmosPayerSeed), + } + }) + } + + public async connect() { + for (const network of this.networks) { + if (!network.sdk) { + await this.initializeSdkForNetwork(network) + } else { + this.logger.debug(`Not connecting to network ${network} as SDK already initialized`) + } + } + } + + private async getSdk(did: string) { + const parsedDid = parseCheqdDid(did) + if (!parsedDid) { + throw new Error('Invalid DID') + } + if (this.networks.length === 0) { + throw new Error('No cheqd networks configured') + } + + const network = this.networks.find((network) => network.network === parsedDid.network) + if (!network) { + throw new Error(`Network ${network} not found in cheqd networks configuration`) + } + + if (!network.sdk) { + const sdk = await this.initializeSdkForNetwork(network) + if (!sdk) throw new Error(`Cheqd SDK not initialized for network ${parsedDid.network}`) + return sdk + } + + try { + const sdk = await network.sdk + return sdk + } catch (error) { + throw new Error(`Error initializing cheqd sdk for network ${parsedDid.network}: ${error.message}`) + } + } + + private async initializeSdkForNetwork(network: ICheqdLedgerConfig) { + try { + // Initialize cheqd sdk with promise + network.sdk = createCheqdSDK({ + modules: [DIDModule as unknown as AbstractCheqdSDKModule, ResourceModule as unknown as AbstractCheqdSDKModule], + rpcUrl: network.rpcUrl, + wallet: await network.cosmosPayerWallet.catch((error) => { + throw new CredoError(`Error initializing cosmos payer wallet: ${error.message}`, { cause: error }) + }), + }) + + return await network.sdk + } catch (error) { + this.logger.error( + `Skipping connection for network ${network.network} in cheqd sdk due to error in initialization: ${error.message}` + ) + network.sdk = undefined + return undefined + } + } + + public async create( + didPayload: DIDDocument, + signInputs: SignInfo[], + versionId?: string | undefined, + fee?: DidStdFee + ) { + const sdk = await this.getSdk(didPayload.id) + return sdk.createDidDocTx(signInputs, didPayload, '', fee, undefined, versionId) + } + + public async update( + didPayload: DIDDocument, + signInputs: SignInfo[], + versionId?: string | undefined, + fee?: DidStdFee + ) { + const sdk = await this.getSdk(didPayload.id) + return sdk.updateDidDocTx(signInputs, didPayload, '', fee, undefined, versionId) + } + + public async deactivate( + didPayload: DIDDocument, + signInputs: SignInfo[], + versionId?: string | undefined, + fee?: DidStdFee + ) { + const sdk = await this.getSdk(didPayload.id) + return sdk.deactivateDidDocTx(signInputs, didPayload, '', fee, undefined, versionId) + } + + public async resolve(did: string, version?: string) { + const sdk = await this.getSdk(did) + return version ? sdk.queryDidDocVersion(did, version) : sdk.queryDidDoc(did) + } + + public async resolveMetadata(did: string): Promise<{ + didDocumentVersionsMetadata: DidDocumentMetadata[] + pagination: QueryAllDidDocVersionsMetadataResponse['pagination'] + }> { + const sdk = await this.getSdk(did) + return sdk.queryAllDidDocVersionsMetadata(did) + } + + public async createResource( + did: string, + resourcePayload: Partial, + signInputs: SignInfo[], + fee?: DidStdFee + ) { + const sdk = await this.getSdk(did) + return sdk.createLinkedResourceTx(signInputs, resourcePayload, '', fee, undefined) + } + + public async resolveResource(did: string, collectionId: string, resourceId: string) { + const sdk = await this.getSdk(did) + return sdk.queryLinkedResource(collectionId, resourceId) + } + + public async resolveCollectionResources(did: string, collectionId: string) { + const sdk = await this.getSdk(did) + return sdk.queryLinkedResources(collectionId) + } + + public async resolveResourceMetadata(did: string, collectionId: string, resourceId: string) { + const sdk = await this.getSdk(did) + return sdk.queryLinkedResourceMetadata(collectionId, resourceId) + } +} diff --git a/packages/cheqd/src/ledger/index.ts b/packages/cheqd/src/ledger/index.ts new file mode 100644 index 0000000000..db2eec776a --- /dev/null +++ b/packages/cheqd/src/ledger/index.ts @@ -0,0 +1 @@ +export { CheqdLedgerService } from './CheqdLedgerService' diff --git a/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts b/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts new file mode 100644 index 0000000000..2cd830c6b0 --- /dev/null +++ b/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts @@ -0,0 +1,236 @@ +import type { AnonCredsTestsAgent } from '../../anoncreds/tests/anoncredsSetup' +import type { EventReplaySubject } from '../../core/tests' +import type { DifPresentationExchangeDefinitionV2 } from '@credo-ts/core' + +import { + AutoAcceptCredential, + CredentialExchangeRecord, + CredentialState, + ProofState, + W3cCredential, + W3cCredentialSubject, +} from '@credo-ts/core' + +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { setupAnonCredsTests } from '../../anoncreds/tests/anoncredsSetup' +import { presentationDefinition } from '../../anoncreds/tests/fixtures/presentation-definition' +import { createDidKidVerificationMethod } from '../../core/tests' +import { waitForCredentialRecordSubject, waitForProofExchangeRecord } from '../../core/tests/helpers' + +import { cheqdPayerSeeds } from './setupCheqdModule' + +describe('anoncreds w3c data integrity e2e tests', () => { + let issuerId: string + let issuerAgent: AnonCredsTestsAgent + let holderAgent: AnonCredsTestsAgent + let credentialDefinitionId: string + let issuerHolderConnectionId: string + let holderIssuerConnectionId: string + + let issuerReplay: EventReplaySubject + let holderReplay: EventReplaySubject + + afterEach(async () => { + await issuerAgent.shutdown() + await issuerAgent.wallet.delete() + await holderAgent.shutdown() + await holderAgent.wallet.delete() + }) + + test('cheqd issuance and verification flow starting from offer without revocation', async () => { + ;({ + issuerAgent, + issuerReplay, + holderAgent, + holderReplay, + credentialDefinitionId, + issuerHolderConnectionId, + holderIssuerConnectionId, + issuerId, + } = await setupAnonCredsTests({ + issuerName: 'Issuer Agent Credentials v2', + holderName: 'Holder Agent Credentials v2', + attributeNames: ['id', 'name', 'height', 'age'], + registries: [new InMemoryAnonCredsRegistry()], + cheqd: { + seed: cheqdPayerSeeds[3], + }, + })) + + const holderKdv = await createDidKidVerificationMethod(holderAgent.context, '96213c3d7fc8d4d6754c7a0fd969598f') + const linkSecret = await holderAgent.modules.anoncreds.createLinkSecret({ linkSecretId: 'linkSecretId' }) + expect(linkSecret).toBe('linkSecretId') + + const credential = new W3cCredential({ + context: [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2', + { + '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', + }, + ], + type: ['VerifiableCredential'], + issuer: issuerId, + issuanceDate: new Date().toISOString(), + credentialSubject: new W3cCredentialSubject({ + id: holderKdv.did, + claims: { name: 'John', age: '25', height: 173 }, + }), + }) + + // issuer offers credential + let issuerRecord = await issuerAgent.credentials.offerCredential({ + protocolVersion: 'v2', + autoAcceptCredential: AutoAcceptCredential.Never, + connectionId: issuerHolderConnectionId, + credentialFormats: { + dataIntegrity: { + bindingRequired: true, + credential, + anonCredsLinkSecretBinding: { + credentialDefinitionId, + revocationRegistryDefinitionId: undefined, + revocationRegistryIndex: undefined, + }, + didCommSignedAttachmentBinding: {}, + }, + }, + }) + + // Holder processes and accepts offer + let holderRecord = await waitForCredentialRecordSubject(holderReplay, { + state: CredentialState.OfferReceived, + threadId: issuerRecord.threadId, + }) + holderRecord = await holderAgent.credentials.acceptOffer({ + credentialRecordId: holderRecord.id, + autoAcceptCredential: AutoAcceptCredential.Never, + credentialFormats: { + dataIntegrity: { + anonCredsLinkSecret: { + linkSecretId: 'linkSecretId', + }, + }, + }, + }) + + // issuer receives request and accepts + issuerRecord = await waitForCredentialRecordSubject(issuerReplay, { + state: CredentialState.RequestReceived, + threadId: holderRecord.threadId, + }) + issuerRecord = await issuerAgent.credentials.acceptRequest({ + credentialRecordId: issuerRecord.id, + autoAcceptCredential: AutoAcceptCredential.Never, + credentialFormats: { + dataIntegrity: {}, + }, + }) + + holderRecord = await waitForCredentialRecordSubject(holderReplay, { + state: CredentialState.CredentialReceived, + threadId: issuerRecord.threadId, + }) + holderRecord = await holderAgent.credentials.acceptCredential({ + credentialRecordId: holderRecord.id, + }) + + issuerRecord = await waitForCredentialRecordSubject(issuerReplay, { + state: CredentialState.Done, + threadId: holderRecord.threadId, + }) + + expect(holderRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + credentialDefinitionId, + schemaId: expect.any(String), + }, + '_anoncreds/credentialRequest': { + link_secret_blinding_data: { + v_prime: expect.any(String), + vr_prime: null, + }, + nonce: expect.any(String), + link_secret_name: 'linkSecretId', + }, + }, + }, + state: CredentialState.Done, + }) + + const tags = holderRecord.getTags() + expect(tags.credentialIds).toHaveLength(1) + + let issuerProofExchangeRecordPromise = waitForProofExchangeRecord(issuerAgent, { + state: ProofState.ProposalReceived, + }) + + const pdCopy = JSON.parse(JSON.stringify(presentationDefinition)) + pdCopy.input_descriptors.forEach((ide: DifPresentationExchangeDefinitionV2['input_descriptors'][number]) => { + delete ide.constraints?.statuses + if (ide.constraints.fields && ide.constraints.fields[0].filter?.const) { + ide.constraints.fields[0].filter.const = issuerId + } + }) + + let holderProofExchangeRecord = await holderAgent.proofs.proposeProof({ + protocolVersion: 'v2', + connectionId: holderIssuerConnectionId, + proofFormats: { + presentationExchange: { + presentationDefinition: pdCopy, + }, + }, + }) + + let issuerProofExchangeRecord = await issuerProofExchangeRecordPromise + + let holderProofExchangeRecordPromise = waitForProofExchangeRecord(holderAgent, { + state: ProofState.RequestReceived, + }) + + issuerProofExchangeRecord = await issuerAgent.proofs.acceptProposal({ + proofRecordId: issuerProofExchangeRecord.id, + }) + + holderProofExchangeRecord = await holderProofExchangeRecordPromise + + const requestedCredentials = await holderAgent.proofs.selectCredentialsForRequest({ + proofRecordId: holderProofExchangeRecord.id, + }) + + const selectedCredentials = requestedCredentials.proofFormats.presentationExchange?.credentials + if (!selectedCredentials) { + throw new Error('No credentials found for presentation exchange') + } + + issuerProofExchangeRecordPromise = waitForProofExchangeRecord(issuerAgent, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await holderAgent.proofs.acceptRequest({ + proofRecordId: holderProofExchangeRecord.id, + proofFormats: { + presentationExchange: { + credentials: selectedCredentials, + }, + }, + }) + issuerProofExchangeRecord = await issuerProofExchangeRecordPromise + + holderProofExchangeRecordPromise = waitForProofExchangeRecord(holderAgent, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + await issuerAgent.proofs.acceptPresentation({ proofRecordId: issuerProofExchangeRecord.id }) + + holderProofExchangeRecord = await holderProofExchangeRecordPromise + }) +}) diff --git a/packages/cheqd/tests/cheqd-did-registrar.e2e.test.ts b/packages/cheqd/tests/cheqd-did-registrar.e2e.test.ts new file mode 100644 index 0000000000..9be77e675a --- /dev/null +++ b/packages/cheqd/tests/cheqd-did-registrar.e2e.test.ts @@ -0,0 +1,209 @@ +import type { CheqdDidCreateOptions } from '../src' +import type { DidDocument } from '@credo-ts/core' + +import { + SECURITY_JWS_CONTEXT_URL, + DidDocumentBuilder, + getEd25519VerificationKey2018, + getJsonWebKey2020, + KeyType, + utils, + Agent, + TypedArrayEncoder, +} from '@credo-ts/core' +import { generateKeyPairFromSeed } from '@stablelib/ed25519' + +import { getInMemoryAgentOptions } from '../../core/tests/helpers' + +import { validService } from './setup' +import { cheqdPayerSeeds, getCheqdModules } from './setupCheqdModule' + +const agentOptions = getInMemoryAgentOptions('Faber Dids Registrar', {}, getCheqdModules(cheqdPayerSeeds[0])) + +describe('Cheqd DID registrar', () => { + let agent: Agent> + + beforeAll(async () => { + agent = new Agent(agentOptions) + await agent.initialize() + }) + + afterAll(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + it('should create a did:cheqd did', async () => { + // Generate a seed and the cheqd did. This allows us to create a new did every time + // but still check if the created output document is as expected. + const privateKey = TypedArrayEncoder.fromString( + Array(32 + 1) + .join((Math.random().toString(36) + '00000000000000000').slice(2, 18)) + .slice(0, 32) + ) + const publicKeyEd25519 = generateKeyPairFromSeed(privateKey).publicKey + const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(publicKeyEd25519) + const did = await agent.dids.create({ + method: 'cheqd', + secret: { + verificationMethod: { + id: 'key-1', + type: 'Ed25519VerificationKey2018', + privateKey, + }, + }, + options: { + network: 'testnet', + methodSpecificIdAlgo: 'base58btc', + }, + }) + expect(did).toMatchObject({ + didState: { + state: 'finished', + didDocument: { + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + publicKeyBase58: ed25519PublicKeyBase58, + }, + ], + }, + }, + }) + }) + + it('should create a did:cheqd using Ed25519VerificationKey2020', async () => { + const did = await agent.dids.create({ + method: 'cheqd', + secret: { + verificationMethod: { + id: 'key-1', + type: 'Ed25519VerificationKey2020', + }, + }, + options: { + network: 'testnet', + methodSpecificIdAlgo: 'uuid', + }, + }) + expect(did.didState).toMatchObject({ state: 'finished' }) + }) + + it('should create a did:cheqd using JsonWebKey2020', async () => { + const createResult = await agent.dids.create({ + method: 'cheqd', + secret: { + verificationMethod: { + id: 'key-11', + type: 'JsonWebKey2020', + }, + }, + options: { + network: 'testnet', + methodSpecificIdAlgo: 'base58btc', + }, + }) + + expect(createResult).toMatchObject({ + didState: { + state: 'finished', + didDocument: { + verificationMethod: [{ type: 'JsonWebKey2020' }], + }, + }, + }) + expect(createResult.didState.did).toBeDefined() + const did = createResult.didState.did as string + const didDocument = createResult.didState.didDocument as DidDocument + didDocument.service = [validService(did)] + + const updateResult = await agent.dids.update({ + did, + didDocument, + }) + expect(updateResult).toMatchObject({ + didState: { + state: 'finished', + didDocument, + }, + }) + + const deactivateResult = await agent.dids.deactivate({ did }) + expect(deactivateResult.didState.didDocument?.toJSON()).toMatchObject(didDocument.toJSON()) + expect(deactivateResult.didState.state).toEqual('finished') + + const resolvedDocument = await agent.dids.resolve(did, { + useLocalCreatedDidRecord: false, + }) + expect(resolvedDocument.didDocumentMetadata.deactivated).toBe(true) + }) + + it('should create a did:cheqd did using custom did document containing Ed25519 key', async () => { + const did = `did:cheqd:testnet:${utils.uuid()}` + + const ed25519Key = await agent.wallet.createKey({ + keyType: KeyType.Ed25519, + }) + + const createResult = await agent.dids.create({ + method: 'cheqd', + didDocument: new DidDocumentBuilder(did) + .addContext(SECURITY_JWS_CONTEXT_URL) + .addVerificationMethod( + getEd25519VerificationKey2018({ + key: ed25519Key, + controller: did, + id: `${did}#${ed25519Key.fingerprint}`, + }) + ) + .build(), + }) + + expect(createResult).toMatchObject({ + didState: { + state: 'finished', + }, + }) + + expect(createResult.didState.didDocument?.toJSON()).toMatchObject({ + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + verificationMethod: [ + { + controller: did, + type: 'Ed25519VerificationKey2018', + publicKeyBase58: ed25519Key.publicKeyBase58, + }, + ], + }) + }) + + it('should create a did:cheqd did using custom did document containing P256 key', async () => { + const did = `did:cheqd:testnet:${utils.uuid()}` + + const p256Key = await agent.wallet.createKey({ + keyType: KeyType.P256, + }) + + const createResult = await agent.dids.create({ + method: 'cheqd', + didDocument: new DidDocumentBuilder(did) + .addContext(SECURITY_JWS_CONTEXT_URL) + .addVerificationMethod( + getJsonWebKey2020({ + did, + key: p256Key, + verificationMethodId: `${did}#${p256Key.fingerprint}`, + }) + ) + .build(), + }) + + // FIXME: the ES256 signature generated by Credo is invalid for Cheqd + // need to dive deeper into it, but for now adding a failing test so we can fix it in the future + expect(createResult).toMatchObject({ + didState: { + state: 'failed', + }, + }) + }) +}) diff --git a/packages/cheqd/tests/cheqd-did-resolver.e2e.test.ts b/packages/cheqd/tests/cheqd-did-resolver.e2e.test.ts new file mode 100644 index 0000000000..e5a861d224 --- /dev/null +++ b/packages/cheqd/tests/cheqd-did-resolver.e2e.test.ts @@ -0,0 +1,131 @@ +import type { CheqdDidCreateOptions } from '../src' + +import { Agent, JsonTransformer, utils } from '@credo-ts/core' + +import { getInMemoryAgentOptions } from '../../core/tests/helpers' +import { CheqdDidRegistrar } from '../src' +import { getClosestResourceVersion } from '../src/dids/didCheqdUtil' + +import { cheqdPayerSeeds, getCheqdModules } from './setupCheqdModule' + +export const resolverAgent = new Agent( + getInMemoryAgentOptions('Cheqd resolver', {}, getCheqdModules(cheqdPayerSeeds[1])) +) + +describe('Cheqd DID resolver', () => { + let did: string + let resourceResult1: Awaited> + let resourceResult2: Awaited> + let resourceResult3: Awaited> + + beforeAll(async () => { + await resolverAgent.initialize() + const cheqdDidRegistrar = resolverAgent.dependencyManager.resolve(CheqdDidRegistrar) + + const didResult = await resolverAgent.dids.create({ + method: 'cheqd', + secret: { + verificationMethod: { + id: 'key-1', + type: 'Ed25519VerificationKey2020', + }, + }, + options: { + network: 'testnet', + methodSpecificIdAlgo: 'uuid', + }, + }) + + if (!didResult.didState.did) { + throw new Error('No DID created') + } + did = didResult.didState.did + + resourceResult1 = await cheqdDidRegistrar.createResource(resolverAgent.context, did, { + id: utils.uuid(), + name: 'LocalResource', + resourceType: 'test', + data: { hello: 'world' }, + version: '1', + }) + resourceResult2 = await cheqdDidRegistrar.createResource(resolverAgent.context, did, { + id: utils.uuid(), + name: 'LocalResource1', + resourceType: 'test', + data: { hello: 'world' }, + version: '1', + }) + + resourceResult3 = await cheqdDidRegistrar.createResource(resolverAgent.context, did, { + id: utils.uuid(), + name: 'LocalResource2', + resourceType: 'test', + data: { hello: 'world' }, + version: '1', + }) + + for (const resource of [resourceResult1, resourceResult2, resourceResult3]) { + if (resource.resourceState.state !== 'finished') { + throw new Error(`Resource creation failed: ${resource.resourceState.reason}`) + } + } + }) + + afterAll(async () => { + await resolverAgent.shutdown() + await resolverAgent.wallet.delete() + }) + + it('should resolve a did:cheqd did from local testnet', async () => { + const resolveResult = await resolverAgent.dids.resolve(did, { + useLocalCreatedDidRecord: false, + }) + expect(JsonTransformer.toJSON(resolveResult)).toMatchObject({ + didDocument: { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/ed25519-2020/v1'], + id: did, + controller: [did], + verificationMethod: [ + { + controller: did, + id: `${did}#key-1`, + publicKeyMultibase: expect.any(String), + type: 'Ed25519VerificationKey2020', + }, + ], + authentication: [`${did}#key-1`], + }, + didDocumentMetadata: { + created: expect.any(String), + updated: undefined, + deactivated: false, + versionId: expect.any(String), + nextVersionId: '', + }, + didResolutionMetadata: {}, + }) + }) + + it('should getClosestResourceVersion', async () => { + const didResult = await resolverAgent.dids.resolve(did, { + useLocalCreatedDidRecord: false, + }) + + const inFuture = new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 10) // 10 years in future + + // should get the latest resource + let resource = getClosestResourceVersion(didResult.didDocumentMetadata.linkedResourceMetadata, inFuture) + expect(resource).toMatchObject({ + id: resourceResult3.resourceState.resourceId, + }) + + // Date in past should match first created resource + resource = getClosestResourceVersion( + didResult.didDocumentMetadata.linkedResourceMetadata, + new Date('2022-11-16T10:56:34Z') + ) + expect(resource).toMatchObject({ + id: resourceResult1.resourceState.resourceId, + }) + }) +}) diff --git a/packages/cheqd/tests/cheqd-did-utils.test.ts b/packages/cheqd/tests/cheqd-did-utils.test.ts new file mode 100644 index 0000000000..0045879648 --- /dev/null +++ b/packages/cheqd/tests/cheqd-did-utils.test.ts @@ -0,0 +1,48 @@ +import type { DIDDocument } from '@cheqd/sdk' + +import { DidDocument } from '@credo-ts/core' + +import { + createMsgCreateDidDocPayloadToSign, + createMsgDeactivateDidDocPayloadToSign, + validateSpecCompliantPayload, +} from '../src/dids/didCheqdUtil' + +import { validDid, validDidDoc } from './setup' + +describe('Test Cheqd Did Utils', () => { + it('should validate did spec compliant payload', () => { + const didDoc = validDidDoc() + const result = validateSpecCompliantPayload(didDoc) + expect(result.valid).toBe(true) + expect(result.error).toBeUndefined() + }) + + it('should detect invalid verification method', () => { + const result = validateSpecCompliantPayload( + new DidDocument({ + id: validDid, + verificationMethod: [ + { + id: validDid + '#key-1', + publicKeyBase58: 'asca12e3as', + type: 'JsonWebKey2020', + controller: validDid, + }, + ], + }) + ) + expect(result.valid).toBe(false) + expect(result.error).toBeDefined() + }) + + it('should create MsgCreateDidDocPayloadToSign', async () => { + const result = await createMsgCreateDidDocPayloadToSign(validDidDoc().toJSON() as DIDDocument, '1.0') + expect(result).toBeDefined() + }) + + it('should create MsgDeactivateDidDocPayloadToSign', async () => { + const result = createMsgDeactivateDidDocPayloadToSign({ id: validDid }, '2.0') + expect(result).toBeDefined() + }) +}) diff --git a/packages/cheqd/tests/cheqd-sdk-anoncreds-registry.e2e.test.ts b/packages/cheqd/tests/cheqd-sdk-anoncreds-registry.e2e.test.ts new file mode 100644 index 0000000000..148690513b --- /dev/null +++ b/packages/cheqd/tests/cheqd-sdk-anoncreds-registry.e2e.test.ts @@ -0,0 +1,237 @@ +import type { CheqdDidCreateOptions } from '../src' + +import { Agent, JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' + +import { getInMemoryAgentOptions } from '../../core/tests/helpers' +import { CheqdAnonCredsRegistry } from '../src/anoncreds' + +import { cheqdPayerSeeds, getCheqdModules } from './setupCheqdModule' + +const agent = new Agent(getInMemoryAgentOptions('cheqdAnonCredsRegistry', {}, getCheqdModules(cheqdPayerSeeds[2]))) + +const cheqdAnonCredsRegistry = new CheqdAnonCredsRegistry() + +let issuerId: string + +describe('cheqdAnonCredsRegistry', () => { + beforeAll(async () => { + await agent.initialize() + }) + + afterAll(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + // One test as the credential definition depends on the schema + test('register and resolve a schema and credential definition', async () => { + const privateKey = TypedArrayEncoder.fromString('000000000000000000000000000cheqd') + + const did = await agent.dids.create({ + method: 'cheqd', + secret: { + verificationMethod: { + id: 'key-10', + type: 'Ed25519VerificationKey2020', + privateKey, + }, + }, + options: { + network: 'testnet', + methodSpecificIdAlgo: 'uuid', + }, + }) + expect(did.didState).toMatchObject({ state: 'finished' }) + issuerId = did.didState.did as string + + const dynamicVersion = `1.${Math.random() * 100}` + + const schemaResult = await cheqdAnonCredsRegistry.registerSchema(agent.context, { + schema: { + attrNames: ['name'], + issuerId, + name: 'test11', + version: dynamicVersion, + }, + options: {}, + }) + + expect(JsonTransformer.toJSON(schemaResult)).toMatchObject({ + schemaState: { + state: 'finished', + schema: { + attrNames: ['name'], + issuerId, + name: 'test11', + version: dynamicVersion, + }, + schemaId: `${schemaResult.schemaState.schemaId}`, + }, + registrationMetadata: {}, + schemaMetadata: {}, + }) + + const schemaResponse = await cheqdAnonCredsRegistry.getSchema(agent.context, `${schemaResult.schemaState.schemaId}`) + expect(schemaResponse).toMatchObject({ + schema: { + attrNames: ['name'], + name: 'test11', + version: dynamicVersion, + issuerId, + }, + schemaId: `${schemaResult.schemaState.schemaId}`, + resolutionMetadata: {}, + schemaMetadata: {}, + }) + + const credentialDefinitionResult = await cheqdAnonCredsRegistry.registerCredentialDefinition(agent.context, { + credentialDefinition: { + issuerId, + tag: 'TAG', + schemaId: `${schemaResult.schemaState.schemaId}`, + type: 'CL', + value: { + primary: { + n: '92511867718854414868106363741369833735017762038454769060600859608405811709675033445666654908195955460485998711087020152978597220168927505650092431295783175164390266561239892662085428655566792056852960599485298025843840058914610127716620252006466964070280255168745873592143068949458568751438337748294055976926080232538440619420568859737673474560851456027625679328271511966332808025880807996449998057729417608399774744254122385012832309402226532031122728445959276178939234308090390331654445053482963947804769291501664200141562885660084823885847247231002821472258218384342423605116504024514572826071246440130942849549441', + s: '80388543865249952799447792504739237616187770512259677275061283897050980768551818104137338144380636412773836688624071360386172349725818126495487584981520630638409717065318132420766896092370913800616033623618952639023946750307405126873476182540669638841562357523429245685476919178722373320218824590869735129801004394337640642997250464303104754942997839179333543643110326022824394934965538190976474473353762308333205671176627192797138375084260446324344637548455228161138089974447059481109651156379803576163576511072261388342837813901850712083922506433336723723235701670225584863772222447543742649328218950436824219992164', + r: { + age: '676933340341980399002624386891134393471002096508227567343731826159610079436978196421307099268754545293545727546242372579987825752872485684085629459107300175443328323289748793060894500514926703654606851666031895448970879827423190730510730624784665299646624113512701254199984520803796529034094958026048762178753193812250643294518237843809104055653333871102658177900702978008644780459400512716361564897282969982554031820285585105004870317861287847206222714589633178648982299799311192432563797220854755882933052881306804544233529886513105815543097685128456041780804442879272476590077760678785460726492895806240870944398', + master_secret: + '57770757113548032970308439965749734133430520933173186296299026579579930337912607419798836831937319372744879560676750427054135869214212225572618340088847222727882935159356459822445182287686057012197046378986248048722180093079919306125315662058290895629438767985427829790980355162853804522854494960613869765167538645624719923127052541372069255024631093663068055100579264049925388231368871107383977060590248865498902704546409806115171120555709438784189721957301548212242748685629860268468247494986146122636455769804467583612610341632602695197189514316033637331733820369170763954604394734655429769801516997967996980978751', + }, + rctxt: + '19574881057684356733946284215946569464410211018678168661028327420122678446653210056362495902735819742274128834330867933095119512313591151219353395069123546495720010325822330866859140765940839241212947354612836044244554152389691282543839111284006009168728161183863936810142428875817934316327118674532328892591410224676539770085459540786747902789677759379901079898127879301595929571621032704093287675668250862222728331030586585586110859977896767318814398026750215625180255041545607499673023585546720788973882263863911222208020438685873501025545464213035270207099419236974668665979962146355749687924650853489277747454993', + z: '18569464356833363098514177097771727133940629758890641648661259687745137028161881113251218061243607037717553708179509640909238773964066423807945164288256211132195919975343578956381001087353353060599758005375631247614777454313440511375923345538396573548499287265163879524050255226779884271432737062283353279122281220812931572456820130441114446870167673796490210349453498315913599982158253821945225264065364670730546176140788405935081171854642125236557475395879246419105888077042924382595999612137336915304205628167917473420377397118829734604949103124514367857266518654728464539418834291071874052392799652266418817991437', + }, + }, + }, + options: {}, + }) + + expect(credentialDefinitionResult).toMatchObject({ + credentialDefinitionState: { + credentialDefinition: { + issuerId, + tag: 'TAG', + schemaId: `${schemaResult.schemaState.schemaId}`, + type: 'CL', + value: { + primary: { + n: '92511867718854414868106363741369833735017762038454769060600859608405811709675033445666654908195955460485998711087020152978597220168927505650092431295783175164390266561239892662085428655566792056852960599485298025843840058914610127716620252006466964070280255168745873592143068949458568751438337748294055976926080232538440619420568859737673474560851456027625679328271511966332808025880807996449998057729417608399774744254122385012832309402226532031122728445959276178939234308090390331654445053482963947804769291501664200141562885660084823885847247231002821472258218384342423605116504024514572826071246440130942849549441', + s: '80388543865249952799447792504739237616187770512259677275061283897050980768551818104137338144380636412773836688624071360386172349725818126495487584981520630638409717065318132420766896092370913800616033623618952639023946750307405126873476182540669638841562357523429245685476919178722373320218824590869735129801004394337640642997250464303104754942997839179333543643110326022824394934965538190976474473353762308333205671176627192797138375084260446324344637548455228161138089974447059481109651156379803576163576511072261388342837813901850712083922506433336723723235701670225584863772222447543742649328218950436824219992164', + r: { + age: '676933340341980399002624386891134393471002096508227567343731826159610079436978196421307099268754545293545727546242372579987825752872485684085629459107300175443328323289748793060894500514926703654606851666031895448970879827423190730510730624784665299646624113512701254199984520803796529034094958026048762178753193812250643294518237843809104055653333871102658177900702978008644780459400512716361564897282969982554031820285585105004870317861287847206222714589633178648982299799311192432563797220854755882933052881306804544233529886513105815543097685128456041780804442879272476590077760678785460726492895806240870944398', + master_secret: + '57770757113548032970308439965749734133430520933173186296299026579579930337912607419798836831937319372744879560676750427054135869214212225572618340088847222727882935159356459822445182287686057012197046378986248048722180093079919306125315662058290895629438767985427829790980355162853804522854494960613869765167538645624719923127052541372069255024631093663068055100579264049925388231368871107383977060590248865498902704546409806115171120555709438784189721957301548212242748685629860268468247494986146122636455769804467583612610341632602695197189514316033637331733820369170763954604394734655429769801516997967996980978751', + }, + rctxt: + '19574881057684356733946284215946569464410211018678168661028327420122678446653210056362495902735819742274128834330867933095119512313591151219353395069123546495720010325822330866859140765940839241212947354612836044244554152389691282543839111284006009168728161183863936810142428875817934316327118674532328892591410224676539770085459540786747902789677759379901079898127879301595929571621032704093287675668250862222728331030586585586110859977896767318814398026750215625180255041545607499673023585546720788973882263863911222208020438685873501025545464213035270207099419236974668665979962146355749687924650853489277747454993', + z: '18569464356833363098514177097771727133940629758890641648661259687745137028161881113251218061243607037717553708179509640909238773964066423807945164288256211132195919975343578956381001087353353060599758005375631247614777454313440511375923345538396573548499287265163879524050255226779884271432737062283353279122281220812931572456820130441114446870167673796490210349453498315913599982158253821945225264065364670730546176140788405935081171854642125236557475395879246419105888077042924382595999612137336915304205628167917473420377397118829734604949103124514367857266518654728464539418834291071874052392799652266418817991437', + }, + }, + }, + credentialDefinitionId: `${credentialDefinitionResult.credentialDefinitionState.credentialDefinitionId}`, + state: 'finished', + }, + }) + + const credentialDefinitionResponse = await cheqdAnonCredsRegistry.getCredentialDefinition( + agent.context, + credentialDefinitionResult.credentialDefinitionState.credentialDefinitionId as string + ) + + expect(credentialDefinitionResponse).toMatchObject({ + credentialDefinitionId: `${credentialDefinitionResult.credentialDefinitionState.credentialDefinitionId}`, + credentialDefinition: { + issuerId, + schemaId: `${schemaResult.schemaState.schemaId}`, + tag: 'TAG', + type: 'CL', + value: { + primary: { + n: '92511867718854414868106363741369833735017762038454769060600859608405811709675033445666654908195955460485998711087020152978597220168927505650092431295783175164390266561239892662085428655566792056852960599485298025843840058914610127716620252006466964070280255168745873592143068949458568751438337748294055976926080232538440619420568859737673474560851456027625679328271511966332808025880807996449998057729417608399774744254122385012832309402226532031122728445959276178939234308090390331654445053482963947804769291501664200141562885660084823885847247231002821472258218384342423605116504024514572826071246440130942849549441', + r: { + age: '676933340341980399002624386891134393471002096508227567343731826159610079436978196421307099268754545293545727546242372579987825752872485684085629459107300175443328323289748793060894500514926703654606851666031895448970879827423190730510730624784665299646624113512701254199984520803796529034094958026048762178753193812250643294518237843809104055653333871102658177900702978008644780459400512716361564897282969982554031820285585105004870317861287847206222714589633178648982299799311192432563797220854755882933052881306804544233529886513105815543097685128456041780804442879272476590077760678785460726492895806240870944398', + master_secret: + '57770757113548032970308439965749734133430520933173186296299026579579930337912607419798836831937319372744879560676750427054135869214212225572618340088847222727882935159356459822445182287686057012197046378986248048722180093079919306125315662058290895629438767985427829790980355162853804522854494960613869765167538645624719923127052541372069255024631093663068055100579264049925388231368871107383977060590248865498902704546409806115171120555709438784189721957301548212242748685629860268468247494986146122636455769804467583612610341632602695197189514316033637331733820369170763954604394734655429769801516997967996980978751', + }, + rctxt: + '19574881057684356733946284215946569464410211018678168661028327420122678446653210056362495902735819742274128834330867933095119512313591151219353395069123546495720010325822330866859140765940839241212947354612836044244554152389691282543839111284006009168728161183863936810142428875817934316327118674532328892591410224676539770085459540786747902789677759379901079898127879301595929571621032704093287675668250862222728331030586585586110859977896767318814398026750215625180255041545607499673023585546720788973882263863911222208020438685873501025545464213035270207099419236974668665979962146355749687924650853489277747454993', + s: '80388543865249952799447792504739237616187770512259677275061283897050980768551818104137338144380636412773836688624071360386172349725818126495487584981520630638409717065318132420766896092370913800616033623618952639023946750307405126873476182540669638841562357523429245685476919178722373320218824590869735129801004394337640642997250464303104754942997839179333543643110326022824394934965538190976474473353762308333205671176627192797138375084260446324344637548455228161138089974447059481109651156379803576163576511072261388342837813901850712083922506433336723723235701670225584863772222447543742649328218950436824219992164', + z: '18569464356833363098514177097771727133940629758890641648661259687745137028161881113251218061243607037717553708179509640909238773964066423807945164288256211132195919975343578956381001087353353060599758005375631247614777454313440511375923345538396573548499287265163879524050255226779884271432737062283353279122281220812931572456820130441114446870167673796490210349453498315913599982158253821945225264065364670730546176140788405935081171854642125236557475395879246419105888077042924382595999612137336915304205628167917473420377397118829734604949103124514367857266518654728464539418834291071874052392799652266418817991437', + }, + }, + }, + }) + }) + + // Should not resolve invalid schema + test('false test cases', async () => { + const invalidSchemaResourceId = + 'did:cheqd:testnet:d8ac0372-0d4b-413e-8ef5-8e8f07822b2c/resources/ffd001c2-1f80-4cd8-84b2-945fba309457' + const schemaResponse = await cheqdAnonCredsRegistry.getSchema(agent.context, `${invalidSchemaResourceId}`) + + expect(schemaResponse).toMatchObject({ + resolutionMetadata: { + error: 'notFound', + }, + schemaMetadata: {}, + }) + }) + + // Should resolve query based url + test('resolve query based url', async () => { + const schemaResourceId = `${issuerId}?resourceName=test11-Schema&resourceType=anonCredsSchema` + + const schemaResponse = await cheqdAnonCredsRegistry.getSchema(agent.context, schemaResourceId) + expect(schemaResponse).toMatchObject({ + schema: { + attrNames: ['name'], + name: 'test11', + }, + }) + }) + + // TODO: re-add once we support registering revocation registries and revocation status lists + // Should resolve revocationRegistryDefinition and statusList + xtest('resolve revocation registry definition and statusList', async () => { + const revocationRegistryId = 'did:cheqd:testnet:e42ccb8b-78e8-4e54-9d11-f375153d63f8?resourceName=universityDegree' + const revocationDefinitionResponse = await cheqdAnonCredsRegistry.getRevocationRegistryDefinition( + agent.context, + revocationRegistryId + ) + + expect(revocationDefinitionResponse.revocationRegistryDefinition).toMatchObject({ + revocDefType: 'CL_ACCUM', + credDefId: 'did:cheqd:mainnet:zF7rhDBfUt9d1gJPjx7s1J/resources/77465164-5646-42d9-9a0a-f7b2dcb855c0', + tag: '2.0', + value: { + publicKeys: { + accumKey: { + z: '1 08C6E71D1CE1D1690AED25BC769646538BEC69600829CE1FB7AA788479E0B878 1 026909513F9901655B3F9153071DB43A846418F00F305BA25FE851730ED41102 1 10E9D5438AE95AE2BED78A33716BFF923A0F4CA980A9A599C25A24A2295658DA 1 0A04C318A0DFD29ABB1F1D8DD697999F9B89D6682272C591B586D53F8A9D3DC4 1 0501E5FFCE863E08D209C2FA7B390A5AA91F462BB71957CF8DB41EACDC9EB222 1 14BED208817ACB398D8476212C987E7FF77265A72F145EF2853DDB631758AED4 1 180774B2F67179FB62BD452A15F6C034599DA7BF45CC15AA2138212B53A0C110 1 00A0B87DDFFC047BE07235DD11D31226A9F5FA1E03D49C03843AA36A8AF68194 1 10218703955E0B53DB93A8D2D593EB8120A9C9739F127325CB0865ECA4B2B42F 1 08685A263CD0A045FD845AAC6DAA0FDDAAD0EC222C1A0286799B69F37CD75919 1 1FA3D27E70C185C1A16D9A83D3EE7D8CACE727A99C882EE649F87BD52E9EEE47 1 054704706B95A154F5AFC3FBB536D38DC9DCB9702EA0BFDCCB2E36A3AA23F3EC', + }, + }, + maxCredNum: 666, + tailsLocation: 'https://my.revocations.tails/tailsfile.txt', + tailsHash: '91zvq2cFmBZmHCcLqFyzv7bfehHH5rMhdAG5wTjqy2PE', + }, + }) + + const revocationStatusListResponse = await cheqdAnonCredsRegistry.getRevocationStatusList( + agent.context, + revocationRegistryId, + 1680789403 + ) + + expect(revocationStatusListResponse.revocationStatusList).toMatchObject({ + revRegDefId: `${revocationRegistryId}&resourceType=anonCredsRevocRegDef`, + revocationList: [ + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + currentAccumulator: + '21 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C', + }) + }) +}) diff --git a/packages/cheqd/tests/setup.ts b/packages/cheqd/tests/setup.ts new file mode 100644 index 0000000000..f9b3e68f1a --- /dev/null +++ b/packages/cheqd/tests/setup.ts @@ -0,0 +1,33 @@ +jest.setTimeout(60000) + +import { DidDocument, DidDocumentService, VerificationMethod } from '@credo-ts/core' + +export const validDid = 'did:cheqd:testnet:SiVQgrFZ7jFZFrTGstT4ZD' + +export function validVerificationMethod(did: string) { + return new VerificationMethod({ + id: did + '#key-1', + type: 'Ed25519VerificationKey2020', + controller: did, + publicKeyMultibase: 'z6MkkBaWtQKyx7Mr54XaXyMAEpNKqphK4x7ztuBpSfR6Wqwr', + }) +} + +export function validService(did: string) { + return new DidDocumentService({ + id: did + '#service-1', + type: 'CustomType', + serviceEndpoint: 'https://rand.io', + }) +} + +export function validDidDoc() { + const service = [validService(validDid)] + const verificationMethod = [validVerificationMethod(validDid)] + + return new DidDocument({ + id: validDid, + verificationMethod, + service, + }) +} diff --git a/packages/cheqd/tests/setupCheqdModule.ts b/packages/cheqd/tests/setupCheqdModule.ts new file mode 100644 index 0000000000..640a1aba54 --- /dev/null +++ b/packages/cheqd/tests/setupCheqdModule.ts @@ -0,0 +1,39 @@ +import type { CheqdModuleConfigOptions } from '../src' + +import { DidsModule } from '@credo-ts/core' + +import { CheqdModule, CheqdDidRegistrar, CheqdDidResolver } from '../src' + +export const cheqdPayerSeeds = [ + 'sketch mountain erode window enact net enrich smoke claim kangaroo another visual write meat latin bacon pulp similar forum guilt father state erase bright', + + // cheqd1yeahnxhfa583wwpm9xt452xzet4xsgsqacgjkr + 'silk theme damp share lens select artefact orbit artwork weather mixture alarm remain oppose own wolf reduce melody cheap venture lady spy wise loud', + + // cheqd14y3xeqd2xmhl9sxn8cf974k6nntqrveufqpqrs + 'lobster pizza cost soft else rather rich find rose pride catch bar cube switch help joy stable dirt stumble voyage bind cabbage cram exist', + + // cheqd10qh2vl0jrax6yh2mzes03cm6vt27vd47geu375 + 'state online hedgehog turtle daring lab panda bottom agent pottery mixture venue letter decade bridge win snake mandate trust village emerge awkward fire mimic', +] as const + +export const getCheqdModuleConfig = (seed?: string, rpcUrl?: string) => + ({ + networks: [ + { + rpcUrl: rpcUrl || 'http://localhost:26657', + network: 'testnet', + cosmosPayerSeed: + seed || + 'sketch mountain erode window enact net enrich smoke claim kangaroo another visual write meat latin bacon pulp similar forum guilt father state erase bright', + }, + ], + } satisfies CheqdModuleConfigOptions) + +export const getCheqdModules = (seed?: string, rpcUrl?: string) => ({ + cheqdSdk: new CheqdModule(getCheqdModuleConfig(seed, rpcUrl)), + dids: new DidsModule({ + registrars: [new CheqdDidRegistrar()], + resolvers: [new CheqdDidResolver()], + }), +}) diff --git a/packages/cheqd/tsconfig.build.json b/packages/cheqd/tsconfig.build.json new file mode 100644 index 0000000000..6f5bc91833 --- /dev/null +++ b/packages/cheqd/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build", + "baseUrl": ".", + "skipDefaultLibCheck": true, + "paths": { + "@credo-ts/*": ["../*/src"] + } + }, + "include": ["src/**/*"], + "exclude": ["../core"] +} diff --git a/packages/cheqd/tsconfig.json b/packages/cheqd/tsconfig.json new file mode 100644 index 0000000000..7958700c2b --- /dev/null +++ b/packages/cheqd/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"], + "moduleResolution": "node", + "resolveJsonModule": true + } +} diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md new file mode 100644 index 0000000000..c1178790db --- /dev/null +++ b/packages/core/CHANGELOG.md @@ -0,0 +1,673 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release + +## 0.5.5 + +### Patch Changes + +- 3239ef3: pex query fix +- d548fa4: feat: support new 'DIDCommMessaging' didcomm v2 service type (in addition to older 'DIDComm' service type) +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +### Bug Fixes + +- allow did document for didcomm without authentication or keyAgreement ([#1848](https://github.com/openwallet-foundation/credo-ts/issues/1848)) ([5d986f0](https://github.com/openwallet-foundation/credo-ts/commit/5d986f0da67de78b4df2ad7ab92eeb2bdf9f2c83)) +- store recipient keys by default ([#1847](https://github.com/openwallet-foundation/credo-ts/issues/1847)) ([e9238cf](https://github.com/openwallet-foundation/credo-ts/commit/e9238cfde4d76c5b927f6f76b3529d4c80808a3a)) + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +### Bug Fixes + +- oid4vp can be used separate from idtoken ([#1827](https://github.com/openwallet-foundation/credo-ts/issues/1827)) ([ca383c2](https://github.com/openwallet-foundation/credo-ts/commit/ca383c284e2073992a1fd280fca99bee1c2e19f8)) +- remove mediation keys after hangup ([#1843](https://github.com/openwallet-foundation/credo-ts/issues/1843)) ([9c3b950](https://github.com/openwallet-foundation/credo-ts/commit/9c3b9507ec5e33d155cebf9fab97703267b549bd)) + +### Features + +- add disclosures so you know which fields are disclosed ([#1834](https://github.com/openwallet-foundation/credo-ts/issues/1834)) ([6ec43eb](https://github.com/openwallet-foundation/credo-ts/commit/6ec43eb1f539bd8d864d5bbd2ab35459809255ec)) +- apply new version of SD JWT package ([#1787](https://github.com/openwallet-foundation/credo-ts/issues/1787)) ([b41e158](https://github.com/openwallet-foundation/credo-ts/commit/b41e158098773d2f59b5b5cfb82cc6be06a57acd)) +- did rotate event ([#1840](https://github.com/openwallet-foundation/credo-ts/issues/1840)) ([d16bebb](https://github.com/openwallet-foundation/credo-ts/commit/d16bebb7d63bfbad90cedea3c6b4fb3ec20a4be1)) +- queued messages reception time ([#1824](https://github.com/openwallet-foundation/credo-ts/issues/1824)) ([0b4b8dd](https://github.com/openwallet-foundation/credo-ts/commit/0b4b8dd42117eb8e92fcc4be695ff149b49a06c7)) +- support invitationDid when creating an invitation ([#1811](https://github.com/openwallet-foundation/credo-ts/issues/1811)) ([e5c6698](https://github.com/openwallet-foundation/credo-ts/commit/e5c66988e75fd9a5f047fd96774c0bf494061cbc)) + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +### Bug Fixes + +- **openid4vc:** several fixes and improvements ([#1795](https://github.com/openwallet-foundation/credo-ts/issues/1795)) ([b83c517](https://github.com/openwallet-foundation/credo-ts/commit/b83c5173070594448d92f801331b3a31c7ac8049)) +- remove strict w3c subjectId uri validation ([#1805](https://github.com/openwallet-foundation/credo-ts/issues/1805)) ([65f7611](https://github.com/openwallet-foundation/credo-ts/commit/65f7611b7668d3242b4526831f442c68d6cfbea8)) +- unsubscribe from emitter after pickup completion ([#1806](https://github.com/openwallet-foundation/credo-ts/issues/1806)) ([9fb6ae0](https://github.com/openwallet-foundation/credo-ts/commit/9fb6ae0005f11197eefdb864aa8a7cf3b79357f0)) + +### Features + +- credentials api decline offer report ([#1800](https://github.com/openwallet-foundation/credo-ts/issues/1800)) ([15c62a8](https://github.com/openwallet-foundation/credo-ts/commit/15c62a8e20df7189ae8068e3ff42bf7e20a38ad5)) + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +### Bug Fixes + +- abandon proof protocol if presentation fails ([#1610](https://github.com/openwallet-foundation/credo-ts/issues/1610)) ([b2ba7c7](https://github.com/openwallet-foundation/credo-ts/commit/b2ba7c7197139e780cbb95eed77dc0a2ad3b3210)) +- **core:** allow string for did document controller ([#1644](https://github.com/openwallet-foundation/credo-ts/issues/1644)) ([ed874ce](https://github.com/openwallet-foundation/credo-ts/commit/ed874ce38ed1a1a0f01b12958e5b14823661b06a)) +- **core:** query credential and proof records by correct DIDComm role ([#1780](https://github.com/openwallet-foundation/credo-ts/issues/1780)) ([add7e09](https://github.com/openwallet-foundation/credo-ts/commit/add7e091e845fdaddaf604335f19557f47a31079)) +- did:peer:2 creation and parsing ([#1752](https://github.com/openwallet-foundation/credo-ts/issues/1752)) ([7c60918](https://github.com/openwallet-foundation/credo-ts/commit/7c609183b2da16f2a698646ac39b03c2ab44318e)) +- jsonld document loader node 18 ([#1454](https://github.com/openwallet-foundation/credo-ts/issues/1454)) ([3656d49](https://github.com/openwallet-foundation/credo-ts/commit/3656d4902fb832e5e75142b1846074d4f39c11a2)) +- **present-proof:** isolated tests ([#1696](https://github.com/openwallet-foundation/credo-ts/issues/1696)) ([1d33377](https://github.com/openwallet-foundation/credo-ts/commit/1d333770dcc9e261446b43b5f4cd5626fa7ac4a7)) +- presentation submission format ([#1792](https://github.com/openwallet-foundation/credo-ts/issues/1792)) ([1a46e9f](https://github.com/openwallet-foundation/credo-ts/commit/1a46e9f02599ed8b2bf36f5b9d3951d143852f03)) +- properly print key class ([#1684](https://github.com/openwallet-foundation/credo-ts/issues/1684)) ([99b801d](https://github.com/openwallet-foundation/credo-ts/commit/99b801dfb6edcd3b7baaa8108ad361be4e05ff67)) +- query the record by credential and proof role ([#1784](https://github.com/openwallet-foundation/credo-ts/issues/1784)) ([d2b5cd9](https://github.com/openwallet-foundation/credo-ts/commit/d2b5cd9cbbfa95cbdcde9a4fed3305bab6161faf)) +- remove check for DifPresentationExchangeService dependency ([#1702](https://github.com/openwallet-foundation/credo-ts/issues/1702)) ([93d9d8b](https://github.com/openwallet-foundation/credo-ts/commit/93d9d8bb3a93e47197a2c01998807523d783b0bf)) +- some log messages ([#1636](https://github.com/openwallet-foundation/credo-ts/issues/1636)) ([d40bfd1](https://github.com/openwallet-foundation/credo-ts/commit/d40bfd1b96001870a3a1553cb9d6faaefe71e364)) +- support all minor versions handshake ([#1711](https://github.com/openwallet-foundation/credo-ts/issues/1711)) ([40063e0](https://github.com/openwallet-foundation/credo-ts/commit/40063e06ff6afc139516459e81e85b36195985ca)) +- w3c anoncreds ([#1791](https://github.com/openwallet-foundation/credo-ts/issues/1791)) ([913596c](https://github.com/openwallet-foundation/credo-ts/commit/913596c4e843855f77a490428c55daac220bc8c6)) +- websocket outbound transport ([#1788](https://github.com/openwallet-foundation/credo-ts/issues/1788)) ([ed06d00](https://github.com/openwallet-foundation/credo-ts/commit/ed06d002c2c3d1f35b6790b8624cda0e506cf7d4)) + +### Features + +- add goal codes to v2 protocols ([#1739](https://github.com/openwallet-foundation/credo-ts/issues/1739)) ([c5c5b85](https://github.com/openwallet-foundation/credo-ts/commit/c5c5b850f27e66f7a2e39acd5fc14267babee208)) +- add Multikey as supported vm type ([#1720](https://github.com/openwallet-foundation/credo-ts/issues/1720)) ([5562cb1](https://github.com/openwallet-foundation/credo-ts/commit/5562cb1751643eee16b4bf3304a5178a394a7f15)) +- add secp256k1 diddoc and verification method ([#1736](https://github.com/openwallet-foundation/credo-ts/issues/1736)) ([f245386](https://github.com/openwallet-foundation/credo-ts/commit/f245386eef2e0daad7a5c948df29625f60a020ea)) +- add some default contexts ([#1741](https://github.com/openwallet-foundation/credo-ts/issues/1741)) ([0bec03c](https://github.com/openwallet-foundation/credo-ts/commit/0bec03c3b97590a1484e8b803401569998655b87)) +- add support for key type k256 ([#1722](https://github.com/openwallet-foundation/credo-ts/issues/1722)) ([22d5bff](https://github.com/openwallet-foundation/credo-ts/commit/22d5bffc939f6644f324f6ddba4c8269212e9dc4)) +- anoncreds w3c migration ([#1744](https://github.com/openwallet-foundation/credo-ts/issues/1744)) ([d7c2bbb](https://github.com/openwallet-foundation/credo-ts/commit/d7c2bbb4fde57cdacbbf1ed40c6bd1423f7ab015)) +- **anoncreds:** issue revocable credentials ([#1427](https://github.com/openwallet-foundation/credo-ts/issues/1427)) ([c59ad59](https://github.com/openwallet-foundation/credo-ts/commit/c59ad59fbe63b6d3760d19030e0f95fb2ea8488a)) +- did rotate ([#1699](https://github.com/openwallet-foundation/credo-ts/issues/1699)) ([adc7d4e](https://github.com/openwallet-foundation/credo-ts/commit/adc7d4ecfea9be5f707ab7b50d19dbe7690c6d25)) +- did:peer:2 and did:peer:4 support in DID Exchange ([#1550](https://github.com/openwallet-foundation/credo-ts/issues/1550)) ([edf493d](https://github.com/openwallet-foundation/credo-ts/commit/edf493dd7e707543af5bbdbf6daba2b02c74158d)) +- **indy-vdr:** register revocation registry definitions and status list ([#1693](https://github.com/openwallet-foundation/credo-ts/issues/1693)) ([ee34fe7](https://github.com/openwallet-foundation/credo-ts/commit/ee34fe71780a0787db96e28575eeedce3b4704bd)) +- **mesage-pickup:** option for awaiting completion ([#1755](https://github.com/openwallet-foundation/credo-ts/issues/1755)) ([faa390f](https://github.com/openwallet-foundation/credo-ts/commit/faa390f2e2bb438596b5d9e3a69e1442f551ff1e)) +- **openid4vc:** persistance and events ([#1793](https://github.com/openwallet-foundation/credo-ts/issues/1793)) ([f4c386a](https://github.com/openwallet-foundation/credo-ts/commit/f4c386a6ccf8adb829cad30b81d524e6ffddb029)) +- optional backup on storage migration ([#1745](https://github.com/openwallet-foundation/credo-ts/issues/1745)) ([81ff63c](https://github.com/openwallet-foundation/credo-ts/commit/81ff63ccf7c71eccf342899d298a780d66045534)) +- **present-proof:** add support for aries RFC 510 ([#1676](https://github.com/openwallet-foundation/credo-ts/issues/1676)) ([40c9bb6](https://github.com/openwallet-foundation/credo-ts/commit/40c9bb6e9efe6cceb62c79d34366edf77ba84b0d)) +- **presentation-exchange:** added PresentationExchangeService ([#1672](https://github.com/openwallet-foundation/credo-ts/issues/1672)) ([50db5c7](https://github.com/openwallet-foundation/credo-ts/commit/50db5c7d207130b80e38ce5d94afb9e3b96f2fb1)) +- **sd-jwt-vc:** Module for Issuer, Holder and verifier ([#1607](https://github.com/openwallet-foundation/credo-ts/issues/1607)) ([ec3182d](https://github.com/openwallet-foundation/credo-ts/commit/ec3182d9934319b761649edb4c80ede2dd46dbd4)) +- support short legacy connectionless invitations ([#1705](https://github.com/openwallet-foundation/credo-ts/issues/1705)) ([34a6c9f](https://github.com/openwallet-foundation/credo-ts/commit/34a6c9f185d7b177956e5e2c5d79408e52915136)) +- **tenants:** support for tenant storage migration ([#1747](https://github.com/openwallet-foundation/credo-ts/issues/1747)) ([12c617e](https://github.com/openwallet-foundation/credo-ts/commit/12c617efb45d20fda8965b9b4da24c92e975c9a2)) + +## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) + +### Bug Fixes + +- **askar:** throw error if imported wallet exists ([#1593](https://github.com/hyperledger/aries-framework-javascript/issues/1593)) ([c2bb2a5](https://github.com/hyperledger/aries-framework-javascript/commit/c2bb2a52f10add35de883c9a27716db01b9028df)) +- **core:** remove node-fetch dependency ([#1578](https://github.com/hyperledger/aries-framework-javascript/issues/1578)) ([9ee2ce7](https://github.com/hyperledger/aries-framework-javascript/commit/9ee2ce7f0913510fc5b36aef1b7eeffb259b4aed)) +- do not send package via outdated session ([#1559](https://github.com/hyperledger/aries-framework-javascript/issues/1559)) ([de6a735](https://github.com/hyperledger/aries-framework-javascript/commit/de6a735a900b6d7444b17d79e63acaca19cb812a)) +- duplicate service ids in connections protocol ([#1589](https://github.com/hyperledger/aries-framework-javascript/issues/1589)) ([dd75be8](https://github.com/hyperledger/aries-framework-javascript/commit/dd75be88c4e257b6ca76868ceaeb3a8b7d67c185)) +- implicit invitation to specific service ([#1592](https://github.com/hyperledger/aries-framework-javascript/issues/1592)) ([4071dc9](https://github.com/hyperledger/aries-framework-javascript/commit/4071dc97b8ca779e6def3711a538ae821e1e513c)) +- log and throw on WebSocket sending errors ([#1573](https://github.com/hyperledger/aries-framework-javascript/issues/1573)) ([11050af](https://github.com/hyperledger/aries-framework-javascript/commit/11050afc7965adfa9b00107ba34abfbe3afaf874)) +- **oob:** support oob with connection and messages ([#1558](https://github.com/hyperledger/aries-framework-javascript/issues/1558)) ([9732ce4](https://github.com/hyperledger/aries-framework-javascript/commit/9732ce436a0ddee8760b02ac5182e216a75176c2)) +- service validation in OOB invitation objects ([#1575](https://github.com/hyperledger/aries-framework-javascript/issues/1575)) ([91a9434](https://github.com/hyperledger/aries-framework-javascript/commit/91a9434efd53ccbaf80f5613cd908913ad3b806b)) +- update tsyringe for ts 5 support ([#1588](https://github.com/hyperledger/aries-framework-javascript/issues/1588)) ([296955b](https://github.com/hyperledger/aries-framework-javascript/commit/296955b3a648416ac6b502da05a10001920af222)) + +### Features + +- allow connection invitation encoded in oob url param ([#1583](https://github.com/hyperledger/aries-framework-javascript/issues/1583)) ([9d789fa](https://github.com/hyperledger/aries-framework-javascript/commit/9d789fa4e9d159312872f45089d73609eb3d6835)) + +## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) + +### Bug Fixes + +- create message subscription first ([#1549](https://github.com/hyperledger/aries-framework-javascript/issues/1549)) ([93276de](https://github.com/hyperledger/aries-framework-javascript/commit/93276debeff1e56c9803e7700875c4254a48236b)) +- force did:key resolver/registrar presence ([#1535](https://github.com/hyperledger/aries-framework-javascript/issues/1535)) ([aaa13dc](https://github.com/hyperledger/aries-framework-javascript/commit/aaa13dc77d6d5133cd02e768e4173462fa65064a)) +- listen to incoming messages on agent initialize not constructor ([#1542](https://github.com/hyperledger/aries-framework-javascript/issues/1542)) ([8f2d593](https://github.com/hyperledger/aries-framework-javascript/commit/8f2d593bcda0bb2d7bea25ad06b9e37784961997)) +- priority sorting for didcomm services ([#1555](https://github.com/hyperledger/aries-framework-javascript/issues/1555)) ([80c37b3](https://github.com/hyperledger/aries-framework-javascript/commit/80c37b30eb9ac3b438288e14c252f79f619dd12f)) +- race condition singleton records ([#1495](https://github.com/hyperledger/aries-framework-javascript/issues/1495)) ([6c2dda5](https://github.com/hyperledger/aries-framework-javascript/commit/6c2dda544bf5f5d3a972a778c389340da6df97c4)) +- **transport:** Use connection in WebSocket ID ([#1551](https://github.com/hyperledger/aries-framework-javascript/issues/1551)) ([8d2057f](https://github.com/hyperledger/aries-framework-javascript/commit/8d2057f3fe6f3ba236ba5a811b57a7256eae92bf)) + +### Features + +- **anoncreds:** auto create link secret ([#1521](https://github.com/hyperledger/aries-framework-javascript/issues/1521)) ([c6f03e4](https://github.com/hyperledger/aries-framework-javascript/commit/c6f03e49d79a33b1c4b459cef11add93dee051d0)) +- oob without handhsake improvements and routing ([#1511](https://github.com/hyperledger/aries-framework-javascript/issues/1511)) ([9e69cf4](https://github.com/hyperledger/aries-framework-javascript/commit/9e69cf441a75bf7a3c5556cf59e730ee3fce8c28)) +- support askar profiles for multi-tenancy ([#1538](https://github.com/hyperledger/aries-framework-javascript/issues/1538)) ([e448a2a](https://github.com/hyperledger/aries-framework-javascript/commit/e448a2a58dddff2cdf80c4549ea2d842a54b43d1)) +- **w3c:** add convenience methods to vc and vp ([#1477](https://github.com/hyperledger/aries-framework-javascript/issues/1477)) ([83cbfe3](https://github.com/hyperledger/aries-framework-javascript/commit/83cbfe38e788366b616dc244fe34cc49a5a4d331)) + +# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) + +### Bug Fixes + +- connection id in sessions for new connections ([#1383](https://github.com/hyperledger/aries-framework-javascript/issues/1383)) ([0351eec](https://github.com/hyperledger/aries-framework-javascript/commit/0351eec52a9f5e581508819df3005be7b995e59e)) +- **connections:** store imageUrl when using DIDExchange ([#1433](https://github.com/hyperledger/aries-framework-javascript/issues/1433)) ([66afda2](https://github.com/hyperledger/aries-framework-javascript/commit/66afda2fe7311977047928e0b1c857ed2c5602c7)) +- **core:** repository event when calling deleteById ([#1356](https://github.com/hyperledger/aries-framework-javascript/issues/1356)) ([953069a](https://github.com/hyperledger/aries-framework-javascript/commit/953069a785f2a6b8d1e11123aab3a09aab1e65ff)) +- create new socket if socket state is 'closing' ([#1337](https://github.com/hyperledger/aries-framework-javascript/issues/1337)) ([da8f2ad](https://github.com/hyperledger/aries-framework-javascript/commit/da8f2ad36c386497b16075790a364faae50fcd47)) +- Emit RoutingCreated event for mediator routing record ([#1445](https://github.com/hyperledger/aries-framework-javascript/issues/1445)) ([4145957](https://github.com/hyperledger/aries-framework-javascript/commit/414595727d611ff774c4f404a4eeea509cf03a71)) +- imports from core ([#1303](https://github.com/hyperledger/aries-framework-javascript/issues/1303)) ([3e02227](https://github.com/hyperledger/aries-framework-javascript/commit/3e02227a7b23677e9886eb1c03d1a3ec154947a9)) +- isNewSocket logic ([#1355](https://github.com/hyperledger/aries-framework-javascript/issues/1355)) ([18abb18](https://github.com/hyperledger/aries-framework-javascript/commit/18abb18316f155d0375af477dedef9cdfdada70e)) +- issuance with unqualified identifiers ([#1431](https://github.com/hyperledger/aries-framework-javascript/issues/1431)) ([de90caf](https://github.com/hyperledger/aries-framework-javascript/commit/de90cafb8d12b7a940f881184cd745c4b5043cbc)) +- jsonld credential format identifier version ([#1412](https://github.com/hyperledger/aries-framework-javascript/issues/1412)) ([c46a6b8](https://github.com/hyperledger/aries-framework-javascript/commit/c46a6b81b8a1e28e05013c27ffe2eeaee4724130)) +- loosen base64 validation ([#1312](https://github.com/hyperledger/aries-framework-javascript/issues/1312)) ([af384e8](https://github.com/hyperledger/aries-framework-javascript/commit/af384e8a92f877c647999f9356b72a8017308230)) +- registered connection problem report message handler ([#1462](https://github.com/hyperledger/aries-framework-javascript/issues/1462)) ([d2d8ee0](https://github.com/hyperledger/aries-framework-javascript/commit/d2d8ee09c4eb6c050660b2bf9973195fd531df18)) +- seed and private key validation and return type in registrars ([#1324](https://github.com/hyperledger/aries-framework-javascript/issues/1324)) ([c0e5339](https://github.com/hyperledger/aries-framework-javascript/commit/c0e5339edfa32df92f23fb9c920796b4b59adf52)) +- set updateAt on records when updating a record ([#1272](https://github.com/hyperledger/aries-framework-javascript/issues/1272)) ([2669d7d](https://github.com/hyperledger/aries-framework-javascript/commit/2669d7dd3d7c0ddfd1108dfd65e6115dd3418500)) +- thread id improvements ([#1311](https://github.com/hyperledger/aries-framework-javascript/issues/1311)) ([229ed1b](https://github.com/hyperledger/aries-framework-javascript/commit/229ed1b9540ca0c9380b5cca6c763fefd6628960)) + +- refactor!: remove Dispatcher.registerMessageHandler (#1354) ([78ecf1e](https://github.com/hyperledger/aries-framework-javascript/commit/78ecf1ed959c9daba1c119d03f4596f1db16c57c)), closes [#1354](https://github.com/hyperledger/aries-framework-javascript/issues/1354) +- refactor!: set default outbound content type to didcomm v1 (#1314) ([4ab3b54](https://github.com/hyperledger/aries-framework-javascript/commit/4ab3b54e9db630a6ba022af6becdd7276692afc5)), closes [#1314](https://github.com/hyperledger/aries-framework-javascript/issues/1314) +- feat!: add data, cache and temp dirs to FileSystem (#1306) ([ff5596d](https://github.com/hyperledger/aries-framework-javascript/commit/ff5596d0631e93746494c017797d0191b6bdb0b1)), closes [#1306](https://github.com/hyperledger/aries-framework-javascript/issues/1306) + +### Features + +- 0.4.0 migration script ([#1392](https://github.com/hyperledger/aries-framework-javascript/issues/1392)) ([bc5455f](https://github.com/hyperledger/aries-framework-javascript/commit/bc5455f7b42612a2b85e504bc6ddd36283a42bfa)) +- add anoncreds-rs package ([#1275](https://github.com/hyperledger/aries-framework-javascript/issues/1275)) ([efe0271](https://github.com/hyperledger/aries-framework-javascript/commit/efe0271198f21f1307df0f934c380f7a5c720b06)) +- Add cheqd-sdk module ([#1334](https://github.com/hyperledger/aries-framework-javascript/issues/1334)) ([b38525f](https://github.com/hyperledger/aries-framework-javascript/commit/b38525f3433e50418ea149949108b4218ac9ba2a)) +- add fetch indy schema method ([#1290](https://github.com/hyperledger/aries-framework-javascript/issues/1290)) ([1d782f5](https://github.com/hyperledger/aries-framework-javascript/commit/1d782f54bbb4abfeb6b6db6cd4f7164501b6c3d9)) +- add initial askar package ([#1211](https://github.com/hyperledger/aries-framework-javascript/issues/1211)) ([f18d189](https://github.com/hyperledger/aries-framework-javascript/commit/f18d1890546f7d66571fe80f2f3fc1fead1cd4c3)) +- add message pickup module ([#1413](https://github.com/hyperledger/aries-framework-javascript/issues/1413)) ([a8439db](https://github.com/hyperledger/aries-framework-javascript/commit/a8439db90fd11e014b457db476e8327b6ced6358)) +- added endpoint setter to agent InitConfig ([#1278](https://github.com/hyperledger/aries-framework-javascript/issues/1278)) ([1d487b1](https://github.com/hyperledger/aries-framework-javascript/commit/1d487b1a7e11b3f18b5229ba580bd035a7f564a0)) +- allow sending problem report when declining a proof request ([#1408](https://github.com/hyperledger/aries-framework-javascript/issues/1408)) ([b35fec4](https://github.com/hyperledger/aries-framework-javascript/commit/b35fec433f8fab513be2b8b6d073f23c6371b2ee)) +- **anoncreds:** add legacy indy credential format ([#1220](https://github.com/hyperledger/aries-framework-javascript/issues/1220)) ([13f3740](https://github.com/hyperledger/aries-framework-javascript/commit/13f374079262168f90ec7de7c3393beb9651295c)) +- **anoncreds:** legacy indy proof format service ([#1283](https://github.com/hyperledger/aries-framework-javascript/issues/1283)) ([c72fd74](https://github.com/hyperledger/aries-framework-javascript/commit/c72fd7416f2c1bc0497a84036e16adfa80585e49)) +- **anoncreds:** store method name in records ([#1387](https://github.com/hyperledger/aries-framework-javascript/issues/1387)) ([47636b4](https://github.com/hyperledger/aries-framework-javascript/commit/47636b4a08ffbfa9a3f2a5a3c5aebda44f7d16c8)) +- **askar:** import/export wallet support for SQLite ([#1377](https://github.com/hyperledger/aries-framework-javascript/issues/1377)) ([19cefa5](https://github.com/hyperledger/aries-framework-javascript/commit/19cefa54596a4e4848bdbe89306a884a5ce2e991)) +- basic message pthid/thid support ([#1381](https://github.com/hyperledger/aries-framework-javascript/issues/1381)) ([f27fb99](https://github.com/hyperledger/aries-framework-javascript/commit/f27fb9921e11e5bcd654611d97d9fa1c446bc2d5)) +- **cache:** add caching interface ([#1229](https://github.com/hyperledger/aries-framework-javascript/issues/1229)) ([25b2bcf](https://github.com/hyperledger/aries-framework-javascript/commit/25b2bcf81648100b572784e4489a288cc9da0557)) +- **core:** add W3cCredentialsApi ([c888736](https://github.com/hyperledger/aries-framework-javascript/commit/c888736cb6b51014e23f5520fbc4074cf0e49e15)) +- default return route ([#1327](https://github.com/hyperledger/aries-framework-javascript/issues/1327)) ([dbfebb4](https://github.com/hyperledger/aries-framework-javascript/commit/dbfebb4720da731dbe11efdccdd061d1da3d1323)) +- **indy-vdr:** add indy-vdr package and indy vdr pool ([#1160](https://github.com/hyperledger/aries-framework-javascript/issues/1160)) ([e8d6ac3](https://github.com/hyperledger/aries-framework-javascript/commit/e8d6ac31a8e18847d99d7998bd7658439e48875b)) +- **oob:** implicit invitations ([#1348](https://github.com/hyperledger/aries-framework-javascript/issues/1348)) ([fd13bb8](https://github.com/hyperledger/aries-framework-javascript/commit/fd13bb87a9ce9efb73bd780bd076b1da867688c5)) +- **openid4vc-client:** openid authorization flow ([#1384](https://github.com/hyperledger/aries-framework-javascript/issues/1384)) ([996c08f](https://github.com/hyperledger/aries-framework-javascript/commit/996c08f8e32e58605408f5ed5b6d8116cea3b00c)) +- **openid4vc-client:** pre-authorized ([#1243](https://github.com/hyperledger/aries-framework-javascript/issues/1243)) ([3d86e78](https://github.com/hyperledger/aries-framework-javascript/commit/3d86e78a4df87869aa5df4e28b79cd91787b61fb)) +- **openid4vc:** jwt format and more crypto ([#1472](https://github.com/hyperledger/aries-framework-javascript/issues/1472)) ([bd4932d](https://github.com/hyperledger/aries-framework-javascript/commit/bd4932d34f7314a6d49097b6460c7570e1ebc7a8)) +- optional routing for legacy connectionless invitation ([#1271](https://github.com/hyperledger/aries-framework-javascript/issues/1271)) ([7f65ba9](https://github.com/hyperledger/aries-framework-javascript/commit/7f65ba999ad1f49065d24966a1d7f3b82264ea55)) +- outbound message send via session ([#1335](https://github.com/hyperledger/aries-framework-javascript/issues/1335)) ([582c711](https://github.com/hyperledger/aries-framework-javascript/commit/582c711728db12b7d38a0be2e9fa78dbf31b34c6)) +- **proofs:** sort credentials based on revocation ([#1225](https://github.com/hyperledger/aries-framework-javascript/issues/1225)) ([0f6d231](https://github.com/hyperledger/aries-framework-javascript/commit/0f6d2312471efab20f560782c171434f907b6b9d)) +- support for did:jwk and p-256, p-384, p-512 ([#1446](https://github.com/hyperledger/aries-framework-javascript/issues/1446)) ([700d3f8](https://github.com/hyperledger/aries-framework-javascript/commit/700d3f89728ce9d35e22519e505d8203a4c9031e)) +- support more key types in jws service ([#1453](https://github.com/hyperledger/aries-framework-javascript/issues/1453)) ([8a3f03e](https://github.com/hyperledger/aries-framework-javascript/commit/8a3f03eb0dffcf46635556defdcebe1d329cf428)) + +### BREAKING CHANGES + +- `Dispatcher.registerMessageHandler` has been removed in favour of `MessageHandlerRegistry.registerMessageHandler`. If you want to register message handlers in an extension module, you can use directly `agentContext.dependencyManager.registerMessageHandlers`. + +Signed-off-by: Ariel Gentile + +- Agent default outbound content type has been changed to DIDComm V1. If you want to use former behaviour, you can do it so by manually setting `didcommMimeType` in `Agent`'s init config: + +``` + const agent = new Agent({ config: { + ... + didCommMimeType: DidCommMimeType.V0 + }, ... }) +``` + +- Agent-produced files will now be divided in different system paths depending on their nature: data, temp and cache. Previously, they were located at a single location, defaulting to a temporary directory. + +If you specified a custom path in `FileSystem` object constructor, you now must provide an object containing `baseDataPath`, `baseTempPath` and `baseCachePath`. They can point to the same path, although it's recommended to specify different path to avoid future file clashes. + +## [0.3.3](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.2...v0.3.3) (2023-01-18) + +### Bug Fixes + +- fix typing issues with typescript 4.9 ([#1214](https://github.com/hyperledger/aries-framework-javascript/issues/1214)) ([087980f](https://github.com/hyperledger/aries-framework-javascript/commit/087980f1adf3ee0bc434ca9782243a62c6124444)) + +### Features + +- adding trust ping events and trust ping command ([#1182](https://github.com/hyperledger/aries-framework-javascript/issues/1182)) ([fd006f2](https://github.com/hyperledger/aries-framework-javascript/commit/fd006f262a91f901e7f8a9c6e6882ea178230005)) +- **indy-sdk:** add indy-sdk package ([#1200](https://github.com/hyperledger/aries-framework-javascript/issues/1200)) ([9933b35](https://github.com/hyperledger/aries-framework-javascript/commit/9933b35a6aa4524caef8a885e71b742cd0d7186b)) + +## [0.3.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.1...v0.3.2) (2023-01-04) + +### Bug Fixes + +- **credentials:** typing if no modules provided ([#1188](https://github.com/hyperledger/aries-framework-javascript/issues/1188)) ([541356e](https://github.com/hyperledger/aries-framework-javascript/commit/541356e866bcd3ce06c69093d8cb6100dca4d09f)) + +## [0.3.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.0...v0.3.1) (2022-12-27) + +### Bug Fixes + +- missing migration script and exports ([#1184](https://github.com/hyperledger/aries-framework-javascript/issues/1184)) ([460510d](https://github.com/hyperledger/aries-framework-javascript/commit/460510db43a7c63fd8dc1c3614be03fd8772f63c)) + +# [0.3.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.5...v0.3.0) (2022-12-22) + +### Bug Fixes + +- **connections:** do not log AgentContext object ([#1085](https://github.com/hyperledger/aries-framework-javascript/issues/1085)) ([ef20f1e](https://github.com/hyperledger/aries-framework-javascript/commit/ef20f1ef420e5345825cc9e79f52ecfb191489fc)) +- **connections:** use new did for each connection from reusable invitation ([#1174](https://github.com/hyperledger/aries-framework-javascript/issues/1174)) ([c0569b8](https://github.com/hyperledger/aries-framework-javascript/commit/c0569b88c27ee7785cf150ee14a5f9ebcc99898b)) +- credential values encoding ([#1157](https://github.com/hyperledger/aries-framework-javascript/issues/1157)) ([0e89e6c](https://github.com/hyperledger/aries-framework-javascript/commit/0e89e6c9f4a3cdbf98c5d85de2e015becdc3e1fc)) +- expose AttachmentData and DiscoverFeaturesEvents ([#1146](https://github.com/hyperledger/aries-framework-javascript/issues/1146)) ([e48f481](https://github.com/hyperledger/aries-framework-javascript/commit/e48f481024810a0eba17e32b995a8db0730bbcb1)) +- expose OutOfBandEvents ([#1151](https://github.com/hyperledger/aries-framework-javascript/issues/1151)) ([3c040b6](https://github.com/hyperledger/aries-framework-javascript/commit/3c040b68e0c8a7f5625df427a2ace28f0223bfbc)) +- invalid injection symbols in W3cCredService ([#786](https://github.com/hyperledger/aries-framework-javascript/issues/786)) ([38cb106](https://github.com/hyperledger/aries-framework-javascript/commit/38cb1065e6fbf46c676c7ad52e160b721cb1b4e6)) +- **problem-report:** proper string interpolation ([#1120](https://github.com/hyperledger/aries-framework-javascript/issues/1120)) ([c4e9679](https://github.com/hyperledger/aries-framework-javascript/commit/c4e96799d8390225ba5aaecced19c79ec1f12fa8)) +- **proofs:** await shouldAutoRespond to correctly handle the check ([#1116](https://github.com/hyperledger/aries-framework-javascript/issues/1116)) ([f294129](https://github.com/hyperledger/aries-framework-javascript/commit/f294129821cd6fcb9b82d875f19cab5a63310b23)) +- remove sensitive information from agent config toJSON() method ([#1112](https://github.com/hyperledger/aries-framework-javascript/issues/1112)) ([427a80f](https://github.com/hyperledger/aries-framework-javascript/commit/427a80f7759e029222119cf815a866fe9899a170)) +- **routing:** add connection type on mediation grant ([#1147](https://github.com/hyperledger/aries-framework-javascript/issues/1147)) ([979c695](https://github.com/hyperledger/aries-framework-javascript/commit/979c69506996fb1853e200b53d052d474f497bf1)) +- **routing:** async message pickup on init ([#1093](https://github.com/hyperledger/aries-framework-javascript/issues/1093)) ([15cfd91](https://github.com/hyperledger/aries-framework-javascript/commit/15cfd91d1c6ba8e3f8355db4c4941fcbd85382ac)) +- unable to resolve nodejs document loader in react native environment ([#1003](https://github.com/hyperledger/aries-framework-javascript/issues/1003)) ([5cdcfa2](https://github.com/hyperledger/aries-framework-javascript/commit/5cdcfa203e6d457f74250028678dbc3393d8eb5c)) +- use custom document loader in jsonld.frame ([#1119](https://github.com/hyperledger/aries-framework-javascript/issues/1119)) ([36d4656](https://github.com/hyperledger/aries-framework-javascript/commit/36d465669c6714b00167b17fe2924f3c53b5fa68)) +- **vc:** change pubKey input from Buffer to Uint8Array ([#935](https://github.com/hyperledger/aries-framework-javascript/issues/935)) ([80c3740](https://github.com/hyperledger/aries-framework-javascript/commit/80c3740f625328125fe8121035f2d83ce1dee6a5)) + +- refactor!: rename Handler to MessageHandler (#1161) ([5e48696](https://github.com/hyperledger/aries-framework-javascript/commit/5e48696ec16d88321f225628e6cffab243718b4c)), closes [#1161](https://github.com/hyperledger/aries-framework-javascript/issues/1161) +- feat!: use did:key in protocols by default (#1149) ([9f10da8](https://github.com/hyperledger/aries-framework-javascript/commit/9f10da85d8739f7be6c5e6624ba5f53a1d6a3116)), closes [#1149](https://github.com/hyperledger/aries-framework-javascript/issues/1149) +- feat(action-menu)!: move to separate package (#1049) ([e0df0d8](https://github.com/hyperledger/aries-framework-javascript/commit/e0df0d884b1a7816c7c638406606e45f6e169ff4)), closes [#1049](https://github.com/hyperledger/aries-framework-javascript/issues/1049) +- feat(question-answer)!: separate logic to a new module (#1040) ([97d3073](https://github.com/hyperledger/aries-framework-javascript/commit/97d3073aa9300900740c3e8aee8233d38849293d)), closes [#1040](https://github.com/hyperledger/aries-framework-javascript/issues/1040) +- feat!: agent module registration api (#955) ([82a17a3](https://github.com/hyperledger/aries-framework-javascript/commit/82a17a3a1eff61008b2e91695f6527501fe44237)), closes [#955](https://github.com/hyperledger/aries-framework-javascript/issues/955) +- feat!: Discover Features V2 (#991) ([273e353](https://github.com/hyperledger/aries-framework-javascript/commit/273e353f4b36ab5d2420356eb3a53dcfb1c59ec6)), closes [#991](https://github.com/hyperledger/aries-framework-javascript/issues/991) +- refactor!: module to api and module config (#943) ([7cbccb1](https://github.com/hyperledger/aries-framework-javascript/commit/7cbccb1ce9dae2cb1e4887220898f2f74cca8dbe)), closes [#943](https://github.com/hyperledger/aries-framework-javascript/issues/943) +- refactor!: add agent context (#920) ([b47cfcb](https://github.com/hyperledger/aries-framework-javascript/commit/b47cfcba1450cd1d6839bf8192d977bfe33f1bb0)), closes [#920](https://github.com/hyperledger/aries-framework-javascript/issues/920) + +### Features + +- add agent context provider ([#921](https://github.com/hyperledger/aries-framework-javascript/issues/921)) ([a1b1e5a](https://github.com/hyperledger/aries-framework-javascript/commit/a1b1e5a22fd4ab9ef593b5cd7b3c710afcab3142)) +- add base agent class ([#922](https://github.com/hyperledger/aries-framework-javascript/issues/922)) ([113a575](https://github.com/hyperledger/aries-framework-javascript/commit/113a5756ed1b630b3c05929d79f6afcceae4fa6a)) +- add dynamic suite and signing provider ([#949](https://github.com/hyperledger/aries-framework-javascript/issues/949)) ([ab8b8ef](https://github.com/hyperledger/aries-framework-javascript/commit/ab8b8ef1357c7a8dc338eaea16b20d93a0c92d4f)) +- add indynamespace for ledger id for anoncreds ([#965](https://github.com/hyperledger/aries-framework-javascript/issues/965)) ([df3777e](https://github.com/hyperledger/aries-framework-javascript/commit/df3777ee394211a401940bf27b3e5a9e1688f6b2)) +- add present proof v2 ([#979](https://github.com/hyperledger/aries-framework-javascript/issues/979)) ([f38ac05](https://github.com/hyperledger/aries-framework-javascript/commit/f38ac05875e38b6cc130bcb9f603e82657aabe9c)) +- bbs createKey, sign and verify ([#684](https://github.com/hyperledger/aries-framework-javascript/issues/684)) ([5f91738](https://github.com/hyperledger/aries-framework-javascript/commit/5f91738337fac1efbbb4597e7724791e542f0762)) +- **bbs:** extract bbs logic into separate module ([#1035](https://github.com/hyperledger/aries-framework-javascript/issues/1035)) ([991151b](https://github.com/hyperledger/aries-framework-javascript/commit/991151bfff829fa11cd98a1951be9b54a77385a8)) +- **dids:** add did registrar ([#953](https://github.com/hyperledger/aries-framework-javascript/issues/953)) ([93f3c93](https://github.com/hyperledger/aries-framework-javascript/commit/93f3c93310f9dae032daa04a920b7df18e2f8a65)) +- fetch verification method types by proof type ([#913](https://github.com/hyperledger/aries-framework-javascript/issues/913)) ([ed69dac](https://github.com/hyperledger/aries-framework-javascript/commit/ed69dac7784feea7abe430ad685911faa477fa11)) +- issue credentials v2 (W3C/JSON-LD) ([#1092](https://github.com/hyperledger/aries-framework-javascript/issues/1092)) ([574e6a6](https://github.com/hyperledger/aries-framework-javascript/commit/574e6a62ebbd77902c50da821afdfd1b1558abe7)) +- jsonld-credential support ([#718](https://github.com/hyperledger/aries-framework-javascript/issues/718)) ([ea34c47](https://github.com/hyperledger/aries-framework-javascript/commit/ea34c4752712efecf3367c5a5fc4b06e66c1e9d7)) +- **ledger:** smart schema and credential definition registration ([#900](https://github.com/hyperledger/aries-framework-javascript/issues/900)) ([1e708e9](https://github.com/hyperledger/aries-framework-javascript/commit/1e708e9aeeb63977a7305999a5027d9743a56f91)) +- **oob:** receive Invitation with timeout ([#1156](https://github.com/hyperledger/aries-framework-javascript/issues/1156)) ([9352fa5](https://github.com/hyperledger/aries-framework-javascript/commit/9352fa5eea1e01d29acd0757298398aac45fcab2)) +- **proofs:** add getRequestedCredentialsForProofRequest ([#1028](https://github.com/hyperledger/aries-framework-javascript/issues/1028)) ([26bb9c9](https://github.com/hyperledger/aries-framework-javascript/commit/26bb9c9989a97bf22859a7eccbeabc632521a6c2)) +- **proofs:** delete associated didcomm messages ([#1021](https://github.com/hyperledger/aries-framework-javascript/issues/1021)) ([dba46c3](https://github.com/hyperledger/aries-framework-javascript/commit/dba46c3bc3a1d6b5669f296f0c45cd03dc2294b1)) +- **proofs:** proof negotiation ([#1131](https://github.com/hyperledger/aries-framework-javascript/issues/1131)) ([c752461](https://github.com/hyperledger/aries-framework-javascript/commit/c75246147ffc6be3c815c66b0a7ad66e48996568)) +- **proofs:** proofs module migration script for 0.3.0 ([#1020](https://github.com/hyperledger/aries-framework-javascript/issues/1020)) ([5e9e0fc](https://github.com/hyperledger/aries-framework-javascript/commit/5e9e0fcc7f13b8a27e35761464c8fd970c17d28c)) +- remove keys on mediator when deleting connections ([#1143](https://github.com/hyperledger/aries-framework-javascript/issues/1143)) ([1af57fd](https://github.com/hyperledger/aries-framework-javascript/commit/1af57fde5016300e243eafbbdea5ea26bd8ef313)) +- **routing:** add reconnection parameters to RecipientModuleConfig ([#1070](https://github.com/hyperledger/aries-framework-javascript/issues/1070)) ([d4fd1ae](https://github.com/hyperledger/aries-framework-javascript/commit/d4fd1ae16dc1fd99b043835b97b33f4baece6790)) +- **tenants:** initial tenants module ([#932](https://github.com/hyperledger/aries-framework-javascript/issues/932)) ([7cbd08c](https://github.com/hyperledger/aries-framework-javascript/commit/7cbd08c9bb4b14ab2db92b0546d6fcb520f5fec9)) +- **tenants:** tenant lifecycle ([#942](https://github.com/hyperledger/aries-framework-javascript/issues/942)) ([adfa65b](https://github.com/hyperledger/aries-framework-javascript/commit/adfa65b13152a980ba24b03082446e91d8ec5b37)) +- **vc:** delete w3c credential record ([#886](https://github.com/hyperledger/aries-framework-javascript/issues/886)) ([be37011](https://github.com/hyperledger/aries-framework-javascript/commit/be37011c139c5cc69fc591060319d8c373e9508b)) +- **w3c:** add custom document loader option ([#1159](https://github.com/hyperledger/aries-framework-javascript/issues/1159)) ([ff6abdf](https://github.com/hyperledger/aries-framework-javascript/commit/ff6abdfc4e8ca64dd5a3b9859474bfc09e1a6c21)) + +### BREAKING CHANGES + +- Handler has been renamed to MessageHandler to be more descriptive, along with related types and methods. This means: + +Handler is now MessageHandler +HandlerInboundMessage is now MessageHandlerInboundMessage +Dispatcher.registerHandler is now Dispatcher.registerMessageHandlers + +- `useDidKeyInProtocols` configuration parameter is now enabled by default. If your agent only interacts with modern agents (e.g. Credo 0.2.5 and newer) this will not represent any issue. Otherwise it is safer to explicitly set it to `false`. However, keep in mind that we expect this setting to be deprecated in the future, so we encourage you to update all your agents to use did:key. +- action-menu module has been removed from the core and moved to a separate package. To integrate it in an Agent instance, it can be injected in constructor like this: + +```ts +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + actionMenu: new ActionMenuModule(), + /* other custom modules */ + }, +}) +``` + +Then, module API can be accessed in `agent.modules.actionMenu`. + +- question-answer module has been removed from the core and moved to a separate package. To integrate it in an Agent instance, it can be injected in constructor like this: + +```ts +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + questionAnswer: new QuestionAnswerModule(), + /* other custom modules */ + }, +}) +``` + +Then, module API can be accessed in `agent.modules.questionAnswer`. + +- custom modules have been moved to the .modules namespace. In addition the agent constructor has been updated to a single options object that contains the `config` and `dependencies` properties. Instead of constructing the agent like this: + +```ts +const agent = new Agent( + { + /* config */ + }, + agentDependencies +) +``` + +You should now construct it like this: + +```ts +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, +}) +``` + +This allows for the new custom modules to be defined in the agent constructor. + +- - `queryFeatures` method parameters have been unified to a single `QueryFeaturesOptions` object that requires specification of Discover Features protocol to be used. + +* `isProtocolSupported` has been replaced by the more general synchronous mode of `queryFeatures`, which works when `awaitDisclosures` in options is set. Instead of returning a boolean, it returns an object with matching features +* Custom modules implementing protocols must register them in Feature Registry in order to let them be discovered by other agents (this can be done in module `register(dependencyManager, featureRegistry)` method) + +- All module api classes have been renamed from `XXXModule` to `XXXApi`. A module now represents a module plugin, and is separate from the API of a module. If you previously imported e.g. the `CredentialsModule` class, you should now import the `CredentialsApi` class +- To make AFJ multi-tenancy ready, all services and repositories have been made stateless. A new `AgentContext` is introduced that holds the current context, which is passed to each method call. The public API hasn't been affected, but due to the large impact of this change it is marked as breaking. + +## [0.2.5](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.4...v0.2.5) (2022-10-13) + +### Bug Fixes + +- **oob:** allow encoding in content type header ([#1037](https://github.com/hyperledger/aries-framework-javascript/issues/1037)) ([e1d6592](https://github.com/hyperledger/aries-framework-javascript/commit/e1d6592b818bc4348078ca6593eea4641caafae5)) +- **oob:** set connection alias when creating invitation ([#1047](https://github.com/hyperledger/aries-framework-javascript/issues/1047)) ([7be979a](https://github.com/hyperledger/aries-framework-javascript/commit/7be979a74b86c606db403c8df04cfc8be2aae249)) + +### Features + +- connection type ([#994](https://github.com/hyperledger/aries-framework-javascript/issues/994)) ([0d14a71](https://github.com/hyperledger/aries-framework-javascript/commit/0d14a7157e2118592829109dbc5c793faee1e201)) +- expose findAllByQuery method in modules and services ([#1044](https://github.com/hyperledger/aries-framework-javascript/issues/1044)) ([9dd95e8](https://github.com/hyperledger/aries-framework-javascript/commit/9dd95e81770d3140558196d2b5b508723f918f04)) +- improve sending error handling ([#1045](https://github.com/hyperledger/aries-framework-javascript/issues/1045)) ([a230841](https://github.com/hyperledger/aries-framework-javascript/commit/a230841aa99102bcc8b60aa2a23040f13a929a6c)) +- possibility to set masterSecretId inside of WalletConfig ([#1043](https://github.com/hyperledger/aries-framework-javascript/issues/1043)) ([8a89ad2](https://github.com/hyperledger/aries-framework-javascript/commit/8a89ad2624922e5e5455f8881d1ccc656d6b33ec)) +- use did:key flag ([#1029](https://github.com/hyperledger/aries-framework-javascript/issues/1029)) ([8efade5](https://github.com/hyperledger/aries-framework-javascript/commit/8efade5b2a885f0767ac8b10cba8582fe9ff486a)) + +## [0.2.4](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.3...v0.2.4) (2022-09-10) + +### Bug Fixes + +- avoid crash when an unexpected message arrives ([#1019](https://github.com/hyperledger/aries-framework-javascript/issues/1019)) ([2cfadd9](https://github.com/hyperledger/aries-framework-javascript/commit/2cfadd9167438a9446d26b933aa64521d8be75e7)) +- **ledger:** check taa version instad of aml version ([#1013](https://github.com/hyperledger/aries-framework-javascript/issues/1013)) ([4ca56f6](https://github.com/hyperledger/aries-framework-javascript/commit/4ca56f6b677f45aa96c91b5c5ee8df210722609e)) +- **ledger:** remove poolConnected on pool close ([#1011](https://github.com/hyperledger/aries-framework-javascript/issues/1011)) ([f0ca8b6](https://github.com/hyperledger/aries-framework-javascript/commit/f0ca8b6346385fc8c4811fbd531aa25a386fcf30)) +- **question-answer:** question answer protocol state/role check ([#1001](https://github.com/hyperledger/aries-framework-javascript/issues/1001)) ([4b90e87](https://github.com/hyperledger/aries-framework-javascript/commit/4b90e876cc8377e7518e05445beb1a6b524840c4)) + +### Features + +- Action Menu protocol (Aries RFC 0509) implementation ([#974](https://github.com/hyperledger/aries-framework-javascript/issues/974)) ([60a8091](https://github.com/hyperledger/aries-framework-javascript/commit/60a8091d6431c98f764b2b94bff13ee97187b915)) +- **routing:** add settings to control back off strategy on mediator reconnection ([#1017](https://github.com/hyperledger/aries-framework-javascript/issues/1017)) ([543437c](https://github.com/hyperledger/aries-framework-javascript/commit/543437cd94d3023139b259ee04d6ad51cf653794)) + +## [0.2.3](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.2...v0.2.3) (2022-08-30) + +### Bug Fixes + +- export the KeyDerivationMethod ([#958](https://github.com/hyperledger/aries-framework-javascript/issues/958)) ([04ab1cc](https://github.com/hyperledger/aries-framework-javascript/commit/04ab1cca853284d144fd64d35e26e9dfe77d4a1b)) +- expose oob domain ([#990](https://github.com/hyperledger/aries-framework-javascript/issues/990)) ([dad975d](https://github.com/hyperledger/aries-framework-javascript/commit/dad975d9d9b658c6b37749ece2a91381e2a314c9)) +- **generic-records:** support custom id property ([#964](https://github.com/hyperledger/aries-framework-javascript/issues/964)) ([0f690a0](https://github.com/hyperledger/aries-framework-javascript/commit/0f690a0564a25204cacfae7cd958f660f777567e)) + +### Features + +- always initialize mediator ([#985](https://github.com/hyperledger/aries-framework-javascript/issues/985)) ([b699977](https://github.com/hyperledger/aries-framework-javascript/commit/b69997744ac9e30ffba22daac7789216d2683e36)) +- delete by record id ([#983](https://github.com/hyperledger/aries-framework-javascript/issues/983)) ([d8a30d9](https://github.com/hyperledger/aries-framework-javascript/commit/d8a30d94d336cf3417c2cd00a8110185dde6a106)) +- **ledger:** handle REQNACK response for write request ([#967](https://github.com/hyperledger/aries-framework-javascript/issues/967)) ([6468a93](https://github.com/hyperledger/aries-framework-javascript/commit/6468a9311c8458615871e1e85ba3f3b560453715)) +- OOB public did ([#930](https://github.com/hyperledger/aries-framework-javascript/issues/930)) ([c99f3c9](https://github.com/hyperledger/aries-framework-javascript/commit/c99f3c9152a79ca6a0a24fdc93e7f3bebbb9d084)) +- **proofs:** present proof as nested protocol ([#972](https://github.com/hyperledger/aries-framework-javascript/issues/972)) ([52247d9](https://github.com/hyperledger/aries-framework-javascript/commit/52247d997c5910924d3099c736dd2e20ec86a214)) +- **routing:** manual mediator pickup lifecycle management ([#989](https://github.com/hyperledger/aries-framework-javascript/issues/989)) ([69d4906](https://github.com/hyperledger/aries-framework-javascript/commit/69d4906a0ceb8a311ca6bdad5ed6d2048335109a)) +- **routing:** pickup v2 mediator role basic implementation ([#975](https://github.com/hyperledger/aries-framework-javascript/issues/975)) ([a989556](https://github.com/hyperledger/aries-framework-javascript/commit/a98955666853471d504f8a5c8c4623e18ba8c8ed)) +- **routing:** support promise in message repo ([#959](https://github.com/hyperledger/aries-framework-javascript/issues/959)) ([79c5d8d](https://github.com/hyperledger/aries-framework-javascript/commit/79c5d8d76512b641167bce46e82f34cf22bc285e)) + +## [0.2.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.1...v0.2.2) (2022-07-15) + +### Bug Fixes + +- no return routing and wait for ping ([#946](https://github.com/hyperledger/aries-framework-javascript/issues/946)) ([f48f3c1](https://github.com/hyperledger/aries-framework-javascript/commit/f48f3c18bcc550b5304f43d8564dbeb1192490e0)) + +### Features + +- **oob:** support fetching shortened invitation urls ([#840](https://github.com/hyperledger/aries-framework-javascript/issues/840)) ([60ee0e5](https://github.com/hyperledger/aries-framework-javascript/commit/60ee0e59bbcdf7fab0e5880a714f0ca61d5da508)) +- **routing:** support did:key in RFC0211 ([#950](https://github.com/hyperledger/aries-framework-javascript/issues/950)) ([dc45c01](https://github.com/hyperledger/aries-framework-javascript/commit/dc45c01a27fa68f8caacf3e51382c37f26b1d4fa)) + +## [0.2.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.0...v0.2.1) (2022-07-08) + +### Bug Fixes + +- clone record before emitting event ([#938](https://github.com/hyperledger/aries-framework-javascript/issues/938)) ([f907fe9](https://github.com/hyperledger/aries-framework-javascript/commit/f907fe99558dd77dc2f77696be2a1b846466ab95)) +- missing module exports ([#927](https://github.com/hyperledger/aries-framework-javascript/issues/927)) ([95f90a5](https://github.com/hyperledger/aries-framework-javascript/commit/95f90a5dbe16a90ecb697d164324db20115976ae)) +- **oob:** support legacy prefix in attachments ([#931](https://github.com/hyperledger/aries-framework-javascript/issues/931)) ([82863f3](https://github.com/hyperledger/aries-framework-javascript/commit/82863f326d95025c4c01349a4c14b37e6ff6a1db)) + +### Features + +- **credentials:** added credential sendProblemReport method ([#906](https://github.com/hyperledger/aries-framework-javascript/issues/906)) ([90dc7bb](https://github.com/hyperledger/aries-framework-javascript/commit/90dc7bbdb18a77e62026f4d837723ed9a208c19b)) +- initial plugin api ([#907](https://github.com/hyperledger/aries-framework-javascript/issues/907)) ([6d88aa4](https://github.com/hyperledger/aries-framework-javascript/commit/6d88aa4537ab2a9494ffea8cdfb4723cf915f291)) +- **oob:** allow to append attachments to invitations ([#926](https://github.com/hyperledger/aries-framework-javascript/issues/926)) ([4800700](https://github.com/hyperledger/aries-framework-javascript/commit/4800700e9f138f02e67c93e8882f45d723dd22cb)) +- **routing:** add routing service ([#909](https://github.com/hyperledger/aries-framework-javascript/issues/909)) ([6e51e90](https://github.com/hyperledger/aries-framework-javascript/commit/6e51e9023cca524252f40a18bf37ec81ec582a1a)) + +# [0.2.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.1.0...v0.2.0) (2022-06-24) + +### Bug Fixes + +- add BBS context to DidDoc ([#789](https://github.com/hyperledger/aries-framework-javascript/issues/789)) ([c8ca091](https://github.com/hyperledger/aries-framework-javascript/commit/c8ca091f22c58c8d5273be36908df0a188020ddb)) +- add oob state and role check ([#777](https://github.com/hyperledger/aries-framework-javascript/issues/777)) ([1c74618](https://github.com/hyperledger/aries-framework-javascript/commit/1c7461836578a62ec545de3a0c8fcdc7de2f4d8f)) +- agent isinitialized on shutdown ([#665](https://github.com/hyperledger/aries-framework-javascript/issues/665)) ([d1049e0](https://github.com/hyperledger/aries-framework-javascript/commit/d1049e0fe99665e7fff8c4f1fe89f7ce19ccce84)) +- allow agent without inbound endpoint to connect when using multi-use invitation ([#712](https://github.com/hyperledger/aries-framework-javascript/issues/712)) ([01c5bb3](https://github.com/hyperledger/aries-framework-javascript/commit/01c5bb3b67786fa7efa361d02bfddde7d113eacf)), closes [#483](https://github.com/hyperledger/aries-framework-javascript/issues/483) +- **basic-message:** assert connection is ready ([#657](https://github.com/hyperledger/aries-framework-javascript/issues/657)) ([9f9156c](https://github.com/hyperledger/aries-framework-javascript/commit/9f9156cb96a4e8d7013d4968359bd0858830f833)) +- check for "REQNACK" response from indy ledger ([#626](https://github.com/hyperledger/aries-framework-javascript/issues/626)) ([ce66f07](https://github.com/hyperledger/aries-framework-javascript/commit/ce66f0744976e8f2abfa05055bfa384f3d084321)) +- check proof request group names do not overlap ([#638](https://github.com/hyperledger/aries-framework-javascript/issues/638)) ([0731ccd](https://github.com/hyperledger/aries-framework-javascript/commit/0731ccd7683ab1e0e8057fbf3b909bdd3227da88)) +- clone record before emitting event ([#833](https://github.com/hyperledger/aries-framework-javascript/issues/833)) ([8192861](https://github.com/hyperledger/aries-framework-javascript/commit/819286190985934438cb236e8d3f4ea7145f0cec)) +- close session early if no return route ([#715](https://github.com/hyperledger/aries-framework-javascript/issues/715)) ([2e65408](https://github.com/hyperledger/aries-framework-javascript/commit/2e6540806f2d67bef16004f6e8398c5bf7a05bcf)) +- **connections:** allow ; to convert legacy did ([#882](https://github.com/hyperledger/aries-framework-javascript/issues/882)) ([448a29d](https://github.com/hyperledger/aries-framework-javascript/commit/448a29db44e5ec0b8f01d36ba139ac760654a635)) +- **connections:** didexchange to connection state ([#823](https://github.com/hyperledger/aries-framework-javascript/issues/823)) ([dda1bd3](https://github.com/hyperledger/aries-framework-javascript/commit/dda1bd33882f7915a0ef1720eff0b1804f2c946c)) +- **connections:** fix log of object in string ([#904](https://github.com/hyperledger/aries-framework-javascript/issues/904)) ([95d893e](https://github.com/hyperledger/aries-framework-javascript/commit/95d893e6f37014f14bb991c5f12a9da0f4d627ab)) +- **connections:** set image url in create request ([#896](https://github.com/hyperledger/aries-framework-javascript/issues/896)) ([8396965](https://github.com/hyperledger/aries-framework-javascript/commit/8396965bfb2922bd5606383c12788d9c60968918)) +- **core:** allow JSON as input for indy attributes ([#813](https://github.com/hyperledger/aries-framework-javascript/issues/813)) ([478fda3](https://github.com/hyperledger/aries-framework-javascript/commit/478fda3bb28171ce395bb67f25d2f2e3668c52b0)) +- **core:** error if unpacked message does not match JWE structure ([#639](https://github.com/hyperledger/aries-framework-javascript/issues/639)) ([c43cfaa](https://github.com/hyperledger/aries-framework-javascript/commit/c43cfaa340c6ea8f42f015f6f280cbaece8c58bb)) +- **core:** expose CredentialPreviewAttribute ([#796](https://github.com/hyperledger/aries-framework-javascript/issues/796)) ([65d7f15](https://github.com/hyperledger/aries-framework-javascript/commit/65d7f15cff3384c2f34e9b0c64fab574e6299484)) +- **core:** set tags in MediationRecord constructor ([#686](https://github.com/hyperledger/aries-framework-javascript/issues/686)) ([1b01bce](https://github.com/hyperledger/aries-framework-javascript/commit/1b01bceed3435fc7f92b051110fcc315bcac08f3)) +- credential preview attributes mismatch schema attributes ([#625](https://github.com/hyperledger/aries-framework-javascript/issues/625)) ([c0095b8](https://github.com/hyperledger/aries-framework-javascript/commit/c0095b8ee855514c7b3c01010041e623458eb8de)) +- **credentials:** add missing issue credential v1 proposal attributes ([#798](https://github.com/hyperledger/aries-framework-javascript/issues/798)) ([966cc3d](https://github.com/hyperledger/aries-framework-javascript/commit/966cc3d178be7296f073eb815c36792e2137b64b)) +- **credentials:** default for credentials in exchange record ([#816](https://github.com/hyperledger/aries-framework-javascript/issues/816)) ([df1a00b](https://github.com/hyperledger/aries-framework-javascript/commit/df1a00b0968fa42dbaf606c9ec2325b778a0317d)) +- **credentials:** do not store offer attributes ([#892](https://github.com/hyperledger/aries-framework-javascript/issues/892)) ([39c4c0d](https://github.com/hyperledger/aries-framework-javascript/commit/39c4c0ddee5e8b9563b6f174a8ad808d4b9cf307)) +- **credentials:** indy cred attachment format ([#862](https://github.com/hyperledger/aries-framework-javascript/issues/862)) ([16935e2](https://github.com/hyperledger/aries-framework-javascript/commit/16935e2976252aac6bd67c5000779da1c5c1a828)) +- **credentials:** miscellaneous typing issues ([#880](https://github.com/hyperledger/aries-framework-javascript/issues/880)) ([ad35b08](https://github.com/hyperledger/aries-framework-javascript/commit/ad35b0826b5ee592b64d898fe629391bd34444aa)) +- **credentials:** parse and validate preview [@type](https://github.com/type) ([#861](https://github.com/hyperledger/aries-framework-javascript/issues/861)) ([1cc8f46](https://github.com/hyperledger/aries-framework-javascript/commit/1cc8f4661c666fb49625cf935877ff5e5d88b524)) +- **credentials:** proposal preview attribute ([#855](https://github.com/hyperledger/aries-framework-javascript/issues/855)) ([3022bd2](https://github.com/hyperledger/aries-framework-javascript/commit/3022bd2c37dac381f2045f5afab329bcc3806d26)) +- **credentials:** store revocation identifiers ([#864](https://github.com/hyperledger/aries-framework-javascript/issues/864)) ([7374799](https://github.com/hyperledger/aries-framework-javascript/commit/73747996dab4f7d63f616ebfc9758d0fcdffd3eb)) +- **credentials:** use interface in module api ([#856](https://github.com/hyperledger/aries-framework-javascript/issues/856)) ([58e6603](https://github.com/hyperledger/aries-framework-javascript/commit/58e6603ab925aa1f4f41673452b83ef75b538bdc)) +- delete credentials ([#766](https://github.com/hyperledger/aries-framework-javascript/issues/766)) ([cbdff28](https://github.com/hyperledger/aries-framework-javascript/commit/cbdff28d566e3eaabcb806d9158c62476379b5dd)) +- delete credentials ([#770](https://github.com/hyperledger/aries-framework-javascript/issues/770)) ([f1e0412](https://github.com/hyperledger/aries-framework-javascript/commit/f1e0412200fcc77ba928c0af2b099326f7a47ebf)) +- did sov service type resolving ([#689](https://github.com/hyperledger/aries-framework-javascript/issues/689)) ([dbcd8c4](https://github.com/hyperledger/aries-framework-javascript/commit/dbcd8c4ae88afd12098b55acccb70237a8d54cd7)) +- disallow floating promises ([#704](https://github.com/hyperledger/aries-framework-javascript/issues/704)) ([549647d](https://github.com/hyperledger/aries-framework-javascript/commit/549647db6b7492e593022dff1d4162efd2d95a39)) +- disallow usage of global buffer ([#601](https://github.com/hyperledger/aries-framework-javascript/issues/601)) ([87ecd8c](https://github.com/hyperledger/aries-framework-javascript/commit/87ecd8c622c6b602a23af9fa2ecc50820bce32f8)) +- do not import from src dir ([#748](https://github.com/hyperledger/aries-framework-javascript/issues/748)) ([1dfa32e](https://github.com/hyperledger/aries-framework-javascript/commit/1dfa32edc6029793588040de9b8b933a0615e926)) +- do not import test logger in src ([#746](https://github.com/hyperledger/aries-framework-javascript/issues/746)) ([5c80004](https://github.com/hyperledger/aries-framework-javascript/commit/5c80004228211a338c1358c99921a45c344a33bb)) +- do not use basic message id as record id ([#677](https://github.com/hyperledger/aries-framework-javascript/issues/677)) ([3713398](https://github.com/hyperledger/aries-framework-javascript/commit/3713398b87f732841db8131055d2437b0af9a435)) +- extract indy did from peer did in indy credential request ([#790](https://github.com/hyperledger/aries-framework-javascript/issues/790)) ([09e5557](https://github.com/hyperledger/aries-framework-javascript/commit/09e55574440e63418df0697067b9ffad11936027)) +- incorrect encoding of services for did:peer ([#610](https://github.com/hyperledger/aries-framework-javascript/issues/610)) ([28b1715](https://github.com/hyperledger/aries-framework-javascript/commit/28b1715e388f5ed15cb937712b663627c3619465)) +- **indy:** async ledger connection issues on iOS ([#803](https://github.com/hyperledger/aries-framework-javascript/issues/803)) ([8055652](https://github.com/hyperledger/aries-framework-javascript/commit/8055652e63309cf7b20676119b71d846b295d468)) +- issue where attributes and predicates match ([#640](https://github.com/hyperledger/aries-framework-javascript/issues/640)) ([15a5e6b](https://github.com/hyperledger/aries-framework-javascript/commit/15a5e6be73d1d752dbaef40fc26416e545f763a4)) +- leading zeros in credential value encoding ([#632](https://github.com/hyperledger/aries-framework-javascript/issues/632)) ([0d478a7](https://github.com/hyperledger/aries-framework-javascript/commit/0d478a7f198fec2ed5fceada77c9819ebab96a81)) +- mediation record checks for pickup v2 ([#736](https://github.com/hyperledger/aries-framework-javascript/issues/736)) ([2ad600c](https://github.com/hyperledger/aries-framework-javascript/commit/2ad600c066598526c421244cbe82bafc6cfbb85a)) +- miscellaneous issue credential v2 fixes ([#769](https://github.com/hyperledger/aries-framework-javascript/issues/769)) ([537b51e](https://github.com/hyperledger/aries-framework-javascript/commit/537b51efbf5ca1d50cd03e3ca4314da8b431c076)) +- **node:** allow to import node package without postgres ([#757](https://github.com/hyperledger/aries-framework-javascript/issues/757)) ([59e1058](https://github.com/hyperledger/aries-framework-javascript/commit/59e10589acee987fb46f9cbaa3583ba8dcd70b87)) +- **oob:** allow legacy did sov prefix ([#889](https://github.com/hyperledger/aries-framework-javascript/issues/889)) ([c7766d0](https://github.com/hyperledger/aries-framework-javascript/commit/c7766d0454cb764b771bb1ef263e81210368588a)) +- **oob:** check service is string instance ([#814](https://github.com/hyperledger/aries-framework-javascript/issues/814)) ([bd1e677](https://github.com/hyperledger/aries-framework-javascript/commit/bd1e677f41a6d37f75746616681fc6d6ad7ca90e)) +- **oob:** export messages to public ([#828](https://github.com/hyperledger/aries-framework-javascript/issues/828)) ([10cf74d](https://github.com/hyperledger/aries-framework-javascript/commit/10cf74d473ce00dca4bc624d60f379e8a78f9b63)) +- **oob:** expose oob record ([#839](https://github.com/hyperledger/aries-framework-javascript/issues/839)) ([c297dfd](https://github.com/hyperledger/aries-framework-javascript/commit/c297dfd9cbdafcb2cdb1f7bcbd466c42f1b8e319)) +- **oob:** expose parseInvitation publicly ([#834](https://github.com/hyperledger/aries-framework-javascript/issues/834)) ([5767500](https://github.com/hyperledger/aries-framework-javascript/commit/5767500b3a797f794fc9ed8147e501e9566d2675)) +- **oob:** legacy invitation with multiple endpoint ([#825](https://github.com/hyperledger/aries-framework-javascript/issues/825)) ([8dd7f80](https://github.com/hyperledger/aries-framework-javascript/commit/8dd7f8049ea9c566b5c66b0c46c36f69e001ed3a)) +- optional fields in did document ([#726](https://github.com/hyperledger/aries-framework-javascript/issues/726)) ([2da845d](https://github.com/hyperledger/aries-framework-javascript/commit/2da845dd4c88c5e93fa9f02107d69f479946024f)) +- process ws return route messages serially ([#826](https://github.com/hyperledger/aries-framework-javascript/issues/826)) ([2831a8e](https://github.com/hyperledger/aries-framework-javascript/commit/2831a8ee1bcda649e33eb68b002890f6670f660e)) +- **proofs:** allow duplicates in proof attributes ([#848](https://github.com/hyperledger/aries-framework-javascript/issues/848)) ([ca6c1ce](https://github.com/hyperledger/aries-framework-javascript/commit/ca6c1ce82bb84a638f98977191b04a249633be76)) +- propose payload attachment in in snake_case JSON format ([#775](https://github.com/hyperledger/aries-framework-javascript/issues/775)) ([6c2dfdb](https://github.com/hyperledger/aries-framework-javascript/commit/6c2dfdb625f7a8f2504f8bc8cf878e01ee1c50cc)) +- relax validation of thread id in revocation notification ([#768](https://github.com/hyperledger/aries-framework-javascript/issues/768)) ([020e6ef](https://github.com/hyperledger/aries-framework-javascript/commit/020e6efa6e878401dede536dd99b3c9814d9541b)) +- remove deprecated multibase and multihash ([#674](https://github.com/hyperledger/aries-framework-javascript/issues/674)) ([3411f1d](https://github.com/hyperledger/aries-framework-javascript/commit/3411f1d20f09cab47b77bf9eb6b66cf135d19d4c)) +- remove unqualified did from out of band record ([#782](https://github.com/hyperledger/aries-framework-javascript/issues/782)) ([0c1423d](https://github.com/hyperledger/aries-framework-javascript/commit/0c1423d7203d92aea5440aac0488dae5dad6b05e)) +- remove usage of const enum ([#888](https://github.com/hyperledger/aries-framework-javascript/issues/888)) ([a7754bd](https://github.com/hyperledger/aries-framework-javascript/commit/a7754bd7bfeaac1ca30df8437554e041d4cf103e)) +- **routing:** also use pickup strategy from config ([#808](https://github.com/hyperledger/aries-framework-javascript/issues/808)) ([fd08ae3](https://github.com/hyperledger/aries-framework-javascript/commit/fd08ae3afaa334c4644aaacee2b6547f171d9d7d)) +- **routing:** mediation recipient role for recipient ([#661](https://github.com/hyperledger/aries-framework-javascript/issues/661)) ([88ad790](https://github.com/hyperledger/aries-framework-javascript/commit/88ad790d8291aaf9113f0de5c7b13563a4967ee7)) +- **routing:** remove sentTime from request message ([#670](https://github.com/hyperledger/aries-framework-javascript/issues/670)) ([1e9715b](https://github.com/hyperledger/aries-framework-javascript/commit/1e9715b894538f57e6ff3aa2d2e4225f8b2f7dc1)) +- **routing:** sending of trustping in pickup v2 ([#787](https://github.com/hyperledger/aries-framework-javascript/issues/787)) ([45b024d](https://github.com/hyperledger/aries-framework-javascript/commit/45b024d62d370e2c646b20993647740f314356e2)) +- send message to service ([#838](https://github.com/hyperledger/aries-framework-javascript/issues/838)) ([270c347](https://github.com/hyperledger/aries-framework-javascript/commit/270c3478f76ba5c3702377d78027afb71549de5c)) +- support pre-aip2 please ack decorator ([#835](https://github.com/hyperledger/aries-framework-javascript/issues/835)) ([a4bc215](https://github.com/hyperledger/aries-framework-javascript/commit/a4bc2158351129aef5281639bbb44127ebcf5ad8)) +- update inbound message validation ([#678](https://github.com/hyperledger/aries-framework-javascript/issues/678)) ([e383343](https://github.com/hyperledger/aries-framework-javascript/commit/e3833430104e3a0415194bd6f27d71c3b5b5ef9b)) +- verify jws contains at least 1 signature ([#600](https://github.com/hyperledger/aries-framework-javascript/issues/600)) ([9c96518](https://github.com/hyperledger/aries-framework-javascript/commit/9c965185de7908bdde1776369453cce384f9e82c)) + +### Code Refactoring + +- delete credentials by default when deleting exchange ([#767](https://github.com/hyperledger/aries-framework-javascript/issues/767)) ([656ed73](https://github.com/hyperledger/aries-framework-javascript/commit/656ed73b95d8a8483a38ff0b5462a4671cb82898)) +- do not add ~service in createOOBOffer method ([#772](https://github.com/hyperledger/aries-framework-javascript/issues/772)) ([a541949](https://github.com/hyperledger/aries-framework-javascript/commit/a541949c7dbf907e29eb798e60901b92fbec6443)) + +### Features + +- 0.2.0 migration script for connections ([#773](https://github.com/hyperledger/aries-framework-javascript/issues/773)) ([0831b9b](https://github.com/hyperledger/aries-framework-javascript/commit/0831b9b451d8ac74a018fc525cdbac8ec9f6cd1c)) +- ability to add generic records ([#702](https://github.com/hyperledger/aries-framework-javascript/issues/702)) ([e617496](https://github.com/hyperledger/aries-framework-javascript/commit/e61749609a072f0f8d869e6c278d0a4a79938ee4)), closes [#688](https://github.com/hyperledger/aries-framework-javascript/issues/688) +- add didcomm message record ([#593](https://github.com/hyperledger/aries-framework-javascript/issues/593)) ([e547fb1](https://github.com/hyperledger/aries-framework-javascript/commit/e547fb1c0b01f821b5425bf9bb632e885f92b398)) +- add find and save/update methods to DidCommMessageRepository ([#620](https://github.com/hyperledger/aries-framework-javascript/issues/620)) ([beff6b0](https://github.com/hyperledger/aries-framework-javascript/commit/beff6b0ae0ad100ead1a4820ebf6c12fb3ad148d)) +- add generic did resolver ([#554](https://github.com/hyperledger/aries-framework-javascript/issues/554)) ([8e03f35](https://github.com/hyperledger/aries-framework-javascript/commit/8e03f35f8e1cd02dac4df02d1f80f2c5a921dfef)) +- add issue credential v2 ([#745](https://github.com/hyperledger/aries-framework-javascript/issues/745)) ([245223a](https://github.com/hyperledger/aries-framework-javascript/commit/245223acbc6f50de418b310025665e5c1316f1af)) +- add out-of-band and did exchange ([#717](https://github.com/hyperledger/aries-framework-javascript/issues/717)) ([16c6d60](https://github.com/hyperledger/aries-framework-javascript/commit/16c6d6080db93b5f4a86e81bdbd7a3e987728d82)) +- add question answer protocol ([#557](https://github.com/hyperledger/aries-framework-javascript/issues/557)) ([b5a2536](https://github.com/hyperledger/aries-framework-javascript/commit/b5a25364ff523214fc8e56a7133bfa5c1db9b935)) +- add role and method to did record tags ([#692](https://github.com/hyperledger/aries-framework-javascript/issues/692)) ([3b6504b](https://github.com/hyperledger/aries-framework-javascript/commit/3b6504ba6053c62f0841cb64a0e9a5be0e78bf80)) +- add support for did:peer ([#608](https://github.com/hyperledger/aries-framework-javascript/issues/608)) ([c5c4172](https://github.com/hyperledger/aries-framework-javascript/commit/c5c41722e9b626d7cea929faff562c2a69a079fb)) +- add support for signed attachments ([#595](https://github.com/hyperledger/aries-framework-javascript/issues/595)) ([eb49374](https://github.com/hyperledger/aries-framework-javascript/commit/eb49374c7ac7a61c10c8cb9079acffe689d0b402)) +- add update assistant for storage migrations ([#690](https://github.com/hyperledger/aries-framework-javascript/issues/690)) ([c9bff93](https://github.com/hyperledger/aries-framework-javascript/commit/c9bff93cfac43c4ae2cbcad1f96c1a74cde39602)) +- add validation to JSON transformer ([#830](https://github.com/hyperledger/aries-framework-javascript/issues/830)) ([5b9efe3](https://github.com/hyperledger/aries-framework-javascript/commit/5b9efe3b6fdaaec6dda387c542979e0e8fd51d5c)) +- add wallet key derivation method option ([#650](https://github.com/hyperledger/aries-framework-javascript/issues/650)) ([8386506](https://github.com/hyperledger/aries-framework-javascript/commit/83865067402466ffb51ba5008f52ea3e4169c31d)) +- add wallet module with import export ([#652](https://github.com/hyperledger/aries-framework-javascript/issues/652)) ([6cf5a7b](https://github.com/hyperledger/aries-framework-javascript/commit/6cf5a7b9de84dee1be61c315a734328ec209e87d)) +- **core:** add support for postgres wallet type ([#699](https://github.com/hyperledger/aries-framework-javascript/issues/699)) ([83ff0f3](https://github.com/hyperledger/aries-framework-javascript/commit/83ff0f36401cbf6e95c0a1ceb9fa921a82dc6830)) +- **core:** added timeOut to the module level ([#603](https://github.com/hyperledger/aries-framework-javascript/issues/603)) ([09950c7](https://github.com/hyperledger/aries-framework-javascript/commit/09950c706c0827a75eb93ffb05cc926f8472f66d)) +- **core:** allow to set auto accept connetion exchange when accepting invitation ([#589](https://github.com/hyperledger/aries-framework-javascript/issues/589)) ([2d95dce](https://github.com/hyperledger/aries-framework-javascript/commit/2d95dce70fb36dbbae459e17cfb0dea4dbbbe237)) +- **core:** generic repository events ([#842](https://github.com/hyperledger/aries-framework-javascript/issues/842)) ([74dd289](https://github.com/hyperledger/aries-framework-javascript/commit/74dd289669080b1406562ac575dd7c3c3d442e72)) +- **credentials:** add get format data method ([#877](https://github.com/hyperledger/aries-framework-javascript/issues/877)) ([521d489](https://github.com/hyperledger/aries-framework-javascript/commit/521d489cccaf9c4c3f3650ccf980a8dec0b8f729)) +- **credentials:** delete associated didCommMessages ([#870](https://github.com/hyperledger/aries-framework-javascript/issues/870)) ([1f8b6ab](https://github.com/hyperledger/aries-framework-javascript/commit/1f8b6aba9c34bd45ea61cdfdc5f7ab1e825368fc)) +- **credentials:** find didcomm message methods ([#887](https://github.com/hyperledger/aries-framework-javascript/issues/887)) ([dc12427](https://github.com/hyperledger/aries-framework-javascript/commit/dc12427bb308e53bb1c5749c61769b5f08c684c2)) +- delete credential from wallet ([#691](https://github.com/hyperledger/aries-framework-javascript/issues/691)) ([abec3a2](https://github.com/hyperledger/aries-framework-javascript/commit/abec3a2c95815d1c54b22a6370222f024eefb060)) +- extension module creation ([#688](https://github.com/hyperledger/aries-framework-javascript/issues/688)) ([2b6441a](https://github.com/hyperledger/aries-framework-javascript/commit/2b6441a2de5e9940bdf225b1ad9028cdfbf15cd5)) +- filter retrieved credential by revocation state ([#641](https://github.com/hyperledger/aries-framework-javascript/issues/641)) ([5912c0c](https://github.com/hyperledger/aries-framework-javascript/commit/5912c0ce2dbc8f773cec5324ffb19c40b15009b0)) +- indy revocation (prover & verifier) ([#592](https://github.com/hyperledger/aries-framework-javascript/issues/592)) ([fb19ff5](https://github.com/hyperledger/aries-framework-javascript/commit/fb19ff555b7c10c9409450dcd7d385b1eddf41ac)) +- **indy:** add choice for taa mechanism ([#849](https://github.com/hyperledger/aries-framework-javascript/issues/849)) ([ba03fa0](https://github.com/hyperledger/aries-framework-javascript/commit/ba03fa0c23f270274a592dfd6556a35adf387b51)) +- ledger connections happen on agent init in background ([#580](https://github.com/hyperledger/aries-framework-javascript/issues/580)) ([61695ce](https://github.com/hyperledger/aries-framework-javascript/commit/61695ce7737ffef363b60e341ae5b0e67e0e2c90)) +- pickup v2 protocol ([#711](https://github.com/hyperledger/aries-framework-javascript/issues/711)) ([b281673](https://github.com/hyperledger/aries-framework-javascript/commit/b281673b3503bb85ebda7afdd68b6d792d8f5bf5)) +- regex for schemaVersion, issuerDid, credDefId, schemaId, schemaIssuerDid ([#679](https://github.com/hyperledger/aries-framework-javascript/issues/679)) ([36b9d46](https://github.com/hyperledger/aries-framework-javascript/commit/36b9d466d400a0f87f6272bc428965601023581a)) +- **routing:** allow to discover mediator pickup strategy ([#669](https://github.com/hyperledger/aries-framework-javascript/issues/669)) ([5966da1](https://github.com/hyperledger/aries-framework-javascript/commit/5966da130873607a41919bbe1239e5e44afb47e4)) +- support advanced wallet query ([#831](https://github.com/hyperledger/aries-framework-javascript/issues/831)) ([28e0ffa](https://github.com/hyperledger/aries-framework-javascript/commit/28e0ffa151d41a39197f01bcc5f9c9834a0b2537)) +- support handling messages with different minor version ([#714](https://github.com/hyperledger/aries-framework-javascript/issues/714)) ([ad12360](https://github.com/hyperledger/aries-framework-javascript/commit/ad123602682214f02250e82a80ac7cf5255b8d12)) +- support new did document in didcomm message exchange ([#609](https://github.com/hyperledger/aries-framework-javascript/issues/609)) ([a1a3b7d](https://github.com/hyperledger/aries-framework-javascript/commit/a1a3b7d95a6e6656dc5630357ac4e692b33b49bc)) +- support revocation notification messages ([#579](https://github.com/hyperledger/aries-framework-javascript/issues/579)) ([9f04375](https://github.com/hyperledger/aries-framework-javascript/commit/9f04375edc5eaffa0aa3583efcf05c83d74987bb)) +- support wallet key rotation ([#672](https://github.com/hyperledger/aries-framework-javascript/issues/672)) ([5cd1598](https://github.com/hyperledger/aries-framework-javascript/commit/5cd1598b496a832c82f35a363fabe8f408abd439)) +- update recursive backoff & trust ping record updates ([#631](https://github.com/hyperledger/aries-framework-javascript/issues/631)) ([f64a9da](https://github.com/hyperledger/aries-framework-javascript/commit/f64a9da2ef9fda9693b23ddbd25bd885b88cdb1e)) + +### BREAKING CHANGES + +- **indy:** the transaction author agreement acceptance mechanism was previously automatically the first acceptance mechanism from the acceptance mechanism list. With this addition, the framework never automatically selects the acceptance mechanism anymore and it needs to be specified in the transactionAuthorAgreement in the indyLedgers agent config array. +- the credentials associated with a credential exchange record are now deleted by default when deleting a credential exchange record. If you only want to delete the credential exchange record and not the associated credentials, you can pass the deleteAssociatedCredentials to the deleteById method: + +```ts +await agent.credentials.deleteById('credentialExchangeId', { + deleteAssociatedCredentials: false, +}) +``` + +- with the addition of the out of band module `credentials.createOutOfBandOffer` is renamed to `credentials.createOffer` and no longer adds the `~service` decorator to the message. You need to call `oob.createLegacyConnectionlessInvitation` afterwards to use it for AIP-1 style connectionless exchanges. See [Migrating from AFJ 0.1.0 to 0.2.x](https://github.com/hyperledger/aries-framework-javascript/blob/main/docs/migration/0.1-to-0.2.md) for detailed migration instructions. +- the connections module has been extended with an out of band module and support for the DID Exchange protocol. Some methods have been moved to the out of band module, see [Migrating from AFJ 0.1.0 to 0.2.x](https://github.com/hyperledger/aries-framework-javascript/blob/main/docs/migration/0.1-to-0.2.md) for detailed migration instructions. +- The mediator pickup strategy enum value `MediatorPickupStrategy.Explicit` has been renamed to `MediatorPickupStrategy.PickUpV1` to better align with the naming of the new `MediatorPickupStrategy.PickUpV2` +- attachment method `getDataAsJson` is now located one level up. So instead of `attachment.data.getDataAsJson()` you should now call `attachment.getDataAsJson()` + +# 0.1.0 (2021-12-23) + +### Bug Fixes + +- add details to connection signing error ([#484](https://github.com/hyperledger/aries-framework-javascript/issues/484)) ([e24eafd](https://github.com/hyperledger/aries-framework-javascript/commit/e24eafd83f53a9833b95bc3a4587cf825ee5d975)) +- add option check to attribute constructor ([#450](https://github.com/hyperledger/aries-framework-javascript/issues/450)) ([8aad3e9](https://github.com/hyperledger/aries-framework-javascript/commit/8aad3e9f16c249e9f9291388ec8efc9bf27213c8)) +- added ariesframeworkerror to httpoutboundtransport ([#438](https://github.com/hyperledger/aries-framework-javascript/issues/438)) ([ee1a229](https://github.com/hyperledger/aries-framework-javascript/commit/ee1a229f8fc21739bca05c516a7b561f53726b91)) +- alter mediation recipient websocket transport priority ([#434](https://github.com/hyperledger/aries-framework-javascript/issues/434)) ([52c7897](https://github.com/hyperledger/aries-framework-javascript/commit/52c789724c731340daa8528b7d7b4b7fdcb40032)) +- **core:** convert legacy prefix for inner msgs ([#479](https://github.com/hyperledger/aries-framework-javascript/issues/479)) ([a2b655a](https://github.com/hyperledger/aries-framework-javascript/commit/a2b655ac79bf0c7460671c8d31e92828e6f5ccf0)) +- **core:** do not throw error on timeout in http ([#512](https://github.com/hyperledger/aries-framework-javascript/issues/512)) ([4e73a7b](https://github.com/hyperledger/aries-framework-javascript/commit/4e73a7b0d9224bc102b396d821a8ea502a9a509d)) +- **core:** do not use did-communication service ([#402](https://github.com/hyperledger/aries-framework-javascript/issues/402)) ([cdf2edd](https://github.com/hyperledger/aries-framework-javascript/commit/cdf2eddc61e12f7ecd5a29e260eef82394d2e467)) +- **core:** export AgentMessage ([#480](https://github.com/hyperledger/aries-framework-javascript/issues/480)) ([af39ad5](https://github.com/hyperledger/aries-framework-javascript/commit/af39ad535320133ee38fc592309f42670a8517a1)) +- **core:** expose record metadata types ([#556](https://github.com/hyperledger/aries-framework-javascript/issues/556)) ([68995d7](https://github.com/hyperledger/aries-framework-javascript/commit/68995d7e2b049ff6496723d8a895e07b72fe72fb)) +- **core:** fix empty error log in console logger ([#524](https://github.com/hyperledger/aries-framework-javascript/issues/524)) ([7d9c541](https://github.com/hyperledger/aries-framework-javascript/commit/7d9c541de22fb2644455cf1949184abf3d8e528c)) +- **core:** improve wallet not initialized error ([#513](https://github.com/hyperledger/aries-framework-javascript/issues/513)) ([b948d4c](https://github.com/hyperledger/aries-framework-javascript/commit/b948d4c83b4eb0ab0594ae2117c0bb05b0955b21)) +- **core:** improved present-proof tests ([#482](https://github.com/hyperledger/aries-framework-javascript/issues/482)) ([41d9282](https://github.com/hyperledger/aries-framework-javascript/commit/41d9282ca561ca823b28f179d409c70a22d95e9b)) +- **core:** log errors if message is undeliverable ([#528](https://github.com/hyperledger/aries-framework-javascript/issues/528)) ([20b586d](https://github.com/hyperledger/aries-framework-javascript/commit/20b586db6eb9f92cce16d87d0dcfa4919f27ffa8)) +- **core:** remove isPositive validation decorators ([#477](https://github.com/hyperledger/aries-framework-javascript/issues/477)) ([e316e04](https://github.com/hyperledger/aries-framework-javascript/commit/e316e047b3e5aeefb929a5c47ad65d8edd4caba5)) +- **core:** remove unused url import ([#466](https://github.com/hyperledger/aries-framework-javascript/issues/466)) ([0f1323f](https://github.com/hyperledger/aries-framework-javascript/commit/0f1323f5bccc2dc3b67426525b161d7e578bb961)) +- **core:** requested predicates transform type ([#393](https://github.com/hyperledger/aries-framework-javascript/issues/393)) ([69684bc](https://github.com/hyperledger/aries-framework-javascript/commit/69684bc48a4002483662a211ec1ddd289dbaf59b)) +- **core:** send messages now takes a connection id ([#491](https://github.com/hyperledger/aries-framework-javascript/issues/491)) ([ed9db11](https://github.com/hyperledger/aries-framework-javascript/commit/ed9db11592b4948a1d313dbeb074e15d59503d82)) +- **core:** using query-string to parse URLs ([#457](https://github.com/hyperledger/aries-framework-javascript/issues/457)) ([78e5057](https://github.com/hyperledger/aries-framework-javascript/commit/78e505750557f296cc72ef19c0edd8db8e1eaa7d)) +- date parsing ([#426](https://github.com/hyperledger/aries-framework-javascript/issues/426)) ([2d31b87](https://github.com/hyperledger/aries-framework-javascript/commit/2d31b87e99d04136f57cb457e2c67397ad65cc62)) +- export indy pool config ([#504](https://github.com/hyperledger/aries-framework-javascript/issues/504)) ([b1e2b8c](https://github.com/hyperledger/aries-framework-javascript/commit/b1e2b8c54e909927e5afa8b8212e0c8e156b97f7)) +- include error when message cannot be handled ([#533](https://github.com/hyperledger/aries-framework-javascript/issues/533)) ([febfb05](https://github.com/hyperledger/aries-framework-javascript/commit/febfb05330c097aa918087ec3853a247d6a31b7c)) +- incorrect recip key with multi routing keys ([#446](https://github.com/hyperledger/aries-framework-javascript/issues/446)) ([db76823](https://github.com/hyperledger/aries-framework-javascript/commit/db76823400cfecc531575584ef7210af0c3b3e5c)) +- make records serializable ([#448](https://github.com/hyperledger/aries-framework-javascript/issues/448)) ([7e2946e](https://github.com/hyperledger/aries-framework-javascript/commit/7e2946eaa9e35083f3aa70c26c732a972f6eb12f)) +- mediator transports ([#419](https://github.com/hyperledger/aries-framework-javascript/issues/419)) ([87bc589](https://github.com/hyperledger/aries-framework-javascript/commit/87bc589695505de21294a1373afcf874fe8d22f6)) +- mediator updates ([#432](https://github.com/hyperledger/aries-framework-javascript/issues/432)) ([163cda1](https://github.com/hyperledger/aries-framework-javascript/commit/163cda19ba8437894a48c9bc948528ea0486ccdf)) +- proof configurable on proofRecord ([#397](https://github.com/hyperledger/aries-framework-javascript/issues/397)) ([8e83c03](https://github.com/hyperledger/aries-framework-javascript/commit/8e83c037e1d59c670cfd4a8a575d4459999a64f8)) +- removed check for senderkey for connectionless exchange ([#555](https://github.com/hyperledger/aries-framework-javascript/issues/555)) ([ba3f17e](https://github.com/hyperledger/aries-framework-javascript/commit/ba3f17e073b28ee5f16031f0346de0b71119e6f3)) +- support mediation for connectionless exchange ([#577](https://github.com/hyperledger/aries-framework-javascript/issues/577)) ([3dadfc7](https://github.com/hyperledger/aries-framework-javascript/commit/3dadfc7a202b3642e93e39cd79c9fd98a3dc4de2)) +- their did doc not ours ([#436](https://github.com/hyperledger/aries-framework-javascript/issues/436)) ([0226609](https://github.com/hyperledger/aries-framework-javascript/commit/0226609a279303f5e8d09a2c01e54ff97cf61839)) + +- fix(core)!: Improved typing on metadata api (#585) ([4ab8d73](https://github.com/hyperledger/aries-framework-javascript/commit/4ab8d73e5fc866a91085f95f973022846ed431fb)), closes [#585](https://github.com/hyperledger/aries-framework-javascript/issues/585) +- fix(core)!: update class transformer library (#547) ([dee03e3](https://github.com/hyperledger/aries-framework-javascript/commit/dee03e38d2732ba0bd38eeacca6ad58b191e87f8)), closes [#547](https://github.com/hyperledger/aries-framework-javascript/issues/547) +- fix(core)!: prefixed internal metadata with \_internal/ (#535) ([aa1b320](https://github.com/hyperledger/aries-framework-javascript/commit/aa1b3206027fdb71e6aaa4c6491f8ba84dca7b9a)), closes [#535](https://github.com/hyperledger/aries-framework-javascript/issues/535) +- feat(core)!: metadata on records (#505) ([c92393a](https://github.com/hyperledger/aries-framework-javascript/commit/c92393a8b5d8abd38d274c605cd5c3f97f96cee9)), closes [#505](https://github.com/hyperledger/aries-framework-javascript/issues/505) +- fix(core)!: do not request ping res for connection (#527) ([3db5519](https://github.com/hyperledger/aries-framework-javascript/commit/3db5519f0d9f49b71b647ca86be3b336399459cb)), closes [#527](https://github.com/hyperledger/aries-framework-javascript/issues/527) +- refactor(core)!: simplify get creds for proof api (#523) ([ba9698d](https://github.com/hyperledger/aries-framework-javascript/commit/ba9698de2606e5c78f018dc5e5253aeb1f5fc616)), closes [#523](https://github.com/hyperledger/aries-framework-javascript/issues/523) +- fix(core)!: improve proof request validation (#525) ([1b4d8d6](https://github.com/hyperledger/aries-framework-javascript/commit/1b4d8d6b6c06821a2a981fffb6c47f728cac803e)), closes [#525](https://github.com/hyperledger/aries-framework-javascript/issues/525) +- feat(core)!: added basic message sent event (#507) ([d2c04c3](https://github.com/hyperledger/aries-framework-javascript/commit/d2c04c36c00d772943530bd599dbe56f3e1fb17d)), closes [#507](https://github.com/hyperledger/aries-framework-javascript/issues/507) + +### Features + +- add delete methods to services and modules ([#447](https://github.com/hyperledger/aries-framework-javascript/issues/447)) ([e7ed602](https://github.com/hyperledger/aries-framework-javascript/commit/e7ed6027d2aa9be7f64d5968c4338e63e56657fb)) +- add from record method to cred preview ([#428](https://github.com/hyperledger/aries-framework-javascript/issues/428)) ([895f7d0](https://github.com/hyperledger/aries-framework-javascript/commit/895f7d084287f99221c9492a25fed58191868edd)) +- add multiple inbound transports ([#433](https://github.com/hyperledger/aries-framework-javascript/issues/433)) ([56cb9f2](https://github.com/hyperledger/aries-framework-javascript/commit/56cb9f2202deb83b3c133905f21651bfefcb63f7)) +- add problem report protocol ([#560](https://github.com/hyperledger/aries-framework-javascript/issues/560)) ([baee5db](https://github.com/hyperledger/aries-framework-javascript/commit/baee5db29f3d545c16a651c80392ddcbbca6bf0e)) +- add toJson method to BaseRecord ([#455](https://github.com/hyperledger/aries-framework-javascript/issues/455)) ([f3790c9](https://github.com/hyperledger/aries-framework-javascript/commit/f3790c97c4d9a0aaec9abdce417ecd5429c6026f)) +- added decline credential offer method ([#416](https://github.com/hyperledger/aries-framework-javascript/issues/416)) ([d9ac141](https://github.com/hyperledger/aries-framework-javascript/commit/d9ac141122f1d4902f91f9537e6526796fef1e01)) +- added declined proof state and decline method for presentations ([e5aedd0](https://github.com/hyperledger/aries-framework-javascript/commit/e5aedd02737d3764871c6b5d4ae61a3a33ed8398)) +- allow to use legacy did sov prefix ([#442](https://github.com/hyperledger/aries-framework-javascript/issues/442)) ([c41526f](https://github.com/hyperledger/aries-framework-javascript/commit/c41526fb57a7e2e89e923b95ede43f890a6cbcbb)) +- auto accept proofs ([#367](https://github.com/hyperledger/aries-framework-javascript/issues/367)) ([735d578](https://github.com/hyperledger/aries-framework-javascript/commit/735d578f72fc5f3bfcbcf40d27394bd013e7cf4f)) +- break out indy wallet, better indy handling ([#396](https://github.com/hyperledger/aries-framework-javascript/issues/396)) ([9f1a4a7](https://github.com/hyperledger/aries-framework-javascript/commit/9f1a4a754a61573ce3fee78d52615363c7e25d58)) +- **core:** add discover features protocol ([#390](https://github.com/hyperledger/aries-framework-javascript/issues/390)) ([3347424](https://github.com/hyperledger/aries-framework-javascript/commit/3347424326cd15e8bf2544a8af53b2fa57b1dbb8)) +- **core:** add support for multi use inviations ([#460](https://github.com/hyperledger/aries-framework-javascript/issues/460)) ([540ad7b](https://github.com/hyperledger/aries-framework-javascript/commit/540ad7be2133ee6609c2336b22b726270db98d6c)) +- **core:** connection-less issuance and verification ([#359](https://github.com/hyperledger/aries-framework-javascript/issues/359)) ([fb46ade](https://github.com/hyperledger/aries-framework-javascript/commit/fb46ade4bc2dd4f3b63d4194bb170d2f329562b7)) +- **core:** d_m invitation parameter and invitation image ([#456](https://github.com/hyperledger/aries-framework-javascript/issues/456)) ([f92c322](https://github.com/hyperledger/aries-framework-javascript/commit/f92c322b97be4a4867a82c3a35159d6068951f0b)) +- **core:** ledger module registerPublicDid implementation ([#398](https://github.com/hyperledger/aries-framework-javascript/issues/398)) ([5f2d512](https://github.com/hyperledger/aries-framework-javascript/commit/5f2d5126baed2ff58268c38755c2dbe75a654947)) +- **core:** store mediator id in connection record ([#503](https://github.com/hyperledger/aries-framework-javascript/issues/503)) ([da51f2e](https://github.com/hyperledger/aries-framework-javascript/commit/da51f2e8337f5774d23e9aeae0459bd7355a3760)) +- **core:** support image url in invitations ([#463](https://github.com/hyperledger/aries-framework-javascript/issues/463)) ([9fda24e](https://github.com/hyperledger/aries-framework-javascript/commit/9fda24ecf55fdfeba74211447e9fadfdcbf57385)) +- **core:** support multiple indy ledgers ([#474](https://github.com/hyperledger/aries-framework-javascript/issues/474)) ([47149bc](https://github.com/hyperledger/aries-framework-javascript/commit/47149bc5742456f4f0b75e0944ce276972e645b8)) +- **core:** update agent label and imageUrl plus per connection label and imageUrl ([#516](https://github.com/hyperledger/aries-framework-javascript/issues/516)) ([5e9a641](https://github.com/hyperledger/aries-framework-javascript/commit/5e9a64130c02c8a5fdf11f0e25d0c23929a33a4f)) +- **core:** validate outbound messages ([#526](https://github.com/hyperledger/aries-framework-javascript/issues/526)) ([9c3910f](https://github.com/hyperledger/aries-framework-javascript/commit/9c3910f1e67200b71bb4888c6fee62942afaff20)) +- expose wallet API ([#566](https://github.com/hyperledger/aries-framework-javascript/issues/566)) ([4027fc9](https://github.com/hyperledger/aries-framework-javascript/commit/4027fc975d7e4118892f43cb8c6a0eea412eaad4)) +- generic attachment handler ([#578](https://github.com/hyperledger/aries-framework-javascript/issues/578)) ([4d7d3c1](https://github.com/hyperledger/aries-framework-javascript/commit/4d7d3c1502d5eafa2b884a4a84934e072fe70ea6)) +- **node:** add http and ws inbound transport ([#392](https://github.com/hyperledger/aries-framework-javascript/issues/392)) ([34a6ff2](https://github.com/hyperledger/aries-framework-javascript/commit/34a6ff2699197b9d525422a0a405e241582a476c)) + +### BREAKING CHANGES + +- removed the getAll() function. +- The agent’s `shutdown` method does not delete the wallet anymore. If you want to delete the wallet, you can do it via exposed wallet API. +- class-transformer released a breaking change in a patch version, causing AFJ to break. I updated to the newer version and pinned the version exactly as this is the second time this has happened now. + +Signed-off-by: Timo Glastra + +- internal metadata is now prefixed with \_internal to avoid clashing and accidental overwriting of internal data. + +- fix(core): added \_internal/ prefix on metadata + +Signed-off-by: Berend Sliedrecht + +- credentialRecord.credentialMetadata has been replaced by credentialRecord.metadata. + +Signed-off-by: Berend Sliedrecht + +- a trust ping response will not be requested anymore after completing a connection. This is not required, and also non-standard behaviour. It was also causing some tests to be flaky as response messages were stil being sent after one of the agents had already shut down. + +Signed-off-by: Timo Glastra + +- The `ProofsModule.getRequestedCredentialsForProofRequest` expected some low level message objects as input. This is not in line with the public API of the rest of the framework and has been simplified to only require a proof record id and optionally a boolean whether the retrieved credentials should be filtered based on the proof proposal (if available). + +Signed-off-by: Timo Glastra + +- Proof request requestedAttributes and requestedPredicates are now a map instead of record. This is needed to have proper validation using class-validator. + +Signed-off-by: Timo Glastra + +- `BasicMessageReceivedEvent` has been replaced by the more general `BasicMessageStateChanged` event which triggers when a basic message is received or sent. + +Signed-off-by: NeilSMyers diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000000..65126213a7 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,31 @@ +

+
+ Credo Logo +

+

Credo - Core

+

+ License + typescript + @credo-ts/core version + +

+
+ +Credo Core provides the core functionality of Credo. See the [Getting Started Guide](https://credo.js.org/guides/getting-started) for installation instructions. diff --git a/packages/core/jest.config.ts b/packages/core/jest.config.ts new file mode 100644 index 0000000000..7b6ec7f1c5 --- /dev/null +++ b/packages/core/jest.config.ts @@ -0,0 +1,15 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +process.env.TZ = 'GMT' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000000..0fcad357f8 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,76 @@ +{ + "name": "@credo-ts/core", + "main": "build/index", + "types": "build/index", + "version": "0.5.6-revocation", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/core", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/core" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build" + }, + "dependencies": { + "@digitalcredentials/jsonld": "^6.0.0", + "@digitalcredentials/jsonld-signatures": "^9.4.0", + "@digitalcredentials/vc": "^6.0.1", + "@multiformats/base-x": "^4.0.1", + "@sd-jwt/core": "^0.7.0", + "@sd-jwt/decode": "^0.7.0", + "@sd-jwt/jwt-status-list": "^0.7.0", + "@sd-jwt/sd-jwt-vc": "^0.7.0", + "@sd-jwt/types": "^0.7.0", + "@sd-jwt/utils": "^0.7.0", + "@sphereon/pex": "^3.3.2", + "@sphereon/pex-models": "^2.2.4", + "@sphereon/ssi-types": "^0.23.0", + "@stablelib/ed25519": "^1.0.2", + "@stablelib/sha256": "^1.0.1", + "@types/ws": "^8.5.4", + "abort-controller": "^3.0.0", + "big-integer": "^1.6.51", + "borc": "^3.0.0", + "buffer": "^6.0.3", + "class-transformer": "0.5.1", + "class-validator": "0.14.1", + "did-resolver": "^4.1.0", + "jsonpath": "^1.1.1", + "lru_map": "^0.4.1", + "luxon": "^3.3.0", + "make-error": "^1.3.6", + "object-inspect": "^1.10.3", + "query-string": "^7.0.1", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.0", + "tsyringe": "^4.8.0", + "uuid": "^9.0.0", + "varint": "^6.0.0", + "web-did-resolver": "^2.0.21" + }, + "devDependencies": { + "@types/events": "^3.0.0", + "@types/jsonpath": "^0.2.4", + "@types/luxon": "^3.2.0", + "@types/object-inspect": "^1.8.0", + "@types/uuid": "^9.0.1", + "@types/varint": "^6.0.0", + "nock": "^13.3.0", + "rimraf": "^4.4.0", + "tslog": "^4.8.2", + "typescript": "~5.5.2" + } +} diff --git a/packages/core/src/agent/Agent.ts b/packages/core/src/agent/Agent.ts new file mode 100644 index 0000000000..8179d7bef5 --- /dev/null +++ b/packages/core/src/agent/Agent.ts @@ -0,0 +1,251 @@ +import type { AgentDependencies } from './AgentDependencies' +import type { AgentModulesInput } from './AgentModules' +import type { AgentMessageReceivedEvent } from './Events' +import type { Module } from '../plugins' +import type { InboundTransport } from '../transport/InboundTransport' +import type { OutboundTransport } from '../transport/OutboundTransport' +import type { InitConfig } from '../types' +import type { Subscription } from 'rxjs' + +import { Subject } from 'rxjs' +import { concatMap, takeUntil } from 'rxjs/operators' + +import { InjectionSymbols } from '../constants' +import { SigningProviderToken } from '../crypto' +import { JwsService } from '../crypto/JwsService' +import { CredoError } from '../error' +import { DependencyManager } from '../plugins' +import { DidCommMessageRepository, StorageUpdateService, StorageVersionRepository } from '../storage' + +import { AgentConfig } from './AgentConfig' +import { extendModulesWithDefaultModules } from './AgentModules' +import { BaseAgent } from './BaseAgent' +import { Dispatcher } from './Dispatcher' +import { EnvelopeService } from './EnvelopeService' +import { EventEmitter } from './EventEmitter' +import { AgentEventTypes } from './Events' +import { FeatureRegistry } from './FeatureRegistry' +import { MessageHandlerRegistry } from './MessageHandlerRegistry' +import { MessageReceiver } from './MessageReceiver' +import { MessageSender } from './MessageSender' +import { TransportService } from './TransportService' +import { AgentContext, DefaultAgentContextProvider } from './context' + +interface AgentOptions { + config: InitConfig + modules?: AgentModules + dependencies: AgentDependencies +} + +// Any makes sure you can use Agent as a type without always needing to specify the exact generics for the agent +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class Agent extends BaseAgent { + private messageSubscription?: Subscription + + public constructor(options: AgentOptions, dependencyManager = new DependencyManager()) { + const agentConfig = new AgentConfig(options.config, options.dependencies) + const modulesWithDefaultModules = extendModulesWithDefaultModules(options.modules) + + // Register internal dependencies + dependencyManager.registerSingleton(MessageHandlerRegistry) + dependencyManager.registerSingleton(EventEmitter) + dependencyManager.registerSingleton(MessageSender) + dependencyManager.registerSingleton(MessageReceiver) + dependencyManager.registerSingleton(TransportService) + dependencyManager.registerSingleton(Dispatcher) + dependencyManager.registerSingleton(EnvelopeService) + dependencyManager.registerSingleton(FeatureRegistry) + dependencyManager.registerSingleton(JwsService) + dependencyManager.registerSingleton(DidCommMessageRepository) + dependencyManager.registerSingleton(StorageVersionRepository) + dependencyManager.registerSingleton(StorageUpdateService) + + // This is a really ugly hack to make tsyringe work without any SigningProviders registered + // It is currently impossible to use @injectAll if there are no instances registered for the + // token. We register a value of `default` by default and will filter that out in the registry. + // Once we have a signing provider that should always be registered we can remove this. We can make an ed25519 + // signer using the @stablelib/ed25519 library. + dependencyManager.registerInstance(SigningProviderToken, 'default') + + dependencyManager.registerInstance(AgentConfig, agentConfig) + dependencyManager.registerInstance(InjectionSymbols.AgentDependencies, agentConfig.agentDependencies) + dependencyManager.registerInstance(InjectionSymbols.Stop$, new Subject()) + dependencyManager.registerInstance(InjectionSymbols.FileSystem, new agentConfig.agentDependencies.FileSystem()) + + // Register all modules. This will also include the default modules + dependencyManager.registerModules(modulesWithDefaultModules) + + // Register possibly already defined services + if (!dependencyManager.isRegistered(InjectionSymbols.Wallet)) { + throw new CredoError( + "Missing required dependency: 'Wallet'. You can register it using the AskarModule, or implement your own." + ) + } + if (!dependencyManager.isRegistered(InjectionSymbols.Logger)) { + dependencyManager.registerInstance(InjectionSymbols.Logger, agentConfig.logger) + } + if (!dependencyManager.isRegistered(InjectionSymbols.StorageService)) { + throw new CredoError( + "Missing required dependency: 'StorageService'. You can register it using the AskarModule, or implement your own." + ) + } + + // TODO: contextCorrelationId for base wallet + // Bind the default agent context to the container for use in modules etc. + dependencyManager.registerInstance( + AgentContext, + new AgentContext({ + dependencyManager, + contextCorrelationId: 'default', + }) + ) + + // If no agent context provider has been registered we use the default agent context provider. + if (!dependencyManager.isRegistered(InjectionSymbols.AgentContextProvider)) { + dependencyManager.registerSingleton(InjectionSymbols.AgentContextProvider, DefaultAgentContextProvider) + } + + super(agentConfig, dependencyManager) + } + + public registerInboundTransport(inboundTransport: InboundTransport) { + this.messageReceiver.registerInboundTransport(inboundTransport) + } + + public async unregisterInboundTransport(inboundTransport: InboundTransport) { + await this.messageReceiver.unregisterInboundTransport(inboundTransport) + } + + public get inboundTransports() { + return this.messageReceiver.inboundTransports + } + + public registerOutboundTransport(outboundTransport: OutboundTransport) { + this.messageSender.registerOutboundTransport(outboundTransport) + } + + public async unregisterOutboundTransport(outboundTransport: OutboundTransport) { + await this.messageSender.unregisterOutboundTransport(outboundTransport) + } + + public get outboundTransports() { + return this.messageSender.outboundTransports + } + + public get events() { + return this.eventEmitter + } + + /** + * Agent's feature registry + */ + public get features() { + return this.featureRegistry + } + + public async initialize() { + const stop$ = this.dependencyManager.resolve>(InjectionSymbols.Stop$) + + // Listen for new messages (either from transports or somewhere else in the framework / extensions) + // We create this before doing any other initialization, so the initialization could already receive messages + this.messageSubscription = this.eventEmitter + .observable(AgentEventTypes.AgentMessageReceived) + .pipe( + takeUntil(stop$), + concatMap((e) => + this.messageReceiver + .receiveMessage(e.payload.message, { + connection: e.payload.connection, + contextCorrelationId: e.payload.contextCorrelationId, + receivedAt: e.payload.receivedAt, + }) + .catch((error) => { + this.logger.error('Failed to process message', { error }) + }) + ) + ) + .subscribe() + + await super.initialize() + + for (const [, module] of Object.entries(this.dependencyManager.registeredModules) as [string, Module][]) { + if (module.initialize) { + await module.initialize(this.agentContext) + } + } + + for (const transport of this.inboundTransports) { + await transport.start(this) + } + + for (const transport of this.outboundTransports) { + await transport.start(this) + } + + // Connect to mediator through provided invitation if provided in config + // Also requests mediation ans sets as default mediator + // Because this requires the connections module, we do this in the agent constructor + if (this.mediationRecipient.config.mediatorInvitationUrl) { + this.logger.debug('Provision mediation with invitation', { + mediatorInvitationUrl: this.mediationRecipient.config.mediatorInvitationUrl, + }) + const mediationConnection = await this.getMediationConnection( + this.mediationRecipient.config.mediatorInvitationUrl + ) + await this.mediationRecipient.provision(mediationConnection) + } + + await this.messagePickup.initialize() + await this.mediator.initialize() + await this.mediationRecipient.initialize() + + this._isInitialized = true + } + + public async shutdown() { + const stop$ = this.dependencyManager.resolve>(InjectionSymbols.Stop$) + // All observables use takeUntil with the stop$ observable + // this means all observables will stop running if a value is emitted on this observable + stop$.next(true) + + // Stop transports + const allTransports = [...this.inboundTransports, ...this.outboundTransports] + const transportPromises = allTransports.map((transport) => transport.stop()) + await Promise.all(transportPromises) + + if (this.wallet.isInitialized) { + await this.wallet.close() + } + + this._isInitialized = false + } + + protected async getMediationConnection(mediatorInvitationUrl: string) { + const outOfBandInvitation = await this.oob.parseInvitation(mediatorInvitationUrl) + const outOfBandRecord = await this.oob.findByReceivedInvitationId(outOfBandInvitation.id) + const [connection] = outOfBandRecord ? await this.connections.findAllByOutOfBandId(outOfBandRecord.id) : [] + + if (!connection) { + this.logger.debug('Mediation connection does not exist, creating connection') + // We don't want to use the current default mediator when connecting to another mediator + const routing = await this.mediationRecipient.getRouting({ useDefaultMediator: false }) + + this.logger.debug('Routing created', routing) + const { connectionRecord: newConnection } = await this.oob.receiveInvitation(outOfBandInvitation, { + routing, + }) + this.logger.debug(`Mediation invitation processed`, { outOfBandInvitation }) + + if (!newConnection) { + throw new CredoError('No connection record to provision mediation.') + } + + return this.connections.returnWhenIsConnected(newConnection.id) + } + + if (!connection.isReady) { + return this.connections.returnWhenIsConnected(connection.id) + } + return connection + } +} diff --git a/packages/core/src/agent/AgentConfig.ts b/packages/core/src/agent/AgentConfig.ts new file mode 100644 index 0000000000..a6a4cb379f --- /dev/null +++ b/packages/core/src/agent/AgentConfig.ts @@ -0,0 +1,101 @@ +import type { AgentDependencies } from './AgentDependencies' +import type { Logger } from '../logger' +import type { InitConfig } from '../types' + +import { DID_COMM_TRANSPORT_QUEUE } from '../constants' +import { ConsoleLogger, LogLevel } from '../logger' +import { DidCommMimeType } from '../types' + +export class AgentConfig { + private initConfig: InitConfig + private _endpoints: string[] | undefined + public label: string + public logger: Logger + public readonly agentDependencies: AgentDependencies + + public constructor(initConfig: InitConfig, agentDependencies: AgentDependencies) { + this.initConfig = initConfig + this._endpoints = initConfig.endpoints + this.label = initConfig.label + this.logger = initConfig.logger ?? new ConsoleLogger(LogLevel.off) + this.agentDependencies = agentDependencies + } + + /** + * @todo move to context configuration + */ + public get walletConfig() { + return this.initConfig.walletConfig + } + + public get didCommMimeType() { + return this.initConfig.didCommMimeType ?? DidCommMimeType.V1 + } + + /** + * Encode keys in did:key format instead of 'naked' keys, as stated in Aries RFC 0360. + * + * This setting will not be taken into account if the other party has previously used naked keys + * in a given protocol (i.e. it does not support Aries RFC 0360). + */ + public get useDidKeyInProtocols() { + return this.initConfig.useDidKeyInProtocols ?? true + } + + public get endpoints(): [string, ...string[]] { + // if endpoints is not set, return queue endpoint + // https://github.com/hyperledger/aries-rfcs/issues/405#issuecomment-582612875 + if (!this._endpoints || this._endpoints.length === 0) { + return [DID_COMM_TRANSPORT_QUEUE] + } + + return this._endpoints as [string, ...string[]] + } + + public set endpoints(endpoints: string[]) { + this._endpoints = endpoints + } + + public get useDidSovPrefixWhereAllowed() { + return this.initConfig.useDidSovPrefixWhereAllowed ?? false + } + + /** + * @todo move to context configuration + */ + public get connectionImageUrl() { + return this.initConfig.connectionImageUrl + } + + public get autoUpdateStorageOnStartup() { + return this.initConfig.autoUpdateStorageOnStartup ?? false + } + + public get backupBeforeStorageUpdate() { + return this.initConfig.backupBeforeStorageUpdate ?? true + } + + public extend(config: Partial): AgentConfig { + return new AgentConfig( + { ...this.initConfig, logger: this.logger, label: this.label, ...config }, + this.agentDependencies + ) + } + + public toJSON() { + return { + ...this.initConfig, + walletConfig: { + ...this.walletConfig, + key: this.walletConfig?.key ? '[*****]' : undefined, + storage: { + ...this.walletConfig?.storage, + credentials: this.walletConfig?.storage?.credentials ? '[*****]' : undefined, + }, + }, + logger: this.logger.logLevel, + agentDependencies: Boolean(this.agentDependencies), + label: this.label, + } + } +} diff --git a/packages/core/src/agent/AgentDependencies.ts b/packages/core/src/agent/AgentDependencies.ts new file mode 100644 index 0000000000..15ba4da53b --- /dev/null +++ b/packages/core/src/agent/AgentDependencies.ts @@ -0,0 +1,12 @@ +import type { FileSystem } from '../storage/FileSystem' +import type { EventEmitter } from 'events' +import type WebSocket from 'ws' + +export interface AgentDependencies { + FileSystem: { + new (): FileSystem + } + EventEmitterClass: typeof EventEmitter + fetch: typeof fetch + WebSocketClass: typeof WebSocket +} diff --git a/packages/core/src/agent/AgentMessage.ts b/packages/core/src/agent/AgentMessage.ts new file mode 100644 index 0000000000..fdca23daa9 --- /dev/null +++ b/packages/core/src/agent/AgentMessage.ts @@ -0,0 +1,60 @@ +import type { PlaintextMessage } from '../types' +import type { ParsedMessageType } from '../utils/messageType' +import type { Constructor } from '../utils/mixins' + +import { Exclude } from 'class-transformer' + +import { AckDecorated } from '../decorators/ack/AckDecoratorExtension' +import { AttachmentDecorated } from '../decorators/attachment/AttachmentExtension' +import { L10nDecorated } from '../decorators/l10n/L10nDecoratorExtension' +import { ServiceDecorated } from '../decorators/service/ServiceDecoratorExtension' +import { ThreadDecorated } from '../decorators/thread/ThreadDecoratorExtension' +import { TimingDecorated } from '../decorators/timing/TimingDecoratorExtension' +import { TransportDecorated } from '../decorators/transport/TransportDecoratorExtension' +import { JsonTransformer } from '../utils/JsonTransformer' +import { replaceNewDidCommPrefixWithLegacyDidSovOnMessage } from '../utils/messageType' + +import { BaseMessage } from './BaseMessage' + +export type ConstructableAgentMessage = Constructor & { type: ParsedMessageType } + +const Decorated = ThreadDecorated( + L10nDecorated(TransportDecorated(TimingDecorated(AckDecorated(AttachmentDecorated(ServiceDecorated(BaseMessage)))))) +) + +export class AgentMessage extends Decorated { + /** + * Whether the protocol RFC was initially written using the legacy did:prefix instead of the + * new https://didcomm.org message type prefix. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0348-transition-msg-type-to-https/README.md + */ + @Exclude() + public readonly allowDidSovPrefix: boolean = false + + /** + * Whether to use Queue Transport in case the recipient of this message does not have a reliable + * endpoint available + * + * @see https://github.com/decentralized-identity/didcomm-messaging/blob/main/extensions/return_route/main.md#queue-transport + */ + @Exclude() + public readonly allowQueueTransport: boolean = true + + public toJSON({ useDidSovPrefixWhereAllowed }: { useDidSovPrefixWhereAllowed?: boolean } = {}): PlaintextMessage { + const json = JsonTransformer.toJSON(this) + + // If we have `useDidSovPrefixWhereAllowed` enabled, we want to replace the new https://didcomm.org prefix with the legacy did:sov prefix. + // However, we only do this if the protocol RFC was initially written with the did:sov message type prefix + // See https://github.com/hyperledger/aries-rfcs/blob/main/features/0348-transition-msg-type-to-https/README.md + if (this.allowDidSovPrefix && useDidSovPrefixWhereAllowed) { + replaceNewDidCommPrefixWithLegacyDidSovOnMessage(json) + } + + return json as PlaintextMessage + } + + public is(Class: C): this is InstanceType { + return this.type === Class.type.messageTypeUri + } +} diff --git a/packages/core/src/agent/AgentModules.ts b/packages/core/src/agent/AgentModules.ts new file mode 100644 index 0000000000..efe603a40f --- /dev/null +++ b/packages/core/src/agent/AgentModules.ts @@ -0,0 +1,216 @@ +import type { Module, DependencyManager, ApiModule } from '../plugins' +import type { IsAny } from '../types' +import type { Constructor } from '../utils/mixins' + +import { BasicMessagesModule } from '../modules/basic-messages' +import { CacheModule } from '../modules/cache' +import { ConnectionsModule } from '../modules/connections' +import { CredentialsModule } from '../modules/credentials' +import { DidsModule } from '../modules/dids' +import { DifPresentationExchangeModule } from '../modules/dif-presentation-exchange' +import { DiscoverFeaturesModule } from '../modules/discover-features' +import { GenericRecordsModule } from '../modules/generic-records' +import { MessagePickupModule } from '../modules/message-pickup' +import { OutOfBandModule } from '../modules/oob' +import { ProofsModule } from '../modules/proofs' +import { MediationRecipientModule, MediatorModule } from '../modules/routing' +import { SdJwtVcModule } from '../modules/sd-jwt-vc' +import { W3cCredentialsModule } from '../modules/vc' +import { WalletModule } from '../wallet' + +/** + * Simple utility type that represent a map of modules. This is used to map from moduleKey (api key) to the api in the framework. + */ +export type ModulesMap = { [key: string]: Module } + +// eslint-disable-next-line @typescript-eslint/ban-types +export type EmptyModuleMap = {} + +/** + * Default modules can be optionally defined to provide custom configuration. This type makes it so that it is not + * possible to use a different key for the default modules + */ +export type AgentModulesInput = Partial & ModulesMap + +/** + * Defines the input type for the default agent modules. This is overwritten as we + * want the input type to allow for generics to be passed in for the credentials module. + */ +export type DefaultAgentModulesInput = Omit & { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + credentials: CredentialsModule + // eslint-disable-next-line @typescript-eslint/no-explicit-any + proofs: ProofsModule +} + +/** + * Type that represents the default agent modules. This is the {@link ModulesMap} variant for the default modules in the framework. + * It uses the return type of the {@link getDefaultAgentModules} method to automatically infer which modules are always available on + * the agent and in the agent. namespace. + */ +export type DefaultAgentModules = { + [moduleKey in keyof ReturnType]: ReturnType< + ReturnType[moduleKey] + > +} + +export type WithoutDefaultModules = { + [moduleKey in Exclude]: Modules[moduleKey] +} + +/** + * Type that represents the api object of the agent (`agent.xxx`). It will extract all keys of the modules and map this to the + * registered {@link Module.api} class instance. If the module does not have an api class registered, the property will be removed + * and won't be available on the api object. + * + * @example + * If the following AgentModules type was passed: + * ```ts + * { + * connections: ConnectionsModule + * indy: IndyModule + * } + * ``` + * + * And we use the `AgentApi` type like this: + * ```ts + * type MyAgentApi = AgentApi<{ + * connections: ConnectionsModule + * indy: IndyModule + * }> + * ``` + * + * the resulting agent api will look like: + * + * ```ts + * { + * connections: ConnectionsApi + * } + * ``` + * + * The `indy` module has been ignored because it doesn't define an api class. + */ +export type AgentApi = { + [moduleKey in keyof Modules as Modules[moduleKey]['api'] extends Constructor + ? moduleKey + : never]: Modules[moduleKey]['api'] extends Constructor ? InstanceType : never +} + +/** + * Returns the `api` type from the CustomModuleType if the module is an ApiModule. If the module is not defined + * which is the case if you don't configure a default agent module (e.g. credentials module), it will use the default + * module type and use that for the typing. This will contain the default typing, and thus provide the correct agent api + * interface + */ +export type CustomOrDefaultApi< + CustomModuleType, + DefaultModuleType extends ApiModule +> = IsAny extends true + ? InstanceType + : CustomModuleType extends ApiModule + ? InstanceType + : CustomModuleType extends Module + ? never + : InstanceType + +/** + * Method to get the default agent modules to be registered on any agent instance. It doens't configure the modules in any way, + * and if that's needed the user needs to provide the module in the agent constructor + */ +function getDefaultAgentModules() { + return { + connections: () => new ConnectionsModule(), + credentials: () => new CredentialsModule(), + proofs: () => new ProofsModule(), + mediator: () => new MediatorModule(), + mediationRecipient: () => new MediationRecipientModule(), + messagePickup: () => new MessagePickupModule(), + basicMessages: () => new BasicMessagesModule(), + genericRecords: () => new GenericRecordsModule(), + discovery: () => new DiscoverFeaturesModule(), + dids: () => new DidsModule(), + wallet: () => new WalletModule(), + oob: () => new OutOfBandModule(), + w3cCredentials: () => new W3cCredentialsModule(), + cache: () => new CacheModule(), + pex: () => new DifPresentationExchangeModule(), + sdJwtVc: () => new SdJwtVcModule(), + } as const +} + +/** + * Extend the provided modules object with the default agent modules. If the modules property already contains a module with the same + * name as a default module, the module won't be added to the extended module object. This allows users of the framework to override + * the modules with custom configuration. The agent constructor type ensures you can't provide a different module for a key that registered + * on the default agent. + */ +export function extendModulesWithDefaultModules( + modules?: AgentModules +): AgentModules & DefaultAgentModules { + const extendedModules: Record = { ...modules } + const defaultAgentModules = getDefaultAgentModules() + + // Register all default modules, if not registered yet + for (const [moduleKey, getConfiguredModule] of Object.entries(defaultAgentModules)) { + // Do not register if the module is already registered. + if (modules && modules[moduleKey]) continue + + extendedModules[moduleKey] = getConfiguredModule() + } + + return extendedModules as AgentModules & DefaultAgentModules +} + +/** + * Get the agent api object based on the modules registered in the dependency manager. For each registered module on the + * dependency manager, the method will extract the api class from the module, resolve it and assign it to the module key + * as provided in the agent constructor (or the {@link getDefaultAgentModules} method). + * + * Modules that don't have an api class defined ({@link Module.api} is undefined) will be ignored and won't be added to the + * api object. + * + * If the api of a module is passed in the `excluded` array, the api will not be added to the resulting api object. + * + * @example + * If the dependency manager has the following modules configured: + * ```ts + * { + * connections: ConnectionsModule + * indy: IndyModule + * } + * ``` + * + * And we call the `getAgentApi` method like this: + * ```ts + * const api = getAgentApi(dependencyManager) + * ``` + * + * the resulting agent api will look like: + * + * ```ts + * { + * connections: ConnectionsApi + * } + * ``` + * + * The `indy` module has been ignored because it doesn't define an api class. + */ +export function getAgentApi( + dependencyManager: DependencyManager, + excludedApis: unknown[] = [] +): AgentApi { + // Create the api object based on the `api` properties on the modules. If no `api` exists + // on the module it will be ignored. + const api = Object.entries(dependencyManager.registeredModules).reduce((api, [moduleKey, module]) => { + // Module has no api + if (!module.api) return api + + const apiInstance = dependencyManager.resolve(module.api) + + // Api is excluded + if (excludedApis.includes(apiInstance)) return api + return { ...api, [moduleKey]: apiInstance } + }, {}) as AgentApi + + return api +} diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts new file mode 100644 index 0000000000..116b07ce32 --- /dev/null +++ b/packages/core/src/agent/BaseAgent.ts @@ -0,0 +1,202 @@ +import type { AgentConfig } from './AgentConfig' +import type { AgentApi, CustomOrDefaultApi, EmptyModuleMap, ModulesMap, WithoutDefaultModules } from './AgentModules' +import type { TransportSession } from './TransportService' +import type { Logger } from '../logger' +import type { CredentialsModule } from '../modules/credentials' +import type { MessagePickupModule } from '../modules/message-pickup' +import type { ProofsModule } from '../modules/proofs' +import type { DependencyManager } from '../plugins' + +import { CredoError } from '../error' +import { BasicMessagesApi } from '../modules/basic-messages' +import { ConnectionsApi } from '../modules/connections' +import { CredentialsApi } from '../modules/credentials' +import { DidsApi } from '../modules/dids' +import { DiscoverFeaturesApi } from '../modules/discover-features' +import { GenericRecordsApi } from '../modules/generic-records' +import { MessagePickupApi } from '../modules/message-pickup/MessagePickupApi' +import { OutOfBandApi } from '../modules/oob' +import { ProofsApi } from '../modules/proofs' +import { MediatorApi, MediationRecipientApi } from '../modules/routing' +import { SdJwtVcApi } from '../modules/sd-jwt-vc' +import { W3cCredentialsApi } from '../modules/vc/W3cCredentialsApi' +import { StorageUpdateService } from '../storage' +import { UpdateAssistant } from '../storage/migration/UpdateAssistant' +import { WalletApi } from '../wallet' +import { WalletError } from '../wallet/error' + +import { getAgentApi } from './AgentModules' +import { EventEmitter } from './EventEmitter' +import { FeatureRegistry } from './FeatureRegistry' +import { MessageReceiver } from './MessageReceiver' +import { MessageSender } from './MessageSender' +import { TransportService } from './TransportService' +import { AgentContext } from './context' + +export abstract class BaseAgent { + protected agentConfig: AgentConfig + protected logger: Logger + public readonly dependencyManager: DependencyManager + protected eventEmitter: EventEmitter + protected featureRegistry: FeatureRegistry + protected messageReceiver: MessageReceiver + protected transportService: TransportService + protected messageSender: MessageSender + protected _isInitialized = false + protected agentContext: AgentContext + + public readonly connections: ConnectionsApi + public readonly credentials: CustomOrDefaultApi + public readonly proofs: CustomOrDefaultApi + public readonly mediator: MediatorApi + public readonly mediationRecipient: MediationRecipientApi + public readonly messagePickup: CustomOrDefaultApi + public readonly basicMessages: BasicMessagesApi + public readonly genericRecords: GenericRecordsApi + public readonly discovery: DiscoverFeaturesApi + public readonly dids: DidsApi + public readonly wallet: WalletApi + public readonly oob: OutOfBandApi + public readonly w3cCredentials: W3cCredentialsApi + public readonly sdJwtVc: SdJwtVcApi + + public readonly modules: AgentApi> + + public constructor(agentConfig: AgentConfig, dependencyManager: DependencyManager) { + this.dependencyManager = dependencyManager + + this.agentConfig = agentConfig + this.logger = this.agentConfig.logger + + this.logger.info('Creating agent with config', { + agentConfig: agentConfig.toJSON(), + }) + + if (!this.agentConfig.walletConfig) { + this.logger.warn( + 'Wallet config has not been set on the agent config. ' + + 'Make sure to initialize the wallet yourself before initializing the agent, ' + + 'or provide the required wallet configuration in the agent constructor' + ) + } + + // Resolve instances after everything is registered + this.eventEmitter = this.dependencyManager.resolve(EventEmitter) + this.featureRegistry = this.dependencyManager.resolve(FeatureRegistry) + this.messageSender = this.dependencyManager.resolve(MessageSender) + this.messageReceiver = this.dependencyManager.resolve(MessageReceiver) + this.transportService = this.dependencyManager.resolve(TransportService) + this.agentContext = this.dependencyManager.resolve(AgentContext) + + this.connections = this.dependencyManager.resolve(ConnectionsApi) + this.credentials = this.dependencyManager.resolve(CredentialsApi) as CustomOrDefaultApi< + AgentModules['credentials'], + CredentialsModule + > + this.proofs = this.dependencyManager.resolve(ProofsApi) as CustomOrDefaultApi + this.mediator = this.dependencyManager.resolve(MediatorApi) + this.mediationRecipient = this.dependencyManager.resolve(MediationRecipientApi) + this.messagePickup = this.dependencyManager.resolve(MessagePickupApi) as CustomOrDefaultApi< + AgentModules['messagePickup'], + MessagePickupModule + > + this.basicMessages = this.dependencyManager.resolve(BasicMessagesApi) + this.genericRecords = this.dependencyManager.resolve(GenericRecordsApi) + this.discovery = this.dependencyManager.resolve(DiscoverFeaturesApi) + this.dids = this.dependencyManager.resolve(DidsApi) + this.wallet = this.dependencyManager.resolve(WalletApi) + this.oob = this.dependencyManager.resolve(OutOfBandApi) + this.w3cCredentials = this.dependencyManager.resolve(W3cCredentialsApi) + this.sdJwtVc = this.dependencyManager.resolve(SdJwtVcApi) + + const defaultApis = [ + this.connections, + this.credentials, + this.proofs, + this.mediator, + this.mediationRecipient, + this.messagePickup, + this.basicMessages, + this.genericRecords, + this.discovery, + this.dids, + this.wallet, + this.oob, + this.w3cCredentials, + this.sdJwtVc, + ] + + // Set the api of the registered modules on the agent, excluding the default apis + this.modules = getAgentApi(this.dependencyManager, defaultApis) + } + + public get isInitialized() { + return this._isInitialized && this.wallet.isInitialized + } + + public async initialize() { + const { walletConfig } = this.agentConfig + + if (this._isInitialized) { + throw new CredoError( + 'Agent already initialized. Currently it is not supported to re-initialize an already initialized agent.' + ) + } + + if (!this.wallet.isInitialized && walletConfig) { + await this.wallet.initialize(walletConfig) + } else if (!this.wallet.isInitialized) { + throw new WalletError( + 'Wallet config has not been set on the agent config. ' + + 'Make sure to initialize the wallet yourself before initializing the agent, ' + + 'or provide the required wallet configuration in the agent constructor' + ) + } + + // Make sure the storage is up to date + const storageUpdateService = this.dependencyManager.resolve(StorageUpdateService) + const isStorageUpToDate = await storageUpdateService.isUpToDate(this.agentContext) + this.logger.info(`Agent storage is ${isStorageUpToDate ? '' : 'not '}up to date.`) + + if (!isStorageUpToDate && this.agentConfig.autoUpdateStorageOnStartup) { + const updateAssistant = new UpdateAssistant(this) + + await updateAssistant.initialize() + await updateAssistant.update({ backupBeforeStorageUpdate: this.agentConfig.backupBeforeStorageUpdate }) + } else if (!isStorageUpToDate) { + const currentVersion = await storageUpdateService.getCurrentStorageVersion(this.agentContext) + // Close wallet to prevent un-initialized agent with initialized wallet + await this.wallet.close() + throw new CredoError( + // TODO: add link to where documentation on how to update can be found. + `Current agent storage is not up to date. ` + + `To prevent the framework state from getting corrupted the agent initialization is aborted. ` + + `Make sure to update the agent storage (currently at ${currentVersion}) to the latest version (${UpdateAssistant.frameworkStorageVersion}). ` + + `You can also downgrade your version of Credo.` + ) + } + } + + /** + * Receive a message. + * + * @deprecated Use {@link OutOfBandApi.receiveInvitationFromUrl} instead for receiving legacy connection-less messages. + * For receiving messages that originated from a transport, use the {@link MessageReceiver} + * for this. The `receiveMessage` method on the `Agent` class will associate the current context to the message, which + * may not be what should happen (e.g. in case of multi tenancy). + */ + public async receiveMessage(inboundMessage: unknown, session?: TransportSession) { + return await this.messageReceiver.receiveMessage(inboundMessage, { + session, + contextCorrelationId: this.agentContext.contextCorrelationId, + }) + } + + public get config() { + return this.agentConfig + } + + public get context() { + return this.agentContext + } +} diff --git a/packages/core/src/agent/BaseMessage.ts b/packages/core/src/agent/BaseMessage.ts new file mode 100644 index 0000000000..9ff18ccaf5 --- /dev/null +++ b/packages/core/src/agent/BaseMessage.ts @@ -0,0 +1,27 @@ +import type { ParsedMessageType } from '../utils/messageType' +import type { Constructor } from '../utils/mixins' + +import { Expose } from 'class-transformer' +import { Matches } from 'class-validator' + +import { uuid } from '../utils/uuid' + +export const MessageIdRegExp = /[-_./a-zA-Z0-9]{8,64}/ +export const MessageTypeRegExp = /(.*?)([a-zA-Z0-9._-]+)\/(\d[^/]*)\/([a-zA-Z0-9._-]+)$/ + +export type BaseMessageConstructor = Constructor + +export class BaseMessage { + @Matches(MessageIdRegExp) + @Expose({ name: '@id' }) + public id!: string + + @Expose({ name: '@type' }) + @Matches(MessageTypeRegExp) + public readonly type!: string + public static readonly type: ParsedMessageType + + public generateId() { + return uuid() + } +} diff --git a/packages/core/src/agent/Dispatcher.ts b/packages/core/src/agent/Dispatcher.ts new file mode 100644 index 0000000000..b42f9aa6ca --- /dev/null +++ b/packages/core/src/agent/Dispatcher.ts @@ -0,0 +1,146 @@ +import type { AgentMessage } from './AgentMessage' +import type { AgentMessageProcessedEvent } from './Events' +import type { MessageHandlerMiddleware } from './MessageHandlerMiddleware' +import type { InboundMessageContext } from './models/InboundMessageContext' + +import { InjectionSymbols } from '../constants' +import { CredoError } from '../error' +import { Logger } from '../logger' +import { ProblemReportError, ProblemReportReason } from '../modules/problem-reports' +import { injectable, inject } from '../plugins' +import { canHandleMessageType, parseMessageType } from '../utils/messageType' + +import { ProblemReportMessage } from './../modules/problem-reports/messages/ProblemReportMessage' +import { EventEmitter } from './EventEmitter' +import { AgentEventTypes } from './Events' +import { MessageHandlerMiddlewareRunner } from './MessageHandlerMiddleware' +import { MessageHandlerRegistry } from './MessageHandlerRegistry' +import { MessageSender } from './MessageSender' +import { OutboundMessageContext } from './models' + +@injectable() +class Dispatcher { + private messageHandlerRegistry: MessageHandlerRegistry + private messageSender: MessageSender + private eventEmitter: EventEmitter + private logger: Logger + + public constructor( + messageSender: MessageSender, + eventEmitter: EventEmitter, + messageHandlerRegistry: MessageHandlerRegistry, + @inject(InjectionSymbols.Logger) logger: Logger + ) { + this.messageSender = messageSender + this.eventEmitter = eventEmitter + this.messageHandlerRegistry = messageHandlerRegistry + this.logger = logger + } + + private defaultHandlerMiddleware: MessageHandlerMiddleware = async (inboundMessageContext, next) => { + let messageHandler = inboundMessageContext.messageHandler + + if (!messageHandler && inboundMessageContext.agentContext.dependencyManager.fallbackMessageHandler) { + messageHandler = { + supportedMessages: [], + handle: inboundMessageContext.agentContext.dependencyManager.fallbackMessageHandler, + } + } + + if (!messageHandler) { + throw new ProblemReportError( + `Error handling message ${inboundMessageContext.message.id} with type ${inboundMessageContext.message.type}. The message type is not supported`, + { + problemCode: ProblemReportReason.MessageParseFailure, + } + ) + } + + const outboundMessage = await messageHandler.handle(inboundMessageContext) + if (outboundMessage) { + inboundMessageContext.setResponseMessage(outboundMessage) + } + + await next() + } + + public async dispatch(messageContext: InboundMessageContext): Promise { + const { agentContext, connection, senderKey, recipientKey, message } = messageContext + + // Set default handler if available, middleware can still override the message handler + const messageHandler = this.messageHandlerRegistry.getHandlerForMessageType(message.type) + if (messageHandler) { + messageContext.setMessageHandler(messageHandler) + } + + let outboundMessage: OutboundMessageContext | undefined + + try { + const middlewares = [...agentContext.dependencyManager.messageHandlerMiddlewares, this.defaultHandlerMiddleware] + await MessageHandlerMiddlewareRunner.run(middlewares, messageContext) + + outboundMessage = messageContext.responseMessage + } catch (error) { + const problemReportMessage = error.problemReport + + if (problemReportMessage instanceof ProblemReportMessage && messageContext.connection) { + const messageType = parseMessageType(messageContext.message.type) + if (canHandleMessageType(ProblemReportMessage, messageType)) { + throw new CredoError(`Not sending problem report in response to problem report: ${message}`) + } + + const { protocolUri: problemReportProtocolUri } = parseMessageType(problemReportMessage.type) + const { protocolUri: inboundProtocolUri } = parseMessageType(messageContext.message.type) + + // If the inbound protocol uri is the same as the problem report protocol uri, we can see the interaction as the same thread + // However if it is no the same we should see it as a new thread, where the inbound message `@id` is the parentThreadId + if (inboundProtocolUri === problemReportProtocolUri) { + problemReportMessage.setThread({ + threadId: message.threadId, + }) + } else { + problemReportMessage.setThread({ + parentThreadId: message.id, + }) + } + + outboundMessage = new OutboundMessageContext(problemReportMessage, { + agentContext, + connection: messageContext.connection, + inboundMessageContext: messageContext, + }) + } else { + this.logger.error(`Error handling message with type ${message.type}`, { + message: message.toJSON(), + error, + senderKey: senderKey?.fingerprint, + recipientKey: recipientKey?.fingerprint, + connectionId: connection?.id, + }) + + throw error + } + } + + if (outboundMessage) { + // set the inbound message context, if not already defined + if (!outboundMessage.inboundMessageContext) { + outboundMessage.inboundMessageContext = messageContext + } + + await this.messageSender.sendMessage(outboundMessage) + } + + // Emit event that allows to hook into received messages + this.eventEmitter.emit(agentContext, { + type: AgentEventTypes.AgentMessageProcessed, + payload: { + message, + connection, + receivedAt: messageContext.receivedAt, + }, + }) + } +} + +export { Dispatcher } diff --git a/packages/core/src/agent/EnvelopeService.ts b/packages/core/src/agent/EnvelopeService.ts new file mode 100644 index 0000000000..37b341cd6f --- /dev/null +++ b/packages/core/src/agent/EnvelopeService.ts @@ -0,0 +1,81 @@ +import type { AgentMessage } from './AgentMessage' +import type { AgentContext } from './context' +import type { EncryptedMessage, PlaintextMessage } from '../types' + +import { InjectionSymbols } from '../constants' +import { Key, KeyType } from '../crypto' +import { Logger } from '../logger' +import { ForwardMessage } from '../modules/routing/messages' +import { inject, injectable } from '../plugins' + +export interface EnvelopeKeys { + recipientKeys: Key[] + routingKeys: Key[] + senderKey: Key | null +} + +@injectable() +export class EnvelopeService { + private logger: Logger + + public constructor(@inject(InjectionSymbols.Logger) logger: Logger) { + this.logger = logger + } + + public async packMessage( + agentContext: AgentContext, + payload: AgentMessage, + keys: EnvelopeKeys + ): Promise { + const { recipientKeys, routingKeys, senderKey } = keys + let recipientKeysBase58 = recipientKeys.map((key) => key.publicKeyBase58) + const routingKeysBase58 = routingKeys.map((key) => key.publicKeyBase58) + const senderKeyBase58 = senderKey && senderKey.publicKeyBase58 + + // pass whether we want to use legacy did sov prefix + const message = payload.toJSON({ useDidSovPrefixWhereAllowed: agentContext.config.useDidSovPrefixWhereAllowed }) + + this.logger.debug(`Pack outbound message ${message['@type']}`) + + let encryptedMessage = await agentContext.wallet.pack(message, recipientKeysBase58, senderKeyBase58 ?? undefined) + + // If the message has routing keys (mediator) pack for each mediator + for (const routingKeyBase58 of routingKeysBase58) { + const forwardMessage = new ForwardMessage({ + // Forward to first recipient key + to: recipientKeysBase58[0], + message: encryptedMessage, + }) + recipientKeysBase58 = [routingKeyBase58] + this.logger.debug('Forward message created', forwardMessage) + + const forwardJson = forwardMessage.toJSON({ + useDidSovPrefixWhereAllowed: agentContext.config.useDidSovPrefixWhereAllowed, + }) + + // Forward messages are anon packed + encryptedMessage = await agentContext.wallet.pack(forwardJson, [routingKeyBase58], undefined) + } + + return encryptedMessage + } + + public async unpackMessage( + agentContext: AgentContext, + encryptedMessage: EncryptedMessage + ): Promise { + const decryptedMessage = await agentContext.wallet.unpack(encryptedMessage) + const { recipientKey, senderKey, plaintextMessage } = decryptedMessage + return { + recipientKey: recipientKey ? Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519) : undefined, + senderKey: senderKey ? Key.fromPublicKeyBase58(senderKey, KeyType.Ed25519) : undefined, + plaintextMessage, + } + } +} + +export interface DecryptedMessageContext { + plaintextMessage: PlaintextMessage + senderKey?: Key + recipientKey?: Key +} diff --git a/packages/core/src/agent/EventEmitter.ts b/packages/core/src/agent/EventEmitter.ts new file mode 100644 index 0000000000..3d30e5fa32 --- /dev/null +++ b/packages/core/src/agent/EventEmitter.ts @@ -0,0 +1,52 @@ +import type { BaseEvent } from './Events' +import type { AgentContext } from './context' +import type { EventEmitter as NativeEventEmitter } from 'events' + +import { fromEventPattern, Subject } from 'rxjs' +import { takeUntil } from 'rxjs/operators' + +import { InjectionSymbols } from '../constants' +import { injectable, inject } from '../plugins' + +import { AgentDependencies } from './AgentDependencies' + +type EmitEvent = Omit + +@injectable() +export class EventEmitter { + private eventEmitter: NativeEventEmitter + private stop$: Subject + + public constructor( + @inject(InjectionSymbols.AgentDependencies) agentDependencies: AgentDependencies, + @inject(InjectionSymbols.Stop$) stop$: Subject + ) { + this.eventEmitter = new agentDependencies.EventEmitterClass() + this.stop$ = stop$ + } + + // agentContext is currently not used, but already making required as it will be used soon + public emit(agentContext: AgentContext, data: EmitEvent) { + this.eventEmitter.emit(data.type, { + ...data, + metadata: { + contextCorrelationId: agentContext.contextCorrelationId, + }, + }) + } + + public on(event: T['type'], listener: (data: T) => void | Promise) { + this.eventEmitter.on(event, listener) + } + + public off(event: T['type'], listener: (data: T) => void | Promise) { + this.eventEmitter.off(event, listener) + } + + public observable(event: T['type']) { + return fromEventPattern( + (handler) => this.on(event, handler), + (handler) => this.off(event, handler) + ).pipe(takeUntil(this.stop$)) + } +} diff --git a/packages/core/src/agent/Events.ts b/packages/core/src/agent/Events.ts new file mode 100644 index 0000000000..8a889a237c --- /dev/null +++ b/packages/core/src/agent/Events.ts @@ -0,0 +1,55 @@ +import type { AgentMessage } from './AgentMessage' +import type { OutboundMessageContext, OutboundMessageSendStatus } from './models' +import type { ConnectionRecord } from '../modules/connections' +import type { Observable } from 'rxjs' + +import { filter } from 'rxjs' + +export function filterContextCorrelationId(contextCorrelationId: string) { + return (source: Observable) => { + return source.pipe(filter((event) => event.metadata.contextCorrelationId === contextCorrelationId)) + } +} + +export enum AgentEventTypes { + AgentMessageReceived = 'AgentMessageReceived', + AgentMessageProcessed = 'AgentMessageProcessed', + AgentMessageSent = 'AgentMessageSent', +} + +export interface EventMetadata { + contextCorrelationId: string +} + +export interface BaseEvent { + type: string + payload: Record + metadata: EventMetadata +} + +export interface AgentMessageReceivedEvent extends BaseEvent { + type: typeof AgentEventTypes.AgentMessageReceived + payload: { + message: unknown + connection?: ConnectionRecord + contextCorrelationId?: string + receivedAt?: Date + } +} + +export interface AgentMessageProcessedEvent extends BaseEvent { + type: typeof AgentEventTypes.AgentMessageProcessed + payload: { + message: AgentMessage + connection?: ConnectionRecord + receivedAt?: Date + } +} + +export interface AgentMessageSentEvent extends BaseEvent { + type: typeof AgentEventTypes.AgentMessageSent + payload: { + message: OutboundMessageContext + status: OutboundMessageSendStatus + } +} diff --git a/packages/core/src/agent/FeatureRegistry.ts b/packages/core/src/agent/FeatureRegistry.ts new file mode 100644 index 0000000000..bfcf3e5f8c --- /dev/null +++ b/packages/core/src/agent/FeatureRegistry.ts @@ -0,0 +1,56 @@ +import type { FeatureQuery, Feature } from './models' + +import { injectable } from 'tsyringe' + +@injectable() +class FeatureRegistry { + private features: Feature[] = [] + + /** + * Register a single or set of Features on the registry + * + * @param features set of {Feature} objects or any inherited class + */ + public register(...features: Feature[]) { + for (const feature of features) { + const index = this.features.findIndex((item) => item.type === feature.type && item.id === feature.id) + + if (index > -1) { + this.features[index] = this.features[index].combine(feature) + } else { + this.features.push(feature) + } + } + } + + /** + * Perform a set of queries in the registry, supporting wildcards (*) as + * expressed in Aries RFC 0557. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/560ffd23361f16a01e34ccb7dcc908ec28c5ddb1/features/0557-discover-features-v2/README.md + * + * @param queries set of {FeatureQuery} objects to query features + * @returns array containing all matching features (can be empty) + */ + public query(...queries: FeatureQuery[]) { + const output = [] + for (const query of queries) { + const items = this.features.filter((item) => item.type === query.featureType) + // An * will return all features of a given type (e.g. all protocols, all goal codes, all AIP configs) + if (query.match === '*') { + output.push(...items) + // An string ending with * will return a family of features of a certain type + // (e.g. all versions of a given protocol, all subsets of an AIP, etc.) + } else if (query.match.endsWith('*')) { + const match = query.match.slice(0, -1) + output.push(...items.filter((m) => m.id.startsWith(match))) + // Exact matching (single feature) + } else { + output.push(...items.filter((m) => m.id === query.match)) + } + } + return output + } +} + +export { FeatureRegistry } diff --git a/packages/core/src/agent/MessageHandler.ts b/packages/core/src/agent/MessageHandler.ts new file mode 100644 index 0000000000..692cea5cc5 --- /dev/null +++ b/packages/core/src/agent/MessageHandler.ts @@ -0,0 +1,19 @@ +import type { ConstructableAgentMessage } from './AgentMessage' +import type { InboundMessageContext, OutboundMessageContext } from './models' + +export interface MessageHandler { + readonly supportedMessages: readonly ConstructableAgentMessage[] + + handle(messageContext: InboundMessageContext): Promise +} + +/** + * Provides exact typing for the AgentMessage in the message context in the `handle` function + * of a handler. It takes all possible types from `supportedMessageTypes` + * + * @example + * async handle(messageContext: MessageHandlerInboundMessage) {} + */ +export type MessageHandlerInboundMessage = InboundMessageContext< + InstanceType +> diff --git a/packages/core/src/agent/MessageHandlerMiddleware.ts b/packages/core/src/agent/MessageHandlerMiddleware.ts new file mode 100644 index 0000000000..9eff446bbd --- /dev/null +++ b/packages/core/src/agent/MessageHandlerMiddleware.ts @@ -0,0 +1,26 @@ +import type { InboundMessageContext } from './models/InboundMessageContext' + +export interface MessageHandlerMiddleware { + (inboundMessageContext: InboundMessageContext, next: () => Promise): Promise +} + +export class MessageHandlerMiddlewareRunner { + public static async run(middlewares: MessageHandlerMiddleware[], inboundMessageContext: InboundMessageContext) { + const compose = (middlewares: MessageHandlerMiddleware[]) => { + return async function (inboundMessageContext: InboundMessageContext) { + let index = -1 + async function dispatch(i: number): Promise { + if (i <= index) throw new Error('next() called multiple times') + index = i + const fn = middlewares[i] + if (!fn) return + await fn(inboundMessageContext, () => dispatch(i + 1)) + } + await dispatch(0) + } + } + + const composed = compose(middlewares) + await composed(inboundMessageContext) + } +} diff --git a/packages/core/src/agent/MessageHandlerRegistry.ts b/packages/core/src/agent/MessageHandlerRegistry.ts new file mode 100644 index 0000000000..67c1de08fb --- /dev/null +++ b/packages/core/src/agent/MessageHandlerRegistry.ts @@ -0,0 +1,90 @@ +import type { AgentMessage } from './AgentMessage' +import type { MessageHandler } from './MessageHandler' +import type { MessageHandlerMiddleware } from './MessageHandlerMiddleware' +import type { ParsedDidCommProtocolUri } from '../utils/messageType' + +import { injectable } from 'tsyringe' + +import { supportsIncomingDidCommProtocolUri, canHandleMessageType, parseMessageType } from '../utils/messageType' + +@injectable() +export class MessageHandlerRegistry { + private messageHandlers: MessageHandler[] = [] + public readonly messageHandlerMiddlewares: MessageHandlerMiddleware[] = [] + private _fallbackMessageHandler?: MessageHandler['handle'] + + public registerMessageHandler(messageHandler: MessageHandler) { + this.messageHandlers.push(messageHandler) + } + + public registerMessageHandlerMiddleware(messageHandlerMiddleware: MessageHandlerMiddleware) { + this.messageHandlerMiddlewares.push(messageHandlerMiddleware) + } + + public get fallbackMessageHandler() { + return this._fallbackMessageHandler + } + + /** + * Sets the fallback message handler, the message handler that will be called if no handler + * is registered for an incoming message type. + */ + public setFallbackMessageHandler(fallbackMessageHandler: MessageHandler['handle']) { + this._fallbackMessageHandler = fallbackMessageHandler + } + + public getHandlerForMessageType(messageType: string): MessageHandler | undefined { + const incomingMessageType = parseMessageType(messageType) + + for (const handler of this.messageHandlers) { + for (const MessageClass of handler.supportedMessages) { + if (canHandleMessageType(MessageClass, incomingMessageType)) return handler + } + } + } + + public getMessageClassForMessageType(messageType: string): typeof AgentMessage | undefined { + const incomingMessageType = parseMessageType(messageType) + + for (const handler of this.messageHandlers) { + for (const MessageClass of handler.supportedMessages) { + if (canHandleMessageType(MessageClass, incomingMessageType)) return MessageClass + } + } + } + + /** + * Returns array of message types that dispatcher is able to handle. + * Message type format is MTURI specified at https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0003-protocols/README.md#mturi. + */ + public get supportedMessageTypes() { + return this.messageHandlers + .reduce<(typeof AgentMessage)[]>((all, cur) => [...all, ...cur.supportedMessages], []) + .map((m) => m.type) + } + + /** + * Returns array of protocol IDs that dispatcher is able to handle. + * Protocol ID format is PIURI specified at https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0003-protocols/README.md#piuri. + */ + public get supportedProtocolUris() { + const seenProtocolUris = new Set() + + const protocolUris: ParsedDidCommProtocolUri[] = this.supportedMessageTypes + .filter((m) => { + const has = seenProtocolUris.has(m.protocolUri) + seenProtocolUris.add(m.protocolUri) + return !has + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .map(({ messageName, messageTypeUri, ...parsedProtocolUri }) => parsedProtocolUri) + + return protocolUris + } + + public filterSupportedProtocolsByProtocolUris(parsedProtocolUris: ParsedDidCommProtocolUri[]) { + return this.supportedProtocolUris.filter((supportedProtocol) => + parsedProtocolUris.some((p) => supportsIncomingDidCommProtocolUri(supportedProtocol, p)) + ) + } +} diff --git a/packages/core/src/agent/MessageReceiver.ts b/packages/core/src/agent/MessageReceiver.ts new file mode 100644 index 0000000000..02f94d1e7b --- /dev/null +++ b/packages/core/src/agent/MessageReceiver.ts @@ -0,0 +1,301 @@ +import type { DecryptedMessageContext } from './EnvelopeService' +import type { TransportSession } from './TransportService' +import type { AgentContext } from './context' +import type { ConnectionRecord } from '../modules/connections' +import type { InboundTransport } from '../transport' +import type { EncryptedMessage, PlaintextMessage } from '../types' + +import { InjectionSymbols } from '../constants' +import { CredoError } from '../error' +import { Logger } from '../logger' +import { ConnectionService } from '../modules/connections' +import { ProblemReportError, ProblemReportMessage, ProblemReportReason } from '../modules/problem-reports' +import { inject, injectable } from '../plugins' +import { isValidJweStructure } from '../utils/JWE' +import { JsonTransformer } from '../utils/JsonTransformer' +import { canHandleMessageType, parseMessageType, replaceLegacyDidSovPrefixOnMessage } from '../utils/messageType' + +import { AgentMessage } from './AgentMessage' +import { Dispatcher } from './Dispatcher' +import { EnvelopeService } from './EnvelopeService' +import { MessageHandlerRegistry } from './MessageHandlerRegistry' +import { MessageSender } from './MessageSender' +import { TransportService } from './TransportService' +import { AgentContextProvider } from './context' +import { InboundMessageContext, OutboundMessageContext } from './models' + +@injectable() +export class MessageReceiver { + private envelopeService: EnvelopeService + private transportService: TransportService + private messageSender: MessageSender + private dispatcher: Dispatcher + private logger: Logger + private connectionService: ConnectionService + private messageHandlerRegistry: MessageHandlerRegistry + private agentContextProvider: AgentContextProvider + private _inboundTransports: InboundTransport[] = [] + + public constructor( + envelopeService: EnvelopeService, + transportService: TransportService, + messageSender: MessageSender, + connectionService: ConnectionService, + dispatcher: Dispatcher, + messageHandlerRegistry: MessageHandlerRegistry, + @inject(InjectionSymbols.AgentContextProvider) agentContextProvider: AgentContextProvider, + @inject(InjectionSymbols.Logger) logger: Logger + ) { + this.envelopeService = envelopeService + this.transportService = transportService + this.messageSender = messageSender + this.connectionService = connectionService + this.dispatcher = dispatcher + this.messageHandlerRegistry = messageHandlerRegistry + this.agentContextProvider = agentContextProvider + this.logger = logger + this._inboundTransports = [] + } + + public get inboundTransports() { + return this._inboundTransports + } + + public registerInboundTransport(inboundTransport: InboundTransport) { + this._inboundTransports.push(inboundTransport) + } + + public async unregisterInboundTransport(inboundTransport: InboundTransport) { + this._inboundTransports = this._inboundTransports.filter((transport) => transport !== inboundTransport) + await inboundTransport.stop() + } + + /** + * Receive and handle an inbound DIDComm message. It will determine the agent context, decrypt the message, transform it + * to it's corresponding message class and finally dispatch it to the dispatcher. + * + * @param inboundMessage the message to receive and handle + */ + public async receiveMessage( + inboundMessage: unknown, + { + session, + connection, + contextCorrelationId, + receivedAt, + }: { + session?: TransportSession + connection?: ConnectionRecord + contextCorrelationId?: string + receivedAt?: Date + } = {} + ) { + this.logger.debug(`Agent received message`) + + // Find agent context for the inbound message + const agentContext = await this.agentContextProvider.getContextForInboundMessage(inboundMessage, { + contextCorrelationId, + }) + + try { + if (this.isEncryptedMessage(inboundMessage)) { + await this.receiveEncryptedMessage(agentContext, inboundMessage as EncryptedMessage, session, receivedAt) + } else if (this.isPlaintextMessage(inboundMessage)) { + await this.receivePlaintextMessage(agentContext, inboundMessage, connection, receivedAt) + } else { + throw new CredoError('Unable to parse incoming message: unrecognized format') + } + } finally { + // Always end the session for the agent context after handling the message. + await agentContext.endSession() + } + } + + private async receivePlaintextMessage( + agentContext: AgentContext, + plaintextMessage: PlaintextMessage, + connection?: ConnectionRecord, + receivedAt?: Date + ) { + const message = await this.transformAndValidate(agentContext, plaintextMessage) + const messageContext = new InboundMessageContext(message, { connection, agentContext, receivedAt }) + await this.dispatcher.dispatch(messageContext) + } + + private async receiveEncryptedMessage( + agentContext: AgentContext, + encryptedMessage: EncryptedMessage, + session?: TransportSession, + receivedAt?: Date + ) { + const decryptedMessage = await this.decryptMessage(agentContext, encryptedMessage) + const { plaintextMessage, senderKey, recipientKey } = decryptedMessage + + this.logger.info( + `Received message with type '${plaintextMessage['@type']}', recipient key ${recipientKey?.fingerprint} and sender key ${senderKey?.fingerprint}`, + plaintextMessage + ) + + const connection = await this.findConnectionByMessageKeys(agentContext, decryptedMessage) + + const message = await this.transformAndValidate(agentContext, plaintextMessage, connection) + + const messageContext = new InboundMessageContext(message, { + // Only make the connection available in message context if the connection is ready + // To prevent unwanted usage of unready connections. Connections can still be retrieved from + // Storage if the specific protocol allows an unready connection to be used. + connection: connection?.isReady ? connection : undefined, + senderKey, + recipientKey, + agentContext, + receivedAt, + }) + + // We want to save a session if there is a chance of returning outbound message via inbound transport. + // That can happen when inbound message has `return_route` set to `all` or `thread`. + // If `return_route` defines just `thread`, we decide later whether to use session according to outbound message `threadId`. + if (senderKey && recipientKey && message.hasAnyReturnRoute() && session) { + this.logger.debug(`Storing session for inbound message '${message.id}'`) + const keys = { + recipientKeys: [senderKey], + routingKeys: [], + senderKey: recipientKey, + } + session.keys = keys + session.inboundMessage = message + // We allow unready connections to be attached to the session as we want to be able to + // use return routing to make connections. This is especially useful for creating connections + // with mediators when you don't have a public endpoint yet. + session.connectionId = connection?.id + messageContext.sessionId = session.id + this.transportService.saveSession(session) + } else if (session) { + // No need to wait for session to stay open if we're not actually going to respond to the message. + await session.close() + } + + await this.dispatcher.dispatch(messageContext) + } + + /** + * Decrypt a message using the envelope service. + * + * @param message the received inbound message to decrypt + */ + private async decryptMessage( + agentContext: AgentContext, + message: EncryptedMessage + ): Promise { + try { + return await this.envelopeService.unpackMessage(agentContext, message) + } catch (error) { + this.logger.error('Error while decrypting message', { + error, + encryptedMessage: message, + errorMessage: error instanceof Error ? error.message : error, + }) + throw error + } + } + + private isPlaintextMessage(message: unknown): message is PlaintextMessage { + if (typeof message !== 'object' || message == null) { + return false + } + // If the message has a @type field we assume the message is in plaintext and it is not encrypted. + return '@type' in message + } + + private isEncryptedMessage(message: unknown): message is EncryptedMessage { + // If the message does has valid JWE structure, we can assume the message is encrypted. + return isValidJweStructure(message) + } + + private async transformAndValidate( + agentContext: AgentContext, + plaintextMessage: PlaintextMessage, + connection?: ConnectionRecord | null + ): Promise { + let message: AgentMessage + try { + message = await this.transformMessage(plaintextMessage) + } catch (error) { + if (connection) await this.sendProblemReportMessage(agentContext, error.message, connection, plaintextMessage) + throw error + } + return message + } + + private async findConnectionByMessageKeys( + agentContext: AgentContext, + { recipientKey, senderKey }: DecryptedMessageContext + ): Promise { + // We only fetch connections that are sent in AuthCrypt mode + if (!recipientKey || !senderKey) return null + + // Try to find the did records that holds the sender and recipient keys + return this.connectionService.findByKeys(agentContext, { + senderKey, + recipientKey, + }) + } + + /** + * Transform an plaintext DIDComm message into it's corresponding message class. Will look at all message types in the registered handlers. + * + * @param message the plaintext message for which to transform the message in to a class instance + */ + private async transformMessage(message: PlaintextMessage): Promise { + // replace did:sov:BzCbsNYhMrjHiqZDTUASHg;spec prefix for message type with https://didcomm.org + replaceLegacyDidSovPrefixOnMessage(message) + + const messageType = message['@type'] + const MessageClass = this.messageHandlerRegistry.getMessageClassForMessageType(messageType) ?? AgentMessage + + // Cast the plain JSON object to specific instance of Message extended from AgentMessage + let messageTransformed: AgentMessage + try { + messageTransformed = JsonTransformer.fromJSON(message, MessageClass) + } catch (error) { + this.logger.error(`Error validating message ${message.type}`, { + errors: error, + message: JSON.stringify(message), + }) + throw new ProblemReportError(`Error validating message ${message.type}`, { + problemCode: ProblemReportReason.MessageParseFailure, + }) + } + return messageTransformed + } + + /** + * Send the problem report message (https://didcomm.org/notification/1.0/problem-report) to the recipient. + * @param message error message to send + * @param connection connection to send the message to + * @param plaintextMessage received inbound message + */ + private async sendProblemReportMessage( + agentContext: AgentContext, + message: string, + connection: ConnectionRecord, + plaintextMessage: PlaintextMessage + ) { + const messageType = parseMessageType(plaintextMessage['@type']) + if (canHandleMessageType(ProblemReportMessage, messageType)) { + throw new CredoError(`Not sending problem report in response to problem report: ${message}`) + } + const problemReportMessage = new ProblemReportMessage({ + description: { + en: message, + code: ProblemReportReason.MessageParseFailure, + }, + }) + problemReportMessage.setThread({ + parentThreadId: plaintextMessage['@id'], + }) + const outboundMessageContext = new OutboundMessageContext(problemReportMessage, { agentContext, connection }) + if (outboundMessageContext) { + await this.messageSender.sendMessage(outboundMessageContext) + } + } +} diff --git a/packages/core/src/agent/MessageSender.ts b/packages/core/src/agent/MessageSender.ts new file mode 100644 index 0000000000..1787f7ac02 --- /dev/null +++ b/packages/core/src/agent/MessageSender.ts @@ -0,0 +1,565 @@ +import type { AgentMessage } from './AgentMessage' +import type { EnvelopeKeys } from './EnvelopeService' +import type { AgentMessageSentEvent } from './Events' +import type { TransportSession } from './TransportService' +import type { AgentContext } from './context' +import type { ConnectionRecord } from '../modules/connections' +import type { ResolvedDidCommService } from '../modules/didcomm' +import type { OutOfBandRecord } from '../modules/oob/repository' +import type { OutboundTransport } from '../transport/OutboundTransport' +import type { EncryptedMessage, OutboundPackage } from '../types' + +import { DID_COMM_TRANSPORT_QUEUE, InjectionSymbols } from '../constants' +import { ReturnRouteTypes } from '../decorators/transport/TransportDecorator' +import { CredoError, MessageSendingError } from '../error' +import { Logger } from '../logger' +import { DidCommDocumentService } from '../modules/didcomm' +import { DidKey, type DidDocument } from '../modules/dids' +import { getKeyFromVerificationMethod } from '../modules/dids/domain/key-type' +import { didKeyToInstanceOfKey, verkeyToDidKey } from '../modules/dids/helpers' +import { DidResolverService } from '../modules/dids/services/DidResolverService' +import { MessagePickupRepository } from '../modules/message-pickup/storage' +import { inject, injectable } from '../plugins' +import { MessageValidator } from '../utils/MessageValidator' +import { getProtocolScheme } from '../utils/uri' + +import { EnvelopeService } from './EnvelopeService' +import { EventEmitter } from './EventEmitter' +import { AgentEventTypes } from './Events' +import { TransportService } from './TransportService' +import { OutboundMessageContext, OutboundMessageSendStatus } from './models' + +export interface TransportPriorityOptions { + schemes: string[] + restrictive?: boolean +} + +@injectable() +export class MessageSender { + private envelopeService: EnvelopeService + private transportService: TransportService + private messagePickupRepository: MessagePickupRepository + private logger: Logger + private didResolverService: DidResolverService + private didCommDocumentService: DidCommDocumentService + private eventEmitter: EventEmitter + private _outboundTransports: OutboundTransport[] = [] + + public constructor( + envelopeService: EnvelopeService, + transportService: TransportService, + @inject(InjectionSymbols.MessagePickupRepository) messagePickupRepository: MessagePickupRepository, + @inject(InjectionSymbols.Logger) logger: Logger, + didResolverService: DidResolverService, + didCommDocumentService: DidCommDocumentService, + eventEmitter: EventEmitter + ) { + this.envelopeService = envelopeService + this.transportService = transportService + this.messagePickupRepository = messagePickupRepository + this.logger = logger + this.didResolverService = didResolverService + this.didCommDocumentService = didCommDocumentService + this.eventEmitter = eventEmitter + this._outboundTransports = [] + } + + public get outboundTransports() { + return this._outboundTransports + } + + public registerOutboundTransport(outboundTransport: OutboundTransport) { + this._outboundTransports.push(outboundTransport) + } + + public async unregisterOutboundTransport(outboundTransport: OutboundTransport) { + this._outboundTransports = this.outboundTransports.filter((transport) => transport !== outboundTransport) + await outboundTransport.stop() + } + + public async packMessage( + agentContext: AgentContext, + { + keys, + message, + endpoint, + }: { + keys: EnvelopeKeys + message: AgentMessage + endpoint: string + } + ): Promise { + const encryptedMessage = await this.envelopeService.packMessage(agentContext, message, keys) + + return { + payload: encryptedMessage, + responseRequested: message.hasAnyReturnRoute(), + endpoint, + } + } + + private async sendMessageToSession(agentContext: AgentContext, session: TransportSession, message: AgentMessage) { + this.logger.debug(`Packing message and sending it via existing session ${session.type}...`) + if (!session.keys) { + throw new CredoError(`There are no keys for the given ${session.type} transport session.`) + } + const encryptedMessage = await this.envelopeService.packMessage(agentContext, message, session.keys) + this.logger.debug('Sending message') + await session.send(agentContext, encryptedMessage) + } + + public async sendPackage( + agentContext: AgentContext, + { + connection, + encryptedMessage, + recipientKey, + options, + }: { + connection: ConnectionRecord + recipientKey: string + encryptedMessage: EncryptedMessage + options?: { transportPriority?: TransportPriorityOptions } + } + ) { + const errors: Error[] = [] + + // Try to send to already open session + const session = this.transportService.findSessionByConnectionId(connection.id) + if (session?.inboundMessage?.hasReturnRouting()) { + try { + await session.send(agentContext, encryptedMessage) + return + } catch (error) { + errors.push(error) + this.logger.debug(`Sending packed message via session failed with error: ${error.message}.`, error) + } + } + + // Retrieve DIDComm services + const { services, queueService } = await this.retrieveServicesByConnection( + agentContext, + connection, + options?.transportPriority + ) + + if (this.outboundTransports.length === 0 && !queueService) { + throw new CredoError('Agent has no outbound transport!') + } + + // Loop trough all available services and try to send the message + for await (const service of services) { + this.logger.debug(`Sending outbound message to service:`, { service }) + try { + const protocolScheme = getProtocolScheme(service.serviceEndpoint) + for (const transport of this.outboundTransports) { + if (transport.supportedSchemes.includes(protocolScheme)) { + await transport.sendMessage({ + payload: encryptedMessage, + endpoint: service.serviceEndpoint, + connectionId: connection.id, + }) + break + } + } + return + } catch (error) { + this.logger.debug( + `Sending outbound message to service with id ${service.id} failed with the following error:`, + { + message: error.message, + error: error, + } + ) + } + } + + // We didn't succeed to send the message over open session, or directly to serviceEndpoint + // If the other party shared a queue service endpoint in their did doc we queue the message + if (queueService) { + this.logger.debug(`Queue packed message for connection ${connection.id} (${connection.theirLabel})`) + await this.messagePickupRepository.addMessage({ + connectionId: connection.id, + recipientDids: [verkeyToDidKey(recipientKey)], + payload: encryptedMessage, + }) + return + } + + // Message is undeliverable + this.logger.error(`Message is undeliverable to connection ${connection.id} (${connection.theirLabel})`, { + message: encryptedMessage, + errors, + connection, + }) + throw new CredoError(`Message is undeliverable to connection ${connection.id} (${connection.theirLabel})`) + } + + public async sendMessage( + outboundMessageContext: OutboundMessageContext, + options?: { + transportPriority?: TransportPriorityOptions + } + ) { + const { agentContext, connection, outOfBand, message } = outboundMessageContext + const errors: Error[] = [] + + if (outboundMessageContext.isOutboundServiceMessage()) { + return this.sendMessageToService(outboundMessageContext) + } + + if (!connection) { + this.logger.error('Outbound message has no associated connection') + this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.Undeliverable) + throw new MessageSendingError('Outbound message has no associated connection', { + outboundMessageContext, + }) + } + + this.logger.debug('Send outbound message', { + message, + connectionId: connection.id, + }) + + const session = this.findSessionForOutboundContext(outboundMessageContext) + + if (session) { + this.logger.debug(`Found session with return routing for message '${message.id}' (connection '${connection.id}'`) + + try { + await this.sendMessageToSession(agentContext, session, message) + this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.SentToSession) + return + } catch (error) { + errors.push(error) + this.logger.debug(`Sending an outbound message via session failed with error: ${error.message}.`, error) + } + } + + // Retrieve DIDComm services + let services: ResolvedDidCommService[] = [] + let queueService: ResolvedDidCommService | undefined + + try { + ;({ services, queueService } = await this.retrieveServicesByConnection( + agentContext, + connection, + options?.transportPriority, + outOfBand + )) + } catch (error) { + this.logger.error(`Unable to retrieve services for connection '${connection.id}. ${error.message}`) + this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.Undeliverable) + throw new MessageSendingError(`Unable to retrieve services for connection '${connection.id}`, { + outboundMessageContext, + cause: error, + }) + } + + if (!connection.did) { + this.logger.error(`Unable to send message using connection '${connection.id}' that doesn't have a did`) + this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.Undeliverable) + throw new MessageSendingError( + `Unable to send message using connection '${connection.id}' that doesn't have a did`, + { outboundMessageContext } + ) + } + + let ourDidDocument: DidDocument + try { + ourDidDocument = await this.didResolverService.resolveDidDocument(agentContext, connection.did) + } catch (error) { + this.logger.error(`Unable to resolve DID Document for '${connection.did}`) + this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.Undeliverable) + throw new MessageSendingError(`Unable to resolve DID Document for '${connection.did}`, { + outboundMessageContext, + cause: error, + }) + } + + const ourAuthenticationKeys = getAuthenticationKeys(ourDidDocument) + + // TODO We're selecting just the first authentication key. Is it ok? + // We can probably learn something from the didcomm-rust implementation, which looks at crypto compatibility to make sure the + // other party can decrypt the message. https://github.com/sicpa-dlab/didcomm-rust/blob/9a24b3b60f07a11822666dda46e5616a138af056/src/message/pack_encrypted/mod.rs#L33-L44 + // This will become more relevant when we support different encrypt envelopes. One thing to take into account though is that currently we only store the recipientKeys + // as defined in the didcomm services, while it could be for example that the first authentication key is not defined in the recipientKeys, in which case we wouldn't + // even be interoperable between two Credo agents. So we should either pick the first key that is defined in the recipientKeys, or we should make sure to store all + // keys defined in the did document as tags so we can retrieve it, even if it's not defined in the recipientKeys. This, again, will become simpler once we use didcomm v2 + // as the `from` field in a received message will identity the did used so we don't have to store all keys in tags to be able to find the connections associated with + // an incoming message. + const [firstOurAuthenticationKey] = ourAuthenticationKeys + // If the returnRoute is already set we won't override it. This allows to set the returnRoute manually if this is desired. + const shouldAddReturnRoute = + message.transport?.returnRoute === undefined && !this.transportService.hasInboundEndpoint(ourDidDocument) + + // Loop trough all available services and try to send the message + for await (const service of services) { + try { + // Enable return routing if the our did document does not have any inbound endpoint for given sender key + await this.sendToService( + new OutboundMessageContext(message, { + agentContext, + serviceParams: { + service, + senderKey: firstOurAuthenticationKey, + returnRoute: shouldAddReturnRoute, + }, + connection, + }) + ) + this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.SentToTransport) + return + } catch (error) { + errors.push(error) + this.logger.debug( + `Sending outbound message to service with id ${service.id} failed with the following error:`, + { + message: error.message, + error: error, + } + ) + } + } + + // We didn't succeed to send the message over open session, or directly to serviceEndpoint + // If the other party shared a queue service endpoint in their did doc we queue the message + if (queueService && message.allowQueueTransport) { + this.logger.debug(`Queue message for connection ${connection.id} (${connection.theirLabel})`) + + const keys = { + recipientKeys: queueService.recipientKeys, + routingKeys: queueService.routingKeys, + senderKey: firstOurAuthenticationKey, + } + + const encryptedMessage = await this.envelopeService.packMessage(agentContext, message, keys) + await this.messagePickupRepository.addMessage({ + connectionId: connection.id, + recipientDids: keys.recipientKeys.map((item) => new DidKey(item).did), + payload: encryptedMessage, + }) + + this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.QueuedForPickup) + + return + } + + // Message is undeliverable + this.logger.error(`Message is undeliverable to connection ${connection.id} (${connection.theirLabel})`, { + message, + errors, + connection, + }) + this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.Undeliverable) + + throw new MessageSendingError( + `Message is undeliverable to connection ${connection.id} (${connection.theirLabel})`, + { outboundMessageContext } + ) + } + + private async sendMessageToService(outboundMessageContext: OutboundMessageContext) { + const session = this.findSessionForOutboundContext(outboundMessageContext) + + if (session) { + this.logger.debug(`Found session with return routing for message '${outboundMessageContext.message.id}'`) + try { + await this.sendMessageToSession(outboundMessageContext.agentContext, session, outboundMessageContext.message) + this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.SentToSession) + return + } catch (error) { + this.logger.debug(`Sending an outbound message via session failed with error: ${error.message}.`, error) + } + } + + // If there is no session try sending to service instead + try { + await this.sendToService(outboundMessageContext) + this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.SentToTransport) + } catch (error) { + this.logger.error( + `Message is undeliverable to service with id ${outboundMessageContext.serviceParams?.service.id}: ${error.message}`, + { + message: outboundMessageContext.message, + error, + } + ) + this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.Undeliverable) + + throw new MessageSendingError( + `Message is undeliverable to service with id ${outboundMessageContext.serviceParams?.service.id}: ${error.message}`, + { outboundMessageContext } + ) + } + } + + private async sendToService(outboundMessageContext: OutboundMessageContext) { + const { agentContext, message, serviceParams, connection } = outboundMessageContext + + if (!serviceParams) { + throw new CredoError('No service parameters found in outbound message context') + } + const { service, senderKey, returnRoute } = serviceParams + + if (this.outboundTransports.length === 0) { + throw new CredoError('Agent has no outbound transport!') + } + + this.logger.debug(`Sending outbound message to service:`, { + messageId: message.id, + service: { ...service, recipientKeys: 'omitted...', routingKeys: 'omitted...' }, + }) + + const keys = { + recipientKeys: service.recipientKeys, + routingKeys: service.routingKeys, + senderKey, + } + + // Set return routing for message if requested + if (returnRoute) { + message.setReturnRouting(ReturnRouteTypes.all) + } + + try { + MessageValidator.validateSync(message) + } catch (error) { + this.logger.error( + `Aborting sending outbound message ${message.type} to ${service.serviceEndpoint}. Message validation failed`, + { + errors: error, + message: message.toJSON(), + } + ) + + throw error + } + + const outboundPackage = await this.packMessage(agentContext, { message, keys, endpoint: service.serviceEndpoint }) + outboundPackage.endpoint = service.serviceEndpoint + outboundPackage.connectionId = connection?.id + for (const transport of this.outboundTransports) { + const protocolScheme = getProtocolScheme(service.serviceEndpoint) + if (!protocolScheme) { + this.logger.warn('Service does not have a protocol scheme.') + } else if (transport.supportedSchemes.includes(protocolScheme)) { + await transport.sendMessage(outboundPackage) + return + } + } + throw new MessageSendingError(`Unable to send message to service: ${service.serviceEndpoint}`, { + outboundMessageContext, + }) + } + + private findSessionForOutboundContext(outboundContext: OutboundMessageContext) { + let session: TransportSession | undefined = undefined + + // Use session id from outbound context if present, or use the session from the inbound message context + const sessionId = outboundContext.sessionId ?? outboundContext.inboundMessageContext?.sessionId + + // Try to find session by id + if (sessionId) { + session = this.transportService.findSessionById(sessionId) + } + + // Try to find session by connection id + if (!session && outboundContext.connection?.id) { + session = this.transportService.findSessionByConnectionId(outboundContext.connection.id) + } + + return session && session.inboundMessage?.hasAnyReturnRoute() ? session : null + } + + private async retrieveServicesByConnection( + agentContext: AgentContext, + connection: ConnectionRecord, + transportPriority?: TransportPriorityOptions, + outOfBand?: OutOfBandRecord + ) { + this.logger.debug(`Retrieving services for connection '${connection.id}' (${connection.theirLabel})`, { + transportPriority, + connection, + }) + + let didCommServices: ResolvedDidCommService[] = [] + + if (connection.theirDid) { + this.logger.debug(`Resolving services for connection theirDid ${connection.theirDid}.`) + didCommServices = await this.didCommDocumentService.resolveServicesFromDid(agentContext, connection.theirDid) + } else if (outOfBand) { + this.logger.debug(`Resolving services from out-of-band record ${outOfBand.id}.`) + if (connection.isRequester) { + for (const service of outOfBand.outOfBandInvitation.getServices()) { + // Resolve dids to DIDDocs to retrieve services + if (typeof service === 'string') { + this.logger.debug(`Resolving services for did ${service}.`) + didCommServices.push(...(await this.didCommDocumentService.resolveServicesFromDid(agentContext, service))) + } else { + // Out of band inline service contains keys encoded as did:key references + didCommServices.push({ + id: service.id, + recipientKeys: service.recipientKeys.map(didKeyToInstanceOfKey), + routingKeys: service.routingKeys?.map(didKeyToInstanceOfKey) || [], + serviceEndpoint: service.serviceEndpoint, + }) + } + } + } + } + + // Separate queue service out + let services = didCommServices.filter((s) => !isDidCommTransportQueue(s.serviceEndpoint)) + const queueService = didCommServices.find((s) => isDidCommTransportQueue(s.serviceEndpoint)) + + // If restrictive will remove services not listed in schemes list + if (transportPriority?.restrictive) { + services = services.filter((service) => { + const serviceSchema = getProtocolScheme(service.serviceEndpoint) + return transportPriority.schemes.includes(serviceSchema) + }) + } + + // If transport priority is set we will sort services by our priority + if (transportPriority?.schemes) { + services = services.sort(function (a, b) { + const aScheme = getProtocolScheme(a.serviceEndpoint) + const bScheme = getProtocolScheme(b.serviceEndpoint) + return transportPriority?.schemes.indexOf(aScheme) - transportPriority?.schemes.indexOf(bScheme) + }) + } + + this.logger.debug( + `Retrieved ${services.length} services for message to connection '${connection.id}'(${connection.theirLabel})'`, + { hasQueueService: queueService !== undefined } + ) + return { services, queueService } + } + + private emitMessageSentEvent(outboundMessageContext: OutboundMessageContext, status: OutboundMessageSendStatus) { + const { agentContext } = outboundMessageContext + this.eventEmitter.emit(agentContext, { + type: AgentEventTypes.AgentMessageSent, + payload: { + message: outboundMessageContext, + status, + }, + }) + } +} + +export function isDidCommTransportQueue(serviceEndpoint: string): serviceEndpoint is typeof DID_COMM_TRANSPORT_QUEUE { + return serviceEndpoint === DID_COMM_TRANSPORT_QUEUE +} + +function getAuthenticationKeys(didDocument: DidDocument) { + return ( + didDocument.authentication?.map((authentication) => { + const verificationMethod = + typeof authentication === 'string' ? didDocument.dereferenceVerificationMethod(authentication) : authentication + const key = getKeyFromVerificationMethod(verificationMethod) + return key + }) ?? [] + ) +} diff --git a/packages/core/src/agent/TransportService.ts b/packages/core/src/agent/TransportService.ts new file mode 100644 index 0000000000..fee9f1166e --- /dev/null +++ b/packages/core/src/agent/TransportService.ts @@ -0,0 +1,117 @@ +import type { AgentMessage } from './AgentMessage' +import type { EnvelopeKeys } from './EnvelopeService' +import type { DidDocument } from '../modules/dids' +import type { TransportSessionRemovedEvent, TransportSessionSavedEvent } from '../transport' +import type { EncryptedMessage } from '../types' + +import { DID_COMM_TRANSPORT_QUEUE } from '../constants' +import { CredoError } from '../error' +import { injectable } from '../plugins' +import { TransportEventTypes } from '../transport' + +import { EventEmitter } from './EventEmitter' +import { AgentContext } from './context' + +@injectable() +export class TransportService { + public transportSessionTable: TransportSessionTable = {} + private agentContext: AgentContext + private eventEmitter: EventEmitter + + public constructor(agentContext: AgentContext, eventEmitter: EventEmitter) { + this.agentContext = agentContext + this.eventEmitter = eventEmitter + } + + public saveSession(session: TransportSession) { + if (session.connectionId) { + const oldSessions = this.getExistingSessionsForConnectionIdAndType(session.connectionId, session.type) + oldSessions.forEach((oldSession) => { + if (oldSession && oldSession.id !== session.id) { + this.removeSession(oldSession) + } + }) + } + this.transportSessionTable[session.id] = session + + this.eventEmitter.emit(this.agentContext, { + type: TransportEventTypes.TransportSessionSaved, + payload: { + session, + }, + }) + } + + public findSessionByConnectionId(connectionId: string) { + return Object.values(this.transportSessionTable).find((session) => session?.connectionId === connectionId) + } + + public setConnectionIdForSession(sessionId: string, connectionId: string) { + const session = this.findSessionById(sessionId) + if (!session) { + throw new CredoError(`Session not found with id ${sessionId}`) + } + session.connectionId = connectionId + this.saveSession(session) + } + + public hasInboundEndpoint(didDocument: DidDocument): boolean { + return Boolean(didDocument.didCommServices?.find((s) => s.serviceEndpoint !== DID_COMM_TRANSPORT_QUEUE)) + } + + public findSessionById(sessionId: string) { + return this.transportSessionTable[sessionId] + } + + public removeSession(session: TransportSession) { + delete this.transportSessionTable[session.id] + this.eventEmitter.emit(this.agentContext, { + type: TransportEventTypes.TransportSessionRemoved, + payload: { + session, + }, + }) + } + + private getExistingSessionsForConnectionIdAndType(connectionId: string, type: string) { + return Object.values(this.transportSessionTable).filter( + (session) => session?.connectionId === connectionId && session.type === type + ) + } +} + +interface TransportSessionTable { + [sessionId: string]: TransportSession | undefined +} + +// In the framework Transport sessions are used for communication. A session is +// associated with a connection and it can be reused when we want to respond to +// a message. If the message, for example, does not contain any way to reply to +// this message, the session should be closed. When a new sequence of messages +// starts it can be used again. A session will be deleted when a WebSocket +// closes, for the WsTransportSession that is. +export interface TransportSession { + // unique identifier for a transport session. This can a uuid, or anything else, as long + // as it uniquely identifies a transport. + id: string + + // The type is something that explicitly defines the transport type. For WebSocket it would + // be "WebSocket" and for HTTP it would be "HTTP". + type: string + + // The enveloping keys that can be used during the transport. This is used so the framework + // does not have to look up the associated keys for sending a message. + keys?: EnvelopeKeys + + // A received message that will be used to check whether it has any return routing. + inboundMessage?: AgentMessage + + // A stored connection id used to find this session via the `TransportService` for a specific connection + connectionId?: string + + // Send an encrypted message + send(agentContext: AgentContext, encryptedMessage: EncryptedMessage): Promise + + // Close the session to prevent dangling sessions. + close(): Promise +} diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts new file mode 100644 index 0000000000..57a33aecdc --- /dev/null +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -0,0 +1,266 @@ +import type { Module } from '../../plugins' + +import { injectable } from 'tsyringe' + +import { InMemoryWalletModule } from '../../../../../tests/InMemoryWalletModule' +import { getInMemoryAgentOptions } from '../../../tests/helpers' +import { InjectionSymbols } from '../../constants' +import { BasicMessageRepository, BasicMessageService } from '../../modules/basic-messages' +import { BasicMessagesApi } from '../../modules/basic-messages/BasicMessagesApi' +import { DidRotateService } from '../../modules/connections' +import { ConnectionsApi } from '../../modules/connections/ConnectionsApi' +import { ConnectionRepository } from '../../modules/connections/repository/ConnectionRepository' +import { ConnectionService } from '../../modules/connections/services/ConnectionService' +import { TrustPingService } from '../../modules/connections/services/TrustPingService' +import { CredentialRepository } from '../../modules/credentials' +import { CredentialsApi } from '../../modules/credentials/CredentialsApi' +import { MessagePickupApi, InMemoryMessagePickupRepository } from '../../modules/message-pickup' +import { ProofRepository } from '../../modules/proofs' +import { ProofsApi } from '../../modules/proofs/ProofsApi' +import { + MediationRecipientService, + MediationRepository, + MediatorApi, + MediatorService, + MediationRecipientApi, + MediationRecipientModule, +} from '../../modules/routing' +import { WalletError } from '../../wallet/error' +import { Agent } from '../Agent' +import { Dispatcher } from '../Dispatcher' +import { EnvelopeService } from '../EnvelopeService' +import { FeatureRegistry } from '../FeatureRegistry' +import { MessageReceiver } from '../MessageReceiver' +import { MessageSender } from '../MessageSender' + +const agentOptions = getInMemoryAgentOptions('Agent Class Test') + +const myModuleMethod = jest.fn() +@injectable() +class MyApi { + public myModuleMethod = myModuleMethod +} + +class MyModule implements Module { + public api = MyApi + public register() { + // noop + } +} + +describe('Agent', () => { + describe('Module registration', () => { + test('does not return default modules on modules key if no modules were provided', () => { + const agent = new Agent(agentOptions) + + expect(agent.modules).toEqual({}) + }) + + test('registers custom and default modules if custom modules are provided', () => { + const agent = new Agent({ + ...agentOptions, + modules: { + myModule: new MyModule(), + inMemory: new InMemoryWalletModule(), + }, + }) + + expect(agent.modules.myModule.myModuleMethod).toBe(myModuleMethod) + expect(agent.modules).toEqual({ + myModule: expect.any(MyApi), + }) + }) + + test('override default module configuration', () => { + const agent = new Agent({ + ...agentOptions, + modules: { + myModule: new MyModule(), + mediationRecipient: new MediationRecipientModule({ + maximumMessagePickup: 42, + }), + inMemory: new InMemoryWalletModule(), + }, + }) + + // Should be custom module config property, not the default value + expect(agent.mediationRecipient.config.maximumMessagePickup).toBe(42) + expect(agent.modules).toEqual({ + myModule: expect.any(MyApi), + }) + }) + }) + + describe('Initialization', () => { + let agent: Agent + + afterEach(async () => { + const wallet = agent.context.wallet + + if (wallet.isInitialized) { + await wallet.delete() + } + }) + + it('isInitialized should only return true after initialization', async () => { + expect.assertions(2) + + agent = new Agent(agentOptions) + + expect(agent.isInitialized).toBe(false) + await agent.initialize() + expect(agent.isInitialized).toBe(true) + }) + + it('wallet isInitialized should return true after agent initialization if wallet config is set in agent constructor', async () => { + expect.assertions(4) + + agent = new Agent(agentOptions) + const wallet = agent.context.wallet + + expect(agent.isInitialized).toBe(false) + expect(wallet.isInitialized).toBe(false) + await agent.initialize() + expect(agent.isInitialized).toBe(true) + expect(wallet.isInitialized).toBe(true) + }) + + it('wallet must be initialized if wallet config is not set before agent can be initialized', async () => { + expect.assertions(9) + + const { walletConfig, ...withoutWalletConfig } = agentOptions.config + agent = new Agent({ ...agentOptions, config: withoutWalletConfig }) + + expect(agent.isInitialized).toBe(false) + expect(agent.wallet.isInitialized).toBe(false) + + expect(agent.initialize()).rejects.toThrowError(WalletError) + expect(agent.isInitialized).toBe(false) + expect(agent.wallet.isInitialized).toBe(false) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await agent.wallet.initialize(walletConfig!) + expect(agent.isInitialized).toBe(false) + expect(agent.wallet.isInitialized).toBe(true) + + await agent.initialize() + expect(agent.wallet.isInitialized).toBe(true) + expect(agent.isInitialized).toBe(true) + }) + }) + + describe('Dependency Injection', () => { + it('should be able to resolve registered instances', () => { + const agent = new Agent(agentOptions) + const container = agent.dependencyManager + + // Modules + expect(container.resolve(ConnectionsApi)).toBeInstanceOf(ConnectionsApi) + expect(container.resolve(ConnectionService)).toBeInstanceOf(ConnectionService) + expect(container.resolve(ConnectionRepository)).toBeInstanceOf(ConnectionRepository) + expect(container.resolve(DidRotateService)).toBeInstanceOf(DidRotateService) + expect(container.resolve(TrustPingService)).toBeInstanceOf(TrustPingService) + + expect(container.resolve(ProofsApi)).toBeInstanceOf(ProofsApi) + expect(container.resolve(ProofRepository)).toBeInstanceOf(ProofRepository) + + expect(container.resolve(CredentialsApi)).toBeInstanceOf(CredentialsApi) + expect(container.resolve(CredentialRepository)).toBeInstanceOf(CredentialRepository) + + expect(container.resolve(BasicMessagesApi)).toBeInstanceOf(BasicMessagesApi) + expect(container.resolve(BasicMessageService)).toBeInstanceOf(BasicMessageService) + expect(container.resolve(BasicMessageRepository)).toBeInstanceOf(BasicMessageRepository) + + expect(container.resolve(MediatorApi)).toBeInstanceOf(MediatorApi) + expect(container.resolve(MediationRecipientApi)).toBeInstanceOf(MediationRecipientApi) + expect(container.resolve(MessagePickupApi)).toBeInstanceOf(MessagePickupApi) + expect(container.resolve(MediationRepository)).toBeInstanceOf(MediationRepository) + expect(container.resolve(MediatorService)).toBeInstanceOf(MediatorService) + expect(container.resolve(MediationRecipientService)).toBeInstanceOf(MediationRecipientService) + + // Symbols, interface based + expect(container.resolve(InjectionSymbols.Logger)).toBe(agentOptions.config.logger) + expect(container.resolve(InjectionSymbols.MessagePickupRepository)).toBeInstanceOf( + InMemoryMessagePickupRepository + ) + + // Agent + expect(container.resolve(MessageSender)).toBeInstanceOf(MessageSender) + expect(container.resolve(MessageReceiver)).toBeInstanceOf(MessageReceiver) + expect(container.resolve(Dispatcher)).toBeInstanceOf(Dispatcher) + expect(container.resolve(EnvelopeService)).toBeInstanceOf(EnvelopeService) + }) + + it('should return the same instance for consequent resolves', () => { + const agent = new Agent(agentOptions) + const container = agent.dependencyManager + + // Modules + expect(container.resolve(ConnectionsApi)).toBe(container.resolve(ConnectionsApi)) + expect(container.resolve(ConnectionService)).toBe(container.resolve(ConnectionService)) + expect(container.resolve(ConnectionRepository)).toBe(container.resolve(ConnectionRepository)) + expect(container.resolve(TrustPingService)).toBe(container.resolve(TrustPingService)) + expect(container.resolve(DidRotateService)).toBe(container.resolve(DidRotateService)) + + expect(container.resolve(ProofsApi)).toBe(container.resolve(ProofsApi)) + expect(container.resolve(ProofRepository)).toBe(container.resolve(ProofRepository)) + + expect(container.resolve(CredentialsApi)).toBe(container.resolve(CredentialsApi)) + expect(container.resolve(CredentialRepository)).toBe(container.resolve(CredentialRepository)) + + expect(container.resolve(BasicMessagesApi)).toBe(container.resolve(BasicMessagesApi)) + expect(container.resolve(BasicMessageService)).toBe(container.resolve(BasicMessageService)) + expect(container.resolve(BasicMessageRepository)).toBe(container.resolve(BasicMessageRepository)) + + expect(container.resolve(MediatorApi)).toBe(container.resolve(MediatorApi)) + expect(container.resolve(MediationRecipientApi)).toBe(container.resolve(MediationRecipientApi)) + expect(container.resolve(MessagePickupApi)).toBe(container.resolve(MessagePickupApi)) + expect(container.resolve(MediationRepository)).toBe(container.resolve(MediationRepository)) + expect(container.resolve(MediatorService)).toBe(container.resolve(MediatorService)) + expect(container.resolve(MediationRecipientService)).toBe(container.resolve(MediationRecipientService)) + + // Symbols, interface based + expect(container.resolve(InjectionSymbols.Logger)).toBe(container.resolve(InjectionSymbols.Logger)) + expect(container.resolve(InjectionSymbols.MessagePickupRepository)).toBe( + container.resolve(InjectionSymbols.MessagePickupRepository) + ) + expect(container.resolve(InjectionSymbols.StorageService)).toBe( + container.resolve(InjectionSymbols.StorageService) + ) + + // Agent + expect(container.resolve(MessageSender)).toBe(container.resolve(MessageSender)) + expect(container.resolve(MessageReceiver)).toBe(container.resolve(MessageReceiver)) + expect(container.resolve(Dispatcher)).toBe(container.resolve(Dispatcher)) + expect(container.resolve(FeatureRegistry)).toBe(container.resolve(FeatureRegistry)) + expect(container.resolve(EnvelopeService)).toBe(container.resolve(EnvelopeService)) + }) + }) + + it('all core features are properly registered', () => { + const agent = new Agent(agentOptions) + const registry = agent.dependencyManager.resolve(FeatureRegistry) + + const protocols = registry.query({ featureType: 'protocol', match: '*' }).map((p) => p.id) + + expect(protocols).toEqual( + expect.arrayContaining([ + 'https://didcomm.org/basicmessage/1.0', + 'https://didcomm.org/connections/1.0', + 'https://didcomm.org/coordinate-mediation/1.0', + 'https://didcomm.org/issue-credential/2.0', + 'https://didcomm.org/present-proof/2.0', + 'https://didcomm.org/didexchange/1.1', + 'https://didcomm.org/did-rotate/1.0', + 'https://didcomm.org/discover-features/1.0', + 'https://didcomm.org/discover-features/2.0', + 'https://didcomm.org/messagepickup/1.0', + 'https://didcomm.org/messagepickup/2.0', + 'https://didcomm.org/out-of-band/1.1', + 'https://didcomm.org/revocation_notification/1.0', + 'https://didcomm.org/revocation_notification/2.0', + ]) + ) + expect(protocols.length).toEqual(14) + }) +}) diff --git a/packages/core/src/agent/__tests__/AgentConfig.test.ts b/packages/core/src/agent/__tests__/AgentConfig.test.ts new file mode 100644 index 0000000000..59a32da7f3 --- /dev/null +++ b/packages/core/src/agent/__tests__/AgentConfig.test.ts @@ -0,0 +1,83 @@ +import { agentDependencies, getAgentConfig } from '../../../tests/helpers' +import { AgentConfig } from '../AgentConfig' + +describe('AgentConfig', () => { + describe('endpoints', () => { + it('should return the config endpoint if no inbound connection is available', () => { + const endpoint = 'https://local-url.com' + + const agentConfig = getAgentConfig('AgentConfig Test', { + endpoints: [endpoint], + }) + + expect(agentConfig.endpoints).toEqual([endpoint]) + }) + + it("should return ['didcomm:transport/queue'] if no inbound connection or config endpoint or host/port is available", () => { + const agentConfig = getAgentConfig('AgentConfig Test') + + expect(agentConfig.endpoints).toStrictEqual(['didcomm:transport/queue']) + }) + + it('should return the new config endpoint after setter is called', () => { + const endpoint = 'https://local-url.com' + const newEndpoint = 'https://new-local-url.com' + + const agentConfig = getAgentConfig('AgentConfig Test', { + endpoints: [endpoint], + }) + + agentConfig.endpoints = [newEndpoint] + expect(agentConfig.endpoints).toEqual([newEndpoint]) + }) + }) + + describe('label', () => { + it('should return new label after setter is called', async () => { + expect.assertions(2) + const newLabel = 'Agent: Agent Class Test 2' + + const agentConfig = getAgentConfig('AgentConfig Test', { + label: 'Test', + }) + expect(agentConfig.label).toBe('Test') + + agentConfig.label = newLabel + expect(agentConfig.label).toBe(newLabel) + }) + }) + + describe('extend()', () => { + it('extends the existing AgentConfig', () => { + const agentConfig = new AgentConfig( + { + label: 'hello', + }, + agentDependencies + ) + + const newAgentConfig = agentConfig.extend({}) + + expect(newAgentConfig).toMatchObject({ + label: 'hello', + }) + }) + + it('takes the init config from the extend method', () => { + const agentConfig = new AgentConfig( + { + label: 'hello', + }, + agentDependencies + ) + + const newAgentConfig = agentConfig.extend({ + label: 'anotherLabel', + }) + + expect(newAgentConfig).toMatchObject({ + label: 'anotherLabel', + }) + }) + }) +}) diff --git a/packages/core/src/agent/__tests__/AgentMessage.test.ts b/packages/core/src/agent/__tests__/AgentMessage.test.ts new file mode 100644 index 0000000000..cfe2f70796 --- /dev/null +++ b/packages/core/src/agent/__tests__/AgentMessage.test.ts @@ -0,0 +1,117 @@ +import { TestMessage } from '../../../tests/TestMessage' +import { ClassValidationError } from '../../error/ClassValidationError' +import { JsonTransformer } from '../../utils' +import { IsValidMessageType, parseMessageType } from '../../utils/messageType' +import { AgentMessage } from '../AgentMessage' + +class CustomProtocolMessage extends AgentMessage { + @IsValidMessageType(CustomProtocolMessage.type) + public readonly type = CustomProtocolMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/fake-protocol/1.5/message') +} + +class LegacyDidSovPrefixMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + + @IsValidMessageType(LegacyDidSovPrefixMessage.type) + public readonly type = LegacyDidSovPrefixMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/fake-protocol/1.5/another-message') +} + +describe('AgentMessage', () => { + describe('toJSON', () => { + it('should only use did:sov message prefix if useDidSovPrefixWhereAllowed and allowDidSovPrefix are both true', () => { + const message = new TestMessage() + const legacyPrefixMessage = new LegacyDidSovPrefixMessage() + + // useDidSovPrefixWhereAllowed & allowDidSovPrefix are both false + let testMessageJson = message.toJSON() + expect(testMessageJson['@type']).toBe('https://didcomm.org/connections/1.0/invitation') + + // useDidSovPrefixWhereAllowed is true, but allowDidSovPrefix is false + testMessageJson = message.toJSON({ useDidSovPrefixWhereAllowed: true }) + expect(testMessageJson['@type']).toBe('https://didcomm.org/connections/1.0/invitation') + + // useDidSovPrefixWhereAllowed is false, but allowDidSovPrefix is true + testMessageJson = legacyPrefixMessage.toJSON() + expect(testMessageJson['@type']).toBe('https://didcomm.org/fake-protocol/1.5/another-message') + + // useDidSovPrefixWhereAllowed & allowDidSovPrefix are both true + testMessageJson = legacyPrefixMessage.toJSON({ useDidSovPrefixWhereAllowed: true }) + expect(testMessageJson['@type']).toBe('did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/fake-protocol/1.5/another-message') + }) + }) + + describe('@IsValidMessageType', () => { + it('successfully validates if the message type is exactly the supported message type', async () => { + const json = { + '@id': 'd61c7e3d-d4af-469b-8d42-33fd14262e17', + '@type': 'https://didcomm.org/fake-protocol/1.5/message', + } + + const message = JsonTransformer.fromJSON(json, CustomProtocolMessage) + + expect(message).toBeInstanceOf(CustomProtocolMessage) + }) + + it('successfully validates if the message type minor version is lower than the supported message type', async () => { + const json = { + '@id': 'd61c7e3d-d4af-469b-8d42-33fd14262e17', + '@type': 'https://didcomm.org/fake-protocol/1.2/message', + } + + const message = JsonTransformer.fromJSON(json, CustomProtocolMessage) + + expect(message).toBeInstanceOf(CustomProtocolMessage) + }) + + it('successfully validates if the message type minor version is higher than the supported message type', () => { + const json = { + '@id': 'd61c7e3d-d4af-469b-8d42-33fd14262e17', + '@type': 'https://didcomm.org/fake-protocol/1.8/message', + } + + const message = JsonTransformer.fromJSON(json, CustomProtocolMessage) + + expect(message).toBeInstanceOf(CustomProtocolMessage) + }) + + it('throws a validation error if the message type major version differs from the supported message type', () => { + const json = { + '@id': 'd61c7e3d-d4af-469b-8d42-33fd14262e17', + '@type': 'https://didcomm.org/fake-protocol/2.0/message', + } + + expect(() => JsonTransformer.fromJSON(json, CustomProtocolMessage)).toThrowError(ClassValidationError) + try { + JsonTransformer.fromJSON(json, CustomProtocolMessage) + } catch (error) { + const thrownError = error as ClassValidationError + expect(thrownError.message).toEqual( + 'CustomProtocolMessage: Failed to validate class.\nAn instance of CustomProtocolMessage has failed the validation:\n - property type has failed the following constraints: type does not match the expected message type (only minor version may be lower) \n' + ) + expect(thrownError.validationErrors).toMatchObject([ + { + target: { + appendedAttachments: undefined, + id: 'd61c7e3d-d4af-469b-8d42-33fd14262e17', + l10n: undefined, + pleaseAck: undefined, + service: undefined, + thread: undefined, + timing: undefined, + transport: undefined, + type: 'https://didcomm.org/fake-protocol/2.0/message', + }, + value: 'https://didcomm.org/fake-protocol/2.0/message', + property: 'type', + children: [], + constraints: { + isValidMessageType: 'type does not match the expected message type (only minor version may be lower)', + }, + }, + ]) + } + }) + }) +}) diff --git a/packages/core/src/agent/__tests__/AgentModules.test.ts b/packages/core/src/agent/__tests__/AgentModules.test.ts new file mode 100644 index 0000000000..1e11d806c1 --- /dev/null +++ b/packages/core/src/agent/__tests__/AgentModules.test.ts @@ -0,0 +1,135 @@ +import type { Module } from '../../plugins' + +import { BasicMessagesModule } from '../../modules/basic-messages' +import { CacheModule } from '../../modules/cache' +import { ConnectionsModule } from '../../modules/connections' +import { CredentialsModule } from '../../modules/credentials' +import { DidsModule } from '../../modules/dids' +import { DifPresentationExchangeModule } from '../../modules/dif-presentation-exchange' +import { DiscoverFeaturesModule } from '../../modules/discover-features' +import { GenericRecordsModule } from '../../modules/generic-records' +import { MessagePickupModule } from '../../modules/message-pickup' +import { OutOfBandModule } from '../../modules/oob' +import { ProofsModule } from '../../modules/proofs' +import { MediationRecipientModule, MediatorModule } from '../../modules/routing' +import { SdJwtVcModule } from '../../modules/sd-jwt-vc' +import { W3cCredentialsModule } from '../../modules/vc' +import { DependencyManager, injectable } from '../../plugins' +import { WalletModule } from '../../wallet' +import { extendModulesWithDefaultModules, getAgentApi } from '../AgentModules' + +@injectable() +class MyApi {} + +class MyModuleWithApi implements Module { + public api = MyApi + public register() { + // nothing to register + } +} + +class MyModuleWithoutApi implements Module { + public register() { + // nothing to register + } +} + +describe('AgentModules', () => { + describe('getAgentApi', () => { + test('returns object with all api instances for modules with public api in dependency manager', () => { + const dependencyManager = new DependencyManager() + + dependencyManager.registerModules({ + withApi: new MyModuleWithApi(), + withoutApi: new MyModuleWithoutApi(), + }) + + const api = getAgentApi(dependencyManager) + + expect(api).toEqual({ + withApi: expect.any(MyApi), + }) + }) + }) + + describe('extendModulesWithDefaultModules', () => { + test('returns default modules if no modules were provided', () => { + const extendedModules = extendModulesWithDefaultModules() + + expect(extendedModules).toEqual({ + connections: expect.any(ConnectionsModule), + credentials: expect.any(CredentialsModule), + proofs: expect.any(ProofsModule), + mediator: expect.any(MediatorModule), + mediationRecipient: expect.any(MediationRecipientModule), + messagePickup: expect.any(MessagePickupModule), + basicMessages: expect.any(BasicMessagesModule), + pex: expect.any(DifPresentationExchangeModule), + genericRecords: expect.any(GenericRecordsModule), + discovery: expect.any(DiscoverFeaturesModule), + dids: expect.any(DidsModule), + wallet: expect.any(WalletModule), + oob: expect.any(OutOfBandModule), + w3cCredentials: expect.any(W3cCredentialsModule), + sdJwtVc: expect.any(SdJwtVcModule), + cache: expect.any(CacheModule), + }) + }) + + test('returns custom and default modules if custom modules are provided', () => { + const myModule = new MyModuleWithApi() + const extendedModules = extendModulesWithDefaultModules({ + myModule, + }) + + expect(extendedModules).toEqual({ + connections: expect.any(ConnectionsModule), + credentials: expect.any(CredentialsModule), + proofs: expect.any(ProofsModule), + mediator: expect.any(MediatorModule), + mediationRecipient: expect.any(MediationRecipientModule), + messagePickup: expect.any(MessagePickupModule), + basicMessages: expect.any(BasicMessagesModule), + pex: expect.any(DifPresentationExchangeModule), + genericRecords: expect.any(GenericRecordsModule), + discovery: expect.any(DiscoverFeaturesModule), + dids: expect.any(DidsModule), + wallet: expect.any(WalletModule), + oob: expect.any(OutOfBandModule), + w3cCredentials: expect.any(W3cCredentialsModule), + cache: expect.any(CacheModule), + sdJwtVc: expect.any(SdJwtVcModule), + myModule, + }) + }) + + test('does not override default module if provided as custom module', () => { + const myModule = new MyModuleWithApi() + const connections = new ConnectionsModule() + const extendedModules = extendModulesWithDefaultModules({ + myModule, + connections, + }) + + expect(extendedModules).toEqual({ + connections: connections, + credentials: expect.any(CredentialsModule), + proofs: expect.any(ProofsModule), + mediator: expect.any(MediatorModule), + mediationRecipient: expect.any(MediationRecipientModule), + messagePickup: expect.any(MessagePickupModule), + basicMessages: expect.any(BasicMessagesModule), + pex: expect.any(DifPresentationExchangeModule), + genericRecords: expect.any(GenericRecordsModule), + discovery: expect.any(DiscoverFeaturesModule), + dids: expect.any(DidsModule), + wallet: expect.any(WalletModule), + oob: expect.any(OutOfBandModule), + w3cCredentials: expect.any(W3cCredentialsModule), + cache: expect.any(CacheModule), + sdJwtVc: expect.any(SdJwtVcModule), + myModule, + }) + }) + }) +}) diff --git a/packages/core/src/agent/__tests__/Dispatcher.test.ts b/packages/core/src/agent/__tests__/Dispatcher.test.ts new file mode 100644 index 0000000000..6326fe78e0 --- /dev/null +++ b/packages/core/src/agent/__tests__/Dispatcher.test.ts @@ -0,0 +1,297 @@ +import type { ConnectionRecord } from '../../modules/connections' + +import { Subject } from 'rxjs' + +import { getAgentConfig, getAgentContext } from '../../../tests/helpers' +import { parseMessageType } from '../../utils/messageType' +import { AgentMessage } from '../AgentMessage' +import { Dispatcher } from '../Dispatcher' +import { EventEmitter } from '../EventEmitter' +import { MessageHandlerRegistry } from '../MessageHandlerRegistry' +import { MessageSender } from '../MessageSender' +import { getOutboundMessageContext } from '../getOutboundMessageContext' +import { InboundMessageContext } from '../models/InboundMessageContext' + +jest.mock('../MessageSender') + +class CustomProtocolMessage extends AgentMessage { + public readonly type = CustomProtocolMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/fake-protocol/1.5/message') + + public constructor(options: { id?: string }) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + } + } +} + +describe('Dispatcher', () => { + const agentConfig = getAgentConfig('DispatcherTest') + const agentContext = getAgentContext() + const MessageSenderMock = MessageSender as jest.Mock + const eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) + + describe('dispatch()', () => { + it('calls the handle method of the handler', async () => { + const messageHandlerRegistry = new MessageHandlerRegistry() + const dispatcher = new Dispatcher( + new MessageSenderMock(), + eventEmitter, + messageHandlerRegistry, + agentConfig.logger + ) + const customProtocolMessage = new CustomProtocolMessage({}) + const inboundMessageContext = new InboundMessageContext(customProtocolMessage, { agentContext }) + + const mockHandle = jest.fn() + messageHandlerRegistry.registerMessageHandler({ supportedMessages: [CustomProtocolMessage], handle: mockHandle }) + + await dispatcher.dispatch(inboundMessageContext) + + expect(mockHandle).toHaveBeenNthCalledWith(1, inboundMessageContext) + }) + + it('throws an error if no handler for the message could be found', async () => { + const messageHandlerRegistry = new MessageHandlerRegistry() + const dispatcher = new Dispatcher( + new MessageSenderMock(), + eventEmitter, + new MessageHandlerRegistry(), + agentConfig.logger + ) + const customProtocolMessage = new CustomProtocolMessage({ + id: '55170d10-b91f-4df2-9dcd-6deb4e806c1b', + }) + const inboundMessageContext = new InboundMessageContext(customProtocolMessage, { agentContext }) + + const mockHandle = jest.fn() + messageHandlerRegistry.registerMessageHandler({ supportedMessages: [], handle: mockHandle }) + + await expect(dispatcher.dispatch(inboundMessageContext)).rejects.toThrow( + 'Error handling message 55170d10-b91f-4df2-9dcd-6deb4e806c1b with type https://didcomm.org/fake-protocol/1.5/message. The message type is not supported' + ) + }) + + it('calls the middleware in the order they are registered', async () => { + const agentContext = getAgentContext() + + // Replace the MessageHandlerRegistry instance with a empty one + agentContext.dependencyManager.registerInstance(MessageHandlerRegistry, new MessageHandlerRegistry()) + + const dispatcher = new Dispatcher( + new MessageSenderMock(), + eventEmitter, + agentContext.dependencyManager.resolve(MessageHandlerRegistry), + agentConfig.logger + ) + + const customProtocolMessage = new CustomProtocolMessage({ + id: '55170d10-b91f-4df2-9dcd-6deb4e806c1b', + }) + const inboundMessageContext = new InboundMessageContext(customProtocolMessage, { agentContext }) + + const firstMiddleware = jest.fn().mockImplementation(async (_, next) => next()) + const secondMiddleware = jest.fn() + agentContext.dependencyManager.registerMessageHandlerMiddleware(firstMiddleware) + agentContext.dependencyManager.registerMessageHandlerMiddleware(secondMiddleware) + + await dispatcher.dispatch(inboundMessageContext) + + expect(firstMiddleware).toHaveBeenCalled() + expect(secondMiddleware).toHaveBeenCalled() + + // Verify the order of calls + const firstMiddlewareCallOrder = firstMiddleware.mock.invocationCallOrder[0] + const secondMiddlewareCallOrder = secondMiddleware.mock.invocationCallOrder[0] + expect(firstMiddlewareCallOrder).toBeLessThan(secondMiddlewareCallOrder) + }) + + it('calls the middleware in the order they are registered', async () => { + const agentContext = getAgentContext() + + // Replace the MessageHandlerRegistry instance with a empty one + agentContext.dependencyManager.registerInstance(MessageHandlerRegistry, new MessageHandlerRegistry()) + + const dispatcher = new Dispatcher( + new MessageSenderMock(), + eventEmitter, + agentContext.dependencyManager.resolve(MessageHandlerRegistry), + agentConfig.logger + ) + + const customProtocolMessage = new CustomProtocolMessage({ + id: '55170d10-b91f-4df2-9dcd-6deb4e806c1b', + }) + const inboundMessageContext = new InboundMessageContext(customProtocolMessage, { agentContext }) + + const firstMiddleware = jest.fn().mockImplementation(async (_, next) => next()) + const secondMiddleware = jest.fn() + agentContext.dependencyManager.registerMessageHandlerMiddleware(firstMiddleware) + agentContext.dependencyManager.registerMessageHandlerMiddleware(secondMiddleware) + + await dispatcher.dispatch(inboundMessageContext) + + expect(firstMiddleware).toHaveBeenCalled() + expect(secondMiddleware).toHaveBeenCalled() + + // Verify the order of calls + const firstMiddlewareCallOrder = firstMiddleware.mock.invocationCallOrder[0] + const secondMiddlewareCallOrder = secondMiddleware.mock.invocationCallOrder[0] + expect(firstMiddlewareCallOrder).toBeLessThan(secondMiddlewareCallOrder) + }) + + it('correctly calls the fallback message handler if no message handler is registered for the message type', async () => { + const agentContext = getAgentContext() + + // Replace the MessageHandlerRegistry instance with a empty one + agentContext.dependencyManager.registerInstance(MessageHandlerRegistry, new MessageHandlerRegistry()) + + const dispatcher = new Dispatcher( + new MessageSenderMock(), + eventEmitter, + agentContext.dependencyManager.resolve(MessageHandlerRegistry), + agentConfig.logger + ) + + const customProtocolMessage = new CustomProtocolMessage({ + id: '55170d10-b91f-4df2-9dcd-6deb4e806c1b', + }) + const inboundMessageContext = new InboundMessageContext(customProtocolMessage, { agentContext }) + + const fallbackMessageHandler = jest.fn() + agentContext.dependencyManager.setFallbackMessageHandler(fallbackMessageHandler) + + await dispatcher.dispatch(inboundMessageContext) + + expect(fallbackMessageHandler).toHaveBeenCalled() + }) + + it('will not call the message handler if the middleware does not call next (intercept incoming message handling)', async () => { + const agentContext = getAgentContext() + + // Replace the MessageHandlerRegistry instance with a empty one + agentContext.dependencyManager.registerInstance(MessageHandlerRegistry, new MessageHandlerRegistry()) + + const dispatcher = new Dispatcher( + new MessageSenderMock(), + eventEmitter, + agentContext.dependencyManager.resolve(MessageHandlerRegistry), + agentConfig.logger + ) + + const customProtocolMessage = new CustomProtocolMessage({ + id: '55170d10-b91f-4df2-9dcd-6deb4e806c1b', + }) + const inboundMessageContext = new InboundMessageContext(customProtocolMessage, { agentContext }) + + const mockHandle = jest.fn() + agentContext.dependencyManager.registerMessageHandlers([ + { + supportedMessages: [CustomProtocolMessage], + handle: mockHandle, + }, + ]) + + const middleware = jest.fn() + agentContext.dependencyManager.registerMessageHandlerMiddleware(middleware) + await dispatcher.dispatch(inboundMessageContext) + expect(mockHandle).not.toHaveBeenCalled() + + // Not it should call it, as the middleware calls next + middleware.mockImplementationOnce((_, next) => next()) + await dispatcher.dispatch(inboundMessageContext) + expect(mockHandle).toHaveBeenCalled() + }) + + it('calls the message handler set by the middleware', async () => { + const agentContext = getAgentContext() + + // Replace the MessageHandlerRegistry instance with a empty one + agentContext.dependencyManager.registerInstance(MessageHandlerRegistry, new MessageHandlerRegistry()) + + const dispatcher = new Dispatcher( + new MessageSenderMock(), + eventEmitter, + agentContext.dependencyManager.resolve(MessageHandlerRegistry), + agentConfig.logger + ) + + const customProtocolMessage = new CustomProtocolMessage({ + id: '55170d10-b91f-4df2-9dcd-6deb4e806c1b', + }) + const inboundMessageContext = new InboundMessageContext(customProtocolMessage, { agentContext }) + + const handle = jest.fn() + const middleware = jest + .fn() + .mockImplementationOnce(async (inboundMessageContext: InboundMessageContext, next) => { + inboundMessageContext.messageHandler = { + supportedMessages: [], + handle: handle, + } + + await next() + }) + + agentContext.dependencyManager.registerMessageHandlerMiddleware(middleware) + await dispatcher.dispatch(inboundMessageContext) + expect(middleware).toHaveBeenCalled() + expect(handle).toHaveBeenCalled() + }) + + it('sends the response message set by the middleware', async () => { + const agentContext = getAgentContext({ + agentConfig, + }) + const messageSenderMock = new MessageSenderMock() + + // Replace the MessageHandlerRegistry instance with a empty one + agentContext.dependencyManager.registerInstance(MessageHandlerRegistry, new MessageHandlerRegistry()) + + const dispatcher = new Dispatcher( + messageSenderMock, + eventEmitter, + agentContext.dependencyManager.resolve(MessageHandlerRegistry), + agentConfig.logger + ) + + const connectionMock = jest.fn() as unknown as ConnectionRecord + + const customProtocolMessage = new CustomProtocolMessage({ + id: '55170d10-b91f-4df2-9dcd-6deb4e806c1b', + }) + const inboundMessageContext = new InboundMessageContext(customProtocolMessage, { + agentContext, + connection: connectionMock, + }) + + const middleware = jest.fn().mockImplementationOnce(async (inboundMessageContext: InboundMessageContext) => { + // We do not call next + inboundMessageContext.responseMessage = await getOutboundMessageContext(inboundMessageContext.agentContext, { + message: new CustomProtocolMessage({ + id: 'static-id', + }), + connectionRecord: inboundMessageContext.connection, + }) + }) + + agentContext.dependencyManager.registerMessageHandlerMiddleware(middleware) + await dispatcher.dispatch(inboundMessageContext) + expect(middleware).toHaveBeenCalled() + expect(messageSenderMock.sendMessage).toHaveBeenCalledWith({ + inboundMessageContext, + agentContext, + associatedRecord: undefined, + connection: connectionMock, + message: new CustomProtocolMessage({ + id: 'static-id', + }), + outOfBand: undefined, + serviceParams: undefined, + sessionId: undefined, + }) + }) + }) +}) diff --git a/packages/core/src/agent/__tests__/EventEmitter.test.ts b/packages/core/src/agent/__tests__/EventEmitter.test.ts new file mode 100644 index 0000000000..480ccbedbb --- /dev/null +++ b/packages/core/src/agent/__tests__/EventEmitter.test.ts @@ -0,0 +1,59 @@ +import type { EventEmitter as NativeEventEmitter } from 'events' + +import { Subject } from 'rxjs' + +import { agentDependencies, getAgentContext } from '../../../tests/helpers' +import { EventEmitter } from '../EventEmitter' + +const mockEmit = jest.fn() +const mockOn = jest.fn() +const mockOff = jest.fn() +const mock = jest.fn().mockImplementation(() => { + return { emit: mockEmit, on: mockOn, off: mockOff } +}) as jest.Mock + +const eventEmitter = new EventEmitter( + { ...agentDependencies, EventEmitterClass: mock as unknown as typeof NativeEventEmitter }, + new Subject() +) +const agentContext = getAgentContext({}) + +describe('EventEmitter', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('emit', () => { + test("calls 'emit' on native event emitter instance", () => { + eventEmitter.emit(agentContext, { + payload: { some: 'payload' }, + type: 'some-event', + }) + + expect(mockEmit).toHaveBeenCalledWith('some-event', { + payload: { some: 'payload' }, + type: 'some-event', + metadata: { + contextCorrelationId: agentContext.contextCorrelationId, + }, + }) + }) + }) + + describe('on', () => { + test("calls 'on' on native event emitter instance", () => { + const listener = jest.fn() + eventEmitter.on('some-event', listener) + + expect(mockOn).toHaveBeenCalledWith('some-event', listener) + }) + }) + describe('off', () => { + test("calls 'off' on native event emitter instance", () => { + const listener = jest.fn() + eventEmitter.off('some-event', listener) + + expect(mockOff).toHaveBeenCalledWith('some-event', listener) + }) + }) +}) diff --git a/packages/core/src/agent/__tests__/MessageHandlerRegistry.test.ts b/packages/core/src/agent/__tests__/MessageHandlerRegistry.test.ts new file mode 100644 index 0000000000..4c20ac1fa2 --- /dev/null +++ b/packages/core/src/agent/__tests__/MessageHandlerRegistry.test.ts @@ -0,0 +1,139 @@ +import type { MessageHandler } from '../MessageHandler' + +import { parseDidCommProtocolUri, parseMessageType } from '../../utils/messageType' +import { AgentMessage } from '../AgentMessage' +import { MessageHandlerRegistry } from '../MessageHandlerRegistry' + +class ConnectionInvitationTestMessage extends AgentMessage { + public static readonly type = parseMessageType('https://didcomm.org/connections/1.0/invitation') +} +class ConnectionRequestTestMessage extends AgentMessage { + public static readonly type = parseMessageType('https://didcomm.org/connections/1.0/request') +} + +class ConnectionResponseTestMessage extends AgentMessage { + public static readonly type = parseMessageType('https://didcomm.org/connections/1.0/response') +} + +class NotificationAckTestMessage extends AgentMessage { + public static readonly type = parseMessageType('https://didcomm.org/notification/1.0/ack') +} +class CredentialProposalTestMessage extends AgentMessage { + public readonly type = CredentialProposalTestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/credential-proposal') +} + +class CustomProtocolMessage extends AgentMessage { + public readonly type = CustomProtocolMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/fake-protocol/1.5/message') +} + +class TestHandler implements MessageHandler { + // We want to pass various classes to test various behaviours so we dont need to strictly type it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public constructor(classes: any[]) { + this.supportedMessages = classes + } + + public supportedMessages + + // We don't need an implementation in test handler so we can disable lint. + // eslint-disable-next-line @typescript-eslint/no-empty-function + public async handle() {} +} + +describe('MessageHandlerRegistry', () => { + const fakeProtocolHandler = new TestHandler([CustomProtocolMessage]) + const connectionHandler = new TestHandler([ + ConnectionInvitationTestMessage, + ConnectionRequestTestMessage, + ConnectionResponseTestMessage, + ]) + + const messageHandlerRegistry = new MessageHandlerRegistry() + + messageHandlerRegistry.registerMessageHandler(connectionHandler) + messageHandlerRegistry.registerMessageHandler(new TestHandler([NotificationAckTestMessage])) + messageHandlerRegistry.registerMessageHandler(new TestHandler([CredentialProposalTestMessage])) + messageHandlerRegistry.registerMessageHandler(fakeProtocolHandler) + + describe('supportedMessageTypes', () => { + test('return all supported message types URIs', async () => { + const messageTypes = messageHandlerRegistry.supportedMessageTypes + + expect(messageTypes).toMatchObject([ + { messageTypeUri: 'https://didcomm.org/connections/1.0/invitation' }, + { messageTypeUri: 'https://didcomm.org/connections/1.0/request' }, + { messageTypeUri: 'https://didcomm.org/connections/1.0/response' }, + { messageTypeUri: 'https://didcomm.org/notification/1.0/ack' }, + { messageTypeUri: 'https://didcomm.org/issue-credential/1.0/credential-proposal' }, + { messageTypeUri: 'https://didcomm.org/fake-protocol/1.5/message' }, + ]) + }) + }) + + describe('supportedProtocols', () => { + test('return all supported message protocols URIs', async () => { + const messageTypes = messageHandlerRegistry.supportedProtocolUris + + expect(messageTypes).toEqual([ + parseDidCommProtocolUri('https://didcomm.org/connections/1.0'), + parseDidCommProtocolUri('https://didcomm.org/notification/1.0'), + parseDidCommProtocolUri('https://didcomm.org/issue-credential/1.0'), + parseDidCommProtocolUri('https://didcomm.org/fake-protocol/1.5'), + ]) + }) + }) + + describe('filterSupportedProtocolsByProtocolUris', () => { + it('should return empty array when input is empty array', async () => { + const supportedProtocols = messageHandlerRegistry.filterSupportedProtocolsByProtocolUris([]) + expect(supportedProtocols).toEqual([]) + }) + + it('should return empty array when input contains only unsupported protocol', async () => { + const supportedProtocols = messageHandlerRegistry.filterSupportedProtocolsByProtocolUris([ + parseDidCommProtocolUri('https://didcomm.org/unsupported-protocol/1.0'), + ]) + expect(supportedProtocols).toEqual([]) + }) + + it('should return array with only supported protocol when input contains supported and unsupported protocol', async () => { + const supportedProtocols = messageHandlerRegistry.filterSupportedProtocolsByProtocolUris([ + parseDidCommProtocolUri('https://didcomm.org/connections/1.0'), + parseDidCommProtocolUri('https://didcomm.org/didexchange/1.0'), + ]) + expect(supportedProtocols).toEqual([parseDidCommProtocolUri('https://didcomm.org/connections/1.0')]) + }) + }) + + describe('getMessageClassForMessageType()', () => { + it('should return the correct message class for a registered message type', () => { + const messageClass = messageHandlerRegistry.getMessageClassForMessageType( + 'https://didcomm.org/connections/1.0/invitation' + ) + expect(messageClass).toBe(ConnectionInvitationTestMessage) + }) + + it('should return undefined if no message class is registered for the message type', () => { + const messageClass = messageHandlerRegistry.getMessageClassForMessageType( + 'https://didcomm.org/non-existing/1.0/invitation' + ) + expect(messageClass).toBeUndefined() + }) + + it('should return the message class with a higher minor version for the message type', () => { + const messageClass = messageHandlerRegistry.getMessageClassForMessageType( + 'https://didcomm.org/fake-protocol/1.0/message' + ) + expect(messageClass).toBe(CustomProtocolMessage) + }) + + it('should not return the message class with a different major version', () => { + const messageClass = messageHandlerRegistry.getMessageClassForMessageType( + 'https://didcomm.org/fake-protocol/2.0/message' + ) + expect(messageClass).toBeUndefined() + }) + }) +}) diff --git a/packages/core/src/agent/__tests__/MessageSender.test.ts b/packages/core/src/agent/__tests__/MessageSender.test.ts new file mode 100644 index 0000000000..0d6682a90d --- /dev/null +++ b/packages/core/src/agent/__tests__/MessageSender.test.ts @@ -0,0 +1,703 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import type { ConnectionRecord } from '../../modules/connections' +import type { ResolvedDidCommService } from '../../modules/didcomm' +import type { DidDocumentService, IndyAgentService } from '../../modules/dids' +import type { MessagePickupRepository } from '../../modules/message-pickup/storage' +import type { OutboundTransport } from '../../transport' +import type { EncryptedMessage } from '../../types' +import type { AgentMessageSentEvent } from '../Events' + +import { Subject } from 'rxjs' + +import { TestMessage } from '../../../tests/TestMessage' +import { + agentDependencies, + getAgentConfig, + getAgentContext, + getMockConnection, + mockFunction, +} from '../../../tests/helpers' +import testLogger from '../../../tests/logger' +import { Key, KeyType } from '../../crypto' +import { ReturnRouteTypes } from '../../decorators/transport/TransportDecorator' +import { DidCommDocumentService } from '../../modules/didcomm' +import { DidResolverService, DidDocument, VerificationMethod } from '../../modules/dids' +import { DidCommV1Service } from '../../modules/dids/domain/service/DidCommV1Service' +import { verkeyToInstanceOfKey } from '../../modules/dids/helpers' +import { InMemoryMessagePickupRepository } from '../../modules/message-pickup/storage' +import { EnvelopeService as EnvelopeServiceImpl } from '../EnvelopeService' +import { EventEmitter } from '../EventEmitter' +import { AgentEventTypes } from '../Events' +import { MessageSender } from '../MessageSender' +import { TransportService } from '../TransportService' +import { OutboundMessageContext, OutboundMessageSendStatus } from '../models' + +import { DummyTransportSession } from './stubs' + +jest.mock('../TransportService') +jest.mock('../EnvelopeService') +jest.mock('../../modules/dids/services/DidResolverService') +jest.mock('../../modules/didcomm/services/DidCommDocumentService') + +const logger = testLogger + +const TransportServiceMock = TransportService as jest.MockedClass +const DidResolverServiceMock = DidResolverService as jest.Mock +const DidCommDocumentServiceMock = DidCommDocumentService as jest.Mock + +class DummyHttpOutboundTransport implements OutboundTransport { + public start(): Promise { + throw new Error('Method not implemented.') + } + + public stop(): Promise { + throw new Error('Method not implemented.') + } + + public supportedSchemes: string[] = ['https'] + + public sendMessage() { + return Promise.resolve() + } +} + +class DummyWsOutboundTransport implements OutboundTransport { + public start(): Promise { + throw new Error('Method not implemented.') + } + + public stop(): Promise { + throw new Error('Method not implemented.') + } + + public supportedSchemes: string[] = ['wss'] + + public sendMessage() { + return Promise.resolve() + } +} + +describe('MessageSender', () => { + const EnvelopeService = >(EnvelopeServiceImpl) + + const encryptedMessage: EncryptedMessage = { + protected: 'base64url', + iv: 'base64url', + ciphertext: 'base64url', + tag: 'base64url', + } + + const enveloperService = new EnvelopeService() + const envelopeServicePackMessageMock = mockFunction(enveloperService.packMessage) + + const didResolverService = new DidResolverServiceMock() + const didCommDocumentService = new DidCommDocumentServiceMock() + const eventEmitter = new EventEmitter(agentDependencies, new Subject()) + const didResolverServiceResolveMock = mockFunction(didResolverService.resolveDidDocument) + const didResolverServiceResolveDidServicesMock = mockFunction(didCommDocumentService.resolveServicesFromDid) + + const inboundMessage = new TestMessage() + inboundMessage.setReturnRouting(ReturnRouteTypes.all) + + const recipientKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) + const senderKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) + const session = new DummyTransportSession('session-123') + session.keys = { + recipientKeys: [recipientKey], + routingKeys: [], + senderKey: senderKey, + } + session.inboundMessage = inboundMessage + session.send = jest.fn() + + const sessionWithoutKeys = new DummyTransportSession('sessionWithoutKeys-123') + sessionWithoutKeys.inboundMessage = inboundMessage + sessionWithoutKeys.send = jest.fn() + + const transportService = new TransportService(getAgentContext(), eventEmitter) + const transportServiceFindSessionMock = mockFunction(transportService.findSessionByConnectionId) + const transportServiceFindSessionByIdMock = mockFunction(transportService.findSessionById) + const transportServiceHasInboundEndpoint = mockFunction(transportService.hasInboundEndpoint) + + const firstDidCommService = new DidCommV1Service({ + id: `;indy`, + serviceEndpoint: 'https://www.first-endpoint.com', + recipientKeys: ['#authentication-1'], + }) + const secondDidCommService = new DidCommV1Service({ + id: `;indy`, + serviceEndpoint: 'https://www.second-endpoint.com', + recipientKeys: ['#authentication-1'], + }) + + let messageSender: MessageSender + let outboundTransport: OutboundTransport + let messagePickupRepository: MessagePickupRepository + let connection: ConnectionRecord + let outboundMessageContext: OutboundMessageContext + const agentConfig = getAgentConfig('MessageSender') + const agentContext = getAgentContext() + const eventListenerMock = jest.fn() + + describe('sendMessage', () => { + beforeEach(() => { + TransportServiceMock.mockClear() + DidResolverServiceMock.mockClear() + + eventEmitter.on(AgentEventTypes.AgentMessageSent, eventListenerMock) + + outboundTransport = new DummyHttpOutboundTransport() + messagePickupRepository = new InMemoryMessagePickupRepository(agentConfig.logger) + messageSender = new MessageSender( + enveloperService, + transportService, + messagePickupRepository, + logger, + didResolverService, + didCommDocumentService, + eventEmitter + ) + connection = getMockConnection({ + id: 'test-123', + did: 'did:peer:1mydid', + theirDid: 'did:peer:1theirdid', + theirLabel: 'Test 123', + }) + outboundMessageContext = new OutboundMessageContext(new TestMessage(), { agentContext, connection }) + + envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(encryptedMessage)) + transportServiceHasInboundEndpoint.mockReturnValue(true) + + const didDocumentInstance = getMockDidDocument({ + service: [firstDidCommService, secondDidCommService], + }) + didResolverServiceResolveMock.mockResolvedValue(didDocumentInstance) + didResolverServiceResolveDidServicesMock.mockResolvedValue([ + getMockResolvedDidService(firstDidCommService), + getMockResolvedDidService(secondDidCommService), + ]) + }) + + afterEach(() => { + eventEmitter.off(AgentEventTypes.AgentMessageSent, eventListenerMock) + + jest.resetAllMocks() + }) + + test('throw error when there is no outbound transport', async () => { + await expect(messageSender.sendMessage(outboundMessageContext)).rejects.toThrow( + /Message is undeliverable to connection/ + ) + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: outboundMessageContext, + status: OutboundMessageSendStatus.Undeliverable, + }, + }) + }) + + test('throw error when there is no service or queue', async () => { + messageSender.registerOutboundTransport(outboundTransport) + + didResolverServiceResolveMock.mockResolvedValue(getMockDidDocument({ service: [] })) + didResolverServiceResolveDidServicesMock.mockResolvedValue([]) + + await expect(messageSender.sendMessage(outboundMessageContext)).rejects.toThrow( + `Message is undeliverable to connection test-123 (Test 123)` + ) + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: outboundMessageContext, + status: OutboundMessageSendStatus.Undeliverable, + }, + }) + }) + + test('call send message when session send method fails', async () => { + messageSender.registerOutboundTransport(outboundTransport) + transportServiceFindSessionMock.mockReturnValue(session) + session.send = jest.fn().mockRejectedValue(new Error('some error')) + + messageSender.registerOutboundTransport(outboundTransport) + const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage') + + await messageSender.sendMessage(outboundMessageContext) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: outboundMessageContext, + status: OutboundMessageSendStatus.SentToTransport, + }, + }) + + expect(sendMessageSpy).toHaveBeenCalledWith({ + connectionId: 'test-123', + payload: encryptedMessage, + endpoint: firstDidCommService.serviceEndpoint, + responseRequested: false, + }) + expect(sendMessageSpy).toHaveBeenCalledTimes(1) + }) + + test("resolves the did service using the did resolver if connection.theirDid starts with 'did:'", async () => { + messageSender.registerOutboundTransport(outboundTransport) + + const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage') + + await messageSender.sendMessage(outboundMessageContext) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: outboundMessageContext, + status: OutboundMessageSendStatus.SentToTransport, + }, + }) + + expect(didResolverServiceResolveDidServicesMock).toHaveBeenCalledWith(agentContext, connection.theirDid) + expect(sendMessageSpy).toHaveBeenCalledWith({ + connectionId: 'test-123', + payload: encryptedMessage, + endpoint: firstDidCommService.serviceEndpoint, + responseRequested: false, + }) + expect(sendMessageSpy).toHaveBeenCalledTimes(1) + }) + + test("throws an error if connection.theirDid starts with 'did:' but the resolver can't resolve the did document", async () => { + messageSender.registerOutboundTransport(outboundTransport) + + didResolverServiceResolveMock.mockRejectedValue( + new Error(`Unable to resolve did document for did '${connection.theirDid}': notFound`) + ) + + await expect(messageSender.sendMessage(outboundMessageContext)).rejects.toThrowError( + `Unable to resolve DID Document for '${connection.did}` + ) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: outboundMessageContext, + status: OutboundMessageSendStatus.Undeliverable, + }, + }) + }) + + test('call send message when session send method fails with missing keys', async () => { + messageSender.registerOutboundTransport(outboundTransport) + transportServiceFindSessionMock.mockReturnValue(sessionWithoutKeys) + + messageSender.registerOutboundTransport(outboundTransport) + const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage') + + await messageSender.sendMessage(outboundMessageContext) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: outboundMessageContext, + status: OutboundMessageSendStatus.SentToTransport, + }, + }) + + expect(sendMessageSpy).toHaveBeenCalledWith({ + connectionId: 'test-123', + payload: encryptedMessage, + endpoint: firstDidCommService.serviceEndpoint, + responseRequested: false, + }) + expect(sendMessageSpy).toHaveBeenCalledTimes(1) + }) + + test('call send message on session when outbound message has sessionId attached', async () => { + transportServiceFindSessionByIdMock.mockReturnValue(session) + messageSender.registerOutboundTransport(outboundTransport) + const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage') + // @ts-ignore + const sendMessageToServiceSpy = jest.spyOn(messageSender, 'sendMessageToService') + + const contextWithSessionId = new OutboundMessageContext(outboundMessageContext.message, { + agentContext: outboundMessageContext.agentContext, + connection: outboundMessageContext.connection, + sessionId: 'session-123', + }) + + await messageSender.sendMessage(contextWithSessionId) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: contextWithSessionId, + status: OutboundMessageSendStatus.SentToSession, + }, + }) + + expect(session.send).toHaveBeenCalledTimes(1) + expect(session.send).toHaveBeenNthCalledWith(1, agentContext, encryptedMessage) + expect(sendMessageSpy).toHaveBeenCalledTimes(0) + expect(sendMessageToServiceSpy).toHaveBeenCalledTimes(0) + expect(transportServiceFindSessionByIdMock).toHaveBeenCalledWith('session-123') + }) + + test('call send message on session when there is a session for a given connection', async () => { + messageSender.registerOutboundTransport(outboundTransport) + const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage') + //@ts-ignore + const sendToServiceSpy = jest.spyOn(messageSender, 'sendToService') + + await messageSender.sendMessage(outboundMessageContext) + + //@ts-ignore + const [[sendMessage]] = sendToServiceSpy.mock.calls + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: outboundMessageContext, + status: OutboundMessageSendStatus.SentToTransport, + }, + }) + expect(sendMessage).toMatchObject({ + connection: { + id: 'test-123', + }, + message: outboundMessageContext.message, + serviceParams: { + returnRoute: false, + service: { + serviceEndpoint: firstDidCommService.serviceEndpoint, + }, + }, + }) + + //@ts-ignore + expect(sendMessage.serviceParams.senderKey.publicKeyBase58).toEqual( + 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d' + ) + + //@ts-ignore + expect(sendMessage.serviceParams.service.recipientKeys.map((key) => key.publicKeyBase58)).toEqual([ + 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d', + ]) + + expect(sendToServiceSpy).toHaveBeenCalledTimes(1) + expect(sendMessageSpy).toHaveBeenCalledTimes(1) + }) + + test('calls sendToService with payload and endpoint from second DidComm service when the first fails', async () => { + messageSender.registerOutboundTransport(outboundTransport) + const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage') + //@ts-ignore + const sendToServiceSpy = jest.spyOn(messageSender, 'sendToService') + + // Simulate the case when the first call fails + sendMessageSpy.mockRejectedValueOnce(new Error()) + + await messageSender.sendMessage(outboundMessageContext) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: outboundMessageContext, + status: OutboundMessageSendStatus.SentToTransport, + }, + }) + + //@ts-ignore + const [, [sendMessage]] = sendToServiceSpy.mock.calls + + expect(sendMessage).toMatchObject({ + agentContext, + connection: { + id: 'test-123', + }, + message: outboundMessageContext.message, + serviceParams: { + returnRoute: false, + service: { + serviceEndpoint: secondDidCommService.serviceEndpoint, + }, + }, + }) + + //@ts-ignore + expect(sendMessage.serviceParams.senderKey.publicKeyBase58).toEqual( + 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d' + ) + //@ts-ignore + expect(sendMessage.serviceParams.service.recipientKeys.map((key) => key.publicKeyBase58)).toEqual([ + 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d', + ]) + + expect(sendToServiceSpy).toHaveBeenCalledTimes(2) + expect(sendMessageSpy).toHaveBeenCalledTimes(2) + }) + + test('throw error when message endpoint is not supported by outbound transport schemes', async () => { + messageSender.registerOutboundTransport(new DummyWsOutboundTransport()) + await expect(messageSender.sendMessage(outboundMessageContext)).rejects.toThrow( + /Message is undeliverable to connection/ + ) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: outboundMessageContext, + status: OutboundMessageSendStatus.Undeliverable, + }, + }) + }) + }) + + describe('sendMessageToService', () => { + const service: ResolvedDidCommService = { + id: 'out-of-band', + recipientKeys: [Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL')], + routingKeys: [], + serviceEndpoint: 'https://example.com', + } + const senderKey = Key.fromFingerprint('z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th') + + beforeEach(() => { + outboundTransport = new DummyHttpOutboundTransport() + messageSender = new MessageSender( + enveloperService, + transportService, + new InMemoryMessagePickupRepository(agentConfig.logger), + logger, + didResolverService, + didCommDocumentService, + eventEmitter + ) + + eventEmitter.on(AgentEventTypes.AgentMessageSent, eventListenerMock) + + envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(encryptedMessage)) + }) + + afterEach(() => { + jest.resetAllMocks() + eventEmitter.off(AgentEventTypes.AgentMessageSent, eventListenerMock) + }) + + test('throws error when there is no outbound transport', async () => { + outboundMessageContext = new OutboundMessageContext(new TestMessage(), { + agentContext, + serviceParams: { + senderKey, + service, + }, + }) + await expect(messageSender.sendMessage(outboundMessageContext)).rejects.toThrow( + `Agent has no outbound transport!` + ) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: outboundMessageContext, + status: OutboundMessageSendStatus.Undeliverable, + }, + }) + }) + + test('calls send message with payload and endpoint from DIDComm service', async () => { + messageSender.registerOutboundTransport(outboundTransport) + const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage') + + outboundMessageContext = new OutboundMessageContext(new TestMessage(), { + agentContext, + serviceParams: { + senderKey, + service, + }, + }) + + await messageSender.sendMessage(outboundMessageContext) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: outboundMessageContext, + status: OutboundMessageSendStatus.SentToTransport, + }, + }) + + expect(sendMessageSpy).toHaveBeenCalledWith({ + payload: encryptedMessage, + endpoint: service.serviceEndpoint, + responseRequested: false, + }) + expect(sendMessageSpy).toHaveBeenCalledTimes(1) + }) + + test('call send message with responseRequested when message has return route', async () => { + messageSender.registerOutboundTransport(outboundTransport) + const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage') + + const message = new TestMessage() + message.setReturnRouting(ReturnRouteTypes.all) + + outboundMessageContext = new OutboundMessageContext(message, { + agentContext, + serviceParams: { + senderKey, + service, + }, + }) + + await messageSender.sendMessage(outboundMessageContext) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: outboundMessageContext, + status: OutboundMessageSendStatus.SentToTransport, + }, + }) + + expect(sendMessageSpy).toHaveBeenCalledWith({ + payload: encryptedMessage, + endpoint: service.serviceEndpoint, + responseRequested: true, + }) + expect(sendMessageSpy).toHaveBeenCalledTimes(1) + }) + + test('throw error when message endpoint is not supported by outbound transport schemes', async () => { + messageSender.registerOutboundTransport(new DummyWsOutboundTransport()) + outboundMessageContext = new OutboundMessageContext(new TestMessage(), { + agentContext, + serviceParams: { + senderKey, + service, + }, + }) + + await expect(messageSender.sendMessage(outboundMessageContext)).rejects.toThrow( + /Unable to send message to service/ + ) + expect(eventListenerMock).toHaveBeenCalledWith({ + type: AgentEventTypes.AgentMessageSent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + message: outboundMessageContext, + status: OutboundMessageSendStatus.Undeliverable, + }, + }) + }) + }) + + describe('packMessage', () => { + beforeEach(() => { + outboundTransport = new DummyHttpOutboundTransport() + messagePickupRepository = new InMemoryMessagePickupRepository(agentConfig.logger) + messageSender = new MessageSender( + enveloperService, + transportService, + messagePickupRepository, + logger, + didResolverService, + didCommDocumentService, + eventEmitter + ) + connection = getMockConnection() + + envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(encryptedMessage)) + }) + + afterEach(() => { + jest.resetAllMocks() + }) + + test('return outbound message context with connection, payload and endpoint', async () => { + const message = new TestMessage() + const endpoint = 'https://example.com' + + const keys = { + recipientKeys: [recipientKey], + routingKeys: [], + senderKey: senderKey, + } + const result = await messageSender.packMessage(agentContext, { message, keys, endpoint }) + + expect(result).toEqual({ + payload: encryptedMessage, + responseRequested: message.hasAnyReturnRoute(), + endpoint, + }) + }) + }) +}) + +function getMockDidDocument({ service }: { service: DidDocumentService[] }) { + return new DidDocument({ + id: 'did:sov:SKJVx2kn373FNgvff1SbJo', + alsoKnownAs: ['did:sov:SKJVx2kn373FNgvff1SbJo'], + controller: ['did:sov:SKJVx2kn373FNgvff1SbJo'], + verificationMethod: [], + service, + authentication: [ + new VerificationMethod({ + id: 'did:sov:SKJVx2kn373FNgvff1SbJo#authentication-1', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyBase58: 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d', + }), + ], + }) +} + +function getMockResolvedDidService(service: DidCommV1Service | IndyAgentService): ResolvedDidCommService { + return { + id: service.id, + serviceEndpoint: service.serviceEndpoint, + recipientKeys: [verkeyToInstanceOfKey('EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d')], + routingKeys: [], + } +} diff --git a/packages/core/src/agent/__tests__/TransportService.test.ts b/packages/core/src/agent/__tests__/TransportService.test.ts new file mode 100644 index 0000000000..fdfbc57bf9 --- /dev/null +++ b/packages/core/src/agent/__tests__/TransportService.test.ts @@ -0,0 +1,30 @@ +import { Subject } from 'rxjs' + +import { agentDependencies, getAgentContext, getMockConnection } from '../../../tests/helpers' +import { DidExchangeRole } from '../../modules/connections' +import { EventEmitter } from '../EventEmitter' +import { TransportService } from '../TransportService' + +import { DummyTransportSession } from './stubs' + +describe('TransportService', () => { + describe('removeSession', () => { + let transportService: TransportService + + beforeEach(() => { + transportService = new TransportService(getAgentContext(), new EventEmitter(agentDependencies, new Subject())) + }) + + test(`remove session saved for a given connection`, () => { + const connection = getMockConnection({ id: 'test-123', role: DidExchangeRole.Responder }) + const session = new DummyTransportSession('dummy-session-123') + session.connectionId = connection.id + + transportService.saveSession(session) + expect(transportService.findSessionByConnectionId(connection.id)).toEqual(session) + + transportService.removeSession(session) + expect(transportService.findSessionByConnectionId(connection.id)).toEqual(undefined) + }) + }) +}) diff --git a/packages/core/src/agent/__tests__/stubs.ts b/packages/core/src/agent/__tests__/stubs.ts new file mode 100644 index 0000000000..afd7ec4aaa --- /dev/null +++ b/packages/core/src/agent/__tests__/stubs.ts @@ -0,0 +1,23 @@ +import type { AgentMessage } from '../AgentMessage' +import type { EnvelopeKeys } from '../EnvelopeService' +import type { TransportSession } from '../TransportService' + +export class DummyTransportSession implements TransportSession { + public id: string + public readonly type = 'http' + public keys?: EnvelopeKeys + public inboundMessage?: AgentMessage + public connectionId?: string + + public constructor(id: string) { + this.id = id + } + + public send(): Promise { + throw new Error('Method not implemented.') + } + + public close(): Promise { + throw new Error('Method not implemented.') + } +} diff --git a/packages/core/src/agent/context/AgentContext.ts b/packages/core/src/agent/context/AgentContext.ts new file mode 100644 index 0000000000..73a857d518 --- /dev/null +++ b/packages/core/src/agent/context/AgentContext.ts @@ -0,0 +1,67 @@ +import type { AgentContextProvider } from './AgentContextProvider' +import type { DependencyManager } from '../../plugins' +import type { Wallet } from '../../wallet' + +import { InjectionSymbols } from '../../constants' +import { AgentConfig } from '../AgentConfig' + +export class AgentContext { + /** + * Dependency manager holds all dependencies for the current context. Possibly a child of a parent dependency manager, + * in which case all singleton dependencies from the parent context are also available to this context. + */ + public readonly dependencyManager: DependencyManager + + /** + * An identifier that allows to correlate this context across sessions. This identifier is created by the `AgentContextProvider` + * and should only be meaningful to the `AgentContextProvider`. The `contextCorrelationId` MUST uniquely identity the context and + * should be enough to start a new session. + * + * An example of the `contextCorrelationId` is for example the id of the `TenantRecord` that is associated with this context when using the tenant module. + * The `TenantAgentContextProvider` will set the `contextCorrelationId` to the `TenantRecord` id when creating the context, and will be able to create a context + * for a specific tenant using the `contextCorrelationId`. + */ + public readonly contextCorrelationId: string + + public constructor({ + dependencyManager, + contextCorrelationId, + }: { + dependencyManager: DependencyManager + contextCorrelationId: string + }) { + this.dependencyManager = dependencyManager + this.contextCorrelationId = contextCorrelationId + } + + /** + * Convenience method to access the agent config for the current context. + */ + public get config() { + return this.dependencyManager.resolve(AgentConfig) + } + + /** + * Convenience method to access the wallet for the current context. + */ + public get wallet() { + return this.dependencyManager.resolve(InjectionSymbols.Wallet) + } + + /** + * End session the current agent context + */ + public async endSession() { + const agentContextProvider = this.dependencyManager.resolve( + InjectionSymbols.AgentContextProvider + ) + + await agentContextProvider.endSessionForAgentContext(this) + } + + public toJSON() { + return { + contextCorrelationId: this.contextCorrelationId, + } + } +} diff --git a/packages/core/src/agent/context/AgentContextProvider.ts b/packages/core/src/agent/context/AgentContextProvider.ts new file mode 100644 index 0000000000..14ba9984c5 --- /dev/null +++ b/packages/core/src/agent/context/AgentContextProvider.ts @@ -0,0 +1,30 @@ +import type { AgentContext } from './AgentContext' + +export interface AgentContextProvider { + /** + * Get the agent context for an inbound message. It's possible to provide a contextCorrelationId to make it + * easier for the context provider implementation to correlate inbound messages to the correct context. This can be useful if + * a plaintext message is passed and the context provider can't determine the context based on the recipient public keys + * of the inbound message. + * + * The implementation of this method could range from a very simple one that always returns the same context to + * a complex one that manages the context for a multi-tenant agent. + */ + getContextForInboundMessage( + inboundMessage: unknown, + options?: { contextCorrelationId?: string } + ): Promise + + /** + * Get the agent context for a context correlation id. Will throw an error if no AgentContext could be retrieved + * for the specified contextCorrelationId. + */ + getAgentContextForContextCorrelationId(contextCorrelationId: string): Promise + + /** + * End sessions for the provided agent context. This does not necessarily mean the wallet will be closed or the dependency manager will + * be disposed, it is to inform the agent context provider this session for the agent context is no longer in use. This should only be + * called once for every session and the agent context MUST not be used after this method is called. + */ + endSessionForAgentContext(agentContext: AgentContext): Promise +} diff --git a/packages/core/src/agent/context/DefaultAgentContextProvider.ts b/packages/core/src/agent/context/DefaultAgentContextProvider.ts new file mode 100644 index 0000000000..7f9ec4d918 --- /dev/null +++ b/packages/core/src/agent/context/DefaultAgentContextProvider.ts @@ -0,0 +1,55 @@ +import type { AgentContextProvider } from './AgentContextProvider' + +import { CredoError } from '../../error' +import { injectable } from '../../plugins' + +import { AgentContext } from './AgentContext' + +/** + * Default implementation of AgentContextProvider. + * + * Holds a single `AgentContext` instance that will be used for all messages, i.e. a + * a single tenant agent. + */ +@injectable() +export class DefaultAgentContextProvider implements AgentContextProvider { + private agentContext: AgentContext + + public constructor(agentContext: AgentContext) { + this.agentContext = agentContext + } + + public async getAgentContextForContextCorrelationId(contextCorrelationId: string): Promise { + if (contextCorrelationId !== this.agentContext.contextCorrelationId) { + throw new CredoError( + `Could not get agent context for contextCorrelationId '${contextCorrelationId}'. Only contextCorrelationId '${this.agentContext.contextCorrelationId}' is supported.` + ) + } + + return this.agentContext + } + + public async getContextForInboundMessage( + // We don't need to look at the message as we always use the same context in the default agent context provider + _: unknown, + options?: { contextCorrelationId?: string } + ): Promise { + // This will throw an error if the contextCorrelationId does not match with the contextCorrelationId of the agent context property of this class. + if (options?.contextCorrelationId) { + return this.getAgentContextForContextCorrelationId(options.contextCorrelationId) + } + + return this.agentContext + } + + public async endSessionForAgentContext(agentContext: AgentContext) { + // Throw an error if the context correlation id does not match to prevent misuse. + if (agentContext.contextCorrelationId !== this.agentContext.contextCorrelationId) { + throw new CredoError( + `Could not end session for agent context with contextCorrelationId '${agentContext.contextCorrelationId}'. Only contextCorrelationId '${this.agentContext.contextCorrelationId}' is provided by this provider.` + ) + } + + // We won't dispose the agent context as we don't keep track of the total number of sessions for the root agent context.65 + } +} diff --git a/packages/core/src/agent/context/__tests__/DefaultAgentContextProvider.test.ts b/packages/core/src/agent/context/__tests__/DefaultAgentContextProvider.test.ts new file mode 100644 index 0000000000..05019a0e76 --- /dev/null +++ b/packages/core/src/agent/context/__tests__/DefaultAgentContextProvider.test.ts @@ -0,0 +1,65 @@ +import type { AgentContextProvider } from '../AgentContextProvider' + +import { getAgentContext } from '../../../../tests/helpers' +import { DefaultAgentContextProvider } from '../DefaultAgentContextProvider' + +const agentContext = getAgentContext() + +describe('DefaultAgentContextProvider', () => { + describe('getContextForInboundMessage()', () => { + test('returns the agent context provided in the constructor', async () => { + const agentContextProvider: AgentContextProvider = new DefaultAgentContextProvider(agentContext) + + const message = {} + + await expect(agentContextProvider.getContextForInboundMessage(message)).resolves.toBe(agentContext) + }) + + test('throws an error if the provided contextCorrelationId does not match with the contextCorrelationId from the constructor agent context', async () => { + const agentContextProvider: AgentContextProvider = new DefaultAgentContextProvider(agentContext) + + const message = {} + + await expect( + agentContextProvider.getContextForInboundMessage(message, { contextCorrelationId: 'wrong' }) + ).rejects.toThrowError( + `Could not get agent context for contextCorrelationId 'wrong'. Only contextCorrelationId 'mock' is supported.` + ) + }) + }) + + describe('getAgentContextForContextCorrelationId()', () => { + test('returns the agent context provided in the constructor if contextCorrelationId matches', async () => { + const agentContextProvider: AgentContextProvider = new DefaultAgentContextProvider(agentContext) + + await expect(agentContextProvider.getAgentContextForContextCorrelationId('mock')).resolves.toBe(agentContext) + }) + + test('throws an error if the contextCorrelationId does not match with the contextCorrelationId from the constructor agent context', async () => { + const agentContextProvider: AgentContextProvider = new DefaultAgentContextProvider(agentContext) + + await expect(agentContextProvider.getAgentContextForContextCorrelationId('wrong')).rejects.toThrowError( + `Could not get agent context for contextCorrelationId 'wrong'. Only contextCorrelationId 'mock' is supported.` + ) + }) + }) + + describe('endSessionForAgentContext()', () => { + test('resolves when the correct agent context is passed', async () => { + const agentContextProvider: AgentContextProvider = new DefaultAgentContextProvider(agentContext) + + await expect(agentContextProvider.endSessionForAgentContext(agentContext)).resolves.toBeUndefined() + }) + + test('throws an error if the contextCorrelationId does not match with the contextCorrelationId from the constructor agent context', async () => { + const agentContextProvider: AgentContextProvider = new DefaultAgentContextProvider(agentContext) + const agentContext2 = getAgentContext({ + contextCorrelationId: 'mock2', + }) + + await expect(agentContextProvider.endSessionForAgentContext(agentContext2)).rejects.toThrowError( + `Could not end session for agent context with contextCorrelationId 'mock2'. Only contextCorrelationId 'mock' is provided by this provider.` + ) + }) + }) +}) diff --git a/packages/core/src/agent/context/index.ts b/packages/core/src/agent/context/index.ts new file mode 100644 index 0000000000..6f46b27942 --- /dev/null +++ b/packages/core/src/agent/context/index.ts @@ -0,0 +1,3 @@ +export * from './AgentContext' +export * from './AgentContextProvider' +export * from './DefaultAgentContextProvider' diff --git a/packages/core/src/agent/getOutboundMessageContext.ts b/packages/core/src/agent/getOutboundMessageContext.ts new file mode 100644 index 0000000000..86af4462f9 --- /dev/null +++ b/packages/core/src/agent/getOutboundMessageContext.ts @@ -0,0 +1,320 @@ +import type { AgentMessage } from './AgentMessage' +import type { AgentContext } from './context' +import type { ConnectionRecord, Routing } from '../modules/connections' +import type { ResolvedDidCommService } from '../modules/didcomm' +import type { OutOfBandRecord } from '../modules/oob' +import type { BaseRecordAny } from '../storage/BaseRecord' + +import { Key } from '../crypto' +import { ServiceDecorator } from '../decorators/service/ServiceDecorator' +import { CredoError } from '../error' +import { InvitationType, OutOfBandRepository, OutOfBandRole, OutOfBandService } from '../modules/oob' +import { OutOfBandRecordMetadataKeys } from '../modules/oob/repository/outOfBandRecordMetadataTypes' +import { RoutingService } from '../modules/routing' +import { DidCommMessageRepository, DidCommMessageRole } from '../storage/didcomm' +import { uuid } from '../utils/uuid' + +import { OutboundMessageContext } from './models' + +/** + * Maybe these methods should be moved to a service, but that would require + * extra injection in the sender functions, and I'm not 100% sure what would + * be the best place to host these. + */ + +/** + * Get the outbound message context for a message. Will use the connection record if available, + * and otherwise try to create a connectionless message context. + */ +export async function getOutboundMessageContext( + agentContext: AgentContext, + { + message, + connectionRecord, + associatedRecord, + lastReceivedMessage, + lastSentMessage, + }: { + connectionRecord?: ConnectionRecord + associatedRecord?: BaseRecordAny + message: AgentMessage + lastReceivedMessage?: AgentMessage + lastSentMessage?: AgentMessage + } +) { + // TODO: even if using a connection record, we should check if there's an oob record associated and this + // is the first response to the oob invitation. If so, we should add the parentThreadId to the message + if (connectionRecord) { + agentContext.config.logger.debug( + `Creating outbound message context for message ${message.id} with connection ${connectionRecord.id}` + ) + return new OutboundMessageContext(message, { + agentContext, + associatedRecord, + connection: connectionRecord, + }) + } + + if (!lastReceivedMessage) { + throw new CredoError( + 'No connection record and no lastReceivedMessage was supplied. For connection-less exchanges the lastReceivedMessage is required.' + ) + } + + if (!associatedRecord) { + throw new CredoError( + 'No associated record was supplied. This is required for connection-less exchanges to store the associated ~service decorator on the message.' + ) + } + + // Connectionless + return getConnectionlessOutboundMessageContext(agentContext, { + message, + associatedRecord, + lastReceivedMessage, + lastSentMessage, + }) +} + +export async function getConnectionlessOutboundMessageContext( + agentContext: AgentContext, + { + message, + lastReceivedMessage, + lastSentMessage, + associatedRecord, + }: { + message: AgentMessage + associatedRecord: BaseRecordAny + lastReceivedMessage: AgentMessage + lastSentMessage?: AgentMessage + } +) { + agentContext.config.logger.debug( + `Creating outbound message context for message ${message.id} using connection-less exchange` + ) + + const outOfBandRecord = await getOutOfBandRecordForMessage(agentContext, message) + // eslint-disable-next-line prefer-const + let { recipientService, ourService } = await getServicesForMessage(agentContext, { + lastReceivedMessage, + lastSentMessage, + message, + outOfBandRecord, + }) + + // We need to set up routing for this exchange if we haven't sent any messages yet. + if (!lastSentMessage) { + ourService = await createOurService(agentContext, { outOfBandRecord, message }) + } + + // These errors should not happen as they will be caught by the checks above. But if there's a path missed, + // and to make typescript happy we add these checks. + if (!ourService) { + throw new CredoError(`Could not determine our service for connection-less exchange for message ${message.id}.`) + } + if (!recipientService) { + throw new CredoError( + `Could not determine recipient service for connection-less exchange for message ${message.id}.` + ) + } + + // Adds the ~service and ~thread.pthid (if oob is used) to the message and updates it in storage. + await addExchangeDataToMessage(agentContext, { message, ourService, outOfBandRecord, associatedRecord }) + + return new OutboundMessageContext(message, { + agentContext: agentContext, + associatedRecord, + serviceParams: { + service: recipientService, + senderKey: ourService.recipientKeys[0], + returnRoute: true, + }, + }) +} + +/** + * Retrieves the out of band record associated with the message based on the thread id of the message. + */ +async function getOutOfBandRecordForMessage(agentContext: AgentContext, message: AgentMessage) { + agentContext.config.logger.debug( + `Looking for out-of-band record for message ${message.id} with thread id ${message.threadId}` + ) + const outOfBandRepository = agentContext.dependencyManager.resolve(OutOfBandRepository) + + const outOfBandRecord = await outOfBandRepository.findSingleByQuery(agentContext, { + invitationRequestsThreadIds: [message.threadId], + }) + + return outOfBandRecord ?? undefined +} + +/** + * Returns the services to use for the message. When available it will extract the services from the + * lastSentMessage and lastReceivedMessage. If not available it will try to extract the services from + * the out of band record. + * + * If the required services and fields are not available, an error will be thrown. + */ +async function getServicesForMessage( + agentContext: AgentContext, + { + lastSentMessage, + lastReceivedMessage, + message, + outOfBandRecord, + }: { + lastSentMessage?: AgentMessage + lastReceivedMessage: AgentMessage + message: AgentMessage + outOfBandRecord?: OutOfBandRecord + } +) { + let ourService = lastSentMessage?.service?.resolvedDidCommService + let recipientService = lastReceivedMessage.service?.resolvedDidCommService + + const outOfBandService = agentContext.dependencyManager.resolve(OutOfBandService) + + // Check if valid + if (outOfBandRecord?.role === OutOfBandRole.Sender) { + // Extract ourService from the oob record if not on a previous message + if (!ourService) { + ourService = await outOfBandService.getResolvedServiceForOutOfBandServices( + agentContext, + outOfBandRecord.outOfBandInvitation.getServices() + ) + } + + if (!recipientService) { + throw new CredoError( + `Could not find a service to send the message to. Please make sure the connection has a service or provide a service to send the message to.` + ) + } + + // We have created the oob record with a message, that message should be provided here as well + if (!lastSentMessage) { + throw new CredoError('Must have lastSentMessage when out of band record has role Sender') + } + } else if (outOfBandRecord?.role === OutOfBandRole.Receiver) { + // Extract recipientService from the oob record if not on a previous message + if (!recipientService) { + recipientService = await outOfBandService.getResolvedServiceForOutOfBandServices( + agentContext, + outOfBandRecord.outOfBandInvitation.getServices() + ) + } + + if (lastSentMessage && !ourService) { + throw new CredoError( + `Could not find a service to send the message to. Please make sure the connection has a service or provide a service to send the message to.` + ) + } + } + // we either miss ourService (even though a message was sent) or we miss recipientService + // we check in separate if statements to provide a more specific error message + else { + if (lastSentMessage && !ourService) { + agentContext.config.logger.error( + `No out of band record associated and missing our service for connection-less exchange for message ${message.id}, while previous message has already been sent.` + ) + throw new CredoError( + `No out of band record associated and missing our service for connection-less exchange for message ${message.id}, while previous message has already been sent.` + ) + } + + if (!recipientService) { + agentContext.config.logger.error( + `No out of band record associated and missing recipient service for connection-less exchange for message ${message.id}.` + ) + throw new CredoError( + `No out of band record associated and missing recipient service for connection-less exchange for message ${message.id}.` + ) + } + } + + return { ourService, recipientService } +} + +/** + * Creates a new service for us as the sender to be used in a connection-less exchange. + * + * Will creating routing, which takes into account mediators, and will optionally extract + * routing configuration from the out of band record if available. + */ +async function createOurService( + agentContext: AgentContext, + { outOfBandRecord, message }: { outOfBandRecord?: OutOfBandRecord; message: AgentMessage } +): Promise { + agentContext.config.logger.debug( + `No previous sent message in thread for outbound message ${message.id}, setting up routing` + ) + + let routing: Routing | undefined = undefined + + // Extract routing from out of band record if possible + const oobRecordRecipientRouting = outOfBandRecord?.metadata.get(OutOfBandRecordMetadataKeys.RecipientRouting) + if (oobRecordRecipientRouting) { + routing = { + recipientKey: Key.fromFingerprint(oobRecordRecipientRouting.recipientKeyFingerprint), + routingKeys: oobRecordRecipientRouting.routingKeyFingerprints.map((fingerprint) => + Key.fromFingerprint(fingerprint) + ), + endpoints: oobRecordRecipientRouting.endpoints, + mediatorId: oobRecordRecipientRouting.mediatorId, + } + } + + if (!routing) { + const routingService = agentContext.dependencyManager.resolve(RoutingService) + routing = await routingService.getRouting(agentContext, { + mediatorId: outOfBandRecord?.mediatorId, + }) + } + + return { + id: uuid(), + serviceEndpoint: routing.endpoints[0], + recipientKeys: [routing.recipientKey], + routingKeys: routing.routingKeys, + } +} + +async function addExchangeDataToMessage( + agentContext: AgentContext, + { + message, + ourService, + outOfBandRecord, + associatedRecord, + }: { + message: AgentMessage + ourService: ResolvedDidCommService + outOfBandRecord?: OutOfBandRecord + associatedRecord: BaseRecordAny + } +) { + const legacyInvitationMetadata = outOfBandRecord?.metadata.get(OutOfBandRecordMetadataKeys.LegacyInvitation) + + // Set the parentThreadId on the message from the oob invitation + // If connectionless is used, we should not add the parentThreadId + if (outOfBandRecord && legacyInvitationMetadata?.legacyInvitationType !== InvitationType.Connectionless) { + if (!message.thread) { + message.setThread({ + parentThreadId: outOfBandRecord.outOfBandInvitation.id, + }) + } else { + message.thread.parentThreadId = outOfBandRecord.outOfBandInvitation.id + } + } + + // Set the service on the message and save service decorator to record (to remember our verkey) + // TODO: we should store this in the OOB record, but that would be a breaking change for now. + // We can change this in 0.5.0 + message.service = ServiceDecorator.fromResolvedDidCommService(ourService) + + await agentContext.dependencyManager.resolve(DidCommMessageRepository).saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: associatedRecord.id, + }) +} diff --git a/packages/core/src/agent/index.ts b/packages/core/src/agent/index.ts new file mode 100644 index 0000000000..630b4d7e78 --- /dev/null +++ b/packages/core/src/agent/index.ts @@ -0,0 +1 @@ +export * from './context' diff --git a/packages/core/src/agent/models/InboundMessageContext.ts b/packages/core/src/agent/models/InboundMessageContext.ts new file mode 100644 index 0000000000..886210f5f5 --- /dev/null +++ b/packages/core/src/agent/models/InboundMessageContext.ts @@ -0,0 +1,75 @@ +import type { OutboundMessageContext } from './OutboundMessageContext' +import type { Key } from '../../crypto' +import type { ConnectionRecord } from '../../modules/connections' +import type { AgentMessage } from '../AgentMessage' +import type { MessageHandler } from '../MessageHandler' +import type { AgentContext } from '../context' + +import { CredoError } from '../../error' + +export interface MessageContextParams { + connection?: ConnectionRecord + sessionId?: string + senderKey?: Key + recipientKey?: Key + agentContext: AgentContext + receivedAt?: Date +} + +export class InboundMessageContext { + public connection?: ConnectionRecord + public sessionId?: string + public senderKey?: Key + public recipientKey?: Key + public receivedAt: Date + + public readonly agentContext: AgentContext + + public message: T + public messageHandler?: MessageHandler + public responseMessage?: OutboundMessageContext + + public constructor(message: T, context: MessageContextParams) { + this.message = message + this.recipientKey = context.recipientKey + this.senderKey = context.senderKey + this.connection = context.connection + this.sessionId = context.sessionId + this.agentContext = context.agentContext + this.receivedAt = context.receivedAt ?? new Date() + } + + public setMessageHandler(messageHandler: MessageHandler) { + this.messageHandler = messageHandler + } + + public setResponseMessage(outboundMessageContext: OutboundMessageContext) { + this.responseMessage = outboundMessageContext + } + + /** + * Assert the inbound message has a ready connection associated with it. + * + * @throws {CredoError} if there is no connection or the connection is not ready + */ + public assertReadyConnection(): ConnectionRecord { + if (!this.connection) { + throw new CredoError(`No connection associated with incoming message ${this.message.type}`) + } + + // Make sure connection is ready + this.connection.assertReady() + + return this.connection + } + + public toJSON() { + return { + message: this.message, + recipientKey: this.recipientKey?.fingerprint, + senderKey: this.senderKey?.fingerprint, + sessionId: this.sessionId, + agentContext: this.agentContext.toJSON(), + } + } +} diff --git a/packages/core/src/agent/models/OutboundMessageContext.ts b/packages/core/src/agent/models/OutboundMessageContext.ts new file mode 100644 index 0000000000..bb031594c1 --- /dev/null +++ b/packages/core/src/agent/models/OutboundMessageContext.ts @@ -0,0 +1,81 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { InboundMessageContext } from './InboundMessageContext' +import type { Key } from '../../crypto' +import type { ConnectionRecord } from '../../modules/connections' +import type { ResolvedDidCommService } from '../../modules/didcomm' +import type { OutOfBandRecord } from '../../modules/oob' +import type { BaseRecord } from '../../storage/BaseRecord' +import type { AgentMessage } from '../AgentMessage' +import type { AgentContext } from '../context' + +import { CredoError } from '../../error' + +export interface ServiceMessageParams { + senderKey: Key + service: ResolvedDidCommService + returnRoute?: boolean +} + +export interface OutboundMessageContextParams { + agentContext: AgentContext + inboundMessageContext?: InboundMessageContext + associatedRecord?: BaseRecord + connection?: ConnectionRecord + serviceParams?: ServiceMessageParams + outOfBand?: OutOfBandRecord + sessionId?: string +} + +export class OutboundMessageContext { + public message: T + public connection?: ConnectionRecord + public serviceParams?: ServiceMessageParams + public outOfBand?: OutOfBandRecord + public associatedRecord?: BaseRecord + public sessionId?: string + public inboundMessageContext?: InboundMessageContext + public readonly agentContext: AgentContext + + public constructor(message: T, context: OutboundMessageContextParams) { + this.message = message + this.connection = context.connection + this.sessionId = context.sessionId + this.outOfBand = context.outOfBand + this.serviceParams = context.serviceParams + this.associatedRecord = context.associatedRecord + this.inboundMessageContext = context.inboundMessageContext + this.agentContext = context.agentContext + } + + /** + * Assert the outbound message has a ready connection associated with it. + * + * @throws {CredoError} if there is no connection or the connection is not ready + */ + public assertReadyConnection(): ConnectionRecord { + if (!this.connection) { + throw new CredoError(`No connection associated with outgoing message ${this.message.type}`) + } + + // Make sure connection is ready + this.connection.assertReady() + + return this.connection + } + + public isOutboundServiceMessage(): boolean { + return this.serviceParams?.service !== undefined + } + + public toJSON() { + return { + message: this.message, + outOfBand: this.outOfBand, + associatedRecord: this.associatedRecord, + sessionId: this.sessionId, + serviceParams: this.serviceParams, + agentContext: this.agentContext.toJSON(), + connection: this.connection, + } + } +} diff --git a/packages/core/src/agent/models/OutboundMessageSendStatus.ts b/packages/core/src/agent/models/OutboundMessageSendStatus.ts new file mode 100644 index 0000000000..6fdb4f7f68 --- /dev/null +++ b/packages/core/src/agent/models/OutboundMessageSendStatus.ts @@ -0,0 +1,6 @@ +export enum OutboundMessageSendStatus { + SentToSession = 'SentToSession', + SentToTransport = 'SentToTransport', + QueuedForPickup = 'QueuedForPickup', + Undeliverable = 'Undeliverable', +} diff --git a/packages/core/src/agent/models/features/Feature.ts b/packages/core/src/agent/models/features/Feature.ts new file mode 100644 index 0000000000..00464ee77f --- /dev/null +++ b/packages/core/src/agent/models/features/Feature.ts @@ -0,0 +1,58 @@ +import { Expose } from 'class-transformer' +import { IsString } from 'class-validator' + +import { CredoError } from '../../../error' +import { JsonTransformer } from '../../../utils/JsonTransformer' + +export interface FeatureOptions { + id: string + type: string +} + +export class Feature { + public id!: string + + public constructor(props: FeatureOptions) { + if (props) { + this.id = props.id + this.type = props.type + } + } + + @IsString() + @Expose({ name: 'feature-type' }) + public readonly type!: string + + /** + * Combine this feature with another one, provided both are from the same type + * and have the same id + * + * @param feature object to combine with this one + * @returns a new object resulting from the combination between this and feature + */ + public combine(feature: this) { + if (feature.id !== this.id) { + throw new CredoError('Can only combine with a feature with the same id') + } + + const obj1 = JsonTransformer.toJSON(this) + const obj2 = JsonTransformer.toJSON(feature) + + for (const key in obj2) { + try { + if (Array.isArray(obj2[key])) { + obj1[key] = [...new Set([...obj1[key], ...obj2[key]])] + } else { + obj1[key] = obj2[key] + } + } catch (e) { + obj1[key] = obj2[key] + } + } + return JsonTransformer.fromJSON(obj1, Feature) + } + + public toJSON(): Record { + return JsonTransformer.toJSON(this) + } +} diff --git a/packages/core/src/agent/models/features/FeatureQuery.ts b/packages/core/src/agent/models/features/FeatureQuery.ts new file mode 100644 index 0000000000..aab269b5db --- /dev/null +++ b/packages/core/src/agent/models/features/FeatureQuery.ts @@ -0,0 +1,23 @@ +import { Expose } from 'class-transformer' +import { IsString } from 'class-validator' + +export interface FeatureQueryOptions { + featureType: string + match: string +} + +export class FeatureQuery { + public constructor(options: FeatureQueryOptions) { + if (options) { + this.featureType = options.featureType + this.match = options.match + } + } + + @Expose({ name: 'feature-type' }) + @IsString() + public featureType!: string + + @IsString() + public match!: string +} diff --git a/packages/core/src/agent/models/features/GoalCode.ts b/packages/core/src/agent/models/features/GoalCode.ts new file mode 100644 index 0000000000..71a0b9fdd3 --- /dev/null +++ b/packages/core/src/agent/models/features/GoalCode.ts @@ -0,0 +1,13 @@ +import type { FeatureOptions } from './Feature' + +import { Feature } from './Feature' + +export type GoalCodeOptions = Omit + +export class GoalCode extends Feature { + public constructor(props: GoalCodeOptions) { + super({ ...props, type: GoalCode.type }) + } + + public static readonly type = 'goal-code' +} diff --git a/packages/core/src/agent/models/features/GovernanceFramework.ts b/packages/core/src/agent/models/features/GovernanceFramework.ts new file mode 100644 index 0000000000..ce174e6ebd --- /dev/null +++ b/packages/core/src/agent/models/features/GovernanceFramework.ts @@ -0,0 +1,13 @@ +import type { FeatureOptions } from './Feature' + +import { Feature } from './Feature' + +export type GovernanceFrameworkOptions = Omit + +export class GovernanceFramework extends Feature { + public constructor(props: GovernanceFrameworkOptions) { + super({ ...props, type: GovernanceFramework.type }) + } + + public static readonly type = 'gov-fw' +} diff --git a/packages/core/src/agent/models/features/Protocol.ts b/packages/core/src/agent/models/features/Protocol.ts new file mode 100644 index 0000000000..ddfc63d384 --- /dev/null +++ b/packages/core/src/agent/models/features/Protocol.ts @@ -0,0 +1,25 @@ +import type { FeatureOptions } from './Feature' + +import { IsOptional, IsString } from 'class-validator' + +import { Feature } from './Feature' + +export interface ProtocolOptions extends Omit { + roles?: string[] +} + +export class Protocol extends Feature { + public constructor(props: ProtocolOptions) { + super({ ...props, type: Protocol.type }) + + if (props) { + this.roles = props.roles + } + } + + public static readonly type = 'protocol' + + @IsString({ each: true }) + @IsOptional() + public roles?: string[] +} diff --git a/packages/core/src/agent/models/features/index.ts b/packages/core/src/agent/models/features/index.ts new file mode 100644 index 0000000000..ad3b62896c --- /dev/null +++ b/packages/core/src/agent/models/features/index.ts @@ -0,0 +1,5 @@ +export * from './Feature' +export * from './FeatureQuery' +export * from './GoalCode' +export * from './GovernanceFramework' +export * from './Protocol' diff --git a/packages/core/src/agent/models/index.ts b/packages/core/src/agent/models/index.ts new file mode 100644 index 0000000000..1383036898 --- /dev/null +++ b/packages/core/src/agent/models/index.ts @@ -0,0 +1,4 @@ +export * from './features' +export * from './InboundMessageContext' +export * from './OutboundMessageContext' +export * from './OutboundMessageSendStatus' diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts new file mode 100644 index 0000000000..f7d43876ab --- /dev/null +++ b/packages/core/src/constants.ts @@ -0,0 +1,12 @@ +export const InjectionSymbols = { + MessagePickupRepository: Symbol('MessagePickupRepository'), + StorageService: Symbol('StorageService'), + Logger: Symbol('Logger'), + AgentContextProvider: Symbol('AgentContextProvider'), + AgentDependencies: Symbol('AgentDependencies'), + Stop$: Symbol('Stop$'), + FileSystem: Symbol('FileSystem'), + Wallet: Symbol('Wallet'), +} + +export const DID_COMM_TRANSPORT_QUEUE = 'didcomm:transport/queue' diff --git a/packages/core/src/crypto/JwsService.ts b/packages/core/src/crypto/JwsService.ts new file mode 100644 index 0000000000..52afc38e61 --- /dev/null +++ b/packages/core/src/crypto/JwsService.ts @@ -0,0 +1,287 @@ +import type { + Jws, + JwsDetachedFormat, + JwsFlattenedFormat, + JwsGeneralFormat, + JwsProtectedHeaderOptions, +} from './JwsTypes' +import type { Key } from './Key' +import type { Jwk } from './jose/jwk' +import type { JwkJson } from './jose/jwk/Jwk' +import type { AgentContext } from '../agent' +import type { Buffer } from '../utils' + +import { CredoError } from '../error' +import { injectable } from '../plugins' +import { isJsonObject, JsonEncoder, TypedArrayEncoder } from '../utils' +import { WalletError } from '../wallet/error' + +import { JWS_COMPACT_FORMAT_MATCHER } from './JwsTypes' +import { getJwkFromJson, getJwkFromKey } from './jose/jwk' +import { JwtPayload } from './jose/jwt' + +@injectable() +export class JwsService { + private async createJwsBase(agentContext: AgentContext, options: CreateJwsBaseOptions) { + const { jwk, alg } = options.protectedHeaderOptions + const keyJwk = getJwkFromKey(options.key) + + // Make sure the options.key and jwk from protectedHeader are the same. + if (jwk && (jwk.key.keyType !== options.key.keyType || !jwk.key.publicKey.equals(options.key.publicKey))) { + throw new CredoError(`Protected header JWK does not match key for signing.`) + } + + // Validate the options.key used for signing against the jws options + // We use keyJwk instead of jwk, as the user could also use kid instead of jwk + if (keyJwk && !keyJwk.supportsSignatureAlgorithm(alg)) { + throw new CredoError( + `alg '${alg}' is not a valid JWA signature algorithm for this jwk with keyType ${ + keyJwk.keyType + }. Supported algorithms are ${keyJwk.supportedSignatureAlgorithms.join(', ')}` + ) + } + + const payload = + options.payload instanceof JwtPayload ? JsonEncoder.toBuffer(options.payload.toJson()) : options.payload + + const base64Payload = TypedArrayEncoder.toBase64URL(payload) + const base64UrlProtectedHeader = JsonEncoder.toBase64URL(this.buildProtected(options.protectedHeaderOptions)) + + const signature = TypedArrayEncoder.toBase64URL( + await agentContext.wallet.sign({ + data: TypedArrayEncoder.fromString(`${base64UrlProtectedHeader}.${base64Payload}`), + key: options.key, + }) + ) + + return { + base64Payload, + base64UrlProtectedHeader, + signature, + } + } + + public async createJws( + agentContext: AgentContext, + { payload, key, header, protectedHeaderOptions }: CreateJwsOptions + ): Promise { + const { base64UrlProtectedHeader, signature, base64Payload } = await this.createJwsBase(agentContext, { + payload, + key, + protectedHeaderOptions, + }) + + return { + protected: base64UrlProtectedHeader, + signature, + header, + payload: base64Payload, + } + } + + /** + * @see {@link https://www.rfc-editor.org/rfc/rfc7515#section-3.1} + * */ + public async createJwsCompact( + agentContext: AgentContext, + { payload, key, protectedHeaderOptions }: CreateCompactJwsOptions + ): Promise { + const { base64Payload, base64UrlProtectedHeader, signature } = await this.createJwsBase(agentContext, { + payload, + key, + protectedHeaderOptions, + }) + return `${base64UrlProtectedHeader}.${base64Payload}.${signature}` + } + + /** + * Verify a JWS + */ + public async verifyJws(agentContext: AgentContext, { jws, jwkResolver }: VerifyJwsOptions): Promise { + let signatures: JwsDetachedFormat[] = [] + let payload: string + + if (typeof jws === 'string') { + if (!JWS_COMPACT_FORMAT_MATCHER.test(jws)) throw new CredoError(`Invalid JWS compact format for value '${jws}'.`) + + const [protectedHeader, _payload, signature] = jws.split('.') + + payload = _payload + signatures.push({ + header: {}, + protected: protectedHeader, + signature, + }) + } else if ('signatures' in jws) { + signatures = jws.signatures + payload = jws.payload + } else { + signatures.push(jws) + payload = jws.payload + } + + if (signatures.length === 0) { + throw new CredoError('Unable to verify JWS, no signatures present in JWS.') + } + + const jwsFlattened = { + signatures, + payload, + } satisfies JwsFlattenedFormat + + const signerKeys: Key[] = [] + for (const jws of signatures) { + const protectedJson = JsonEncoder.fromBase64(jws.protected) + + if (!isJsonObject(protectedJson)) { + throw new CredoError('Unable to verify JWS, protected header is not a valid JSON object.') + } + + if (!protectedJson.alg || typeof protectedJson.alg !== 'string') { + throw new CredoError('Unable to verify JWS, protected header alg is not provided or not a string.') + } + + const jwk = await this.jwkFromJws({ + jws, + payload, + protectedHeader: { + ...protectedJson, + alg: protectedJson.alg, + }, + jwkResolver, + }) + if (!jwk.supportsSignatureAlgorithm(protectedJson.alg)) { + throw new CredoError( + `alg '${protectedJson.alg}' is not a valid JWA signature algorithm for this jwk with keyType ${ + jwk.keyType + }. Supported algorithms are ${jwk.supportedSignatureAlgorithms.join(', ')}` + ) + } + + const data = TypedArrayEncoder.fromString(`${jws.protected}.${payload}`) + const signature = TypedArrayEncoder.fromBase64(jws.signature) + signerKeys.push(jwk.key) + + try { + const isValid = await agentContext.wallet.verify({ key: jwk.key, data, signature }) + + if (!isValid) { + return { + isValid: false, + signerKeys: [], + jws: jwsFlattened, + } + } + } catch (error) { + // WalletError probably means signature verification failed. Would be useful to add + // more specific error type in wallet.verify method + if (error instanceof WalletError) { + return { + isValid: false, + signerKeys: [], + jws: jwsFlattened, + } + } + + throw error + } + } + + return { isValid: true, signerKeys, jws: jwsFlattened } + } + + private buildProtected(options: JwsProtectedHeaderOptions) { + if (!options.jwk && !options.kid) { + throw new CredoError('Both JWK and kid are undefined. Please provide one or the other.') + } + if (options.jwk && options.kid) { + throw new CredoError('Both JWK and kid are provided. Please only provide one of the two.') + } + + return { + ...options, + alg: options.alg, + jwk: options.jwk?.toJson(), + kid: options.kid, + } + } + + private async jwkFromJws(options: { + jws: JwsDetachedFormat + protectedHeader: { alg: string; [key: string]: unknown } + payload: string + jwkResolver?: JwsJwkResolver + }): Promise { + const { protectedHeader, jwkResolver, jws, payload } = options + + if (protectedHeader.jwk && protectedHeader.kid) { + throw new CredoError('Both JWK and kid are defined in the protected header. Only one of the two is allowed.') + } + + // Jwk + if (protectedHeader.jwk) { + if (!isJsonObject(protectedHeader.jwk)) throw new CredoError('JWK is not a valid JSON object.') + return getJwkFromJson(protectedHeader.jwk as JwkJson) + } + + if (!jwkResolver) { + throw new CredoError(`jwkResolver is required when the JWS protected header does not contain a 'jwk' property.`) + } + + try { + const jwk = await jwkResolver({ + jws, + protectedHeader, + payload, + }) + + return jwk + } catch (error) { + throw new CredoError(`Error when resolving JWK for JWS in jwkResolver. ${error.message}`, { + cause: error, + }) + } + } +} + +export interface CreateJwsOptions { + key: Key + payload: Buffer | JwtPayload + header: Record + protectedHeaderOptions: JwsProtectedHeaderOptions +} + +type CreateJwsBaseOptions = Omit +type CreateCompactJwsOptions = Omit + +export interface VerifyJwsOptions { + jws: Jws + + /* + * Method that should return the JWK public key that was used + * to sign the JWS. + * + * This method is called by the JWS Service when it could not determine the public key. + * + * Currently the JWS Service can only determine the public key if the JWS protected header + * contains a `jwk` property. In all other cases, it's up to the caller to resolve the public + * key based on the JWS. + * + * A common use case is the `kid` property in the JWS protected header. Or determining the key + * base on the `iss` property in the JWT payload. + */ + jwkResolver?: JwsJwkResolver +} + +export type JwsJwkResolver = (options: { + jws: JwsDetachedFormat + payload: string + protectedHeader: { alg: string; jwk?: string; kid?: string; [key: string]: unknown } +}) => Promise | Jwk + +export interface VerifyJwsResult { + isValid: boolean + signerKeys: Key[] + + jws: JwsFlattenedFormat +} diff --git a/packages/core/src/crypto/JwsTypes.ts b/packages/core/src/crypto/JwsTypes.ts new file mode 100644 index 0000000000..7e258677a7 --- /dev/null +++ b/packages/core/src/crypto/JwsTypes.ts @@ -0,0 +1,64 @@ +import type { JwaSignatureAlgorithm } from './jose/jwa' +import type { Jwk } from './jose/jwk' + +export type Kid = string + +export interface JwsProtectedHeaderOptions { + alg: JwaSignatureAlgorithm | string + kid?: Kid + jwk?: Jwk + [key: string]: unknown +} + +export interface JwsGeneralFormat { + /** + * unprotected header + */ + header: Record + + /** + * Base64url encoded signature + */ + signature: string + + /** + * Base64url encoded protected header + */ + protected: string + + /** + * Base64url encoded payload + */ + payload: string +} + +export interface JwsFlattenedFormat { + /** + * Base64url encoded payload + */ + payload: string + + /** + * List of JWS signatures over the payload + * + * The items in this array do not contain the payload. + */ + signatures: JwsDetachedFormat[] +} + +/** + * JWS Compact Serialization + * + * @see https://tools.ietf.org/html/rfc7515#section-7.1 + */ +export type JwsCompactFormat = string + +export type Jws = JwsGeneralFormat | JwsFlattenedFormat | JwsCompactFormat + +// Detached JWS (does not contain payload) +export type JwsDetachedFormat = Omit +export interface JwsFlattenedDetachedFormat { + signatures: JwsDetachedFormat[] +} + +export const JWS_COMPACT_FORMAT_MATCHER = /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/ diff --git a/packages/core/src/crypto/Key.ts b/packages/core/src/crypto/Key.ts new file mode 100644 index 0000000000..ec44eead24 --- /dev/null +++ b/packages/core/src/crypto/Key.ts @@ -0,0 +1,71 @@ +import type { KeyType } from './KeyType' + +import { Buffer, MultiBaseEncoder, TypedArrayEncoder, VarintEncoder } from '../utils' + +import { isEncryptionSupportedForKeyType, isSigningSupportedForKeyType } from './keyUtils' +import { getKeyTypeByMultiCodecPrefix, getMultiCodecPrefixByKeyType } from './multiCodecKey' + +export class Key { + public readonly publicKey: Buffer + public readonly keyType: KeyType + + public constructor(publicKey: Uint8Array, keyType: KeyType) { + this.publicKey = Buffer.from(publicKey) + this.keyType = keyType + } + + public static fromPublicKey(publicKey: Uint8Array, keyType: KeyType) { + return new Key(Buffer.from(publicKey), keyType) + } + + public static fromPublicKeyBase58(publicKey: string, keyType: KeyType) { + const publicKeyBytes = TypedArrayEncoder.fromBase58(publicKey) + + return Key.fromPublicKey(publicKeyBytes, keyType) + } + + public static fromFingerprint(fingerprint: string) { + const { data } = MultiBaseEncoder.decode(fingerprint) + const [code, byteLength] = VarintEncoder.decode(data) + + const publicKey = Buffer.from(data.slice(byteLength)) + const keyType = getKeyTypeByMultiCodecPrefix(code) + + return new Key(publicKey, keyType) + } + + public get prefixedPublicKey() { + const multiCodecPrefix = getMultiCodecPrefixByKeyType(this.keyType) + + // Create Buffer with length of the prefix bytes, then use varint to fill the prefix bytes + const prefixBytes = VarintEncoder.encode(multiCodecPrefix) + + // Combine prefix with public key + return Buffer.concat([prefixBytes, this.publicKey]) + } + + public get fingerprint() { + return `z${TypedArrayEncoder.toBase58(this.prefixedPublicKey)}` + } + + public get publicKeyBase58() { + return TypedArrayEncoder.toBase58(this.publicKey) + } + + public get supportsEncrypting() { + return isEncryptionSupportedForKeyType(this.keyType) + } + + public get supportsSigning() { + return isSigningSupportedForKeyType(this.keyType) + } + + // We return an object structure based on the key, so that when this object is + // serialized to JSON it will be nicely formatted instead of the bytes printed + private toJSON() { + return { + keyType: this.keyType, + publicKeyBase58: this.publicKeyBase58, + } + } +} diff --git a/packages/core/src/crypto/KeyType.ts b/packages/core/src/crypto/KeyType.ts new file mode 100644 index 0000000000..cb85ab608d --- /dev/null +++ b/packages/core/src/crypto/KeyType.ts @@ -0,0 +1,11 @@ +export enum KeyType { + Ed25519 = 'ed25519', + Bls12381g1g2 = 'bls12381g1g2', + Bls12381g1 = 'bls12381g1', + Bls12381g2 = 'bls12381g2', + X25519 = 'x25519', + P256 = 'p256', + P384 = 'p384', + P521 = 'p521', + K256 = 'k256', +} diff --git a/packages/core/src/crypto/WalletKeyPair.ts b/packages/core/src/crypto/WalletKeyPair.ts new file mode 100644 index 0000000000..d853c80fcc --- /dev/null +++ b/packages/core/src/crypto/WalletKeyPair.ts @@ -0,0 +1,121 @@ +import type { Key } from './Key' +import type { LdKeyPairOptions } from '../modules/vc/data-integrity/models/LdKeyPair' +import type { Wallet } from '../wallet' + +import { VerificationMethod } from '../modules/dids' +import { getKeyFromVerificationMethod } from '../modules/dids/domain/key-type/keyDidMapping' +import { LdKeyPair } from '../modules/vc/data-integrity/models/LdKeyPair' +import { JsonTransformer } from '../utils' +import { MessageValidator } from '../utils/MessageValidator' +import { Buffer } from '../utils/buffer' + +interface WalletKeyPairOptions extends LdKeyPairOptions { + wallet: Wallet + key: Key +} + +export function createWalletKeyPairClass(wallet: Wallet) { + return class WalletKeyPair extends LdKeyPair { + public wallet: Wallet + public key: Key + public type: string + + public constructor(options: WalletKeyPairOptions) { + super(options) + this.wallet = options.wallet + this.key = options.key + this.type = options.key.keyType + } + + public static async generate(): Promise { + throw new Error('Not implemented') + } + + public fingerprint(): string { + throw new Error('Method not implemented.') + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public verifyFingerprint(fingerprint: string): boolean { + throw new Error('Method not implemented.') + } + + public static async from(verificationMethod: VerificationMethod): Promise { + const vMethod = JsonTransformer.fromJSON(verificationMethod, VerificationMethod) + MessageValidator.validateSync(vMethod) + const key = getKeyFromVerificationMethod(vMethod) + + return new WalletKeyPair({ + id: vMethod.id, + controller: vMethod.controller, + wallet: wallet, + key: key, + }) + } + + /** + * This method returns a wrapped wallet.sign method. The method is being wrapped so we can covert between Uint8Array and Buffer. This is to make it compatible with the external signature libraries. + */ + public signer(): { sign: (data: { data: Uint8Array | Uint8Array[] }) => Promise } { + // wrap function for conversion + const wrappedSign = async (data: { data: Uint8Array | Uint8Array[] }): Promise => { + let converted: Buffer | Buffer[] = [] + + // convert uint8array to buffer + if (Array.isArray(data.data)) { + converted = data.data.map((d) => Buffer.from(d)) + } else { + converted = Buffer.from(data.data) + } + + // sign + const result = await wallet.sign({ + data: converted, + key: this.key, + }) + + // convert result buffer to uint8array + return Uint8Array.from(result) + } + + return { + sign: wrappedSign.bind(this), + } + } + + /** + * This method returns a wrapped wallet.verify method. The method is being wrapped so we can covert between Uint8Array and Buffer. This is to make it compatible with the external signature libraries. + */ + public verifier(): { + verify: (data: { data: Uint8Array | Uint8Array[]; signature: Uint8Array }) => Promise + } { + const wrappedVerify = async (data: { + data: Uint8Array | Uint8Array[] + signature: Uint8Array + }): Promise => { + let converted: Buffer | Buffer[] = [] + + // convert uint8array to buffer + if (Array.isArray(data.data)) { + converted = data.data.map((d) => Buffer.from(d)) + } else { + converted = Buffer.from(data.data) + } + + // verify + return wallet.verify({ + data: converted, + signature: Buffer.from(data.signature), + key: this.key, + }) + } + return { + verify: wrappedVerify.bind(this), + } + } + + public get publicKeyBuffer(): Uint8Array { + return new Uint8Array(this.key.publicKey) + } + } +} diff --git a/packages/core/src/crypto/__tests__/JwsService.test.ts b/packages/core/src/crypto/__tests__/JwsService.test.ts new file mode 100644 index 0000000000..d6654aaa0d --- /dev/null +++ b/packages/core/src/crypto/__tests__/JwsService.test.ts @@ -0,0 +1,153 @@ +import type { AgentContext } from '../../agent' +import type { Key, Wallet } from '@credo-ts/core' + +import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' +import { getAgentConfig, getAgentContext } from '../../../tests/helpers' +import { DidKey } from '../../modules/dids' +import { JsonEncoder, TypedArrayEncoder } from '../../utils' +import { JwsService } from '../JwsService' +import { KeyType } from '../KeyType' +import { JwaSignatureAlgorithm } from '../jose/jwa' +import { getJwkFromKey } from '../jose/jwk' + +import * as didJwsz6Mkf from './__fixtures__/didJwsz6Mkf' +import * as didJwsz6Mkv from './__fixtures__/didJwsz6Mkv' +import * as didJwszDnaey from './__fixtures__/didJwszDnaey' + +describe('JwsService', () => { + let wallet: Wallet + let agentContext: AgentContext + let jwsService: JwsService + let didJwsz6MkfKey: Key + let didJwsz6MkvKey: Key + let didJwszDnaeyKey: Key + + beforeAll(async () => { + const config = getAgentConfig('JwsService') + wallet = new InMemoryWallet() + agentContext = getAgentContext({ + wallet, + }) + await wallet.createAndOpen(config.walletConfig) + + jwsService = new JwsService() + didJwsz6MkfKey = await wallet.createKey({ + privateKey: TypedArrayEncoder.fromString(didJwsz6Mkf.SEED), + keyType: KeyType.Ed25519, + }) + + didJwsz6MkvKey = await wallet.createKey({ + privateKey: TypedArrayEncoder.fromString(didJwsz6Mkv.SEED), + keyType: KeyType.Ed25519, + }) + + didJwszDnaeyKey = await wallet.createKey({ + privateKey: TypedArrayEncoder.fromString(didJwszDnaey.SEED), + keyType: KeyType.P256, + }) + }) + + afterAll(async () => { + await wallet.delete() + }) + + it('creates a jws for the payload using Ed25519 key', async () => { + const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON) + const kid = new DidKey(didJwsz6MkfKey).did + + const jws = await jwsService.createJws(agentContext, { + payload, + key: didJwsz6MkfKey, + header: { kid }, + protectedHeaderOptions: { + alg: JwaSignatureAlgorithm.EdDSA, + jwk: getJwkFromKey(didJwsz6MkfKey), + }, + }) + + expect(jws).toEqual(didJwsz6Mkf.JWS_JSON) + }) + + it('creates and verify a jws using ES256 alg and P-256 kty', async () => { + const payload = JsonEncoder.toBuffer(didJwszDnaey.DATA_JSON) + const kid = new DidKey(didJwszDnaeyKey).did + + const jws = await jwsService.createJws(agentContext, { + payload, + key: didJwszDnaeyKey, + header: { kid }, + protectedHeaderOptions: { + alg: JwaSignatureAlgorithm.ES256, + jwk: getJwkFromKey(didJwszDnaeyKey), + }, + }) + + expect(jws).toEqual(didJwszDnaey.JWS_JSON) + }) + + it('creates a compact jws', async () => { + const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON) + + const jws = await jwsService.createJwsCompact(agentContext, { + payload, + key: didJwsz6MkfKey, + protectedHeaderOptions: { + alg: JwaSignatureAlgorithm.EdDSA, + jwk: getJwkFromKey(didJwsz6MkfKey), + }, + }) + + expect(jws).toEqual( + `${didJwsz6Mkf.JWS_JSON.protected}.${TypedArrayEncoder.toBase64URL(payload)}.${didJwsz6Mkf.JWS_JSON.signature}` + ) + }) + + describe('verifyJws', () => { + it('returns true if the jws signature matches the payload', async () => { + const { isValid, signerKeys } = await jwsService.verifyJws(agentContext, { + jws: didJwsz6Mkf.JWS_JSON, + }) + + expect(isValid).toBe(true) + expect(signerKeys).toEqual([didJwsz6MkfKey]) + }) + + it('verifies a compact JWS', async () => { + const { isValid, signerKeys } = await jwsService.verifyJws(agentContext, { + jws: `${didJwsz6Mkf.JWS_JSON.protected}.${didJwsz6Mkf.JWS_JSON.payload}.${didJwsz6Mkf.JWS_JSON.signature}`, + }) + + expect(isValid).toBe(true) + expect(signerKeys).toEqual([didJwsz6MkfKey]) + }) + + it('returns all keys that signed the jws', async () => { + const { isValid, signerKeys } = await jwsService.verifyJws(agentContext, { + jws: { signatures: [didJwsz6Mkf.JWS_JSON, didJwsz6Mkv.JWS_JSON], payload: didJwsz6Mkf.JWS_JSON.payload }, + }) + + expect(isValid).toBe(true) + expect(signerKeys).toEqual([didJwsz6MkfKey, didJwsz6MkvKey]) + }) + + it('returns false if the jws signature does not match the payload', async () => { + const { isValid, signerKeys } = await jwsService.verifyJws(agentContext, { + jws: { + ...didJwsz6Mkf.JWS_JSON, + payload: JsonEncoder.toBase64URL({ ...didJwsz6Mkf, did: 'another_did' }), + }, + }) + + expect(isValid).toBe(false) + expect(signerKeys).toMatchObject([]) + }) + + it('throws an error if the jws signatures array does not contain a JWS', async () => { + await expect( + jwsService.verifyJws(agentContext, { + jws: { signatures: [], payload: '' }, + }) + ).rejects.toThrowError('Unable to verify JWS, no signatures present in JWS.') + }) + }) +}) diff --git a/packages/core/src/crypto/__tests__/__fixtures__/didJwsz6Mkf.ts b/packages/core/src/crypto/__tests__/__fixtures__/didJwsz6Mkf.ts new file mode 100644 index 0000000000..cb28c6f3de --- /dev/null +++ b/packages/core/src/crypto/__tests__/__fixtures__/didJwsz6Mkf.ts @@ -0,0 +1,29 @@ +import { JsonEncoder } from '../../../utils' + +export const SEED = '00000000000000000000000000000My2' +export const PUBLIC_KEY_BASE58 = 'kqa2HyagzfMAq42H5f9u3UMwnSBPQx2QfrSyXbUPxMn' + +export const DATA_JSON = { + did: 'did', + did_doc: { + '@context': 'https://w3id.org/did/v1', + service: [ + { + id: 'did:example:123456789abcdefghi#did-communication', + type: 'did-communication', + priority: 0, + recipientKeys: ['someVerkey'], + routingKeys: [], + serviceEndpoint: 'https://agent.example.com/', + }, + ], + }, +} + +export const JWS_JSON = { + header: { kid: 'did:key:z6MkfD6ccYE22Y9pHKtixeczk92MmMi2oJCP6gmNooZVKB9A' }, + protected: + 'eyJhbGciOiJFZERTQSIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IkN6cmtiNjQ1MzdrVUVGRkN5SXI4STgxUWJJRGk2MnNrbU41Rm41LU1zVkUifX0', + signature: 'OsDP4FM8792J9JlessA9IXv4YUYjIGcIAnPPrEJmgxYomMwDoH-h2DMAF5YF2VtsHHyhGN_0HryDjWSEAZdYBQ', + payload: JsonEncoder.toBase64URL(DATA_JSON), +} diff --git a/packages/core/src/crypto/__tests__/__fixtures__/didJwsz6Mkv.ts b/packages/core/src/crypto/__tests__/__fixtures__/didJwsz6Mkv.ts new file mode 100644 index 0000000000..39924e7f44 --- /dev/null +++ b/packages/core/src/crypto/__tests__/__fixtures__/didJwsz6Mkv.ts @@ -0,0 +1,29 @@ +import { JsonEncoder } from '../../../utils' + +export const SEED = '00000000000000000000000000000My1' +export const PUBLIC_KEY_BASE58 = 'GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa' + +export const DATA_JSON = { + did: 'did', + did_doc: { + '@context': 'https://w3id.org/did/v1', + service: [ + { + id: 'did:example:123456789abcdefghi#did-communication', + type: 'did-communication', + priority: 0, + recipientKeys: ['someVerkey'], + routingKeys: [], + serviceEndpoint: 'https://agent.example.com/', + }, + ], + }, +} + +export const JWS_JSON = { + protected: + 'eyJhbGciOiJFZERTQSIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IjZjWjJiWkttS2lVaUY5TUxLQ1Y4SUlZSUVzT0xIc0pHNXFCSjlTclFZQmsifX0', + signature: 'Js_ibaz24b4GRikbGPeLvRe5FyrcVR2aNVZSs26CLl3DCMJdPqUNRxVDNOD-dBnLs0HyTh6_mX9cG9vWEimtBA', + header: { kid: 'did:key:z6MkvBpZTRb7tjuUF5AkmhG1JDV928hZbg5KAQJcogvhz9ax' }, + payload: JsonEncoder.toBase64URL(DATA_JSON), +} diff --git a/packages/core/src/crypto/__tests__/__fixtures__/didJwszDnaey.ts b/packages/core/src/crypto/__tests__/__fixtures__/didJwszDnaey.ts new file mode 100644 index 0000000000..db509c849e --- /dev/null +++ b/packages/core/src/crypto/__tests__/__fixtures__/didJwszDnaey.ts @@ -0,0 +1,29 @@ +import { JsonEncoder } from '../../../utils' + +export const SEED = '00000000000000000000000000000My3' +export const PUBLIC_KEY_BASE58 = '2ARvZ9WjdavGb3db6i1TR3bNW8QxqfG9YPHAJJXCsRj2t' + +export const DATA_JSON = { + did: 'did', + did_doc: { + '@context': 'https://w3id.org/did/v1', + service: [ + { + id: 'did:example:123456789abcdefghi#did-communication', + type: 'did-communication', + priority: 0, + recipientKeys: ['someVerkey'], + routingKeys: [], + serviceEndpoint: 'https://agent.example.com/', + }, + ], + }, +} + +export const JWS_JSON = { + protected: + 'eyJhbGciOiJFUzI1NiIsImp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjZlR0VlUTdwZDB6UXZMdjdneERaN3FKSHpfY2gwWjlzM2JhUFBodmw0QlUiLCJ5IjoiTU8tS25aeUJ4bWo3THVxTU9yV0lNOG1SSzJrSWhXdF9LZF8yN2RvNXRmVSJ9fQ', + signature: '3L6N8rPDpxQ6nBWqyoLIcy_82HRWcNs_foPRnByErtJMAuTCm0fBN_-27xa9FBr-zh6Kumk8pOovXYP8kJrA3g', + header: { kid: 'did:key:zDnaeyQFrnYZJKPp3fnukaXZnhunkBE5yRdfgL8TjsLnnoW5z' }, + payload: JsonEncoder.toBase64URL(DATA_JSON), +} diff --git a/packages/core/src/crypto/index.ts b/packages/core/src/crypto/index.ts new file mode 100644 index 0000000000..d0f9343ca0 --- /dev/null +++ b/packages/core/src/crypto/index.ts @@ -0,0 +1,11 @@ +export { JwsService } from './JwsService' + +export { JwsDetachedFormat } from './JwsTypes' +export * from './keyUtils' + +export { KeyType } from './KeyType' +export { Key } from './Key' + +export * from './jose' + +export * from './signing-provider' diff --git a/packages/core/src/crypto/jose/index.ts b/packages/core/src/crypto/jose/index.ts new file mode 100644 index 0000000000..0dd0dedecf --- /dev/null +++ b/packages/core/src/crypto/jose/index.ts @@ -0,0 +1,3 @@ +export * from './jwa' +export * from './jwk' +export * from './jwt' diff --git a/packages/core/src/crypto/jose/jwa/alg.ts b/packages/core/src/crypto/jose/jwa/alg.ts new file mode 100644 index 0000000000..07e32d98da --- /dev/null +++ b/packages/core/src/crypto/jose/jwa/alg.ts @@ -0,0 +1,39 @@ +export enum JwaSignatureAlgorithm { + HS256 = 'HS256', + HS384 = 'HS384', + HS512 = 'HS512', + RS256 = 'RS256', + RS384 = 'RS384', + RS512 = 'RS512', + ES256 = 'ES256', + ES384 = 'ES384', + ES512 = 'ES512', + PS256 = 'PS256', + PS384 = 'PS384', + PS512 = 'PS512', + EdDSA = 'EdDSA', + ES256K = 'ES256K', + None = 'none', +} + +export enum JwaEncryptionAlgorithm { + RSA15 = 'RSA1_5', + RSAOAEP = 'RSA-OAEP', + RSAOAEP256 = 'RSA-OAEP-256', + A128KW = 'A128KW', + A192KW = 'A192KW', + A256KW = 'A256KW', + Dir = 'dir', + ECDHES = 'ECDH-ES', + ECDHESA128KW = 'ECDH-ES+A128KW', + ECDHESA192KW = 'ECDH-ES+A192KW', + ECDHESA256KW = 'ECDH-ES+A256KW', + A128GCMKW = 'A128GCMKW', + A192GCMKW = 'A192GCMKW', + A256GCMKW = 'A256GCMKW', + PBES2HS256A128KW = 'PBES2-HS256+A128KW', + PBES2HS384A192KW = 'PBES2-HS384+A192KW', + PBES2HS512A256KW = 'PBES2-HS512+A256KW', +} + +export type JwaAlgorithm = JwaSignatureAlgorithm | JwaEncryptionAlgorithm diff --git a/packages/core/src/crypto/jose/jwa/crv.ts b/packages/core/src/crypto/jose/jwa/crv.ts new file mode 100644 index 0000000000..d663c2ebb4 --- /dev/null +++ b/packages/core/src/crypto/jose/jwa/crv.ts @@ -0,0 +1,8 @@ +export enum JwaCurve { + P256 = 'P-256', + P384 = 'P-384', + P521 = 'P-521', + Ed25519 = 'Ed25519', + X25519 = 'X25519', + Secp256k1 = 'secp256k1', +} diff --git a/packages/core/src/crypto/jose/jwa/index.ts b/packages/core/src/crypto/jose/jwa/index.ts new file mode 100644 index 0000000000..9aa115a084 --- /dev/null +++ b/packages/core/src/crypto/jose/jwa/index.ts @@ -0,0 +1,3 @@ +export { JwaAlgorithm, JwaEncryptionAlgorithm, JwaSignatureAlgorithm } from './alg' +export { JwaKeyType } from './kty' +export { JwaCurve } from './crv' diff --git a/packages/core/src/crypto/jose/jwa/kty.ts b/packages/core/src/crypto/jose/jwa/kty.ts new file mode 100644 index 0000000000..0601fb7b02 --- /dev/null +++ b/packages/core/src/crypto/jose/jwa/kty.ts @@ -0,0 +1,6 @@ +export enum JwaKeyType { + EC = 'EC', + RSA = 'RSA', + oct = 'oct', + OKP = 'OKP', +} diff --git a/packages/core/src/crypto/jose/jwk/Ed25519Jwk.ts b/packages/core/src/crypto/jose/jwk/Ed25519Jwk.ts new file mode 100644 index 0000000000..30966cea64 --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/Ed25519Jwk.ts @@ -0,0 +1,92 @@ +import type { JwkJson } from './Jwk' +import type { Buffer } from '../../../utils' +import type { JwaEncryptionAlgorithm } from '../jwa/alg' + +import { TypedArrayEncoder } from '../../../utils' +import { KeyType } from '../../KeyType' +import { JwaCurve, JwaKeyType } from '../jwa' +import { JwaSignatureAlgorithm } from '../jwa/alg' + +import { Jwk } from './Jwk' +import { hasKty, hasCrv, hasX, hasValidUse } from './validate' + +export class Ed25519Jwk extends Jwk { + public static readonly supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] = [] + public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [JwaSignatureAlgorithm.EdDSA] + public static readonly keyType = KeyType.Ed25519 + + public readonly x: string + + public constructor({ x }: { x: string }) { + super() + + this.x = x + } + + public get kty() { + return JwaKeyType.OKP as const + } + + public get crv() { + return JwaCurve.Ed25519 as const + } + + public get publicKey() { + return TypedArrayEncoder.fromBase64(this.x) + } + + public get keyType() { + return Ed25519Jwk.keyType + } + + public get supportedEncryptionAlgorithms() { + return Ed25519Jwk.supportedEncryptionAlgorithms + } + + public get supportedSignatureAlgorithms() { + return Ed25519Jwk.supportedSignatureAlgorithms + } + + public toJson() { + return { + ...super.toJson(), + crv: this.crv, + x: this.x, + } as Ed25519JwkJson + } + + public static fromJson(jwkJson: JwkJson) { + if (!isValidEd25519JwkPublicKey(jwkJson)) { + throw new Error("Invalid 'Ed25519' JWK.") + } + + return new Ed25519Jwk({ + x: jwkJson.x, + }) + } + + public static fromPublicKey(publicKey: Buffer) { + return new Ed25519Jwk({ + x: TypedArrayEncoder.toBase64URL(publicKey), + }) + } +} + +export interface Ed25519JwkJson extends JwkJson { + kty: JwaKeyType.OKP + crv: JwaCurve.Ed25519 + x: string + use?: 'sig' +} + +function isValidEd25519JwkPublicKey(jwk: JwkJson): jwk is Ed25519JwkJson { + return ( + hasKty(jwk, JwaKeyType.OKP) && + hasCrv(jwk, JwaCurve.Ed25519) && + hasX(jwk) && + hasValidUse(jwk, { + supportsEncrypting: false, + supportsSigning: true, + }) + ) +} diff --git a/packages/core/src/crypto/jose/jwk/Jwk.ts b/packages/core/src/crypto/jose/jwk/Jwk.ts new file mode 100644 index 0000000000..a38b384ba9 --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/Jwk.ts @@ -0,0 +1,45 @@ +import type { Buffer } from '../../../utils' +import type { KeyType } from '../../KeyType' +import type { JwaKeyType, JwaEncryptionAlgorithm, JwaSignatureAlgorithm } from '../jwa' + +import { Key } from '../../Key' + +export interface JwkJson { + kty: string + use?: string + [key: string]: unknown +} + +export abstract class Jwk { + public abstract publicKey: Buffer + public abstract supportedSignatureAlgorithms: JwaSignatureAlgorithm[] + public abstract supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] + + /** + * keyType as used by the rest of the framework, can be used in the + * `Wallet`, `Key` and other classes. + */ + public abstract keyType: KeyType + + /** + * key type as defined in [JWA Specification](https://tools.ietf.org/html/rfc7518#section-6.1) + */ + public abstract kty: JwaKeyType + public use?: string + + public toJson(): JwkJson { + return { use: this.use, kty: this.kty } + } + + public get key() { + return new Key(this.publicKey, this.keyType) + } + + public supportsSignatureAlgorithm(algorithm: JwaSignatureAlgorithm | string) { + return this.supportedSignatureAlgorithms.includes(algorithm as JwaSignatureAlgorithm) + } + + public supportsEncryptionAlgorithm(algorithm: JwaEncryptionAlgorithm | string) { + return this.supportedEncryptionAlgorithms.includes(algorithm as JwaEncryptionAlgorithm) + } +} diff --git a/packages/core/src/crypto/jose/jwk/K256Jwk.ts b/packages/core/src/crypto/jose/jwk/K256Jwk.ts new file mode 100644 index 0000000000..914b940d86 --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/K256Jwk.ts @@ -0,0 +1,112 @@ +import type { JwkJson } from './Jwk' +import type { JwaEncryptionAlgorithm } from '../jwa/alg' + +import { TypedArrayEncoder, Buffer } from '../../../utils' +import { KeyType } from '../../KeyType' +import { JwaCurve, JwaKeyType } from '../jwa' +import { JwaSignatureAlgorithm } from '../jwa/alg' + +import { Jwk } from './Jwk' +import { compress, expand } from './ecCompression' +import { hasKty, hasCrv, hasX, hasY, hasValidUse } from './validate' + +export class K256Jwk extends Jwk { + public static readonly supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] = [] + public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [JwaSignatureAlgorithm.ES256K] + public static readonly keyType = KeyType.K256 + + public readonly x: string + public readonly y: string + + public constructor({ x, y }: { x: string; y: string }) { + super() + + this.x = x + this.y = y + } + + public get kty() { + return JwaKeyType.EC as const + } + + public get crv() { + return JwaCurve.Secp256k1 as const + } + + /** + * Returns the public key of the K-256 JWK. + * + * NOTE: this is the compressed variant. We still need to add support for the + * uncompressed variant. + */ + public get publicKey() { + const publicKeyBuffer = Buffer.concat([TypedArrayEncoder.fromBase64(this.x), TypedArrayEncoder.fromBase64(this.y)]) + const compressedPublicKey = compress(publicKeyBuffer) + + return Buffer.from(compressedPublicKey) + } + + public get keyType() { + return K256Jwk.keyType + } + + public get supportedEncryptionAlgorithms() { + return K256Jwk.supportedEncryptionAlgorithms + } + + public get supportedSignatureAlgorithms() { + return K256Jwk.supportedSignatureAlgorithms + } + + public toJson() { + return { + ...super.toJson(), + crv: this.crv, + x: this.x, + y: this.y, + } as K256JwkJson + } + + public static fromJson(jwkJson: JwkJson) { + if (!isValidK256JwkPublicKey(jwkJson)) { + throw new Error("Invalid 'K-256' JWK.") + } + + return new K256Jwk({ + x: jwkJson.x, + y: jwkJson.y, + }) + } + + public static fromPublicKey(publicKey: Buffer) { + const expanded = expand(publicKey, JwaCurve.Secp256k1) + const x = expanded.slice(0, expanded.length / 2) + const y = expanded.slice(expanded.length / 2) + + return new K256Jwk({ + x: TypedArrayEncoder.toBase64URL(x), + y: TypedArrayEncoder.toBase64URL(y), + }) + } +} + +export interface K256JwkJson extends JwkJson { + kty: JwaKeyType.EC + crv: JwaCurve.Secp256k1 + x: string + y: string + use?: 'sig' | 'enc' +} + +export function isValidK256JwkPublicKey(jwk: JwkJson): jwk is K256JwkJson { + return ( + hasKty(jwk, JwaKeyType.EC) && + hasCrv(jwk, JwaCurve.Secp256k1) && + hasX(jwk) && + hasY(jwk) && + hasValidUse(jwk, { + supportsEncrypting: true, + supportsSigning: true, + }) + ) +} diff --git a/packages/core/src/crypto/jose/jwk/P256Jwk.ts b/packages/core/src/crypto/jose/jwk/P256Jwk.ts new file mode 100644 index 0000000000..68427ad9d7 --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/P256Jwk.ts @@ -0,0 +1,112 @@ +import type { JwkJson } from './Jwk' +import type { JwaEncryptionAlgorithm } from '../jwa/alg' + +import { TypedArrayEncoder, Buffer } from '../../../utils' +import { KeyType } from '../../KeyType' +import { JwaCurve, JwaKeyType } from '../jwa' +import { JwaSignatureAlgorithm } from '../jwa/alg' + +import { Jwk } from './Jwk' +import { compress, expand } from './ecCompression' +import { hasKty, hasCrv, hasX, hasY, hasValidUse } from './validate' + +export class P256Jwk extends Jwk { + public static readonly supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] = [] + public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [JwaSignatureAlgorithm.ES256] + public static readonly keyType = KeyType.P256 + + public readonly x: string + public readonly y: string + + public constructor({ x, y }: { x: string; y: string }) { + super() + + this.x = x + this.y = y + } + + public get kty() { + return JwaKeyType.EC as const + } + + public get crv() { + return JwaCurve.P256 as const + } + + /** + * Returns the public key of the P-256 JWK. + * + * NOTE: this is the compressed variant. We still need to add support for the + * uncompressed variant. + */ + public get publicKey() { + const publicKeyBuffer = Buffer.concat([TypedArrayEncoder.fromBase64(this.x), TypedArrayEncoder.fromBase64(this.y)]) + const compressedPublicKey = compress(publicKeyBuffer) + + return Buffer.from(compressedPublicKey) + } + + public get keyType() { + return P256Jwk.keyType + } + + public get supportedEncryptionAlgorithms() { + return P256Jwk.supportedEncryptionAlgorithms + } + + public get supportedSignatureAlgorithms() { + return P256Jwk.supportedSignatureAlgorithms + } + + public toJson() { + return { + ...super.toJson(), + crv: this.crv, + x: this.x, + y: this.y, + } as P256JwkJson + } + + public static fromJson(jwkJson: JwkJson) { + if (!isValidP256JwkPublicKey(jwkJson)) { + throw new Error("Invalid 'P-256' JWK.") + } + + return new P256Jwk({ + x: jwkJson.x, + y: jwkJson.y, + }) + } + + public static fromPublicKey(publicKey: Buffer) { + const expanded = expand(publicKey, JwaCurve.P256) + const x = expanded.slice(0, expanded.length / 2) + const y = expanded.slice(expanded.length / 2) + + return new P256Jwk({ + x: TypedArrayEncoder.toBase64URL(x), + y: TypedArrayEncoder.toBase64URL(y), + }) + } +} + +export interface P256JwkJson extends JwkJson { + kty: JwaKeyType.EC + crv: JwaCurve.P256 + x: string + y: string + use?: 'sig' | 'enc' +} + +export function isValidP256JwkPublicKey(jwk: JwkJson): jwk is P256JwkJson { + return ( + hasKty(jwk, JwaKeyType.EC) && + hasCrv(jwk, JwaCurve.P256) && + hasX(jwk) && + hasY(jwk) && + hasValidUse(jwk, { + supportsEncrypting: true, + supportsSigning: true, + }) + ) +} diff --git a/packages/core/src/crypto/jose/jwk/P384Jwk.ts b/packages/core/src/crypto/jose/jwk/P384Jwk.ts new file mode 100644 index 0000000000..b6f30c15c5 --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/P384Jwk.ts @@ -0,0 +1,112 @@ +import type { JwkJson } from './Jwk' +import type { JwaEncryptionAlgorithm } from '../jwa/alg' + +import { TypedArrayEncoder, Buffer } from '../../../utils' +import { KeyType } from '../../KeyType' +import { JwaCurve, JwaKeyType } from '../jwa' +import { JwaSignatureAlgorithm } from '../jwa/alg' + +import { Jwk } from './Jwk' +import { compress, expand } from './ecCompression' +import { hasKty, hasCrv, hasX, hasY, hasValidUse } from './validate' + +export class P384Jwk extends Jwk { + public static readonly supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] = [] + public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [JwaSignatureAlgorithm.ES384] + public static readonly keyType = KeyType.P384 + + public readonly x: string + public readonly y: string + + public constructor({ x, y }: { x: string; y: string }) { + super() + + this.x = x + this.y = y + } + + public get kty() { + return JwaKeyType.EC as const + } + + public get crv() { + return JwaCurve.P384 as const + } + + public get keyType() { + return P384Jwk.keyType + } + + public get supportedEncryptionAlgorithms() { + return P384Jwk.supportedEncryptionAlgorithms + } + + public get supportedSignatureAlgorithms() { + return P384Jwk.supportedSignatureAlgorithms + } + + /** + * Returns the public key of the P-384 JWK. + * + * NOTE: this is the compressed variant. We still need to add support for the + * uncompressed variant. + */ + public get publicKey() { + const publicKeyBuffer = Buffer.concat([TypedArrayEncoder.fromBase64(this.x), TypedArrayEncoder.fromBase64(this.y)]) + const compressedPublicKey = compress(publicKeyBuffer) + + return Buffer.from(compressedPublicKey) + } + + public toJson() { + return { + ...super.toJson(), + crv: this.crv, + x: this.x, + y: this.y, + } as P384JwkJson + } + + public static fromJson(jwk: JwkJson) { + if (!isValidP384JwkPublicKey(jwk)) { + throw new Error("Invalid 'P-384' JWK.") + } + + return new P384Jwk({ + x: jwk.x, + y: jwk.y, + }) + } + + public static fromPublicKey(publicKey: Buffer) { + const expanded = expand(publicKey, JwaCurve.P384) + const x = expanded.slice(0, expanded.length / 2) + const y = expanded.slice(expanded.length / 2) + + return new P384Jwk({ + x: TypedArrayEncoder.toBase64URL(x), + y: TypedArrayEncoder.toBase64URL(y), + }) + } +} + +export interface P384JwkJson extends JwkJson { + kty: JwaKeyType.EC + crv: JwaCurve.P384 + x: string + y: string + use?: 'sig' | 'enc' +} + +export function isValidP384JwkPublicKey(jwk: JwkJson): jwk is P384JwkJson { + return ( + hasKty(jwk, JwaKeyType.EC) && + hasCrv(jwk, JwaCurve.P384) && + hasX(jwk) && + hasY(jwk) && + hasValidUse(jwk, { + supportsEncrypting: true, + supportsSigning: true, + }) + ) +} diff --git a/packages/core/src/crypto/jose/jwk/P521Jwk.ts b/packages/core/src/crypto/jose/jwk/P521Jwk.ts new file mode 100644 index 0000000000..5b7998eff7 --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/P521Jwk.ts @@ -0,0 +1,112 @@ +import type { JwkJson } from './Jwk' +import type { JwaEncryptionAlgorithm } from '../jwa/alg' + +import { TypedArrayEncoder, Buffer } from '../../../utils' +import { KeyType } from '../../KeyType' +import { JwaCurve, JwaKeyType } from '../jwa' +import { JwaSignatureAlgorithm } from '../jwa/alg' + +import { Jwk } from './Jwk' +import { compress, expand } from './ecCompression' +import { hasKty, hasCrv, hasX, hasY, hasValidUse } from './validate' + +export class P521Jwk extends Jwk { + public static readonly supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] = [] + public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [JwaSignatureAlgorithm.ES512] + public static readonly keyType = KeyType.P521 + + public readonly x: string + public readonly y: string + + public constructor({ x, y }: { x: string; y: string }) { + super() + + this.x = x + this.y = y + } + + public get kty() { + return JwaKeyType.EC as const + } + + public get crv() { + return JwaCurve.P521 as const + } + + public get keyType() { + return P521Jwk.keyType + } + + public get supportedEncryptionAlgorithms() { + return P521Jwk.supportedEncryptionAlgorithms + } + + public get supportedSignatureAlgorithms() { + return P521Jwk.supportedSignatureAlgorithms + } + + /** + * Returns the public key of the P-521 JWK. + * + * NOTE: this is the compressed variant. We still need to add support for the + * uncompressed variant. + */ + public get publicKey() { + const publicKeyBuffer = Buffer.concat([TypedArrayEncoder.fromBase64(this.x), TypedArrayEncoder.fromBase64(this.y)]) + const compressedPublicKey = compress(publicKeyBuffer) + + return Buffer.from(compressedPublicKey) + } + + public toJson() { + return { + ...super.toJson(), + crv: this.crv, + x: this.x, + y: this.y, + } as P521JwkJson + } + + public static fromJson(jwk: JwkJson) { + if (!isValidP521JwkPublicKey(jwk)) { + throw new Error("Invalid 'P-521' JWK.") + } + + return new P521Jwk({ + x: jwk.x, + y: jwk.y, + }) + } + + public static fromPublicKey(publicKey: Buffer) { + const expanded = expand(publicKey, JwaCurve.P521) + const x = expanded.slice(0, expanded.length / 2) + const y = expanded.slice(expanded.length / 2) + + return new P521Jwk({ + x: TypedArrayEncoder.toBase64URL(x), + y: TypedArrayEncoder.toBase64URL(y), + }) + } +} + +export interface P521JwkJson extends JwkJson { + kty: JwaKeyType.EC + crv: JwaCurve.P521 + x: string + y: string + use?: 'sig' | 'enc' +} + +export function isValidP521JwkPublicKey(jwk: JwkJson): jwk is P521JwkJson { + return ( + hasKty(jwk, JwaKeyType.EC) && + hasCrv(jwk, JwaCurve.P521) && + hasX(jwk) && + hasY(jwk) && + hasValidUse(jwk, { + supportsEncrypting: true, + supportsSigning: true, + }) + ) +} diff --git a/packages/core/src/crypto/jose/jwk/X25519Jwk.ts b/packages/core/src/crypto/jose/jwk/X25519Jwk.ts new file mode 100644 index 0000000000..6d1ada04ce --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/X25519Jwk.ts @@ -0,0 +1,96 @@ +import type { JwkJson } from './Jwk' +import type { Buffer } from '../../../utils' +import type { JwaSignatureAlgorithm } from '../jwa' + +import { TypedArrayEncoder } from '../../../utils' +import { KeyType } from '../../KeyType' +import { JwaCurve, JwaKeyType, JwaEncryptionAlgorithm } from '../jwa' + +import { Jwk } from './Jwk' +import { hasCrv, hasKty, hasValidUse, hasX } from './validate' + +export class X25519Jwk extends Jwk { + public static readonly supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] = [ + JwaEncryptionAlgorithm.ECDHESA128KW, + JwaEncryptionAlgorithm.ECDHESA192KW, + JwaEncryptionAlgorithm.ECDHESA256KW, + JwaEncryptionAlgorithm.ECDHES, + ] + public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [] + public static readonly keyType = KeyType.X25519 + + public readonly x: string + + public constructor({ x }: { x: string }) { + super() + + this.x = x + } + + public get kty() { + return JwaKeyType.OKP as const + } + + public get crv() { + return JwaCurve.X25519 as const + } + + public get keyType() { + return X25519Jwk.keyType + } + + public get supportedEncryptionAlgorithms() { + return X25519Jwk.supportedEncryptionAlgorithms + } + + public get supportedSignatureAlgorithms() { + return X25519Jwk.supportedSignatureAlgorithms + } + + public get publicKey() { + return TypedArrayEncoder.fromBase64(this.x) + } + + public toJson() { + return { + ...super.toJson(), + crv: this.crv, + x: this.x, + } as X25519JwkJson + } + + public static fromJson(jwk: JwkJson) { + if (!isValidX25519JwkPublicKey(jwk)) { + throw new Error("Invalid 'X25519' JWK.") + } + + return new X25519Jwk({ + x: jwk.x, + }) + } + + public static fromPublicKey(publicKey: Buffer) { + return new X25519Jwk({ + x: TypedArrayEncoder.toBase64URL(publicKey), + }) + } +} + +export interface X25519JwkJson extends JwkJson { + kty: JwaKeyType.OKP + crv: JwaCurve.X25519 + x: string + use?: 'enc' +} + +function isValidX25519JwkPublicKey(jwk: JwkJson): jwk is X25519JwkJson { + return ( + hasKty(jwk, JwaKeyType.OKP) && + hasCrv(jwk, JwaCurve.X25519) && + hasX(jwk) && + hasValidUse(jwk, { + supportsEncrypting: true, + supportsSigning: false, + }) + ) +} diff --git a/packages/core/src/crypto/jose/jwk/__tests__/Ed25519Jwk.test.ts b/packages/core/src/crypto/jose/jwk/__tests__/Ed25519Jwk.test.ts new file mode 100644 index 0000000000..a2f07ecc7f --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/__tests__/Ed25519Jwk.test.ts @@ -0,0 +1,36 @@ +import { TypedArrayEncoder } from '../../../../utils' +import { KeyType } from '../../../KeyType' +import { Ed25519Jwk } from '../Ed25519Jwk' + +const jwkJson = { + kty: 'OKP', + crv: 'Ed25519', + x: 'O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik', +} + +describe('Ed25519JWk', () => { + test('has correct properties', () => { + const jwk = new Ed25519Jwk({ x: jwkJson.x }) + + expect(jwk.kty).toEqual('OKP') + expect(jwk.crv).toEqual('Ed25519') + expect(jwk.keyType).toEqual(KeyType.Ed25519) + expect(jwk.publicKey).toEqual(TypedArrayEncoder.fromBase64(jwkJson.x)) + expect(jwk.supportedEncryptionAlgorithms).toEqual([]) + expect(jwk.supportedSignatureAlgorithms).toEqual(['EdDSA']) + expect(jwk.key.keyType).toEqual(KeyType.Ed25519) + expect(jwk.toJson()).toEqual(jwkJson) + }) + + test('fromJson', () => { + const jwk = Ed25519Jwk.fromJson(jwkJson) + expect(jwk.x).toEqual(jwkJson.x) + + expect(() => Ed25519Jwk.fromJson({ ...jwkJson, kty: 'test' })).toThrowError("Invalid 'Ed25519' JWK.") + }) + + test('fromPublicKey', () => { + const jwk = Ed25519Jwk.fromPublicKey(TypedArrayEncoder.fromBase64(jwkJson.x)) + expect(jwk.x).toEqual(jwkJson.x) + }) +}) diff --git a/packages/core/src/crypto/jose/jwk/__tests__/P_256Jwk.test.ts b/packages/core/src/crypto/jose/jwk/__tests__/P_256Jwk.test.ts new file mode 100644 index 0000000000..1250d031d9 --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/__tests__/P_256Jwk.test.ts @@ -0,0 +1,52 @@ +import { TypedArrayEncoder, Buffer } from '../../../../utils' +import { KeyType } from '../../../KeyType' +import { P256Jwk } from '../P256Jwk' +import { compress } from '../ecCompression' + +const jwkJson = { + kty: 'EC', + crv: 'P-256', + x: 'igrFmi0whuihKnj9R3Om1SoMph72wUGeFaBbzG2vzns', + y: 'efsX5b10x8yjyrj4ny3pGfLcY7Xby1KzgqOdqnsrJIM', +} + +describe('P_256JWk', () => { + test('has correct properties', () => { + const jwk = new P256Jwk({ x: jwkJson.x, y: jwkJson.y }) + + expect(jwk.kty).toEqual('EC') + expect(jwk.crv).toEqual('P-256') + expect(jwk.keyType).toEqual(KeyType.P256) + + const publicKeyBuffer = Buffer.concat([ + TypedArrayEncoder.fromBase64(jwkJson.x), + TypedArrayEncoder.fromBase64(jwkJson.y), + ]) + const compressedPublicKey = Buffer.from(compress(publicKeyBuffer)) + expect(jwk.publicKey).toEqual(compressedPublicKey) + expect(jwk.supportedEncryptionAlgorithms).toEqual([]) + expect(jwk.supportedSignatureAlgorithms).toEqual(['ES256']) + expect(jwk.key.keyType).toEqual(KeyType.P256) + expect(jwk.toJson()).toEqual(jwkJson) + }) + + test('fromJson', () => { + const jwk = P256Jwk.fromJson(jwkJson) + expect(jwk.x).toEqual(jwkJson.x) + expect(jwk.y).toEqual(jwkJson.y) + + expect(() => P256Jwk.fromJson({ ...jwkJson, kty: 'test' })).toThrowError("Invalid 'P-256' JWK.") + }) + + test('fromPublicKey', () => { + const publicKeyBuffer = Buffer.concat([ + TypedArrayEncoder.fromBase64(jwkJson.x), + TypedArrayEncoder.fromBase64(jwkJson.y), + ]) + const compressedPublicKey = Buffer.from(compress(publicKeyBuffer)) + + const jwk = P256Jwk.fromPublicKey(compressedPublicKey) + expect(jwk.x).toEqual(jwkJson.x) + expect(jwk.y).toEqual(jwkJson.y) + }) +}) diff --git a/packages/core/src/crypto/jose/jwk/__tests__/P_384Jwk.test.ts b/packages/core/src/crypto/jose/jwk/__tests__/P_384Jwk.test.ts new file mode 100644 index 0000000000..0f409ed878 --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/__tests__/P_384Jwk.test.ts @@ -0,0 +1,51 @@ +import { TypedArrayEncoder, Buffer } from '../../../../utils' +import { KeyType } from '../../../KeyType' +import { P384Jwk } from '../P384Jwk' +import { compress } from '../ecCompression' + +const jwkJson = { + kty: 'EC', + crv: 'P-384', + x: 'lInTxl8fjLKp_UCrxI0WDklahi-7-_6JbtiHjiRvMvhedhKVdHBfi2HCY8t_QJyc', + y: 'y6N1IC-2mXxHreETBW7K3mBcw0qGr3CWHCs-yl09yCQRLcyfGv7XhqAngHOu51Zv', +} + +describe('P_384JWk', () => { + test('has correct properties', () => { + const jwk = new P384Jwk({ x: jwkJson.x, y: jwkJson.y }) + + expect(jwk.kty).toEqual('EC') + expect(jwk.crv).toEqual('P-384') + expect(jwk.keyType).toEqual(KeyType.P384) + const publicKeyBuffer = Buffer.concat([ + TypedArrayEncoder.fromBase64(jwkJson.x), + TypedArrayEncoder.fromBase64(jwkJson.y), + ]) + const compressedPublicKey = Buffer.from(compress(publicKeyBuffer)) + expect(jwk.publicKey).toEqual(compressedPublicKey) + expect(jwk.supportedEncryptionAlgorithms).toEqual([]) + expect(jwk.supportedSignatureAlgorithms).toEqual(['ES384']) + expect(jwk.key.keyType).toEqual(KeyType.P384) + expect(jwk.toJson()).toEqual(jwkJson) + }) + + test('fromJson', () => { + const jwk = P384Jwk.fromJson(jwkJson) + expect(jwk.x).toEqual(jwkJson.x) + expect(jwk.y).toEqual(jwkJson.y) + + expect(() => P384Jwk.fromJson({ ...jwkJson, kty: 'test' })).toThrowError("Invalid 'P-384' JWK.") + }) + + test('fromPublicKey', () => { + const publicKeyBuffer = Buffer.concat([ + TypedArrayEncoder.fromBase64(jwkJson.x), + TypedArrayEncoder.fromBase64(jwkJson.y), + ]) + const compressedPublicKey = Buffer.from(compress(publicKeyBuffer)) + + const jwk = P384Jwk.fromPublicKey(compressedPublicKey) + expect(jwk.x).toEqual(jwkJson.x) + expect(jwk.y).toEqual(jwkJson.y) + }) +}) diff --git a/packages/core/src/crypto/jose/jwk/__tests__/P_521Jwk.test.ts b/packages/core/src/crypto/jose/jwk/__tests__/P_521Jwk.test.ts new file mode 100644 index 0000000000..662fb5ee7b --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/__tests__/P_521Jwk.test.ts @@ -0,0 +1,51 @@ +import { TypedArrayEncoder, Buffer } from '../../../../utils' +import { KeyType } from '../../../KeyType' +import { P521Jwk } from '../P521Jwk' +import { compress } from '../ecCompression' + +const jwkJson = { + kty: 'EC', + crv: 'P-521', + x: 'ASUHPMyichQ0QbHZ9ofNx_l4y7luncn5feKLo3OpJ2nSbZoC7mffolj5uy7s6KSKXFmnNWxGJ42IOrjZ47qqwqyS', + y: 'AW9ziIC4ZQQVSNmLlp59yYKrjRY0_VqO-GOIYQ9tYpPraBKUloEId6cI_vynCzlZWZtWpgOM3HPhYEgawQ703RjC', +} + +describe('P_521JWk', () => { + test('has correct properties', () => { + const jwk = new P521Jwk({ x: jwkJson.x, y: jwkJson.y }) + + expect(jwk.kty).toEqual('EC') + expect(jwk.crv).toEqual('P-521') + expect(jwk.keyType).toEqual(KeyType.P521) + const publicKeyBuffer = Buffer.concat([ + TypedArrayEncoder.fromBase64(jwkJson.x), + TypedArrayEncoder.fromBase64(jwkJson.y), + ]) + const compressedPublicKey = Buffer.from(compress(publicKeyBuffer)) + expect(jwk.publicKey).toEqual(compressedPublicKey) + expect(jwk.supportedEncryptionAlgorithms).toEqual([]) + expect(jwk.supportedSignatureAlgorithms).toEqual(['ES512']) + expect(jwk.key.keyType).toEqual(KeyType.P521) + expect(jwk.toJson()).toEqual(jwkJson) + }) + + test('fromJson', () => { + const jwk = P521Jwk.fromJson(jwkJson) + expect(jwk.x).toEqual(jwkJson.x) + expect(jwk.y).toEqual(jwkJson.y) + + expect(() => P521Jwk.fromJson({ ...jwkJson, kty: 'test' })).toThrowError("Invalid 'P-521' JWK.") + }) + + test('fromPublicKey', () => { + const publicKeyBuffer = Buffer.concat([ + TypedArrayEncoder.fromBase64(jwkJson.x), + TypedArrayEncoder.fromBase64(jwkJson.y), + ]) + const compressedPublicKey = Buffer.from(compress(publicKeyBuffer)) + + const jwk = P521Jwk.fromPublicKey(compressedPublicKey) + expect(jwk.x).toEqual(jwkJson.x) + expect(jwk.y).toEqual(jwkJson.y) + }) +}) diff --git a/packages/core/src/crypto/jose/jwk/__tests__/X25519Jwk.test.ts b/packages/core/src/crypto/jose/jwk/__tests__/X25519Jwk.test.ts new file mode 100644 index 0000000000..138e3f59b4 --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/__tests__/X25519Jwk.test.ts @@ -0,0 +1,36 @@ +import { TypedArrayEncoder } from '../../../../utils' +import { KeyType } from '../../../KeyType' +import { X25519Jwk } from '../X25519Jwk' + +const jwkJson = { + kty: 'OKP', + crv: 'X25519', + x: 'W_Vcc7guviK-gPNDBmevVw-uJVamQV5rMNQGUwCqlH0', +} + +describe('X25519JWk', () => { + test('has correct properties', () => { + const jwk = new X25519Jwk({ x: jwkJson.x }) + + expect(jwk.kty).toEqual('OKP') + expect(jwk.crv).toEqual('X25519') + expect(jwk.keyType).toEqual(KeyType.X25519) + expect(jwk.publicKey).toEqual(TypedArrayEncoder.fromBase64(jwkJson.x)) + expect(jwk.supportedEncryptionAlgorithms).toEqual(['ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW', 'ECDH-ES']) + expect(jwk.supportedSignatureAlgorithms).toEqual([]) + expect(jwk.key.keyType).toEqual(KeyType.X25519) + expect(jwk.toJson()).toEqual(jwkJson) + }) + + test('fromJson', () => { + const jwk = X25519Jwk.fromJson(jwkJson) + expect(jwk.x).toEqual(jwkJson.x) + + expect(() => X25519Jwk.fromJson({ ...jwkJson, kty: 'test' })).toThrowError("Invalid 'X25519' JWK.") + }) + + test('fromPublicKey', () => { + const jwk = X25519Jwk.fromPublicKey(TypedArrayEncoder.fromBase64(jwkJson.x)) + expect(jwk.x).toEqual(jwkJson.x) + }) +}) diff --git a/packages/core/src/crypto/jose/jwk/ecCompression.ts b/packages/core/src/crypto/jose/jwk/ecCompression.ts new file mode 100644 index 0000000000..f602191e8d --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/ecCompression.ts @@ -0,0 +1,127 @@ +/** + * Based on https://github.com/transmute-industries/verifiable-data/blob/main/packages/web-crypto-key-pair/src/compression/ec-compression.ts + */ + +// native BigInteger is only supported in React Native 0.70+, so we use big-integer for now. +import bigInt from 'big-integer' + +import { Buffer } from '../../../utils/buffer' +import { JwaCurve } from '../jwa' + +const curveToPointLength = { + [JwaCurve.P256]: 64, + [JwaCurve.P384]: 96, + [JwaCurve.P521]: 132, + [JwaCurve.Secp256k1]: 64, +} + +function getConstantsForCurve(curve: 'P-256' | 'P-384' | 'P-521' | 'secp256k1') { + let two, prime, b, pIdent + + if (curve === 'P-256') { + two = bigInt(2) + prime = two.pow(256).subtract(two.pow(224)).add(two.pow(192)).add(two.pow(96)).subtract(1) + + pIdent = prime.add(1).divide(4) + + b = bigInt('5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b', 16) + } + + if (curve === 'P-384') { + two = bigInt(2) + prime = two.pow(384).subtract(two.pow(128)).subtract(two.pow(96)).add(two.pow(32)).subtract(1) + + pIdent = prime.add(1).divide(4) + b = bigInt('b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef', 16) + } + + if (curve === 'P-521') { + two = bigInt(2) + prime = two.pow(521).subtract(1) + b = bigInt( + '00000051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00', + 16 + ) + pIdent = prime.add(1).divide(4) + } + + // https://en.bitcoin.it/wiki/Secp256k1 + // p = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F + // P = 2256 - 232 - 29 - 28 - 27 - 26 - 24 - 1 + if (curve === JwaCurve.Secp256k1) { + two = bigInt(2) + prime = two + .pow(256) + .subtract(two.pow(32)) + .subtract(two.pow(9)) + .subtract(two.pow(8)) + .subtract(two.pow(7)) + .subtract(two.pow(6)) + .subtract(two.pow(4)) + .subtract(1) + b = bigInt(7) + pIdent = prime.add(1).divide(4) + } + + if (!prime || !b || !pIdent) { + throw new Error(`Unsupported curve ${curve}`) + } + + return { prime, b, pIdent } +} + +// see https://stackoverflow.com/questions/17171542/algorithm-for-elliptic-curve-point-compression +// https://github.com/w3c-ccg/did-method-key/pull/36 +/** + * Point compress elliptic curve key + * @return Compressed representation + */ +function compressECPoint(x: Uint8Array, y: Uint8Array): Uint8Array { + const out = new Uint8Array(x.length + 1) + out[0] = 2 + (y[y.length - 1] & 1) + out.set(x, 1) + return out +} + +function padWithZeroes(number: number | string, length: number) { + let value = '' + number + while (value.length < length) { + value = '0' + value + } + return value +} + +export function compress(publicKey: Uint8Array): Uint8Array { + const publicKeyHex = Buffer.from(publicKey).toString('hex') + const xHex = publicKeyHex.slice(0, publicKeyHex.length / 2) + const yHex = publicKeyHex.slice(publicKeyHex.length / 2, publicKeyHex.length) + const xOctet = Uint8Array.from(Buffer.from(xHex, 'hex')) + const yOctet = Uint8Array.from(Buffer.from(yHex, 'hex')) + return compressECPoint(xOctet, yOctet) +} + +export function expand(publicKey: Uint8Array, curve: 'P-256' | 'P-384' | 'P-521' | 'secp256k1'): Uint8Array { + const publicKeyComponent = Buffer.from(publicKey).toString('hex') + const { prime, b, pIdent } = getConstantsForCurve(curve) + const signY = new Number(publicKeyComponent[1]).valueOf() - 2 + const x = bigInt(publicKeyComponent.substring(2), 16) + + // y^2 = x^3 - 3x + b + let y = x.pow(3).subtract(x.multiply(3)).add(b).modPow(pIdent, prime) + + if (curve === 'secp256k1') { + // y^2 = x^3 + 7 + y = x.pow(3).add(7).modPow(pIdent, prime) + } + + // If the parity doesn't match it's the *other* root + if (y.mod(2).toJSNumber() !== signY) { + // y = prime - y + y = prime.subtract(y) + } + + return Buffer.from( + padWithZeroes(x.toString(16), curveToPointLength[curve]) + padWithZeroes(y.toString(16), curveToPointLength[curve]), + 'hex' + ) +} diff --git a/packages/core/src/crypto/jose/jwk/index.ts b/packages/core/src/crypto/jose/jwk/index.ts new file mode 100644 index 0000000000..7579a74778 --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/index.ts @@ -0,0 +1,7 @@ +export * from './transform' +export { Ed25519Jwk } from './Ed25519Jwk' +export { X25519Jwk } from './X25519Jwk' +export { P256Jwk } from './P256Jwk' +export { P384Jwk } from './P384Jwk' +export { P521Jwk } from './P521Jwk' +export { Jwk, JwkJson } from './Jwk' diff --git a/packages/core/src/crypto/jose/jwk/transform.ts b/packages/core/src/crypto/jose/jwk/transform.ts new file mode 100644 index 0000000000..c2c553e9ad --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/transform.ts @@ -0,0 +1,54 @@ +import type { JwkJson, Jwk } from './Jwk' +import type { Key } from '../../Key' +import type { JwaSignatureAlgorithm } from '../jwa' + +import { CredoError } from '../../../error' +import { KeyType } from '../../KeyType' +import { JwaCurve, JwaKeyType } from '../jwa' + +import { Ed25519Jwk } from './Ed25519Jwk' +import { K256Jwk } from './K256Jwk' +import { P256Jwk } from './P256Jwk' +import { P384Jwk } from './P384Jwk' +import { P521Jwk } from './P521Jwk' +import { X25519Jwk } from './X25519Jwk' +import { hasCrv } from './validate' + +const JwkClasses = [Ed25519Jwk, P256Jwk, P384Jwk, P521Jwk, X25519Jwk, K256Jwk] as const + +export function getJwkFromJson(jwkJson: JwkJson): Jwk { + if (jwkJson.kty === JwaKeyType.OKP) { + if (hasCrv(jwkJson, JwaCurve.Ed25519)) return Ed25519Jwk.fromJson(jwkJson) + if (hasCrv(jwkJson, JwaCurve.X25519)) return X25519Jwk.fromJson(jwkJson) + } + + if (jwkJson.kty === JwaKeyType.EC) { + if (hasCrv(jwkJson, JwaCurve.P256)) return P256Jwk.fromJson(jwkJson) + if (hasCrv(jwkJson, JwaCurve.P384)) return P384Jwk.fromJson(jwkJson) + if (hasCrv(jwkJson, JwaCurve.P521)) return P521Jwk.fromJson(jwkJson) + if (hasCrv(jwkJson, JwaCurve.Secp256k1)) return K256Jwk.fromJson(jwkJson) + } + + throw new Error(`Cannot create JWK from JSON. Unsupported JWK with kty '${jwkJson.kty}'.`) +} + +export function getJwkFromKey(key: Key) { + if (key.keyType === KeyType.Ed25519) return Ed25519Jwk.fromPublicKey(key.publicKey) + if (key.keyType === KeyType.X25519) return X25519Jwk.fromPublicKey(key.publicKey) + + if (key.keyType === KeyType.P256) return P256Jwk.fromPublicKey(key.publicKey) + if (key.keyType === KeyType.P384) return P384Jwk.fromPublicKey(key.publicKey) + if (key.keyType === KeyType.P521) return P521Jwk.fromPublicKey(key.publicKey) + + if (key.keyType === KeyType.K256) return K256Jwk.fromPublicKey(key.publicKey) + + throw new CredoError(`Cannot create JWK from key. Unsupported key with type '${key.keyType}'.`) +} + +export function getJwkClassFromJwaSignatureAlgorithm(alg: JwaSignatureAlgorithm | string) { + return JwkClasses.find((jwkClass) => jwkClass.supportedSignatureAlgorithms.includes(alg as JwaSignatureAlgorithm)) +} + +export function getJwkClassFromKeyType(keyType: KeyType) { + return JwkClasses.find((jwkClass) => jwkClass.keyType === keyType) +} diff --git a/packages/core/src/crypto/jose/jwk/validate.ts b/packages/core/src/crypto/jose/jwk/validate.ts new file mode 100644 index 0000000000..50c39c338b --- /dev/null +++ b/packages/core/src/crypto/jose/jwk/validate.ts @@ -0,0 +1,25 @@ +import type { JwkJson } from './Jwk' +import type { JwaCurve, JwaKeyType } from '../jwa' + +export function hasCrv(jwk: JwkJson, crv: JwaCurve): jwk is JwkJson & { crv: JwaCurve } { + return 'crv' in jwk && jwk.crv === crv +} + +export function hasKty(jwk: JwkJson, kty: JwaKeyType) { + return 'kty' in jwk && jwk.kty === kty +} + +export function hasX(jwk: JwkJson): jwk is JwkJson & { x: string } { + return 'x' in jwk && jwk.x !== undefined +} + +export function hasY(jwk: JwkJson): jwk is JwkJson & { y: string } { + return 'y' in jwk && jwk.y !== undefined +} + +export function hasValidUse( + jwk: JwkJson, + { supportsSigning, supportsEncrypting }: { supportsSigning: boolean; supportsEncrypting: boolean } +) { + return jwk.use === undefined || (supportsSigning && jwk.use === 'sig') || (supportsEncrypting && jwk.use === 'enc') +} diff --git a/packages/core/src/crypto/jose/jwt/Jwt.ts b/packages/core/src/crypto/jose/jwt/Jwt.ts new file mode 100644 index 0000000000..90302f72e8 --- /dev/null +++ b/packages/core/src/crypto/jose/jwt/Jwt.ts @@ -0,0 +1,63 @@ +import type { Buffer } from '../../../utils' +import type { JwkJson } from '../jwk' + +import { CredoError } from '../../../error' +import { JsonEncoder, TypedArrayEncoder } from '../../../utils' + +import { JwtPayload } from './JwtPayload' + +// TODO: JWT Header typing +interface JwtHeader { + alg: string + kid?: string + jwk?: JwkJson + [key: string]: unknown +} + +interface JwtOptions { + payload: JwtPayload + header: JwtHeader + signature: Buffer + + serializedJwt: string +} + +export class Jwt { + private static format = /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/ + + public readonly payload: JwtPayload + public readonly header: JwtHeader + public readonly signature: Buffer + + /** + * Compact serialization of the JWT. Contains the payload, header, and signature. + */ + public readonly serializedJwt: string + + private constructor(options: JwtOptions) { + this.serializedJwt = options.serializedJwt + + this.payload = options.payload + this.header = options.header + this.signature = options.signature + } + + public static fromSerializedJwt(serializedJwt: string) { + if (typeof serializedJwt !== 'string' || !Jwt.format.test(serializedJwt)) { + throw new CredoError(`Invalid JWT. '${serializedJwt}' does not match JWT regex`) + } + + const [header, payload, signature] = serializedJwt.split('.') + + try { + return new Jwt({ + header: JsonEncoder.fromBase64(header), + payload: JwtPayload.fromJson(JsonEncoder.fromBase64(payload)), + signature: TypedArrayEncoder.fromBase64(signature), + serializedJwt, + }) + } catch (error) { + throw new CredoError(`Invalid JWT. ${error instanceof Error ? error.message : JSON.stringify(error)}`) + } + } +} diff --git a/packages/core/src/crypto/jose/jwt/JwtPayload.ts b/packages/core/src/crypto/jose/jwt/JwtPayload.ts new file mode 100644 index 0000000000..1a5cdb4ac8 --- /dev/null +++ b/packages/core/src/crypto/jose/jwt/JwtPayload.ts @@ -0,0 +1,231 @@ +import { CredoError } from '../../../error' + +/** + * The maximum allowed clock skew time in seconds. If an time based validation + * is performed against current time (`now`), the validation can be of by the skew + * time. + * + * See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5 + */ +const DEFAULT_SKEW_TIME = 300 + +export interface JwtPayloadJson { + iss?: string + sub?: string + aud?: string | string[] + exp?: number + nbf?: number + iat?: number + jti?: string + [key: string]: unknown +} + +export interface JwtPayloadOptions { + iss?: string + sub?: string + aud?: string | string[] + exp?: number + nbf?: number + iat?: number + jti?: string + additionalClaims?: Record +} + +export class JwtPayload { + public constructor(options?: JwtPayloadOptions) { + this.iss = options?.iss + this.sub = options?.sub + this.aud = options?.aud + this.exp = options?.exp + this.nbf = options?.nbf + this.iat = options?.iat + this.jti = options?.jti + this.additionalClaims = options?.additionalClaims ?? {} + } + + /** + * identifies the principal that issued the JWT. + * The processing of this claim is generally application specific. + * The "iss" value is a case-sensitive string containing a StringOrURI + * value. + */ + public iss?: string + + /** + * identifies the principal that is the + * subject of the JWT. The Claims in a JWT are normally statements + * about the subject. The subject value MUST either be scoped to be + * locally unique in the context of the issuer or be globally unique. + * The processing of this claim is generally application specific. The + * "sub" value is a case-sensitive string containing a StringOrURI + * value. + */ + public sub?: string + + /** + * identifies the recipients that the JWT is + * intended for. Each principal intended to process the JWT MUST + * identify itself with a value in the audience claim. If the principal + * processing the claim does not identify itself with a value in the + * "aud" claim when this claim is present, then the JWT MUST be + * rejected.In the general case, the "aud" value is an array of case- + * sensitive strings, each containing a StringOrURI value. In the + * special case when the JWT has one audience, the "aud" value MAY be a + * single case-sensitive string containing a StringOrURI value. The + * interpretation of audience values is generally application specific. + */ + public aud?: string | string[] + + /** + * identifies the expiration time on + * or after which the JWT MUST NOT be accepted for processing. The + * processing of the "exp" claim requires that the current date/time + * MUST be before the expiration date/time listed in the "exp" claim. + * Implementers MAY provide for some small leeway, usually no more than + * a few minutes, to account for clock skew. Its value MUST be a number + * containing a NumericDate value. + */ + public exp?: number + + /** + * identifies the time at which the JWT was + * issued. This claim can be used to determine the age of the JWT. Its + * value MUST be a number containing a NumericDate value. + */ + public nbf?: number + + /** + * identifies the time at which the JWT was + * issued. This claim can be used to determine the age of the JWT. Its + * value MUST be a number containing a NumericDate value. + */ + public iat?: number + + /** + * provides a unique identifier for the JWT. + * The identifier value MUST be assigned in a manner that ensures that + * there is a negligible probability that the same value will be + * accidentally assigned to a different data object; if the application + * uses multiple issuers, collisions MUST be prevented among values + * produced by different issuers as well. The "jti" claim can be used + * to prevent the JWT from being replayed. The "jti" value is a case- + * sensitive string. + */ + public jti?: string + + public additionalClaims: Record + + /** + * Validate the JWT payload. This does not verify the signature of the JWT itself. + * + * The following validations are performed: + * - if `nbf` is present, it must be greater than now + * - if `iat` is present, it must be less than now + * - if `exp` is present, it must be greater than now + */ + public validate(options?: { skewTime?: number; now?: number }) { + const { nowSkewedFuture, nowSkewedPast } = getNowSkewed(options?.now, options?.skewTime) + + // Validate nbf + if (typeof this.nbf !== 'number' && typeof this.nbf !== 'undefined') { + throw new CredoError(`JWT payload 'nbf' must be a number if provided. Actual type is ${typeof this.nbf}`) + } + if (typeof this.nbf === 'number' && this.nbf > nowSkewedFuture) { + throw new CredoError(`JWT not valid before ${this.nbf}`) + } + + // Validate iat + if (typeof this.iat !== 'number' && typeof this.iat !== 'undefined') { + throw new CredoError(`JWT payload 'iat' must be a number if provided. Actual type is ${typeof this.iat}`) + } + if (typeof this.iat === 'number' && this.iat > nowSkewedFuture) { + throw new CredoError(`JWT issued in the future at ${this.iat}`) + } + + // Validate exp + if (typeof this.exp !== 'number' && typeof this.exp !== 'undefined') { + throw new CredoError(`JWT payload 'exp' must be a number if provided. Actual type is ${typeof this.exp}`) + } + if (typeof this.exp === 'number' && this.exp < nowSkewedPast) { + throw new CredoError(`JWT expired at ${this.exp}`) + } + + // NOTE: nonce and aud are not validated in here. We could maybe add + // the values as input, so you can provide the expected nonce and aud values + } + + public toJson(): JwtPayloadJson { + return { + ...this.additionalClaims, + iss: this.iss, + sub: this.sub, + aud: this.aud, + exp: this.exp, + nbf: this.nbf, + iat: this.iat, + jti: this.jti, + } + } + + public static fromJson(jwtPayloadJson: JwtPayloadJson) { + const { iss, sub, aud, exp, nbf, iat, jti, ...additionalClaims } = jwtPayloadJson + + // Validate iss + if (iss && typeof iss !== 'string') { + throw new CredoError(`JWT payload iss must be a string`) + } + + // Validate sub + if (sub && typeof sub !== 'string') { + throw new CredoError(`JWT payload sub must be a string`) + } + + // Validate aud + if (aud && typeof aud !== 'string' && !(Array.isArray(aud) && aud.every((aud) => typeof aud === 'string'))) { + throw new CredoError(`JWT payload aud must be a string or an array of strings`) + } + + // Validate exp + if (exp && (typeof exp !== 'number' || exp < 0)) { + throw new CredoError(`JWT payload exp must be a positive number`) + } + + // Validate nbf + if (nbf && (typeof nbf !== 'number' || nbf < 0)) { + throw new CredoError(`JWT payload nbf must be a positive number`) + } + + // Validate iat + if (iat && (typeof iat !== 'number' || iat < 0)) { + throw new CredoError(`JWT payload iat must be a positive number`) + } + + // Validate jti + if (jti && typeof jti !== 'string') { + throw new CredoError(`JWT payload jti must be a string`) + } + + const jwtPayload = new JwtPayload({ + iss, + sub, + aud, + exp, + nbf, + iat, + jti, + additionalClaims, + }) + + return jwtPayload + } +} + +function getNowSkewed(now?: number, skewTime?: number) { + const _now = typeof now === 'number' ? now : Math.floor(Date.now() / 1000) + const _skewTime = typeof skewTime !== 'undefined' && skewTime >= 0 ? skewTime : DEFAULT_SKEW_TIME + + return { + nowSkewedPast: _now - _skewTime, + nowSkewedFuture: _now + _skewTime, + } +} diff --git a/packages/core/src/crypto/jose/jwt/__tests__/Jwt.test.ts b/packages/core/src/crypto/jose/jwt/__tests__/Jwt.test.ts new file mode 100644 index 0000000000..d65594f1cd --- /dev/null +++ b/packages/core/src/crypto/jose/jwt/__tests__/Jwt.test.ts @@ -0,0 +1,71 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { TypedArrayEncoder } from '../../../../utils' +import { Jwt } from '../Jwt' +import { JwtPayload } from '../JwtPayload' + +describe('Jwt', () => { + test('create Jwt instance from serialized jwt', () => { + const jwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpGUXlJc0ltTnlkaUk2SWxBdE1qVTJJaXdpZUNJNklucFJUMjkzU1VNeFoxZEtkR1JrWkVJMVIwRjBOR3hoZFRaTWREaEphSGszTnpGcFFXWmhiUzB4Y0dNaUxDSjVJam9pWTJwRVh6ZHZNMmRrVVRGMloybFJlVE5mYzAxSGN6ZFhjbmREVFZVNVJsRlphVzFCTTBoNGJrMXNkeUo5IzAifQ.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9jb250ZXh0Lmpzb24iXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlZlcmlmaWFibGVDcmVkZW50aWFsRXh0ZW5zaW9uIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOnsibmFtZSI6IkpvYnMgZm9yIHRoZSBGdXR1cmUgKEpGRikiLCJpY29uVXJsIjoiaHR0cHM6Ly93M2MtY2NnLmdpdGh1Yi5pby92Yy1lZC9wbHVnZmVzdC0xLTIwMjIvaW1hZ2VzL0pGRl9Mb2dvTG9ja3VwLnBuZyIsImltYWdlIjoiaHR0cHM6Ly93M2MtY2NnLmdpdGh1Yi5pby92Yy1lZC9wbHVnZmVzdC0xLTIwMjIvaW1hZ2VzL0pGRl9Mb2dvTG9ja3VwLnBuZyJ9LCJuYW1lIjoiSkZGIHggdmMtZWR1IFBsdWdGZXN0IDIiLCJkZXNjcmlwdGlvbiI6Ik1BVFRSJ3Mgc3VibWlzc2lvbiBmb3IgSkZGIFBsdWdmZXN0IDIiLCJjcmVkZW50aWFsQnJhbmRpbmciOnsiYmFja2dyb3VuZENvbG9yIjoiIzQ2NGM0OSJ9LCJjcmVkZW50aWFsU3ViamVjdCI6eyJ0eXBlIjpbIkFjaGlldmVtZW50U3ViamVjdCJdLCJhY2hpZXZlbWVudCI6eyJpZCI6InVybjp1dWlkOmJkNmQ5MzE2LWY3YWUtNDA3My1hMWU1LTJmN2Y1YmQyMjkyMiIsIm5hbWUiOiJKRkYgeCB2Yy1lZHUgUGx1Z0Zlc3QgMiBJbnRlcm9wZXJhYmlsaXR5IiwidHlwZSI6WyJBY2hpZXZlbWVudCJdLCJpbWFnZSI6eyJpZCI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMi0yMDIyL2ltYWdlcy9KRkYtVkMtRURVLVBMVUdGRVNUMi1iYWRnZS1pbWFnZS5wbmciLCJ0eXBlIjoiSW1hZ2UifSwiY3JpdGVyaWEiOnsidHlwZSI6IkNyaXRlcmlhIiwibmFycmF0aXZlIjoiU29sdXRpb25zIHByb3ZpZGVycyBlYXJuZWQgdGhpcyBiYWRnZSBieSBkZW1vbnN0cmF0aW5nIGludGVyb3BlcmFiaWxpdHkgYmV0d2VlbiBtdWx0aXBsZSBwcm92aWRlcnMgYmFzZWQgb24gdGhlIE9CdjMgY2FuZGlkYXRlIGZpbmFsIHN0YW5kYXJkLCB3aXRoIHNvbWUgYWRkaXRpb25hbCByZXF1aXJlZCBmaWVsZHMuIENyZWRlbnRpYWwgaXNzdWVycyBlYXJuaW5nIHRoaXMgYmFkZ2Ugc3VjY2Vzc2Z1bGx5IGlzc3VlZCBhIGNyZWRlbnRpYWwgaW50byBhdCBsZWFzdCB0d28gd2FsbGV0cy4gIFdhbGxldCBpbXBsZW1lbnRlcnMgZWFybmluZyB0aGlzIGJhZGdlIHN1Y2Nlc3NmdWxseSBkaXNwbGF5ZWQgY3JlZGVudGlhbHMgaXNzdWVkIGJ5IGF0IGxlYXN0IHR3byBkaWZmZXJlbnQgY3JlZGVudGlhbCBpc3N1ZXJzLiJ9LCJkZXNjcmlwdGlvbiI6IlRoaXMgY3JlZGVudGlhbCBzb2x1dGlvbiBzdXBwb3J0cyB0aGUgdXNlIG9mIE9CdjMgYW5kIHczYyBWZXJpZmlhYmxlIENyZWRlbnRpYWxzIGFuZCBpcyBpbnRlcm9wZXJhYmxlIHdpdGggYXQgbGVhc3QgdHdvIG90aGVyIHNvbHV0aW9ucy4gIFRoaXMgd2FzIGRlbW9uc3RyYXRlZCBzdWNjZXNzZnVsbHkgZHVyaW5nIEpGRiB4IHZjLWVkdSBQbHVnRmVzdCAyLiJ9fX0sImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpGUXlJc0ltTnlkaUk2SWxBdE1qVTJJaXdpZUNJNklucFJUMjkzU1VNeFoxZEtkR1JrWkVJMVIwRjBOR3hoZFRaTWREaEphSGszTnpGcFFXWmhiUzB4Y0dNaUxDSjVJam9pWTJwRVh6ZHZNMmRrVVRGMloybFJlVE5mYzAxSGN6ZFhjbmREVFZVNVJsRlphVzFCTTBoNGJrMXNkeUo5Iiwic3ViIjoiZGlkOmtleTp6Nk1rcWdrTHJSeUxnNmJxazI3ZGp3YmJhUVdnYVNZZ0ZWQ0txOVlLeFpiTmtwVnYiLCJuYmYiOjE2NzQ2NjU4ODZ9.anABxv424eMpp0xgbTx6aZvZxblkSThq-XbgixhWegFCVz2Q-EtRUiGJuOUjmql5TttTZ_YgtN9PgozOfuTZtg' + + const jwtInstance = Jwt.fromSerializedJwt(jwt) + + expect(jwtInstance.header).toEqual({ + alg: 'ES256', + kid: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InpRT293SUMxZ1dKdGRkZEI1R0F0NGxhdTZMdDhJaHk3NzFpQWZhbS0xcGMiLCJ5IjoiY2pEXzdvM2dkUTF2Z2lReTNfc01HczdXcndDTVU5RlFZaW1BM0h4bk1sdyJ9#0', + typ: 'JWT', + }) + + expect(jwtInstance.payload).toBeInstanceOf(JwtPayload) + expect(jwtInstance.payload.toJson()).toEqual({ + aud: undefined, + exp: undefined, + iat: undefined, + iss: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InpRT293SUMxZ1dKdGRkZEI1R0F0NGxhdTZMdDhJaHk3NzFpQWZhbS0xcGMiLCJ5IjoiY2pEXzdvM2dkUTF2Z2lReTNfc01HczdXcndDTVU5RlFZaW1BM0h4bk1sdyJ9', + jti: undefined, + nbf: 1674665886, + sub: 'did:key:z6MkqgkLrRyLg6bqk27djwbbaQWgaSYgFVCKq9YKxZbNkpVv', + vc: { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://purl.imsglobal.org/spec/ob/v3p0/context.json'], + credentialBranding: { + backgroundColor: '#464c49', + }, + credentialSubject: { + achievement: { + criteria: { + narrative: + 'Solutions providers earned this badge by demonstrating interoperability between multiple providers based on the OBv3 candidate final standard, with some additional required fields. Credential issuers earning this badge successfully issued a credential into at least two wallets. Wallet implementers earning this badge successfully displayed credentials issued by at least two different credential issuers.', + type: 'Criteria', + }, + description: + 'This credential solution supports the use of OBv3 and w3c Verifiable Credentials and is interoperable with at least two other solutions. This was demonstrated successfully during JFF x vc-edu PlugFest 2.', + id: 'urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922', + image: { + id: 'https://w3c-ccg.github.io/vc-ed/plugfest-2-2022/images/JFF-VC-EDU-PLUGFEST2-badge-image.png', + type: 'Image', + }, + name: 'JFF x vc-edu PlugFest 2 Interoperability', + type: ['Achievement'], + }, + type: ['AchievementSubject'], + }, + description: "MATTR's submission for JFF Plugfest 2", + issuer: { + iconUrl: 'https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png', + image: 'https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png', + name: 'Jobs for the Future (JFF)', + }, + name: 'JFF x vc-edu PlugFest 2', + type: ['VerifiableCredential', 'VerifiableCredentialExtension', 'OpenBadgeCredential'], + }, + }) + + expect( + jwtInstance.signature.equals( + TypedArrayEncoder.fromBase64( + 'anABxv424eMpp0xgbTx6aZvZxblkSThq-XbgixhWegFCVz2Q-EtRUiGJuOUjmql5TttTZ_YgtN9PgozOfuTZtg' + ) + ) + ).toBe(true) + }) +}) diff --git a/packages/core/src/crypto/jose/jwt/__tests__/JwtPayload.test.ts b/packages/core/src/crypto/jose/jwt/__tests__/JwtPayload.test.ts new file mode 100644 index 0000000000..3d5020816e --- /dev/null +++ b/packages/core/src/crypto/jose/jwt/__tests__/JwtPayload.test.ts @@ -0,0 +1,103 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { JwtPayload } from '../JwtPayload' + +describe('JwtPayload', () => { + test('create JwtPayload from json', () => { + const jwtPayload = JwtPayload.fromJson({ + iss: 'issuer', + sub: 'subject', + aud: 'audience', + exp: 123, + nbf: 123, + iat: 123, + jti: 'jwtid', + + someAdditional: 'claim', + and: { + another: 'claim', + }, + }) + + expect(jwtPayload.iss).toBe('issuer') + expect(jwtPayload.sub).toBe('subject') + expect(jwtPayload.aud).toBe('audience') + expect(jwtPayload.exp).toBe(123) + expect(jwtPayload.nbf).toBe(123) + expect(jwtPayload.iat).toBe(123) + expect(jwtPayload.jti).toBe('jwtid') + expect(jwtPayload.additionalClaims).toEqual({ + someAdditional: 'claim', + and: { + another: 'claim', + }, + }) + }) + + test('validate jwt payload', () => { + const jwtPayload = JwtPayload.fromJson({}) + + jwtPayload.exp = 123 + expect(() => jwtPayload.validate({ now: 200, skewTime: 1 })).toThrowError('JWT expired at 123') + expect(() => jwtPayload.validate({ now: 100, skewTime: 1 })).not.toThrow() + + jwtPayload.nbf = 80 + expect(() => jwtPayload.validate({ now: 75, skewTime: 1 })).toThrowError('JWT not valid before 80') + expect(() => jwtPayload.validate({ now: 100, skewTime: 1 })).not.toThrow() + + jwtPayload.iat = 90 + expect(() => jwtPayload.validate({ now: 85, skewTime: 1 })).toThrowError('JWT issued in the future at 90') + expect(() => jwtPayload.validate({ now: 100, skewTime: 1 })).not.toThrow() + }) + + test('throws error for invalid values', () => { + expect(() => JwtPayload.fromJson({ iss: {} } as any)).toThrowError('JWT payload iss must be a string') + expect(() => JwtPayload.fromJson({ sub: {} } as any)).toThrowError('JWT payload sub must be a string') + expect(() => JwtPayload.fromJson({ aud: {} } as any)).toThrowError( + 'JWT payload aud must be a string or an array of strings' + ) + expect(() => JwtPayload.fromJson({ aud: [1, 'string'] } as any)).toThrowError( + 'JWT payload aud must be a string or an array of strings' + ) + expect(() => JwtPayload.fromJson({ exp: '10' } as any)).toThrowError('JWT payload exp must be a positive number') + expect(() => JwtPayload.fromJson({ exp: -1 } as any)).toThrowError('JWT payload exp must be a positive number') + expect(() => JwtPayload.fromJson({ nbf: '10' } as any)).toThrowError('JWT payload nbf must be a positive number') + expect(() => JwtPayload.fromJson({ nbf: -1 } as any)).toThrowError('JWT payload nbf must be a positive number') + expect(() => JwtPayload.fromJson({ iat: '10' } as any)).toThrowError('JWT payload iat must be a positive number') + expect(() => JwtPayload.fromJson({ iat: -1 } as any)).toThrowError('JWT payload iat must be a positive number') + expect(() => JwtPayload.fromJson({ jti: {} } as any)).toThrowError('JWT payload jti must be a string') + }) + + test('correctly outputs json', () => { + const jwtPayload = new JwtPayload({ + iss: 'issuer', + sub: 'subject', + aud: 'audience', + exp: 123, + nbf: 123, + iat: 123, + jti: 'jwtid', + + additionalClaims: { + someAdditional: 'claim', + and: { + another: 'claim', + }, + }, + }) + + expect(jwtPayload.toJson()).toEqual({ + iss: 'issuer', + sub: 'subject', + aud: 'audience', + exp: 123, + nbf: 123, + iat: 123, + jti: 'jwtid', + + someAdditional: 'claim', + and: { + another: 'claim', + }, + }) + }) +}) diff --git a/packages/core/src/crypto/jose/jwt/index.ts b/packages/core/src/crypto/jose/jwt/index.ts new file mode 100644 index 0000000000..b02f85fc83 --- /dev/null +++ b/packages/core/src/crypto/jose/jwt/index.ts @@ -0,0 +1,2 @@ +export { Jwt } from './Jwt' +export { JwtPayload, JwtPayloadJson, JwtPayloadOptions } from './JwtPayload' diff --git a/packages/core/src/crypto/keyUtils.ts b/packages/core/src/crypto/keyUtils.ts new file mode 100644 index 0000000000..14b229fc8d --- /dev/null +++ b/packages/core/src/crypto/keyUtils.ts @@ -0,0 +1,67 @@ +import { Buffer } from '../utils' + +import { KeyType } from './KeyType' + +export function isValidSeed(seed: Buffer, keyType: KeyType): boolean { + const minimumSeedLength = { + [KeyType.Ed25519]: 32, + [KeyType.X25519]: 32, + [KeyType.Bls12381g1]: 32, + [KeyType.Bls12381g2]: 32, + [KeyType.Bls12381g1g2]: 32, + [KeyType.P256]: 64, + [KeyType.P384]: 64, + [KeyType.P521]: 64, + [KeyType.K256]: 64, + } as const + + return Buffer.isBuffer(seed) && seed.length >= minimumSeedLength[keyType] +} + +export function isValidPrivateKey(privateKey: Buffer, keyType: KeyType): boolean { + const privateKeyLength = { + [KeyType.Ed25519]: 32, + [KeyType.X25519]: 32, + [KeyType.Bls12381g1]: 32, + [KeyType.Bls12381g2]: 32, + [KeyType.Bls12381g1g2]: 32, + [KeyType.P256]: 32, + [KeyType.P384]: 48, + [KeyType.P521]: 66, + [KeyType.K256]: 32, + } as const + + return Buffer.isBuffer(privateKey) && privateKey.length === privateKeyLength[keyType] +} + +export function isSigningSupportedForKeyType(keyType: KeyType): boolean { + const keyTypeSigningSupportedMapping = { + [KeyType.Ed25519]: true, + [KeyType.X25519]: false, + [KeyType.P256]: true, + [KeyType.P384]: true, + [KeyType.P521]: true, + [KeyType.Bls12381g1]: true, + [KeyType.Bls12381g2]: true, + [KeyType.Bls12381g1g2]: true, + [KeyType.K256]: true, + } as const + + return keyTypeSigningSupportedMapping[keyType] +} + +export function isEncryptionSupportedForKeyType(keyType: KeyType): boolean { + const keyTypeEncryptionSupportedMapping = { + [KeyType.Ed25519]: false, + [KeyType.X25519]: true, + [KeyType.P256]: true, + [KeyType.P384]: true, + [KeyType.P521]: true, + [KeyType.Bls12381g1]: false, + [KeyType.Bls12381g2]: false, + [KeyType.Bls12381g1g2]: false, + [KeyType.K256]: true, + } as const + + return keyTypeEncryptionSupportedMapping[keyType] +} diff --git a/packages/core/src/crypto/multiCodecKey.ts b/packages/core/src/crypto/multiCodecKey.ts new file mode 100644 index 0000000000..249978a4d3 --- /dev/null +++ b/packages/core/src/crypto/multiCodecKey.ts @@ -0,0 +1,35 @@ +import { KeyType } from './KeyType' + +// based on https://github.com/multiformats/multicodec/blob/master/table.csv +const multiCodecPrefixMap: Record = { + 234: KeyType.Bls12381g1, + 235: KeyType.Bls12381g2, + 236: KeyType.X25519, + 237: KeyType.Ed25519, + 238: KeyType.Bls12381g1g2, + 4608: KeyType.P256, + 4609: KeyType.P384, + 4610: KeyType.P521, + 231: KeyType.K256, +} + +export function getKeyTypeByMultiCodecPrefix(multiCodecPrefix: number): KeyType { + const keyType = multiCodecPrefixMap[multiCodecPrefix] + + if (!keyType) { + throw new Error(`Unsupported key type from multicodec code '${multiCodecPrefix}'`) + } + + return keyType +} + +export function getMultiCodecPrefixByKeyType(keyType: KeyType): number { + const codes = Object.keys(multiCodecPrefixMap) + const code = codes.find((key) => multiCodecPrefixMap[key] === keyType) + + if (!code) { + throw new Error(`Could not find multicodec prefix for key type '${keyType}'`) + } + + return Number(code) +} diff --git a/packages/core/src/crypto/signing-provider/SigningProvider.ts b/packages/core/src/crypto/signing-provider/SigningProvider.ts new file mode 100644 index 0000000000..3e70d67694 --- /dev/null +++ b/packages/core/src/crypto/signing-provider/SigningProvider.ts @@ -0,0 +1,33 @@ +import type { Buffer } from '../../utils/buffer' +import type { KeyType } from '../KeyType' + +export interface KeyPair { + publicKeyBase58: string + privateKeyBase58: string + keyType: KeyType +} + +export interface SignOptions { + data: Buffer | Buffer[] + publicKeyBase58: string + privateKeyBase58: string +} + +export interface VerifyOptions { + data: Buffer | Buffer[] + publicKeyBase58: string + signature: Buffer +} + +export interface CreateKeyPairOptions { + seed?: Buffer + privateKey?: Buffer +} + +export interface SigningProvider { + readonly keyType: KeyType + + createKeyPair(options: CreateKeyPairOptions): Promise + sign(options: SignOptions): Promise + verify(options: VerifyOptions): Promise +} diff --git a/packages/core/src/crypto/signing-provider/SigningProviderError.ts b/packages/core/src/crypto/signing-provider/SigningProviderError.ts new file mode 100644 index 0000000000..bf7cae040d --- /dev/null +++ b/packages/core/src/crypto/signing-provider/SigningProviderError.ts @@ -0,0 +1,3 @@ +import { CredoError } from '../../error' + +export class SigningProviderError extends CredoError {} diff --git a/packages/core/src/crypto/signing-provider/SigningProviderRegistry.ts b/packages/core/src/crypto/signing-provider/SigningProviderRegistry.ts new file mode 100644 index 0000000000..4395b1616c --- /dev/null +++ b/packages/core/src/crypto/signing-provider/SigningProviderRegistry.ts @@ -0,0 +1,41 @@ +import type { SigningProvider } from './SigningProvider' +import type { KeyType } from '../KeyType' + +import { CredoError } from '../../error' +import { injectable, injectAll } from '../../plugins' + +export const SigningProviderToken = Symbol('SigningProviderToken') + +@injectable() +export class SigningProviderRegistry { + private signingKeyProviders: SigningProvider[] + + public constructor(@injectAll(SigningProviderToken) signingKeyProviders: Array<'default' | SigningProvider>) { + // This is a really ugly hack to make tsyringe work without any SigningProviders registered + // It is currently impossible to use @injectAll if there are no instances registered for the + // token. We register a value of `default` by default and will filter that out in the registry. + // Once we have a signing provider that should always be registered we can remove this. We can make an ed25519 + // signer using the @stablelib/ed25519 library. + this.signingKeyProviders = signingKeyProviders.filter((provider) => provider !== 'default') as SigningProvider[] + } + + public hasProviderForKeyType(keyType: KeyType): boolean { + const signingKeyProvider = this.signingKeyProviders.find((x) => x.keyType === keyType) + + return signingKeyProvider !== undefined + } + + public getProviderForKeyType(keyType: KeyType): SigningProvider { + const signingKeyProvider = this.signingKeyProviders.find((x) => x.keyType === keyType) + + if (!signingKeyProvider) { + throw new CredoError(`No signing key provider for key type: ${keyType}`) + } + + return signingKeyProvider + } + + public get supportedKeyTypes(): KeyType[] { + return Array.from(new Set(this.signingKeyProviders.map((provider) => provider.keyType))) + } +} diff --git a/packages/core/src/crypto/signing-provider/__tests__/SigningProviderRegistry.test.ts b/packages/core/src/crypto/signing-provider/__tests__/SigningProviderRegistry.test.ts new file mode 100644 index 0000000000..315b76f80a --- /dev/null +++ b/packages/core/src/crypto/signing-provider/__tests__/SigningProviderRegistry.test.ts @@ -0,0 +1,46 @@ +import type { Buffer } from '../../../utils/buffer' +import type { SigningProvider, KeyPair } from '../SigningProvider' + +import { KeyType } from '../../KeyType' +import { SigningProviderRegistry } from '../SigningProviderRegistry' + +class SigningProviderMock implements SigningProvider { + public readonly keyType = KeyType.Bls12381g2 + + public async createKeyPair(): Promise { + throw new Error('Method not implemented.') + } + public async sign(): Promise { + throw new Error('Method not implemented.') + } + public async verify(): Promise { + throw new Error('Method not implemented.') + } +} + +const signingProvider = new SigningProviderMock() +const signingProviderRegistry = new SigningProviderRegistry([signingProvider]) + +describe('SigningProviderRegistry', () => { + describe('hasProviderForKeyType', () => { + test('returns true if the key type is registered', () => { + expect(signingProviderRegistry.hasProviderForKeyType(KeyType.Bls12381g2)).toBe(true) + }) + + test('returns false if the key type is not registered', () => { + expect(signingProviderRegistry.hasProviderForKeyType(KeyType.Ed25519)).toBe(false) + }) + }) + + describe('getProviderForKeyType', () => { + test('returns the correct provider true if the key type is registered', () => { + expect(signingProviderRegistry.getProviderForKeyType(KeyType.Bls12381g2)).toBe(signingProvider) + }) + + test('throws error if the key type is not registered', () => { + expect(() => signingProviderRegistry.getProviderForKeyType(KeyType.Ed25519)).toThrowError( + 'No signing key provider for key type: ed25519' + ) + }) + }) +}) diff --git a/packages/core/src/crypto/signing-provider/index.ts b/packages/core/src/crypto/signing-provider/index.ts new file mode 100644 index 0000000000..e1ee8e8fe0 --- /dev/null +++ b/packages/core/src/crypto/signing-provider/index.ts @@ -0,0 +1,3 @@ +export * from './SigningProvider' +export * from './SigningProviderRegistry' +export * from './SigningProviderError' diff --git a/packages/core/src/decorators/ack/AckDecorator.test.ts b/packages/core/src/decorators/ack/AckDecorator.test.ts new file mode 100644 index 0000000000..7ef68f3693 --- /dev/null +++ b/packages/core/src/decorators/ack/AckDecorator.test.ts @@ -0,0 +1,132 @@ +import { BaseMessage } from '../../agent/BaseMessage' +import { ClassValidationError } from '../../error/ClassValidationError' +import { JsonTransformer } from '../../utils/JsonTransformer' +import { MessageValidator } from '../../utils/MessageValidator' +import { Compose } from '../../utils/mixins' + +import { AckValues } from './AckDecorator' +import { AckDecorated } from './AckDecoratorExtension' + +describe('Decorators | AckDecoratorExtension', () => { + class TestMessage extends Compose(BaseMessage, [AckDecorated]) { + public toJSON(): Record { + return JsonTransformer.toJSON(this) + } + } + + test('transforms AckDecorator class to JSON', () => { + const message = new TestMessage() + message.setPleaseAck([AckValues.Receipt]) + expect(message.toJSON()).toEqual({ + '@id': undefined, + '@type': undefined, + '~please_ack': { + on: ['RECEIPT'], + }, + }) + }) + + test('transforms Json to AckDecorator class', () => { + const transformed = JsonTransformer.fromJSON( + { + '~please_ack': {}, + '@id': '7517433f-1150-46f2-8495-723da61b872a', + '@type': 'https://didcomm.org/test-protocol/1.0/test-message', + }, + TestMessage + ) + + expect(transformed).toEqual({ + id: '7517433f-1150-46f2-8495-723da61b872a', + type: 'https://didcomm.org/test-protocol/1.0/test-message', + pleaseAck: { + on: ['RECEIPT'], + }, + }) + expect(transformed).toBeInstanceOf(TestMessage) + }) + + // this covers the pre-aip 2 please ack decorator + test('sets `on` value to `receipt` if `on` is not present in ack decorator', () => { + const transformed = JsonTransformer.fromJSON( + { + '~please_ack': {}, + '@id': '7517433f-1150-46f2-8495-723da61b872a', + '@type': 'https://didcomm.org/test-protocol/1.0/test-message', + }, + TestMessage + ) + + expect(transformed).toEqual({ + id: '7517433f-1150-46f2-8495-723da61b872a', + type: 'https://didcomm.org/test-protocol/1.0/test-message', + pleaseAck: { + on: ['RECEIPT'], + }, + }) + expect(transformed).toBeInstanceOf(TestMessage) + }) + + test('successfully validates please ack decorator', async () => { + const transformedWithDefault = JsonTransformer.fromJSON( + { + '~please_ack': {}, + '@id': '7517433f-1150-46f2-8495-723da61b872a', + '@type': 'https://didcomm.org/test-protocol/1.0/test-message', + }, + TestMessage + ) + + expect(MessageValidator.validateSync(transformedWithDefault)).toBeUndefined() + }) + + test('transforms Json to AckDecorator class', () => { + const transformed = () => + JsonTransformer.fromJSON( + { + '~please_ack': {}, + '@id': undefined, + '@type': undefined, + }, + TestMessage + ) + + expect(() => transformed()).toThrow(ClassValidationError) + try { + transformed() + } catch (e) { + const caughtError = e as ClassValidationError + expect(caughtError.message).toEqual( + 'TestMessage: Failed to validate class.\nAn instance of TestMessage has failed the validation:\n - property id has failed the following constraints: id must match /[-_./a-zA-Z0-9]{8,64}/ regular expression \n\nAn instance of TestMessage has failed the validation:\n - property type has failed the following constraints: type must match /(.*?)([a-zA-Z0-9._-]+)\\/(\\d[^/]*)\\/([a-zA-Z0-9._-]+)$/ regular expression \n' + ) + expect(caughtError.validationErrors).toMatchObject([ + { + children: [], + constraints: { + matches: 'id must match /[-_./a-zA-Z0-9]{8,64}/ regular expression', + }, + property: 'id', + target: { + pleaseAck: { + on: ['RECEIPT'], + }, + }, + value: undefined, + }, + { + children: [], + constraints: { + matches: 'type must match /(.*?)([a-zA-Z0-9._-]+)\\/(\\d[^/]*)\\/([a-zA-Z0-9._-]+)$/ regular expression', + }, + property: 'type', + target: { + pleaseAck: { + on: ['RECEIPT'], + }, + }, + value: undefined, + }, + ]) + } + }) +}) diff --git a/packages/core/src/decorators/ack/AckDecorator.ts b/packages/core/src/decorators/ack/AckDecorator.ts new file mode 100644 index 0000000000..f93b45121e --- /dev/null +++ b/packages/core/src/decorators/ack/AckDecorator.ts @@ -0,0 +1,23 @@ +import { IsArray, IsEnum } from 'class-validator' + +export enum AckValues { + Receipt = 'RECEIPT', + Outcome = 'OUTCOME', +} + +/** + * Represents `~please_ack` decorator + */ +export class AckDecorator { + public constructor(options: { on: [AckValues.Receipt] }) { + if (options) { + this.on = options.on + } + } + + // pre-aip 2 the on value was not defined yet. We interpret this as + // the value being set to on receipt + @IsEnum(AckValues, { each: true }) + @IsArray() + public on: AckValues[] = [AckValues.Receipt] +} diff --git a/packages/core/src/decorators/ack/AckDecoratorExtension.ts b/packages/core/src/decorators/ack/AckDecoratorExtension.ts new file mode 100644 index 0000000000..0088c241e1 --- /dev/null +++ b/packages/core/src/decorators/ack/AckDecoratorExtension.ts @@ -0,0 +1,31 @@ +import type { BaseMessageConstructor } from '../../agent/BaseMessage' + +import { Expose, Type } from 'class-transformer' +import { IsInstance, IsOptional, ValidateNested } from 'class-validator' + +import { AckDecorator, AckValues } from './AckDecorator' + +export function AckDecorated(Base: T) { + class AckDecoratorExtension extends Base { + @Expose({ name: '~please_ack' }) + @Type(() => AckDecorator) + @ValidateNested() + @IsInstance(AckDecorator) + @IsOptional() + public pleaseAck?: AckDecorator + + public setPleaseAck(on: [AckValues.Receipt] = [AckValues.Receipt]) { + this.pleaseAck = new AckDecorator({ on }) + } + + public getPleaseAck(): AckDecorator | undefined { + return this.pleaseAck + } + + public requiresAck(): boolean { + return this.pleaseAck !== undefined + } + } + + return AckDecoratorExtension +} diff --git a/packages/core/src/decorators/attachment/Attachment.ts b/packages/core/src/decorators/attachment/Attachment.ts new file mode 100644 index 0000000000..0e50069ae0 --- /dev/null +++ b/packages/core/src/decorators/attachment/Attachment.ts @@ -0,0 +1,175 @@ +import type { JwsDetachedFormat, JwsFlattenedDetachedFormat, JwsGeneralFormat } from '../../crypto/JwsTypes' + +import { Expose, Type } from 'class-transformer' +import { IsDate, IsHash, IsInstance, IsInt, IsMimeType, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { CredoError } from '../../error' +import { JsonValue } from '../../types' +import { JsonEncoder } from '../../utils/JsonEncoder' +import { uuid } from '../../utils/uuid' + +export interface AttachmentOptions { + id?: string + description?: string + filename?: string + mimeType?: string + lastmodTime?: Date + byteCount?: number + data: AttachmentDataOptions +} + +export interface AttachmentDataOptions { + base64?: string + json?: JsonValue + links?: string[] + jws?: JwsDetachedFormat | JwsFlattenedDetachedFormat + sha256?: string +} + +/** + * A JSON object that gives access to the actual content of the attachment + */ +export class AttachmentData { + /** + * Base64-encoded data, when representing arbitrary content inline instead of via links. Optional. + */ + @IsOptional() + @IsString() + public base64?: string + + /** + * Directly embedded JSON data, when representing content inline instead of via links, and when the content is natively conveyable as JSON. Optional. + */ + @IsOptional() + public json?: JsonValue + + /** + * A list of zero or more locations at which the content may be fetched. Optional. + */ + @IsOptional() + @IsString({ each: true }) + public links?: string[] + + /** + * A JSON Web Signature over the content of the attachment. Optional. + */ + @IsOptional() + // Signed attachments use JWS detached format + public jws?: JwsDetachedFormat | JwsFlattenedDetachedFormat + + /** + * The hash of the content. Optional. + */ + @IsOptional() + @IsHash('sha256') + public sha256?: string + + public constructor(options: AttachmentDataOptions) { + if (options) { + this.base64 = options.base64 + this.json = options.json + this.links = options.links + this.jws = options.jws + this.sha256 = options.sha256 + } + } +} + +/** + * Represents DIDComm attachment + * https://github.com/hyperledger/aries-rfcs/blob/master/concepts/0017-attachments/README.md + */ +export class Attachment { + public constructor(options: AttachmentOptions) { + if (options) { + this.id = options.id ?? uuid() + this.description = options.description + this.filename = options.filename + this.mimeType = options.mimeType + this.lastmodTime = options.lastmodTime + this.byteCount = options.byteCount + this.data = new AttachmentData(options.data) + } + } + + @Expose({ name: '@id' }) + public id!: string + + /** + * An optional human-readable description of the content. + */ + @IsOptional() + @IsString() + public description?: string + + /** + * A hint about the name that might be used if this attachment is persisted as a file. It is not required, and need not be unique. If this field is present and mime-type is not, the extension on the filename may be used to infer a MIME type. + */ + @IsOptional() + @IsString() + public filename?: string + + /** + * Describes the MIME type of the attached content. Optional but recommended. + */ + @Expose({ name: 'mime-type' }) + @IsOptional() + @IsMimeType() + public mimeType?: string + + /** + * A hint about when the content in this attachment was last modified. + */ + @Expose({ name: 'lastmod_time' }) + @Type(() => Date) + @IsOptional() + @IsDate() + public lastmodTime?: Date + + /** + * Optional, and mostly relevant when content is included by reference instead of by value. Lets the receiver guess how expensive it will be, in time, bandwidth, and storage, to fully fetch the attachment. + */ + @Expose({ name: 'byte_count' }) + @IsOptional() + @IsInt() + public byteCount?: number + + @Type(() => AttachmentData) + @ValidateNested() + @IsInstance(AttachmentData) + public data!: AttachmentData + + /* + * Helper function returning JSON representation of attachment data (if present). Tries to obtain the data from .base64 or .json, throws an error otherwise + */ + public getDataAsJson(): T { + if (typeof this.data.base64 === 'string') { + return JsonEncoder.fromBase64(this.data.base64) as T + } else if (this.data.json) { + return this.data.json as T + } else { + throw new CredoError('No attachment data found in `json` or `base64` data fields.') + } + } + + public addJws(jws: JwsDetachedFormat) { + // Remove payload if user provided a non-detached JWS + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { payload, ...detachedJws } = jws as JwsGeneralFormat + + // If no JWS yet, assign to current JWS + if (!this.data.jws) { + this.data.jws = detachedJws + } + // Is already jws array, add to it + else if ('signatures' in this.data.jws) { + this.data.jws.signatures.push(detachedJws) + } + // If already single JWS, transform to general jws format + else { + this.data.jws = { + signatures: [this.data.jws, detachedJws], + } + } + } +} diff --git a/packages/core/src/decorators/attachment/AttachmentExtension.ts b/packages/core/src/decorators/attachment/AttachmentExtension.ts new file mode 100644 index 0000000000..565e203da8 --- /dev/null +++ b/packages/core/src/decorators/attachment/AttachmentExtension.ts @@ -0,0 +1,34 @@ +import type { BaseMessageConstructor } from '../../agent/BaseMessage' + +import { Expose, Type } from 'class-transformer' +import { IsInstance, IsOptional, ValidateNested } from 'class-validator' + +import { Attachment } from './Attachment' + +export function AttachmentDecorated(Base: T) { + class AttachmentDecoratorExtension extends Base { + /** + * The ~attach decorator is required for appending attachments to a message + */ + @Expose({ name: '~attach' }) + @Type(() => Attachment) + @ValidateNested() + @IsInstance(Attachment, { each: true }) + @IsOptional() + public appendedAttachments?: Attachment[] + + public getAppendedAttachmentById(id: string): Attachment | undefined { + return this.appendedAttachments?.find((attachment) => attachment.id === id) + } + + public addAppendedAttachment(attachment: Attachment): void { + if (this.appendedAttachments) { + this.appendedAttachments.push(attachment) + } else { + this.appendedAttachments = [attachment] + } + } + } + + return AttachmentDecoratorExtension +} diff --git a/packages/core/src/decorators/attachment/__tests__/Attachment.test.ts b/packages/core/src/decorators/attachment/__tests__/Attachment.test.ts new file mode 100644 index 0000000000..682151e402 --- /dev/null +++ b/packages/core/src/decorators/attachment/__tests__/Attachment.test.ts @@ -0,0 +1,130 @@ +import * as didJwsz6Mkf from '../../../crypto/__tests__/__fixtures__/didJwsz6Mkf' +import * as didJwsz6Mkv from '../../../crypto/__tests__/__fixtures__/didJwsz6Mkv' +import { JsonEncoder } from '../../../utils/JsonEncoder' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { Attachment, AttachmentData } from '../Attachment' + +const mockJson = { + '@id': 'ceffce22-6471-43e4-8945-b604091981c9', + description: 'A small picture of a cat', + filename: 'cat.png', + 'mime-type': 'text/plain', + lastmod_time: new Date(), + byte_count: 9200, + data: { + json: { + hello: 'world!', + }, + sha256: '00d7b2068a0b237f14a7979bbfc01ad62f60792e459467bfc4a7d3b9a6dbbe3e', + }, +} + +const mockJsonBase64 = { + '@id': 'ceffce22-6471-43e4-8945-b604091981c9', + description: 'A small picture of a cat', + filename: 'cat.png', + 'mime-type': 'text/plain', + lastmod_time: new Date(), + byte_count: 9200, + data: { + base64: JsonEncoder.toBase64(mockJson.data.json), + }, +} + +const id = 'ceffce22-6471-43e4-8945-b604091981c9' +const description = 'A small picture of a cat' +const filename = 'cat.png' +const mimeType = 'text/plain' +const lastmodTime = new Date() +const byteCount = 9200 +const data = { + json: { + hello: 'world!', + }, + sha256: '00d7b2068a0b237f14a7979bbfc01ad62f60792e459467bfc4a7d3b9a6dbbe3e', +} +const dataInstance = new AttachmentData(data) + +describe('Decorators | Attachment', () => { + it('should correctly transform Json to Attachment class', () => { + const decorator = JsonTransformer.fromJSON(mockJson, Attachment) + + expect(decorator.id).toBe(mockJson['@id']) + expect(decorator.description).toBe(mockJson.description) + expect(decorator.filename).toBe(mockJson.filename) + expect(decorator.lastmodTime).toEqual(mockJson.lastmod_time) + expect(decorator.byteCount).toEqual(mockJson.byte_count) + expect(decorator.data).toMatchObject(mockJson.data) + }) + + it('should correctly transform Attachment class to Json', () => { + const decorator = new Attachment({ + id, + description, + filename, + mimeType, + lastmodTime, + byteCount, + data: dataInstance, + }) + + const json = JsonTransformer.toJSON(decorator) + const transformed = { + '@id': id, + description, + filename, + 'mime-type': mimeType, + lastmod_time: lastmodTime, + byte_count: byteCount, + data, + } + + expect(json).toMatchObject(transformed) + }) + + it('should return the data correctly if only JSON exists', () => { + const decorator = JsonTransformer.fromJSON(mockJson, Attachment) + + const gotData = decorator.getDataAsJson() + expect(decorator.data.json).toEqual(gotData) + }) + + it('should return the data correctly if only Base64 exists', () => { + const decorator = JsonTransformer.fromJSON(mockJsonBase64, Attachment) + + const gotData = decorator.getDataAsJson() + expect(mockJson.data.json).toEqual(gotData) + }) + + describe('addJws', () => { + it('correctly adds the jws to the data', async () => { + const base64 = JsonEncoder.toBase64(didJwsz6Mkf.DATA_JSON) + const attachment = new Attachment({ + id: 'some-uuid', + data: new AttachmentData({ + base64, + }), + }) + + expect(attachment.data.jws).toBeUndefined() + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { payload, ...detachedJws } = didJwsz6Mkf.JWS_JSON + attachment.addJws(didJwsz6Mkf.JWS_JSON) + expect(attachment.data.jws).toEqual(detachedJws) + + attachment.addJws(didJwsz6Mkv.JWS_JSON) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { payload: payload2, ...detachedJws2 } = didJwsz6Mkv.JWS_JSON + expect(attachment.data.jws).toEqual({ signatures: [detachedJws, detachedJws2] }) + + expect(JsonTransformer.toJSON(attachment)).toMatchObject({ + '@id': 'some-uuid', + data: { + base64: JsonEncoder.toBase64(didJwsz6Mkf.DATA_JSON), + jws: { signatures: [detachedJws, detachedJws2] }, + }, + }) + }) + }) +}) diff --git a/packages/core/src/decorators/l10n/L10nDecorator.test.ts b/packages/core/src/decorators/l10n/L10nDecorator.test.ts new file mode 100644 index 0000000000..5c13b5d0ca --- /dev/null +++ b/packages/core/src/decorators/l10n/L10nDecorator.test.ts @@ -0,0 +1,27 @@ +import { JsonTransformer } from '../../utils/JsonTransformer' + +import { L10nDecorator } from './L10nDecorator' + +describe('Decorators | L10nDecorator', () => { + it('should correctly transform Json to L10nDecorator class', () => { + const locale = 'en' + const decorator = JsonTransformer.fromJSON({ locale }, L10nDecorator) + + expect(decorator.locale).toBe(locale) + }) + + it('should correctly transform L10nDecorator class to Json', () => { + const locale = 'nl' + + const decorator = new L10nDecorator({ + locale, + }) + + const json = JsonTransformer.toJSON(decorator) + const transformed = { + locale, + } + + expect(json).toEqual(transformed) + }) +}) diff --git a/packages/core/src/decorators/l10n/L10nDecorator.ts b/packages/core/src/decorators/l10n/L10nDecorator.ts new file mode 100644 index 0000000000..b7f03c4011 --- /dev/null +++ b/packages/core/src/decorators/l10n/L10nDecorator.ts @@ -0,0 +1,10 @@ +/** + * Represents `~l10n` decorator + */ +export class L10nDecorator { + public constructor(partial?: Partial) { + this.locale = partial?.locale + } + + public locale?: string +} diff --git a/packages/core/src/decorators/l10n/L10nDecoratorExtension.ts b/packages/core/src/decorators/l10n/L10nDecoratorExtension.ts new file mode 100644 index 0000000000..ddadc06354 --- /dev/null +++ b/packages/core/src/decorators/l10n/L10nDecoratorExtension.ts @@ -0,0 +1,31 @@ +import type { BaseMessageConstructor } from '../../agent/BaseMessage' + +import { Expose, Type } from 'class-transformer' +import { IsInstance, IsOptional, ValidateNested } from 'class-validator' + +import { L10nDecorator } from './L10nDecorator' + +export function L10nDecorated(Base: T) { + class L10nDecoratorExtension extends Base { + @Expose({ name: '~l10n' }) + @Type(() => L10nDecorator) + @ValidateNested() + @IsOptional() + @IsInstance(L10nDecorator) + public l10n?: L10nDecorator + + public addLocale(locale: string) { + this.l10n = new L10nDecorator({ + locale, + }) + } + + public getLocale(): string | undefined { + if (this.l10n?.locale) return this.l10n.locale + + return undefined + } + } + + return L10nDecoratorExtension +} diff --git a/packages/core/src/decorators/service/ServiceDecorator.test.ts b/packages/core/src/decorators/service/ServiceDecorator.test.ts new file mode 100644 index 0000000000..69936286a3 --- /dev/null +++ b/packages/core/src/decorators/service/ServiceDecorator.test.ts @@ -0,0 +1,36 @@ +import { BaseMessage } from '../../agent/BaseMessage' +import { JsonTransformer } from '../../utils/JsonTransformer' +import { Compose } from '../../utils/mixins' + +import { ServiceDecorated } from './ServiceDecoratorExtension' + +describe('Decorators | ServiceDecoratorExtension', () => { + class TestMessage extends Compose(BaseMessage, [ServiceDecorated]) { + public toJSON(): Record { + return JsonTransformer.toJSON(this) + } + } + + const service = { + recipientKeys: ['test', 'test'], + routingKeys: ['test', 'test'], + serviceEndpoint: 'https://example.com', + } + + test('transforms ServiceDecorator class to JSON', () => { + const message = new TestMessage() + + message.setService(service) + expect(message.toJSON()).toEqual({ '~service': service }) + }) + + test('transforms Json to ServiceDecorator class', () => { + const transformed = JsonTransformer.fromJSON( + { '@id': 'randomID', '@type': 'https://didcomm.org/fake-protocol/1.5/message', '~service': service }, + TestMessage + ) + + expect(transformed.service).toEqual(service) + expect(transformed).toBeInstanceOf(TestMessage) + }) +}) diff --git a/packages/core/src/decorators/service/ServiceDecorator.ts b/packages/core/src/decorators/service/ServiceDecorator.ts new file mode 100644 index 0000000000..793509d678 --- /dev/null +++ b/packages/core/src/decorators/service/ServiceDecorator.ts @@ -0,0 +1,57 @@ +import type { ResolvedDidCommService } from '../../modules/didcomm' + +import { IsArray, IsOptional, IsString } from 'class-validator' + +import { verkeyToInstanceOfKey } from '../../modules/dids/helpers' +import { uuid } from '../../utils/uuid' + +export interface ServiceDecoratorOptions { + recipientKeys: string[] + routingKeys?: string[] + serviceEndpoint: string +} + +/** + * Represents `~service` decorator + * + * Based on specification Aries RFC 0056: Service Decorator + * @see https://github.com/hyperledger/aries-rfcs/tree/master/features/0056-service-decorator + */ +export class ServiceDecorator { + public constructor(options: ServiceDecoratorOptions) { + if (options) { + this.recipientKeys = options.recipientKeys + this.routingKeys = options.routingKeys + this.serviceEndpoint = options.serviceEndpoint + } + } + + @IsArray() + @IsString({ each: true }) + public recipientKeys!: string[] + + @IsArray() + @IsString({ each: true }) + @IsOptional() + public routingKeys?: string[] + + @IsString() + public serviceEndpoint!: string + + public get resolvedDidCommService(): ResolvedDidCommService { + return { + id: uuid(), + recipientKeys: this.recipientKeys.map(verkeyToInstanceOfKey), + routingKeys: this.routingKeys?.map(verkeyToInstanceOfKey) ?? [], + serviceEndpoint: this.serviceEndpoint, + } + } + + public static fromResolvedDidCommService(service: ResolvedDidCommService): ServiceDecorator { + return new ServiceDecorator({ + recipientKeys: service.recipientKeys.map((k) => k.publicKeyBase58), + routingKeys: service.routingKeys.map((k) => k.publicKeyBase58), + serviceEndpoint: service.serviceEndpoint, + }) + } +} diff --git a/packages/core/src/decorators/service/ServiceDecoratorExtension.ts b/packages/core/src/decorators/service/ServiceDecoratorExtension.ts new file mode 100644 index 0000000000..776e8f702f --- /dev/null +++ b/packages/core/src/decorators/service/ServiceDecoratorExtension.ts @@ -0,0 +1,23 @@ +import type { ServiceDecoratorOptions } from './ServiceDecorator' +import type { BaseMessageConstructor } from '../../agent/BaseMessage' + +import { Expose, Type } from 'class-transformer' +import { IsOptional, ValidateNested } from 'class-validator' + +import { ServiceDecorator } from './ServiceDecorator' + +export function ServiceDecorated(Base: T) { + class ServiceDecoratorExtension extends Base { + @Expose({ name: '~service' }) + @Type(() => ServiceDecorator) + @IsOptional() + @ValidateNested() + public service?: ServiceDecorator + + public setService(serviceData: ServiceDecoratorOptions) { + this.service = new ServiceDecorator(serviceData) + } + } + + return ServiceDecoratorExtension +} diff --git a/packages/core/src/decorators/signature/SignatureDecorator.ts b/packages/core/src/decorators/signature/SignatureDecorator.ts new file mode 100644 index 0000000000..2e87ea66f5 --- /dev/null +++ b/packages/core/src/decorators/signature/SignatureDecorator.ts @@ -0,0 +1,39 @@ +import { Expose, Transform } from 'class-transformer' +import { IsString, Matches } from 'class-validator' + +import { MessageTypeRegExp } from '../../agent/BaseMessage' +import { replaceLegacyDidSovPrefix } from '../../utils/messageType' + +/** + * Represents `[field]~sig` decorator + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0234-signature-decorator/README.md + */ +export class SignatureDecorator { + public constructor(options: SignatureDecorator) { + if (options) { + this.signatureType = options.signatureType + this.signatureData = options.signatureData + this.signer = options.signer + this.signature = options.signature + } + } + + @Expose({ name: '@type' }) + @Transform(({ value }) => replaceLegacyDidSovPrefix(value), { + toClassOnly: true, + }) + @Matches(MessageTypeRegExp) + public signatureType!: string + + @Expose({ name: 'sig_data' }) + @IsString() + public signatureData!: string + + @Expose({ name: 'signer' }) + @IsString() + public signer!: string + + @Expose({ name: 'signature' }) + @IsString() + public signature!: string +} diff --git a/packages/core/src/decorators/signature/SignatureDecoratorUtils.test.ts b/packages/core/src/decorators/signature/SignatureDecoratorUtils.test.ts new file mode 100644 index 0000000000..e3fbdbfe11 --- /dev/null +++ b/packages/core/src/decorators/signature/SignatureDecoratorUtils.test.ts @@ -0,0 +1,86 @@ +import type { Wallet } from '../../wallet' + +import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' +import { getAgentConfig } from '../../../tests/helpers' +import { KeyType } from '../../crypto' +import { TypedArrayEncoder } from '../../utils' + +import { SignatureDecorator } from './SignatureDecorator' +import { signData, unpackAndVerifySignatureDecorator } from './SignatureDecoratorUtils' + +jest.mock('../../utils/timestamp', () => { + return { + __esModule: true, + default: jest.fn(() => Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0)), + } +}) + +describe('Decorators | Signature | SignatureDecoratorUtils', () => { + const data = { + did: 'did', + did_doc: { + '@context': 'https://w3id.org/did/v1', + service: [ + { + id: 'did:example:123456789abcdefghi#did-communication', + type: 'did-communication', + priority: 0, + recipientKeys: ['someVerkey'], + routingKeys: [], + serviceEndpoint: 'https://agent.example.com/', + }, + ], + }, + } + + const signedData = new SignatureDecorator({ + signatureType: 'https://didcomm.org/signature/1.0/ed25519Sha512_single', + signature: 'zOSmKNCHKqOJGDJ6OlfUXTPJiirEAXrFn1kPiFDZfvG5hNTBKhsSzqAvlg44apgWBu7O57vGWZsXBF2BWZ5JAw', + signatureData: + 'AAAAAAAAAAB7ImRpZCI6ImRpZCIsImRpZF9kb2MiOnsiQGNvbnRleHQiOiJodHRwczovL3czaWQub3JnL2RpZC92MSIsInNlcnZpY2UiOlt7ImlkIjoiZGlkOmV4YW1wbGU6MTIzNDU2Nzg5YWJjZGVmZ2hpI2RpZC1jb21tdW5pY2F0aW9uIiwidHlwZSI6ImRpZC1jb21tdW5pY2F0aW9uIiwicHJpb3JpdHkiOjAsInJlY2lwaWVudEtleXMiOlsic29tZVZlcmtleSJdLCJyb3V0aW5nS2V5cyI6W10sInNlcnZpY2VFbmRwb2ludCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20vIn1dfX0', + signer: 'GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa', + }) + + let wallet: Wallet + + beforeAll(async () => { + const config = getAgentConfig('SignatureDecoratorUtilsTest') + wallet = new InMemoryWallet() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await wallet.createAndOpen(config.walletConfig!) + }) + + afterAll(async () => { + await wallet.delete() + }) + + test('signData signs json object and returns SignatureDecorator', async () => { + const privateKey = TypedArrayEncoder.fromString('00000000000000000000000000000My1') + const key = await wallet.createKey({ privateKey, keyType: KeyType.Ed25519 }) + + const result = await signData(data, wallet, key.publicKeyBase58) + + expect(result).toEqual(signedData) + }) + + test('unpackAndVerifySignatureDecorator unpacks signature decorator and verifies signature', async () => { + const result = await unpackAndVerifySignatureDecorator(signedData, wallet) + expect(result).toEqual(data) + }) + + test('unpackAndVerifySignatureDecorator throws when signature is not valid', async () => { + const wrongSignature = '6sblL1+OMlTFB3KhIQ8HKKZga8te7NAJAmBVPg2WzNYjMHVjfm+LJP6ZS1GUc2FRtfczRyLEfXrXb86SnzBmBA==' + + const wronglySignedData = new SignatureDecorator({ + ...signedData, + signature: wrongSignature, + }) + + expect.assertions(1) + try { + await unpackAndVerifySignatureDecorator(wronglySignedData, wallet) + } catch (error) { + expect(error.message).toEqual('Signature is not valid') + } + }) +}) diff --git a/packages/core/src/decorators/signature/SignatureDecoratorUtils.ts b/packages/core/src/decorators/signature/SignatureDecoratorUtils.ts new file mode 100644 index 0000000000..55eecf2538 --- /dev/null +++ b/packages/core/src/decorators/signature/SignatureDecoratorUtils.ts @@ -0,0 +1,65 @@ +import type { Wallet } from '../../wallet/Wallet' + +import { Key, KeyType } from '../../crypto' +import { CredoError } from '../../error' +import { JsonEncoder } from '../../utils/JsonEncoder' +import { TypedArrayEncoder } from '../../utils/TypedArrayEncoder' +import { Buffer } from '../../utils/buffer' +import timestamp from '../../utils/timestamp' + +import { SignatureDecorator } from './SignatureDecorator' + +/** + * Unpack and verify signed data before casting it to the supplied type. + * + * @param decorator Signature decorator to unpack and verify + * @param wallet wallet instance + * + * @return Resulting data + */ +export async function unpackAndVerifySignatureDecorator( + decorator: SignatureDecorator, + wallet: Wallet +): Promise> { + const signerVerkey = decorator.signer + const key = Key.fromPublicKeyBase58(signerVerkey, KeyType.Ed25519) + + // first 8 bytes are for 64 bit integer from unix epoch + const signedData = TypedArrayEncoder.fromBase64(decorator.signatureData) + const signature = TypedArrayEncoder.fromBase64(decorator.signature) + + // const isValid = await wallet.verify(signerVerkey, signedData, signature) + const isValid = await wallet.verify({ signature, data: signedData, key }) + + if (!isValid) { + throw new CredoError('Signature is not valid') + } + + // TODO: return Connection instance instead of raw json + return JsonEncoder.fromBuffer(signedData.slice(8)) +} + +/** + * Sign data supplied and return a signature decorator. + * + * @param data the data to sign + * @param wallet the wallet containing a key to use for signing + * @param signerKey signers verkey + * + * @returns Resulting signature decorator. + */ +export async function signData(data: unknown, wallet: Wallet, signerKey: string): Promise { + const dataBuffer = Buffer.concat([timestamp(), JsonEncoder.toBuffer(data)]) + const key = Key.fromPublicKeyBase58(signerKey, KeyType.Ed25519) + + const signatureBuffer = await wallet.sign({ key, data: dataBuffer }) + + const signatureDecorator = new SignatureDecorator({ + signatureType: 'https://didcomm.org/signature/1.0/ed25519Sha512_single', + signature: TypedArrayEncoder.toBase64URL(signatureBuffer), + signatureData: TypedArrayEncoder.toBase64URL(dataBuffer), + signer: signerKey, + }) + + return signatureDecorator +} diff --git a/packages/core/src/decorators/thread/ThreadDecorator.test.ts b/packages/core/src/decorators/thread/ThreadDecorator.test.ts new file mode 100644 index 0000000000..18ef71ab6f --- /dev/null +++ b/packages/core/src/decorators/thread/ThreadDecorator.test.ts @@ -0,0 +1,48 @@ +import { JsonTransformer } from '../../utils/JsonTransformer' + +import { ThreadDecorator } from './ThreadDecorator' + +describe('Decorators | ThreadDecorator', () => { + it('should correctly transform Json to ThreadDecorator class', () => { + const json = { + thid: 'ceffce22-6471-43e4-8945-b604091981c9', + pthid: '917a109d-eae3-42bc-9436-b02426d3ce2c', + sender_order: 2, + received_orders: { + 'did:sov:3ecf688c-cb3f-467b-8636-6b0c7f1d9022': 1, + }, + } + const decorator = JsonTransformer.fromJSON(json, ThreadDecorator) + + expect(decorator.threadId).toBe(json.thid) + expect(decorator.parentThreadId).toBe(json.pthid) + expect(decorator.senderOrder).toBe(json.sender_order) + expect(decorator.receivedOrders).toEqual(json.received_orders) + }) + + it('should correctly transform ThreadDecorator class to Json', () => { + const threadId = 'ceffce22-6471-43e4-8945-b604091981c9' + const parentThreadId = '917a109d-eae3-42bc-9436-b02426d3ce2c' + const senderOrder = 2 + const receivedOrders = { + 'did:sov:3ecf688c-cb3f-467b-8636-6b0c7f1d9022': 1, + } + + const decorator = new ThreadDecorator({ + threadId, + parentThreadId, + senderOrder, + receivedOrders, + }) + + const json = JsonTransformer.toJSON(decorator) + const transformed = { + thid: threadId, + pthid: parentThreadId, + sender_order: senderOrder, + received_orders: receivedOrders, + } + + expect(json).toEqual(transformed) + }) +}) diff --git a/packages/core/src/decorators/thread/ThreadDecorator.ts b/packages/core/src/decorators/thread/ThreadDecorator.ts new file mode 100644 index 0000000000..6614870ef1 --- /dev/null +++ b/packages/core/src/decorators/thread/ThreadDecorator.ts @@ -0,0 +1,49 @@ +import { Expose } from 'class-transformer' +import { IsInt, IsOptional, Matches } from 'class-validator' + +import { MessageIdRegExp } from '../../agent/BaseMessage' + +/** + * Represents `~thread` decorator + * @see https://github.com/hyperledger/aries-rfcs/blob/master/concepts/0008-message-id-and-threading/README.md + */ +export class ThreadDecorator { + public constructor(partial?: Partial) { + this.threadId = partial?.threadId + this.parentThreadId = partial?.parentThreadId + this.senderOrder = partial?.senderOrder + this.receivedOrders = partial?.receivedOrders + } + + /** + * The ID of the message that serves as the thread start. + */ + @Expose({ name: 'thid' }) + @Matches(MessageIdRegExp) + @IsOptional() + public threadId?: string + + /** + * An optional parent `thid`. Used when branching or nesting a new interaction off of an existing one. + */ + @Expose({ name: 'pthid' }) + @Matches(MessageIdRegExp) + @IsOptional() + public parentThreadId?: string + + /** + * A number that tells where this message fits in the sequence of all messages that the current sender has contributed to this thread. + */ + @Expose({ name: 'sender_order' }) + @IsOptional() + @IsInt() + public senderOrder?: number + + /** + * Reports the highest `sender_order` value that the sender has seen from other sender(s) on the thread. + * This value is often missing if it is the first message in an interaction, but should be used otherwise, as it provides an implicit ACK. + */ + @Expose({ name: 'received_orders' }) + @IsOptional() + public receivedOrders?: { [key: string]: number } +} diff --git a/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts b/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts new file mode 100644 index 0000000000..5ba09d8461 --- /dev/null +++ b/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts @@ -0,0 +1,30 @@ +import type { BaseMessageConstructor } from '../../agent/BaseMessage' + +import { Expose, Type } from 'class-transformer' +import { IsInstance, IsOptional, ValidateNested } from 'class-validator' + +import { ThreadDecorator } from './ThreadDecorator' + +export function ThreadDecorated(Base: T) { + class ThreadDecoratorExtension extends Base { + /** + * The ~thread decorator is generally required on any type of response, since this is what connects it with the original request. + */ + @Expose({ name: '~thread' }) + @IsOptional() + @Type(() => ThreadDecorator) + @ValidateNested() + @IsInstance(ThreadDecorator) + public thread?: ThreadDecorator + + public get threadId(): string { + return this.thread?.threadId ?? this.id + } + + public setThread(options: Partial) { + this.thread = new ThreadDecorator(options) + } + } + + return ThreadDecoratorExtension +} diff --git a/packages/core/src/decorators/timing/TimingDecorator.test.ts b/packages/core/src/decorators/timing/TimingDecorator.test.ts new file mode 100644 index 0000000000..321eb44579 --- /dev/null +++ b/packages/core/src/decorators/timing/TimingDecorator.test.ts @@ -0,0 +1,54 @@ +import { JsonTransformer } from '../../utils/JsonTransformer' + +import { TimingDecorator } from './TimingDecorator' + +describe('Decorators | TimingDecorator', () => { + it('should correctly transform Json to TimingDecorator class', () => { + const json = { + in_time: '2019-01-23 18:03:27.123Z', + out_time: '2019-01-23 18:03:27.123Z', + stale_time: '2019-01-24 18:25Z', + expires_time: '2019-01-25 18:25Z', + delay_milli: 12345, + wait_until_time: '2019-01-24 00:00Z', + } + const decorator = JsonTransformer.fromJSON(json, TimingDecorator) + + expect(decorator.inTime).toBeInstanceOf(Date) + expect(decorator.outTime).toBeInstanceOf(Date) + expect(decorator.staleTime).toBeInstanceOf(Date) + expect(decorator.expiresTime).toBeInstanceOf(Date) + expect(decorator.delayMilli).toBe(json.delay_milli) + expect(decorator.waitUntilTime).toBeInstanceOf(Date) + }) + + it('should correctly transform TimingDecorator class to Json', () => { + const inTime = new Date('2019-01-23 18:03:27.123Z') + const outTime = new Date('2019-01-23 18:03:27.123Z') + const staleTime = new Date('2019-01-24 18:25:00.000Z') + const expiresTime = new Date('2019-01-25 18:25:00:000Z') + const delayMilli = 12345 + const waitUntilTime = new Date('2019-01-24 00:00:00.000Z') + + const decorator = new TimingDecorator({ + inTime, + outTime, + staleTime, + expiresTime, + delayMilli, + waitUntilTime, + }) + + const jsonString = JsonTransformer.serialize(decorator) + const transformed = JSON.stringify({ + in_time: '2019-01-23T18:03:27.123Z', + out_time: '2019-01-23T18:03:27.123Z', + stale_time: '2019-01-24T18:25:00.000Z', + expires_time: '2019-01-25T18:25:00.000Z', + delay_milli: 12345, + wait_until_time: '2019-01-24T00:00:00.000Z', + }) + + expect(jsonString).toEqual(transformed) + }) +}) diff --git a/src/lib/decorators/timing/TimingDecorator.ts b/packages/core/src/decorators/timing/TimingDecorator.ts similarity index 78% rename from src/lib/decorators/timing/TimingDecorator.ts rename to packages/core/src/decorators/timing/TimingDecorator.ts index 1de3a5a1e8..68a2b90afd 100644 --- a/src/lib/decorators/timing/TimingDecorator.ts +++ b/packages/core/src/decorators/timing/TimingDecorator.ts @@ -1,5 +1,5 @@ -import { Expose, Type } from 'class-transformer'; -import { IsDate, IsNumber } from 'class-validator'; +import { Expose, Type } from 'class-transformer' +import { IsDate, IsNumber, IsOptional } from 'class-validator' /** * Represents `~timing` decorator @@ -7,12 +7,12 @@ import { IsDate, IsNumber } from 'class-validator'; */ export class TimingDecorator { public constructor(partial?: Partial) { - this.inTime = partial?.inTime; - this.outTime = partial?.outTime; - this.staleTime = partial?.staleTime; - this.expiresTime = partial?.expiresTime; - this.delayMilli = partial?.delayMilli; - this.waitUntilTime = partial?.waitUntilTime; + this.inTime = partial?.inTime + this.outTime = partial?.outTime + this.staleTime = partial?.staleTime + this.expiresTime = partial?.expiresTime + this.delayMilli = partial?.delayMilli + this.waitUntilTime = partial?.waitUntilTime } /** @@ -22,7 +22,8 @@ export class TimingDecorator { @Expose({ name: 'in_time' }) @Type(() => Date) @IsDate() - public inTime?: Date; + @IsOptional() + public inTime?: Date /** * The timestamp when the message was emitted. At least millisecond precision is preferred, though second precision is acceptable. @@ -30,7 +31,8 @@ export class TimingDecorator { @Expose({ name: 'out_time' }) @Type(() => Date) @IsDate() - public outTime?: Date; + @IsOptional() + public outTime?: Date /** * Ideally, the decorated message should be processed by the the specified timestamp. After that, the message may become irrelevant or less meaningful than intended. @@ -39,7 +41,8 @@ export class TimingDecorator { @Expose({ name: 'stale_time' }) @Type(() => Date) @IsDate() - public staleTime?: Date; + @IsOptional() + public staleTime?: Date /** * The decorated message should be considered invalid or expired if encountered after the specified timestamp. @@ -51,7 +54,8 @@ export class TimingDecorator { @Expose({ name: 'expires_time' }) @Type(() => Date) @IsDate() - public expiresTime?: Date; + @IsOptional() + public expiresTime?: Date /** * Wait at least this many milliseconds before processing the message. This may be useful to defeat temporal correlation. @@ -59,7 +63,8 @@ export class TimingDecorator { */ @Expose({ name: 'delay_milli' }) @IsNumber() - public delayMilli?: number; + @IsOptional() + public delayMilli?: number /** * Wait until this time before processing the message. @@ -67,5 +72,6 @@ export class TimingDecorator { @Expose({ name: 'wait_until_time' }) @Type(() => Date) @IsDate() - public waitUntilTime?: Date; + @IsOptional() + public waitUntilTime?: Date } diff --git a/packages/core/src/decorators/timing/TimingDecoratorExtension.ts b/packages/core/src/decorators/timing/TimingDecoratorExtension.ts new file mode 100644 index 0000000000..da4ef6a5cc --- /dev/null +++ b/packages/core/src/decorators/timing/TimingDecoratorExtension.ts @@ -0,0 +1,26 @@ +import type { BaseMessageConstructor } from '../../agent/BaseMessage' + +import { Expose, Type } from 'class-transformer' +import { IsInstance, IsOptional, ValidateNested } from 'class-validator' + +import { TimingDecorator } from './TimingDecorator' + +export function TimingDecorated(Base: T) { + class TimingDecoratorExtension extends Base { + /** + * Timing attributes of messages can be described with the ~timing decorator. + */ + @Expose({ name: '~timing' }) + @Type(() => TimingDecorator) + @ValidateNested() + @IsInstance(TimingDecorator) + @IsOptional() + public timing?: TimingDecorator + + public setTiming(options: Partial) { + this.timing = new TimingDecorator(options) + } + } + + return TimingDecoratorExtension +} diff --git a/packages/core/src/decorators/transport/TransportDecorator.test.ts b/packages/core/src/decorators/transport/TransportDecorator.test.ts new file mode 100644 index 0000000000..edea4acd28 --- /dev/null +++ b/packages/core/src/decorators/transport/TransportDecorator.test.ts @@ -0,0 +1,77 @@ +import { ClassValidationError } from '../../error/ClassValidationError' +import { JsonTransformer } from '../../utils/JsonTransformer' +import { MessageValidator } from '../../utils/MessageValidator' + +import { TransportDecorator, ReturnRouteTypes } from './TransportDecorator' + +const validTransport = (transportJson: Record) => + MessageValidator.validateSync(JsonTransformer.fromJSON(transportJson, TransportDecorator)) +const expectValid = (transportJson: Record) => expect(validTransport(transportJson)).toBeUndefined() +const expectInvalid = (transportJson: Record) => + expect(() => validTransport(transportJson)).toThrowError(ClassValidationError) + +const valid = { + all: { + return_route: 'all', + }, + none: { + return_route: 'none', + }, + thread: { + return_route: 'thread', + return_route_thread: '7d5d797c-db60-489f-8787-87bbd1acdb7e', + }, +} + +const invalid = { + random: { + return_route: 'random', + }, + invalidThreadId: { + return_route: 'thread', + return_route_thread: 'invalid', + }, + missingThreadId: { + return_route: 'thread', + }, +} + +describe('Decorators | TransportDecorator', () => { + it('should correctly transform Json to TransportDecorator class', () => { + const decorator = JsonTransformer.fromJSON(valid.thread, TransportDecorator) + + expect(decorator.returnRoute).toBe(valid.thread.return_route) + expect(decorator.returnRouteThread).toBe(valid.thread.return_route_thread) + }) + + it('should correctly transform TransportDecorator class to Json', () => { + const id = 'f6ce6225-087b-46c1-834a-3e7e24116a00' + const decorator = new TransportDecorator({ + returnRoute: ReturnRouteTypes.thread, + returnRouteThread: id, + }) + + const json = JsonTransformer.toJSON(decorator) + const transformed = { + return_route: 'thread', + return_route_thread: id, + } + + expect(json).toEqual(transformed) + }) + + it('should only allow correct return_route values', () => { + expect.assertions(4) + expectValid(valid.all) + expectValid(valid.none) + expectValid(valid.thread) + expectInvalid(invalid.random) + }) + + it('should require return_route_thread when return_route is thread', async () => { + expect.assertions(3) + expectValid(valid.thread) + expectInvalid(invalid.invalidThreadId) + expectInvalid(invalid.missingThreadId) + }) +}) diff --git a/packages/core/src/decorators/transport/TransportDecorator.ts b/packages/core/src/decorators/transport/TransportDecorator.ts new file mode 100644 index 0000000000..8dac5aa4a7 --- /dev/null +++ b/packages/core/src/decorators/transport/TransportDecorator.ts @@ -0,0 +1,37 @@ +import { Expose } from 'class-transformer' +import { IsEnum, ValidateIf, Matches, IsOptional } from 'class-validator' + +import { MessageIdRegExp } from '../../agent/BaseMessage' + +/** + * Return route types. + */ +export enum ReturnRouteTypes { + /** No messages should be returned over this connection. */ + none = 'none', + /** All messages for this key should be returned over this connection. */ + all = 'all', + /** Send all messages matching this thread over this connection. */ + thread = 'thread', +} + +/** + * Represents `~transport` decorator + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0092-transport-return-route/README.md + */ +export class TransportDecorator { + public constructor(partial?: Partial) { + this.returnRoute = partial?.returnRoute + this.returnRouteThread = partial?.returnRouteThread + } + + @Expose({ name: 'return_route' }) + @IsEnum(ReturnRouteTypes) + @IsOptional() + public returnRoute?: ReturnRouteTypes + + @Expose({ name: 'return_route_thread' }) + @ValidateIf((o: TransportDecorator) => o.returnRoute === ReturnRouteTypes.thread) + @Matches(MessageIdRegExp) + public returnRouteThread?: string +} diff --git a/packages/core/src/decorators/transport/TransportDecoratorExtension.ts b/packages/core/src/decorators/transport/TransportDecoratorExtension.ts new file mode 100644 index 0000000000..b62f985cf7 --- /dev/null +++ b/packages/core/src/decorators/transport/TransportDecoratorExtension.ts @@ -0,0 +1,46 @@ +import type { BaseMessageConstructor } from '../../agent/BaseMessage' + +import { Expose, Type } from 'class-transformer' +import { IsInstance, IsOptional, ValidateNested } from 'class-validator' + +import { TransportDecorator, ReturnRouteTypes } from './TransportDecorator' + +export function TransportDecorated(Base: T) { + class TransportDecoratorExtension extends Base { + @Expose({ name: '~transport' }) + @Type(() => TransportDecorator) + @ValidateNested() + @IsOptional() + @IsInstance(TransportDecorator) + public transport?: TransportDecorator + + public setReturnRouting(type: ReturnRouteTypes, thread?: string) { + this.transport = new TransportDecorator({ + returnRoute: type, + returnRouteThread: thread, + }) + } + + public hasReturnRouting(threadId?: string): boolean { + // transport 'none' or undefined always false + if (!this.transport || !this.transport.returnRoute || this.transport.returnRoute === ReturnRouteTypes.none) { + return false + } + // transport 'all' always true + else if (this.transport.returnRoute === ReturnRouteTypes.all) return true + // transport 'thread' with matching thread id is true + else if (this.transport.returnRoute === ReturnRouteTypes.thread && this.transport.returnRouteThread === threadId) + return true + + // transport is thread but threadId is either missing or doesn't match. Return false + return false + } + + public hasAnyReturnRoute() { + const returnRoute = this.transport?.returnRoute + return returnRoute === ReturnRouteTypes.all || returnRoute === ReturnRouteTypes.thread + } + } + + return TransportDecoratorExtension +} diff --git a/packages/core/src/error/BaseError.ts b/packages/core/src/error/BaseError.ts new file mode 100644 index 0000000000..fc8bc133b7 --- /dev/null +++ b/packages/core/src/error/BaseError.ts @@ -0,0 +1,68 @@ +/** + * Copyright 2015 Blake Embrey + * + * 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. + * + * +/ + +/** + * Original code is from project . + * + * Changes to the original code: + * - Use inspect from `object-inspect` insted of Node.js `util` module. + * - Change `inspect()` method signature + */ + +import makeError from 'make-error' +import inspect from 'object-inspect' + +/** + * @internal + */ +export const SEPARATOR_TEXT = `\n\nThe following exception was the direct cause of the above exception:\n\n` + +/** + * Create a new error instance of `cause` property support. + */ +export class BaseError extends makeError.BaseError { + protected constructor(message?: string, public cause?: Error) { + super(message) + + Object.defineProperty(this, 'cause', { + value: cause, + writable: false, + enumerable: false, + configurable: false, + }) + } + + public inspect() { + return fullStack(this) + } +} + +/** + * Capture the full stack trace of any error instance. + */ +export function fullStack(error: Error | BaseError) { + const chain: Error[] = [] + let cause: Error | undefined = error + + while (cause) { + chain.push(cause) + cause = (cause as BaseError).cause + } + + return chain.map((err) => inspect(err, { customInspect: false })).join(SEPARATOR_TEXT) +} diff --git a/packages/core/src/error/ClassValidationError.ts b/packages/core/src/error/ClassValidationError.ts new file mode 100644 index 0000000000..8c766c7c7e --- /dev/null +++ b/packages/core/src/error/ClassValidationError.ts @@ -0,0 +1,26 @@ +import type { ValidationError } from 'class-validator' + +import { CredoError } from './CredoError' + +export class ClassValidationError extends CredoError { + public validationErrors: ValidationError[] + + public validationErrorsToString() { + return this.validationErrors?.map((error) => error.toString(true)).join('\n') ?? '' + } + + public constructor( + message: string, + { classType, cause, validationErrors }: { classType: string; cause?: Error; validationErrors?: ValidationError[] } + ) { + const validationErrorsStringified = validationErrors + ?.map((error) => error.toString(undefined, undefined, undefined, true)) + .join('\n') + super( + `${classType}: ${message} +${validationErrorsStringified}`, + { cause } + ) + this.validationErrors = validationErrors ?? [] + } +} diff --git a/packages/core/src/error/CredoError.ts b/packages/core/src/error/CredoError.ts new file mode 100644 index 0000000000..484f55b958 --- /dev/null +++ b/packages/core/src/error/CredoError.ts @@ -0,0 +1,12 @@ +import { BaseError } from './BaseError' + +export class CredoError extends BaseError { + /** + * Create base CredoError. + * @param message the error message + * @param cause the error that caused this error to be created + */ + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, cause) + } +} diff --git a/packages/core/src/error/MessageSendingError.ts b/packages/core/src/error/MessageSendingError.ts new file mode 100644 index 0000000000..eb511be5e9 --- /dev/null +++ b/packages/core/src/error/MessageSendingError.ts @@ -0,0 +1,14 @@ +import type { OutboundMessageContext } from '../agent/models' + +import { CredoError } from './CredoError' + +export class MessageSendingError extends CredoError { + public outboundMessageContext: OutboundMessageContext + public constructor( + message: string, + { outboundMessageContext, cause }: { outboundMessageContext: OutboundMessageContext; cause?: Error } + ) { + super(message, { cause }) + this.outboundMessageContext = outboundMessageContext + } +} diff --git a/packages/core/src/error/RecordDuplicateError.ts b/packages/core/src/error/RecordDuplicateError.ts new file mode 100644 index 0000000000..c7480b4bcb --- /dev/null +++ b/packages/core/src/error/RecordDuplicateError.ts @@ -0,0 +1,7 @@ +import { CredoError } from './CredoError' + +export class RecordDuplicateError extends CredoError { + public constructor(message: string, { recordType, cause }: { recordType: string; cause?: Error }) { + super(`${recordType}: ${message}`, { cause }) + } +} diff --git a/packages/core/src/error/RecordNotFoundError.ts b/packages/core/src/error/RecordNotFoundError.ts new file mode 100644 index 0000000000..914e721eb3 --- /dev/null +++ b/packages/core/src/error/RecordNotFoundError.ts @@ -0,0 +1,7 @@ +import { CredoError } from './CredoError' + +export class RecordNotFoundError extends CredoError { + public constructor(message: string, { recordType, cause }: { recordType: string; cause?: Error }) { + super(`${recordType}: ${message}`, { cause }) + } +} diff --git a/packages/core/src/error/ValidationErrorUtils.ts b/packages/core/src/error/ValidationErrorUtils.ts new file mode 100644 index 0000000000..de16ea1330 --- /dev/null +++ b/packages/core/src/error/ValidationErrorUtils.ts @@ -0,0 +1,9 @@ +import { ValidationError } from 'class-validator' + +export function isValidationErrorArray(e: ValidationError[] | unknown): boolean { + if (Array.isArray(e)) { + const isErrorArray = e.length > 0 && e.every((err) => err instanceof ValidationError) + return isErrorArray + } + return false +} diff --git a/packages/core/src/error/__tests__/BaseError.test.ts b/packages/core/src/error/__tests__/BaseError.test.ts new file mode 100644 index 0000000000..9d9de3243c --- /dev/null +++ b/packages/core/src/error/__tests__/BaseError.test.ts @@ -0,0 +1,33 @@ +import { BaseError } from '../BaseError' + +class CustomError extends BaseError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, cause) + } +} + +describe('BaseError', () => { + test('pass cause to custom error', () => { + try { + try { + JSON.parse('') + } catch (error) { + try { + throw new CustomError('Custom first error message', { cause: error }) + } catch (innerError) { + throw new CustomError('Custom second error message', { cause: innerError }) + } + } + } catch (customError) { + expect(customError).toBeInstanceOf(CustomError) + expect(customError.message).toEqual('Custom second error message') + + expect(customError.cause).toBeInstanceOf(CustomError) + expect(customError.cause.message).toEqual('Custom first error message') + + expect(customError.cause.cause).toBeInstanceOf(SyntaxError) + expect(customError.cause.cause.message).toEqual('Unexpected end of JSON input') + } + expect.assertions(6) + }) +}) diff --git a/packages/core/src/error/__tests__/ValidationErrorUtils.test.ts b/packages/core/src/error/__tests__/ValidationErrorUtils.test.ts new file mode 100644 index 0000000000..fada3e6d5b --- /dev/null +++ b/packages/core/src/error/__tests__/ValidationErrorUtils.test.ts @@ -0,0 +1,24 @@ +import { ValidationError } from 'class-validator' + +import { isValidationErrorArray } from '../ValidationErrorUtils' + +describe('ValidationErrorUtils', () => { + test('returns true for an array of ValidationErrors', () => { + const error = new ValidationError() + const errorArray = [error, error] + const isErrorArray = isValidationErrorArray(errorArray) + expect(isErrorArray).toBeTruthy + }) + + test('returns false for an array of strings', () => { + const errorArray = ['hello', 'world'] + const isErrorArray = isValidationErrorArray(errorArray) + expect(isErrorArray).toBeFalsy + }) + + test('returns false for a non array', () => { + const error = new ValidationError() + const isErrorArray = isValidationErrorArray(error) + expect(isErrorArray).toBeFalsy + }) +}) diff --git a/packages/core/src/error/index.ts b/packages/core/src/error/index.ts new file mode 100644 index 0000000000..7eb55c0d1f --- /dev/null +++ b/packages/core/src/error/index.ts @@ -0,0 +1,5 @@ +export * from './CredoError' +export * from './RecordNotFoundError' +export * from './RecordDuplicateError' +export * from './ClassValidationError' +export * from './MessageSendingError' diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000000..c193070092 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,103 @@ +// reflect-metadata used for class-transformer + class-validator +import 'reflect-metadata' + +export { MessageReceiver } from './agent/MessageReceiver' +export { Agent } from './agent/Agent' +export { BaseAgent } from './agent/BaseAgent' +export * from './agent' +export type { ModulesMap, DefaultAgentModules, EmptyModuleMap } from './agent/AgentModules' +export { EventEmitter } from './agent/EventEmitter' +export { FeatureRegistry } from './agent/FeatureRegistry' +export { MessageHandler, MessageHandlerInboundMessage } from './agent/MessageHandler' +export { MessageHandlerRegistry } from './agent/MessageHandlerRegistry' +export * from './agent/models' +export { AgentConfig } from './agent/AgentConfig' +export { AgentMessage } from './agent/AgentMessage' +export { Dispatcher } from './agent/Dispatcher' +export { MessageSender } from './agent/MessageSender' +export type { AgentDependencies } from './agent/AgentDependencies' +export { getOutboundMessageContext } from './agent/getOutboundMessageContext' +export type { + InitConfig, + OutboundPackage, + EncryptedMessage, + WalletConfig, + JsonArray, + JsonObject, + JsonValue, + WalletConfigRekey, + WalletExportImportConfig, + WalletStorageConfig, +} from './types' +export { DidCommMimeType, KeyDerivationMethod } from './types' +export type { FileSystem, DownloadToFileOptions } from './storage/FileSystem' +export * from './storage/BaseRecord' +export { DidCommMessageRecord, DidCommMessageRole, DidCommMessageRepository } from './storage/didcomm' +export { Repository } from './storage/Repository' +export * from './storage/RepositoryEvents' +export { StorageService, Query, QueryOptions, SimpleQuery, BaseRecordConstructor } from './storage/StorageService' +export * from './storage/migration' +export { getDirFromFilePath, joinUriParts } from './utils/path' +export { InjectionSymbols } from './constants' +export * from './wallet' +export type { TransportSession } from './agent/TransportService' +export { TransportService } from './agent/TransportService' +export { Attachment, AttachmentData } from './decorators/attachment/Attachment' +export { ServiceDecorator, ServiceDecoratorOptions } from './decorators/service/ServiceDecorator' +export { ReturnRouteTypes } from './decorators/transport/TransportDecorator' + +export * from './plugins' +export * from './transport' +export * from './modules/basic-messages' +export * from './modules/common' +export * from './modules/credentials' +export * from './modules/discover-features' +export * from './modules/message-pickup' +export * from './modules/problem-reports' +export * from './modules/proofs' +export * from './modules/connections' +export * from './modules/routing' +export * from './modules/oob' +export * from './modules/dids' +export * from './modules/vc' +export * from './modules/cache' +export * from './modules/dif-presentation-exchange' +export * from './modules/sd-jwt-vc' +export { + JsonEncoder, + JsonTransformer, + isJsonObject, + isValidJweStructure, + TypedArrayEncoder, + Buffer, + deepEquality, + isDid, + asArray, + equalsIgnoreOrder, + DateTransformer, +} from './utils' +export * from './logger' +export * from './error' +export * from './wallet/error' +export { VersionString } from './utils/version' +export { parseMessageType, IsValidMessageType, replaceLegacyDidSovPrefix } from './utils/messageType' +export type { Constructor, Constructable } from './utils/mixins' +export * from './agent/Events' +export * from './crypto' + +// TODO: clean up util exports +export { encodeAttachment, isLinkedAttachment } from './utils/attachment' +export type { Optional } from './utils' +export { Hasher, HashName } from './utils/Hasher' +export { MessageValidator } from './utils/MessageValidator' +export { LinkedAttachment, LinkedAttachmentOptions } from './utils/LinkedAttachment' +import { parseInvitationUrl } from './utils/parseInvitation' +import { uuid, isValidUuid } from './utils/uuid' + +const utils = { + uuid, + isValidUuid, + parseInvitationUrl, +} + +export { utils } diff --git a/packages/core/src/logger/BaseLogger.ts b/packages/core/src/logger/BaseLogger.ts new file mode 100644 index 0000000000..b35f653166 --- /dev/null +++ b/packages/core/src/logger/BaseLogger.ts @@ -0,0 +1,25 @@ +import type { Logger } from './Logger' + +import { LogLevel } from './Logger' + +export abstract class BaseLogger implements Logger { + public logLevel: LogLevel + + public constructor(logLevel: LogLevel = LogLevel.off) { + this.logLevel = logLevel + } + + public isEnabled(logLevel: LogLevel) { + return logLevel >= this.logLevel + } + + /* eslint-disable @typescript-eslint/no-explicit-any */ + public abstract test(message: string, data?: Record): void + public abstract trace(message: string, data?: Record): void + public abstract debug(message: string, data?: Record): void + public abstract info(message: string, data?: Record): void + public abstract warn(message: string, data?: Record): void + public abstract error(message: string, data?: Record): void + public abstract fatal(message: string, data?: Record): void + /* eslint-enable @typescript-eslint/no-explicit-any */ +} diff --git a/packages/core/src/logger/ConsoleLogger.ts b/packages/core/src/logger/ConsoleLogger.ts new file mode 100644 index 0000000000..f5c92d9d80 --- /dev/null +++ b/packages/core/src/logger/ConsoleLogger.ts @@ -0,0 +1,64 @@ +/* eslint-disable no-console,@typescript-eslint/no-explicit-any */ + +import { BaseLogger } from './BaseLogger' +import { LogLevel } from './Logger' +import { replaceError } from './replaceError' + +export class ConsoleLogger extends BaseLogger { + // Map our log levels to console levels + private consoleLogMap = { + [LogLevel.test]: 'log', + [LogLevel.trace]: 'log', + [LogLevel.debug]: 'debug', + [LogLevel.info]: 'info', + [LogLevel.warn]: 'warn', + [LogLevel.error]: 'error', + [LogLevel.fatal]: 'error', + } as const + + private log(level: Exclude, message: string, data?: Record): void { + // Get console method from mapping + const consoleLevel = this.consoleLogMap[level] + + // Get logger prefix from log level names in enum + const prefix = LogLevel[level].toUpperCase() + + // Return early if logging is not enabled for this level + if (!this.isEnabled(level)) return + + // Log, with or without data + if (data) { + console[consoleLevel](`${prefix}: ${message}`, JSON.stringify(data, replaceError, 2)) + } else { + console[consoleLevel](`${prefix}: ${message}`) + } + } + + public test(message: string, data?: Record): void { + this.log(LogLevel.test, message, data) + } + + public trace(message: string, data?: Record): void { + this.log(LogLevel.trace, message, data) + } + + public debug(message: string, data?: Record): void { + this.log(LogLevel.debug, message, data) + } + + public info(message: string, data?: Record): void { + this.log(LogLevel.info, message, data) + } + + public warn(message: string, data?: Record): void { + this.log(LogLevel.warn, message, data) + } + + public error(message: string, data?: Record): void { + this.log(LogLevel.error, message, data) + } + + public fatal(message: string, data?: Record): void { + this.log(LogLevel.fatal, message, data) + } +} diff --git a/packages/core/src/logger/Logger.ts b/packages/core/src/logger/Logger.ts new file mode 100644 index 0000000000..f5dec3a5a7 --- /dev/null +++ b/packages/core/src/logger/Logger.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export enum LogLevel { + test = 0, + trace = 1, + debug = 2, + info = 3, + warn = 4, + error = 5, + fatal = 6, + off = 7, +} + +export interface Logger { + logLevel: LogLevel + + test(message: string, data?: Record): void + trace(message: string, data?: Record): void + debug(message: string, data?: Record): void + info(message: string, data?: Record): void + warn(message: string, data?: Record): void + error(message: string, data?: Record): void + fatal(message: string, data?: Record): void +} diff --git a/packages/core/src/logger/index.ts b/packages/core/src/logger/index.ts new file mode 100644 index 0000000000..ab3cf90fc6 --- /dev/null +++ b/packages/core/src/logger/index.ts @@ -0,0 +1,3 @@ +export * from './ConsoleLogger' +export * from './BaseLogger' +export * from './Logger' diff --git a/packages/core/src/logger/replaceError.ts b/packages/core/src/logger/replaceError.ts new file mode 100644 index 0000000000..023679e354 --- /dev/null +++ b/packages/core/src/logger/replaceError.ts @@ -0,0 +1,17 @@ +/* + * The replacer parameter allows you to specify a function that replaces values with your own. We can use it to control what gets stringified. + */ +export function replaceError(_: unknown, value: unknown) { + if (value instanceof Error) { + const newValue = Object.getOwnPropertyNames(value).reduce( + (obj, propName) => { + obj[propName] = (value as unknown as Record)[propName] + return obj + }, + { name: value.name } as Record + ) + return newValue + } + + return value +} diff --git a/packages/core/src/modules/basic-messages/BasicMessageEvents.ts b/packages/core/src/modules/basic-messages/BasicMessageEvents.ts new file mode 100644 index 0000000000..f05873f5de --- /dev/null +++ b/packages/core/src/modules/basic-messages/BasicMessageEvents.ts @@ -0,0 +1,14 @@ +import type { BasicMessage } from './messages' +import type { BasicMessageRecord } from './repository' +import type { BaseEvent } from '../../agent/Events' + +export enum BasicMessageEventTypes { + BasicMessageStateChanged = 'BasicMessageStateChanged', +} +export interface BasicMessageStateChangedEvent extends BaseEvent { + type: typeof BasicMessageEventTypes.BasicMessageStateChanged + payload: { + message: BasicMessage + basicMessageRecord: BasicMessageRecord + } +} diff --git a/packages/core/src/modules/basic-messages/BasicMessageRole.ts b/packages/core/src/modules/basic-messages/BasicMessageRole.ts new file mode 100644 index 0000000000..f21a26c756 --- /dev/null +++ b/packages/core/src/modules/basic-messages/BasicMessageRole.ts @@ -0,0 +1,4 @@ +export enum BasicMessageRole { + Sender = 'sender', + Receiver = 'receiver', +} diff --git a/packages/core/src/modules/basic-messages/BasicMessagesApi.ts b/packages/core/src/modules/basic-messages/BasicMessagesApi.ts new file mode 100644 index 0000000000..644926756a --- /dev/null +++ b/packages/core/src/modules/basic-messages/BasicMessagesApi.ts @@ -0,0 +1,111 @@ +import type { BasicMessageRecord } from './repository/BasicMessageRecord' +import type { Query, QueryOptions } from '../../storage/StorageService' + +import { AgentContext } from '../../agent' +import { MessageHandlerRegistry } from '../../agent/MessageHandlerRegistry' +import { MessageSender } from '../../agent/MessageSender' +import { OutboundMessageContext } from '../../agent/models' +import { injectable } from '../../plugins' +import { ConnectionService } from '../connections' + +import { BasicMessageHandler } from './handlers' +import { BasicMessageService } from './services' + +@injectable() +export class BasicMessagesApi { + private basicMessageService: BasicMessageService + private messageSender: MessageSender + private connectionService: ConnectionService + private agentContext: AgentContext + + public constructor( + messageHandlerRegistry: MessageHandlerRegistry, + basicMessageService: BasicMessageService, + messageSender: MessageSender, + connectionService: ConnectionService, + agentContext: AgentContext + ) { + this.basicMessageService = basicMessageService + this.messageSender = messageSender + this.connectionService = connectionService + this.agentContext = agentContext + this.registerMessageHandlers(messageHandlerRegistry) + } + + /** + * Send a message to an active connection + * + * @param connectionId Connection Id + * @param message Message contents + * @throws {RecordNotFoundError} If connection is not found + * @throws {MessageSendingError} If message is undeliverable + * @returns the created record + */ + public async sendMessage(connectionId: string, message: string, parentThreadId?: string) { + const connection = await this.connectionService.getById(this.agentContext, connectionId) + + const { message: basicMessage, record: basicMessageRecord } = await this.basicMessageService.createMessage( + this.agentContext, + message, + connection, + parentThreadId + ) + const outboundMessageContext = new OutboundMessageContext(basicMessage, { + agentContext: this.agentContext, + connection, + associatedRecord: basicMessageRecord, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + return basicMessageRecord + } + + /** + * Retrieve all basic messages matching a given query + * + * @param query The query + * @param queryOptions The query options + * @returns array containing all matching records + */ + public async findAllByQuery(query: Query, queryOptions?: QueryOptions) { + return this.basicMessageService.findAllByQuery(this.agentContext, query, queryOptions) + } + + /** + * Retrieve a basic message record by id + * + * @param basicMessageRecordId The basic message record id + * @throws {RecordNotFoundError} If no record is found + * @return The basic message record + * + */ + public async getById(basicMessageRecordId: string) { + return this.basicMessageService.getById(this.agentContext, basicMessageRecordId) + } + + /** + * Retrieve a basic message record by thread id + * + * @param threadId The thread id + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * @returns The connection record + */ + public async getByThreadId(basicMessageRecordId: string) { + return this.basicMessageService.getByThreadId(this.agentContext, basicMessageRecordId) + } + + /** + * Delete a basic message record by id + * + * @param connectionId the basic message record id + * @throws {RecordNotFoundError} If no record is found + */ + public async deleteById(basicMessageRecordId: string) { + await this.basicMessageService.deleteById(this.agentContext, basicMessageRecordId) + } + + private registerMessageHandlers(messageHandlerRegistry: MessageHandlerRegistry) { + messageHandlerRegistry.registerMessageHandler(new BasicMessageHandler(this.basicMessageService)) + } +} diff --git a/packages/core/src/modules/basic-messages/BasicMessagesModule.ts b/packages/core/src/modules/basic-messages/BasicMessagesModule.ts new file mode 100644 index 0000000000..346b3bd1c4 --- /dev/null +++ b/packages/core/src/modules/basic-messages/BasicMessagesModule.ts @@ -0,0 +1,32 @@ +import type { FeatureRegistry } from '../../agent/FeatureRegistry' +import type { DependencyManager, Module } from '../../plugins' + +import { Protocol } from '../../agent/models' + +import { BasicMessageRole } from './BasicMessageRole' +import { BasicMessagesApi } from './BasicMessagesApi' +import { BasicMessageRepository } from './repository' +import { BasicMessageService } from './services' + +export class BasicMessagesModule implements Module { + public readonly api = BasicMessagesApi + + /** + * Registers the dependencies of the basic message module on the dependency manager. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Services + dependencyManager.registerSingleton(BasicMessageService) + + // Repositories + dependencyManager.registerSingleton(BasicMessageRepository) + + // Features + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/basicmessage/1.0', + roles: [BasicMessageRole.Sender, BasicMessageRole.Receiver], + }) + ) + } +} diff --git a/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts b/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts new file mode 100644 index 0000000000..83dd0c4c01 --- /dev/null +++ b/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts @@ -0,0 +1,81 @@ +import { getAgentContext, getMockConnection } from '../../../../tests/helpers' +import { EventEmitter } from '../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import { BasicMessageRole } from '../BasicMessageRole' +import { BasicMessage } from '../messages' +import { BasicMessageRecord } from '../repository/BasicMessageRecord' +import { BasicMessageRepository } from '../repository/BasicMessageRepository' +import { BasicMessageService } from '../services' + +jest.mock('../repository/BasicMessageRepository') +const BasicMessageRepositoryMock = BasicMessageRepository as jest.Mock +const basicMessageRepository = new BasicMessageRepositoryMock() + +jest.mock('../../../agent/EventEmitter') +const EventEmitterMock = EventEmitter as jest.Mock +const eventEmitter = new EventEmitterMock() + +const agentContext = getAgentContext() + +describe('BasicMessageService', () => { + let basicMessageService: BasicMessageService + const mockConnectionRecord = getMockConnection({ + id: 'd3849ac3-c981-455b-a1aa-a10bea6cead8', + did: 'did:sov:C2SsBf5QUQpqSAQfhu3sd2', + }) + + beforeEach(() => { + basicMessageService = new BasicMessageService(basicMessageRepository, eventEmitter) + }) + + describe('createMessage', () => { + it(`creates message and record, and emits message and basic message record`, async () => { + const { message } = await basicMessageService.createMessage(agentContext, 'hello', mockConnectionRecord) + + expect(message.content).toBe('hello') + + expect(basicMessageRepository.save).toHaveBeenCalledWith(agentContext, expect.any(BasicMessageRecord)) + expect(eventEmitter.emit).toHaveBeenCalledWith(agentContext, { + type: 'BasicMessageStateChanged', + payload: { + basicMessageRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + id: expect.any(String), + sentTime: expect.any(String), + content: 'hello', + role: BasicMessageRole.Sender, + }), + message, + }, + }) + }) + }) + + describe('save', () => { + it(`stores record and emits message and basic message record`, async () => { + const basicMessage = new BasicMessage({ + id: '123', + content: 'message', + }) + + const messageContext = new InboundMessageContext(basicMessage, { agentContext }) + + await basicMessageService.save(messageContext, mockConnectionRecord) + + expect(basicMessageRepository.save).toHaveBeenCalledWith(agentContext, expect.any(BasicMessageRecord)) + expect(eventEmitter.emit).toHaveBeenCalledWith(agentContext, { + type: 'BasicMessageStateChanged', + payload: { + basicMessageRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + id: expect.any(String), + sentTime: basicMessage.sentTime.toISOString(), + content: basicMessage.content, + role: BasicMessageRole.Receiver, + }), + message: messageContext.message, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts b/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts new file mode 100644 index 0000000000..7354486db1 --- /dev/null +++ b/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts @@ -0,0 +1,25 @@ +import { FeatureRegistry } from '../../../agent/FeatureRegistry' +import { DependencyManager } from '../../../plugins/DependencyManager' +import { BasicMessagesModule } from '../BasicMessagesModule' +import { BasicMessageRepository } from '../repository' +import { BasicMessageService } from '../services' + +jest.mock('../../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +jest.mock('../../../agent/FeatureRegistry') +const FeatureRegistryMock = FeatureRegistry as jest.Mock + +const featureRegistry = new FeatureRegistryMock() + +describe('BasicMessagesModule', () => { + test('registers dependencies on the dependency manager', () => { + new BasicMessagesModule().register(dependencyManager, featureRegistry) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(BasicMessageService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(BasicMessageRepository) + }) +}) diff --git a/packages/core/src/modules/basic-messages/__tests__/basic-messages.test.ts b/packages/core/src/modules/basic-messages/__tests__/basic-messages.test.ts new file mode 100644 index 0000000000..57ceae6bb5 --- /dev/null +++ b/packages/core/src/modules/basic-messages/__tests__/basic-messages.test.ts @@ -0,0 +1,161 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../../../../tests/transport/SubjectInboundTransport' +import type { ConnectionRecord } from '../../../modules/connections' + +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' +import { getInMemoryAgentOptions, makeConnection, waitForBasicMessage } from '../../../../tests/helpers' +import testLogger from '../../../../tests/logger' +import { Agent } from '../../../agent/Agent' +import { MessageSendingError, RecordNotFoundError } from '../../../error' +import { BasicMessage } from '../messages' +import { BasicMessageRecord } from '../repository' + +const faberConfig = getInMemoryAgentOptions('Faber Basic Messages', { + endpoints: ['rxjs:faber'], +}) + +const aliceConfig = getInMemoryAgentOptions('Alice Basic Messages', { + endpoints: ['rxjs:alice'], +}) + +describe('Basic Messages E2E', () => { + let faberAgent: Agent + let aliceAgent: Agent + let faberConnection: ConnectionRecord + let aliceConnection: ConnectionRecord + + beforeEach(async () => { + const faberMessages = new Subject() + const aliceMessages = new Subject() + const subjectMap = { + 'rxjs:faber': faberMessages, + 'rxjs:alice': aliceMessages, + } + + faberAgent = new Agent(faberConfig) + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + + aliceAgent = new Agent(aliceConfig) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + ;[aliceConnection, faberConnection] = await makeConnection(aliceAgent, faberAgent) + }) + + afterEach(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice and Faber exchange messages', async () => { + testLogger.test('Alice sends message to Faber') + const helloRecord = await aliceAgent.basicMessages.sendMessage(aliceConnection.id, 'Hello') + + expect(helloRecord.content).toBe('Hello') + + testLogger.test('Faber waits for message from Alice') + await waitForBasicMessage(faberAgent, { + content: 'Hello', + }) + + testLogger.test('Faber sends message to Alice') + const replyRecord = await faberAgent.basicMessages.sendMessage(faberConnection.id, 'How are you?') + expect(replyRecord.content).toBe('How are you?') + + testLogger.test('Alice waits until she receives message from faber') + await waitForBasicMessage(aliceAgent, { + content: 'How are you?', + }) + }) + + test('Alice and Faber exchange messages using threadId', async () => { + testLogger.test('Alice sends message to Faber') + const helloRecord = await aliceAgent.basicMessages.sendMessage(aliceConnection.id, 'Hello') + + expect(helloRecord.content).toBe('Hello') + + testLogger.test('Faber waits for message from Alice') + const helloMessage = await waitForBasicMessage(faberAgent, { + content: 'Hello', + }) + + testLogger.test('Faber sends message to Alice') + const replyRecord = await faberAgent.basicMessages.sendMessage(faberConnection.id, 'How are you?', helloMessage.id) + expect(replyRecord.content).toBe('How are you?') + expect(replyRecord.parentThreadId).toBe(helloMessage.id) + + testLogger.test('Alice waits until she receives message from faber') + const replyMessage = await waitForBasicMessage(aliceAgent, { + content: 'How are you?', + }) + expect(replyMessage.content).toBe('How are you?') + expect(replyMessage.thread?.parentThreadId).toBe(helloMessage.id) + + // Both sender and recipient shall be able to find the threaded messages + // Hello message + const aliceHelloMessage = await aliceAgent.basicMessages.getByThreadId(helloMessage.id) + const faberHelloMessage = await faberAgent.basicMessages.getByThreadId(helloMessage.id) + expect(aliceHelloMessage).toMatchObject({ + content: helloRecord.content, + threadId: helloRecord.threadId, + }) + expect(faberHelloMessage).toMatchObject({ + content: helloRecord.content, + threadId: helloRecord.threadId, + }) + + // Reply message + const aliceReplyMessages = await aliceAgent.basicMessages.findAllByQuery({ parentThreadId: helloMessage.id }) + const faberReplyMessages = await faberAgent.basicMessages.findAllByQuery({ parentThreadId: helloMessage.id }) + expect(aliceReplyMessages.length).toBe(1) + expect(aliceReplyMessages[0]).toMatchObject({ + content: replyRecord.content, + parentThreadId: replyRecord.parentThreadId, + threadId: replyRecord.threadId, + }) + expect(faberReplyMessages.length).toBe(1) + expect(faberReplyMessages[0]).toMatchObject(replyRecord) + }) + + test('Alice is unable to send a message', async () => { + testLogger.test('Alice sends message to Faber that is undeliverable') + + const spy = jest.spyOn(aliceAgent.outboundTransports[0], 'sendMessage').mockRejectedValue(new Error('any error')) + + await expect(aliceAgent.basicMessages.sendMessage(aliceConnection.id, 'Hello')).rejects.toThrowError( + MessageSendingError + ) + try { + await aliceAgent.basicMessages.sendMessage(aliceConnection.id, 'Hello undeliverable') + } catch (error) { + const thrownError = error as MessageSendingError + expect(thrownError.message).toEqual( + `Message is undeliverable to connection ${aliceConnection.id} (${aliceConnection.theirLabel})` + ) + testLogger.test('Error thrown includes the outbound message and recently created record id') + expect(thrownError.outboundMessageContext.associatedRecord).toBeInstanceOf(BasicMessageRecord) + expect(thrownError.outboundMessageContext.message).toBeInstanceOf(BasicMessage) + expect((thrownError.outboundMessageContext.message as BasicMessage).content).toBe('Hello undeliverable') + + testLogger.test('Created record can be found and deleted by id') + const storedRecord = await aliceAgent.basicMessages.getById( + thrownError.outboundMessageContext.associatedRecord!.id + ) + expect(storedRecord).toBeInstanceOf(BasicMessageRecord) + expect(storedRecord.content).toBe('Hello undeliverable') + + await aliceAgent.basicMessages.deleteById(storedRecord.id) + await expect( + aliceAgent.basicMessages.getById(thrownError.outboundMessageContext.associatedRecord!.id) + ).rejects.toThrowError(RecordNotFoundError) + } + spy.mockClear() + }) +}) diff --git a/packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts b/packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts new file mode 100644 index 0000000000..cec6931983 --- /dev/null +++ b/packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts @@ -0,0 +1,18 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { BasicMessageService } from '../services/BasicMessageService' + +import { BasicMessage } from '../messages' + +export class BasicMessageHandler implements MessageHandler { + private basicMessageService: BasicMessageService + public supportedMessages = [BasicMessage] + + public constructor(basicMessageService: BasicMessageService) { + this.basicMessageService = basicMessageService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const connection = messageContext.assertReadyConnection() + await this.basicMessageService.save(messageContext, connection) + } +} diff --git a/packages/core/src/modules/basic-messages/handlers/index.ts b/packages/core/src/modules/basic-messages/handlers/index.ts new file mode 100644 index 0000000000..64f421dd88 --- /dev/null +++ b/packages/core/src/modules/basic-messages/handlers/index.ts @@ -0,0 +1 @@ +export * from './BasicMessageHandler' diff --git a/packages/core/src/modules/basic-messages/index.ts b/packages/core/src/modules/basic-messages/index.ts new file mode 100644 index 0000000000..e0ca5207d1 --- /dev/null +++ b/packages/core/src/modules/basic-messages/index.ts @@ -0,0 +1,7 @@ +export * from './messages' +export * from './services' +export * from './repository' +export * from './BasicMessageEvents' +export * from './BasicMessagesApi' +export * from './BasicMessageRole' +export * from './BasicMessagesModule' diff --git a/packages/core/src/modules/basic-messages/messages/BasicMessage.ts b/packages/core/src/modules/basic-messages/messages/BasicMessage.ts new file mode 100644 index 0000000000..7dd48b5dde --- /dev/null +++ b/packages/core/src/modules/basic-messages/messages/BasicMessage.ts @@ -0,0 +1,39 @@ +import { Expose, Transform } from 'class-transformer' +import { IsDate, IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { DateParser } from '../../../utils/transformers' + +export class BasicMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + + /** + * Create new BasicMessage instance. + * sentTime will be assigned to new Date if not passed, id will be assigned to uuid/v4 if not passed + * @param options + */ + public constructor(options: { content: string; sentTime?: Date; id?: string; locale?: string }) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.sentTime = options.sentTime || new Date() + this.content = options.content + this.addLocale(options.locale || 'en') + } + } + + @IsValidMessageType(BasicMessage.type) + public readonly type = BasicMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/basicmessage/1.0/message') + + @Expose({ name: 'sent_time' }) + @Transform(({ value }) => DateParser(value)) + @IsDate() + public sentTime!: Date + + @Expose({ name: 'content' }) + @IsString() + public content!: string +} diff --git a/packages/core/src/modules/basic-messages/messages/index.ts b/packages/core/src/modules/basic-messages/messages/index.ts new file mode 100644 index 0000000000..40d57b1840 --- /dev/null +++ b/packages/core/src/modules/basic-messages/messages/index.ts @@ -0,0 +1 @@ +export * from './BasicMessage' diff --git a/packages/core/src/modules/basic-messages/repository/BasicMessageRecord.ts b/packages/core/src/modules/basic-messages/repository/BasicMessageRecord.ts new file mode 100644 index 0000000000..42199106c6 --- /dev/null +++ b/packages/core/src/modules/basic-messages/repository/BasicMessageRecord.ts @@ -0,0 +1,65 @@ +import type { RecordTags, TagsBase } from '../../../storage/BaseRecord' +import type { BasicMessageRole } from '../BasicMessageRole' + +import { BaseRecord } from '../../../storage/BaseRecord' +import { uuid } from '../../../utils/uuid' + +export type CustomBasicMessageTags = TagsBase +export type DefaultBasicMessageTags = { + connectionId: string + role: BasicMessageRole + threadId?: string + parentThreadId?: string +} + +export type BasicMessageTags = RecordTags + +export interface BasicMessageStorageProps { + id?: string + createdAt?: Date + connectionId: string + role: BasicMessageRole + tags?: CustomBasicMessageTags + threadId?: string + parentThreadId?: string + content: string + sentTime: string +} + +export class BasicMessageRecord extends BaseRecord { + public content!: string + public sentTime!: string + public connectionId!: string + public role!: BasicMessageRole + public threadId?: string + public parentThreadId?: string + + public static readonly type = 'BasicMessageRecord' + public readonly type = BasicMessageRecord.type + + public constructor(props: BasicMessageStorageProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.content = props.content + this.sentTime = props.sentTime + this.connectionId = props.connectionId + this._tags = props.tags ?? {} + this.role = props.role + this.threadId = props.threadId + this.parentThreadId = props.parentThreadId + } + } + + public getTags() { + return { + ...this._tags, + connectionId: this.connectionId, + role: this.role, + threadId: this.threadId, + parentThreadId: this.parentThreadId, + } + } +} diff --git a/packages/core/src/modules/basic-messages/repository/BasicMessageRepository.ts b/packages/core/src/modules/basic-messages/repository/BasicMessageRepository.ts new file mode 100644 index 0000000000..102a38c9da --- /dev/null +++ b/packages/core/src/modules/basic-messages/repository/BasicMessageRepository.ts @@ -0,0 +1,17 @@ +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { BasicMessageRecord } from './BasicMessageRecord' + +@injectable() +export class BasicMessageRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(BasicMessageRecord, storageService, eventEmitter) + } +} diff --git a/packages/core/src/modules/basic-messages/repository/index.ts b/packages/core/src/modules/basic-messages/repository/index.ts new file mode 100644 index 0000000000..df02a00416 --- /dev/null +++ b/packages/core/src/modules/basic-messages/repository/index.ts @@ -0,0 +1,2 @@ +export * from './BasicMessageRecord' +export * from './BasicMessageRepository' diff --git a/packages/core/src/modules/basic-messages/services/BasicMessageService.ts b/packages/core/src/modules/basic-messages/services/BasicMessageService.ts new file mode 100644 index 0000000000..2cfd5614c3 --- /dev/null +++ b/packages/core/src/modules/basic-messages/services/BasicMessageService.ts @@ -0,0 +1,104 @@ +import type { AgentContext } from '../../../agent' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { Query, QueryOptions } from '../../../storage/StorageService' +import type { ConnectionRecord } from '../../connections/repository/ConnectionRecord' +import type { BasicMessageStateChangedEvent } from '../BasicMessageEvents' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { injectable } from '../../../plugins' +import { BasicMessageEventTypes } from '../BasicMessageEvents' +import { BasicMessageRole } from '../BasicMessageRole' +import { BasicMessage } from '../messages' +import { BasicMessageRecord, BasicMessageRepository } from '../repository' + +@injectable() +export class BasicMessageService { + private basicMessageRepository: BasicMessageRepository + private eventEmitter: EventEmitter + + public constructor(basicMessageRepository: BasicMessageRepository, eventEmitter: EventEmitter) { + this.basicMessageRepository = basicMessageRepository + this.eventEmitter = eventEmitter + } + + public async createMessage( + agentContext: AgentContext, + message: string, + connectionRecord: ConnectionRecord, + parentThreadId?: string + ) { + const basicMessage = new BasicMessage({ content: message }) + + // If no parentThreadid is defined, there is no need to explicitly send a thread decorator + if (parentThreadId) { + basicMessage.setThread({ parentThreadId }) + } + + const basicMessageRecord = new BasicMessageRecord({ + sentTime: basicMessage.sentTime.toISOString(), + content: basicMessage.content, + connectionId: connectionRecord.id, + role: BasicMessageRole.Sender, + threadId: basicMessage.threadId, + parentThreadId, + }) + + await this.basicMessageRepository.save(agentContext, basicMessageRecord) + this.emitStateChangedEvent(agentContext, basicMessageRecord, basicMessage) + + return { message: basicMessage, record: basicMessageRecord } + } + + /** + * @todo use connection from message context + */ + public async save({ message, agentContext }: InboundMessageContext, connection: ConnectionRecord) { + const basicMessageRecord = new BasicMessageRecord({ + sentTime: message.sentTime.toISOString(), + content: message.content, + connectionId: connection.id, + role: BasicMessageRole.Receiver, + threadId: message.threadId, + parentThreadId: message.thread?.parentThreadId, + }) + + await this.basicMessageRepository.save(agentContext, basicMessageRecord) + this.emitStateChangedEvent(agentContext, basicMessageRecord, message) + } + + private emitStateChangedEvent( + agentContext: AgentContext, + basicMessageRecord: BasicMessageRecord, + basicMessage: BasicMessage + ) { + this.eventEmitter.emit(agentContext, { + type: BasicMessageEventTypes.BasicMessageStateChanged, + payload: { message: basicMessage, basicMessageRecord: basicMessageRecord.clone() }, + }) + } + + public async findAllByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ) { + return this.basicMessageRepository.findByQuery(agentContext, query, queryOptions) + } + + public async getById(agentContext: AgentContext, basicMessageRecordId: string) { + return this.basicMessageRepository.getById(agentContext, basicMessageRecordId) + } + + public async getByThreadId(agentContext: AgentContext, threadId: string) { + return this.basicMessageRepository.getSingleByQuery(agentContext, { threadId }) + } + + public async findAllByParentThreadId(agentContext: AgentContext, parentThreadId: string) { + return this.basicMessageRepository.findByQuery(agentContext, { parentThreadId }) + } + + public async deleteById(agentContext: AgentContext, basicMessageRecordId: string) { + const basicMessageRecord = await this.getById(agentContext, basicMessageRecordId) + return this.basicMessageRepository.delete(agentContext, basicMessageRecord) + } +} diff --git a/packages/core/src/modules/basic-messages/services/index.ts b/packages/core/src/modules/basic-messages/services/index.ts new file mode 100644 index 0000000000..a48826839a --- /dev/null +++ b/packages/core/src/modules/basic-messages/services/index.ts @@ -0,0 +1 @@ +export * from './BasicMessageService' diff --git a/packages/core/src/modules/cache/Cache.ts b/packages/core/src/modules/cache/Cache.ts new file mode 100644 index 0000000000..546e03925d --- /dev/null +++ b/packages/core/src/modules/cache/Cache.ts @@ -0,0 +1,7 @@ +import type { AgentContext } from '../../agent/context' + +export interface Cache { + get(agentContext: AgentContext, key: string): Promise + set(agentContext: AgentContext, key: string, value: CacheValue, expiresInSeconds?: number): Promise + remove(agentContext: AgentContext, key: string): Promise +} diff --git a/packages/core/src/modules/cache/CacheModule.ts b/packages/core/src/modules/cache/CacheModule.ts new file mode 100644 index 0000000000..c4d2ba0e5c --- /dev/null +++ b/packages/core/src/modules/cache/CacheModule.ts @@ -0,0 +1,34 @@ +import type { CacheModuleConfigOptions } from './CacheModuleConfig' +import type { DependencyManager, Module } from '../../plugins' +import type { Optional } from '../../utils' + +import { CacheModuleConfig } from './CacheModuleConfig' +import { SingleContextLruCacheRepository } from './singleContextLruCache/SingleContextLruCacheRepository' +import { SingleContextStorageLruCache } from './singleContextLruCache/SingleContextStorageLruCache' + +// CacheModuleOptions makes the credentialProtocols property optional from the config, as it will set it when not provided. +export type CacheModuleOptions = Optional + +export class CacheModule implements Module { + public readonly config: CacheModuleConfig + + public constructor(config?: CacheModuleOptions) { + this.config = new CacheModuleConfig({ + ...config, + cache: + config?.cache ?? + new SingleContextStorageLruCache({ + limit: 500, + }), + }) + } + + public register(dependencyManager: DependencyManager) { + dependencyManager.registerInstance(CacheModuleConfig, this.config) + + // Custom handling for when we're using the SingleContextStorageLruCache + if (this.config.cache instanceof SingleContextStorageLruCache) { + dependencyManager.registerSingleton(SingleContextLruCacheRepository) + } + } +} diff --git a/packages/core/src/modules/cache/CacheModuleConfig.ts b/packages/core/src/modules/cache/CacheModuleConfig.ts new file mode 100644 index 0000000000..ce5aaf99d7 --- /dev/null +++ b/packages/core/src/modules/cache/CacheModuleConfig.ts @@ -0,0 +1,29 @@ +import type { Cache } from './Cache' + +/** + * CacheModuleConfigOptions defines the interface for the options of the CacheModuleConfig class. + */ +export interface CacheModuleConfigOptions { + /** + * Implementation of the {@link Cache} interface. + * + * NOTE: Starting from Credo 0.4.0 the default cache implementation will be {@link InMemoryLruCache} + * @default SingleContextStorageLruCache - with a limit of 500 + * + * + */ + cache: Cache +} + +export class CacheModuleConfig { + private options: CacheModuleConfigOptions + + public constructor(options: CacheModuleConfigOptions) { + this.options = options + } + + /** See {@link CacheModuleConfigOptions.cache} */ + public get cache() { + return this.options.cache + } +} diff --git a/packages/core/src/modules/cache/InMemoryLruCache.ts b/packages/core/src/modules/cache/InMemoryLruCache.ts new file mode 100644 index 0000000000..4f6ba0733e --- /dev/null +++ b/packages/core/src/modules/cache/InMemoryLruCache.ts @@ -0,0 +1,75 @@ +import type { Cache } from './Cache' +import type { AgentContext } from '../../agent/context' + +import { LRUMap } from 'lru_map' + +export interface InMemoryLruCacheOptions { + /** The maximum number of entries allowed in the cache */ + limit: number +} + +/** + * In memory LRU cache. + * + * This cache can be used with multiple agent context instances, however all instances will share the same cache. + * If you need the cache to be isolated per agent context instance, make sure to use a different cache implementation. + */ +export class InMemoryLruCache implements Cache { + private readonly cache: LRUMap + + public constructor({ limit }: InMemoryLruCacheOptions) { + this.cache = new LRUMap(limit) + } + + public async get(agentContext: AgentContext, key: string) { + this.removeExpiredItems() + const item = this.cache.get(key) + + // Does not exist + if (!item) return null + + return item.value as CacheValue + } + + public async set( + agentContext: AgentContext, + key: string, + value: CacheValue, + expiresInSeconds?: number + ): Promise { + this.removeExpiredItems() + let expiresDate = undefined + + if (expiresInSeconds) { + expiresDate = new Date() + expiresDate.setSeconds(expiresDate.getSeconds() + expiresInSeconds) + } + + this.cache.set(key, { + expiresAt: expiresDate?.getTime(), + value, + }) + } + + public clear() { + this.cache.clear() + } + + public async remove(agentContext: AgentContext, key: string): Promise { + this.removeExpiredItems() + this.cache.delete(key) + } + + private removeExpiredItems() { + this.cache.forEach((value, key) => { + if (value.expiresAt && Date.now() > value.expiresAt) { + this.cache.delete(key) + } + }) + } +} + +interface CacheItem { + expiresAt?: number + value: unknown +} diff --git a/packages/core/src/modules/cache/__tests__/CacheModule.test.ts b/packages/core/src/modules/cache/__tests__/CacheModule.test.ts new file mode 100644 index 0000000000..fe38e2e139 --- /dev/null +++ b/packages/core/src/modules/cache/__tests__/CacheModule.test.ts @@ -0,0 +1,42 @@ +import { DependencyManager } from '../../../plugins/DependencyManager' +import { CacheModule } from '../CacheModule' +import { CacheModuleConfig } from '../CacheModuleConfig' +import { InMemoryLruCache } from '../InMemoryLruCache' +import { SingleContextStorageLruCache } from '../singleContextLruCache' +import { SingleContextLruCacheRepository } from '../singleContextLruCache/SingleContextLruCacheRepository' + +jest.mock('../../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +describe('CacheModule', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + test('registers dependencies on the dependency manager', () => { + const cacheModule = new CacheModule({ + cache: new InMemoryLruCache({ limit: 1 }), + }) + cacheModule.register(dependencyManager) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(CacheModuleConfig, cacheModule.config) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(0) + }) + + test('registers cache repository on the dependency manager if the SingleContextStorageLruCache is used', () => { + const cacheModule = new CacheModule({ + cache: new SingleContextStorageLruCache({ limit: 1 }), + }) + cacheModule.register(dependencyManager) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(CacheModuleConfig, cacheModule.config) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(SingleContextLruCacheRepository) + }) +}) diff --git a/packages/core/src/modules/cache/__tests__/CacheModuleConfig.test.ts b/packages/core/src/modules/cache/__tests__/CacheModuleConfig.test.ts new file mode 100644 index 0000000000..9cbc267122 --- /dev/null +++ b/packages/core/src/modules/cache/__tests__/CacheModuleConfig.test.ts @@ -0,0 +1,14 @@ +import { CacheModuleConfig } from '../CacheModuleConfig' +import { InMemoryLruCache } from '../InMemoryLruCache' + +describe('CacheModuleConfig', () => { + test('sets values', () => { + const cache = new InMemoryLruCache({ limit: 1 }) + + const config = new CacheModuleConfig({ + cache, + }) + + expect(config.cache).toEqual(cache) + }) +}) diff --git a/packages/core/src/modules/cache/__tests__/InMemoryLruCache.test.ts b/packages/core/src/modules/cache/__tests__/InMemoryLruCache.test.ts new file mode 100644 index 0000000000..aa802575c4 --- /dev/null +++ b/packages/core/src/modules/cache/__tests__/InMemoryLruCache.test.ts @@ -0,0 +1,43 @@ +import { getAgentContext } from '../../../../tests/helpers' +import { InMemoryLruCache } from '../InMemoryLruCache' + +const agentContext = getAgentContext() + +describe('InMemoryLruCache', () => { + let cache: InMemoryLruCache + + beforeEach(() => { + cache = new InMemoryLruCache({ limit: 2 }) + }) + + it('should set, get and remove a value', async () => { + expect(await cache.get(agentContext, 'item')).toBeNull() + + await cache.set(agentContext, 'item', 'somevalue') + expect(await cache.get(agentContext, 'item')).toBe('somevalue') + + await cache.remove(agentContext, 'item') + expect(await cache.get(agentContext, 'item')).toBeNull() + }) + + it('should remove least recently used entries if entries are added that exceed the limit', async () => { + // Set first value in cache, resolves fine + await cache.set(agentContext, 'one', 'valueone') + expect(await cache.get(agentContext, 'one')).toBe('valueone') + + // Set two more entries in the cache. Third item + // exceeds limit, so first item gets removed + await cache.set(agentContext, 'two', 'valuetwo') + await cache.set(agentContext, 'three', 'valuethree') + expect(await cache.get(agentContext, 'one')).toBeNull() + expect(await cache.get(agentContext, 'two')).toBe('valuetwo') + expect(await cache.get(agentContext, 'three')).toBe('valuethree') + + // Get two from the cache, meaning three will be removed first now + // because it is not recently used + await cache.get(agentContext, 'two') + await cache.set(agentContext, 'four', 'valuefour') + expect(await cache.get(agentContext, 'three')).toBeNull() + expect(await cache.get(agentContext, 'two')).toBe('valuetwo') + }) +}) diff --git a/packages/core/src/modules/cache/index.ts b/packages/core/src/modules/cache/index.ts new file mode 100644 index 0000000000..5b5d932671 --- /dev/null +++ b/packages/core/src/modules/cache/index.ts @@ -0,0 +1,10 @@ +// Module +export { CacheModule, CacheModuleOptions } from './CacheModule' +export { CacheModuleConfig } from './CacheModuleConfig' + +// Cache +export { Cache } from './Cache' + +// Cache Implementations +export { InMemoryLruCache, InMemoryLruCacheOptions } from './InMemoryLruCache' +export { SingleContextStorageLruCache, SingleContextStorageLruCacheOptions } from './singleContextLruCache' diff --git a/packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRecord.ts b/packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRecord.ts new file mode 100644 index 0000000000..257b6b6080 --- /dev/null +++ b/packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRecord.ts @@ -0,0 +1,44 @@ +import type { TagsBase } from '../../../storage/BaseRecord' + +import { Type } from 'class-transformer' + +import { BaseRecord } from '../../../storage/BaseRecord' +import { uuid } from '../../../utils/uuid' + +export interface SingleContextLruCacheItem { + value: unknown + expiresAt?: number +} + +export interface SingleContextLruCacheProps { + id?: string + createdAt?: Date + tags?: TagsBase + + entries: Map +} + +export class SingleContextLruCacheRecord extends BaseRecord { + @Type(() => Object) + public entries!: Map + + public static readonly type = 'SingleContextLruCacheRecord' + public readonly type = SingleContextLruCacheRecord.type + + public constructor(props: SingleContextLruCacheProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.entries = props.entries + this._tags = props.tags ?? {} + } + } + + public getTags() { + return { + ...this._tags, + } + } +} diff --git a/packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRepository.ts b/packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRepository.ts new file mode 100644 index 0000000000..dab71b9761 --- /dev/null +++ b/packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRepository.ts @@ -0,0 +1,17 @@ +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { SingleContextLruCacheRecord } from './SingleContextLruCacheRecord' + +@injectable() +export class SingleContextLruCacheRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(SingleContextLruCacheRecord, storageService, eventEmitter) + } +} diff --git a/packages/core/src/modules/cache/singleContextLruCache/SingleContextStorageLruCache.ts b/packages/core/src/modules/cache/singleContextLruCache/SingleContextStorageLruCache.ts new file mode 100644 index 0000000000..0d46df0ae4 --- /dev/null +++ b/packages/core/src/modules/cache/singleContextLruCache/SingleContextStorageLruCache.ts @@ -0,0 +1,171 @@ +import type { SingleContextLruCacheItem } from './SingleContextLruCacheRecord' +import type { AgentContext } from '../../../agent/context' +import type { Cache } from '../Cache' + +import { LRUMap } from 'lru_map' + +import { CredoError, RecordDuplicateError } from '../../../error' + +import { SingleContextLruCacheRecord } from './SingleContextLruCacheRecord' +import { SingleContextLruCacheRepository } from './SingleContextLruCacheRepository' + +const CONTEXT_STORAGE_LRU_CACHE_ID = 'CONTEXT_STORAGE_LRU_CACHE_ID' + +export interface SingleContextStorageLruCacheOptions { + /** The maximum number of entries allowed in the cache */ + limit: number +} + +/** + * Cache that leverages the storage associated with the agent context to store cache records. + * It will keep an in-memory cache of the records to avoid hitting the storage on every read request. + * Therefor this cache is meant to be used with a single instance of the agent. + * + * Due to keeping an in-memory copy of the cache, it is also not meant to be used with multiple + * agent context instances (meaning multi-tenancy), as they will overwrite the in-memory cache. + * + * However, this means the cache is not meant for usage with multiple instances. + */ +export class SingleContextStorageLruCache implements Cache { + private limit: number + private _cache?: LRUMap + private _contextCorrelationId?: string + + public constructor({ limit }: SingleContextStorageLruCacheOptions) { + this.limit = limit + } + + public async get(agentContext: AgentContext, key: string) { + this.assertContextCorrelationId(agentContext) + + const cache = await this.getCache(agentContext) + this.removeExpiredItems(cache) + + const item = cache.get(key) + + // Does not exist + if (!item) return null + + // Expired + if (item.expiresAt && Date.now() > item.expiresAt) { + cache.delete(key) + await this.persistCache(agentContext) + return null + } + + return item.value as CacheValue + } + + public async set( + agentContext: AgentContext, + key: string, + value: CacheValue, + expiresInSeconds?: number + ): Promise { + this.assertContextCorrelationId(agentContext) + + let expiresDate = undefined + + if (expiresInSeconds) { + expiresDate = new Date() + expiresDate.setSeconds(expiresDate.getSeconds() + expiresInSeconds) + } + + const cache = await this.getCache(agentContext) + this.removeExpiredItems(cache) + + cache.set(key, { + expiresAt: expiresDate?.getTime(), + value, + }) + await this.persistCache(agentContext) + } + + public async remove(agentContext: AgentContext, key: string): Promise { + this.assertContextCorrelationId(agentContext) + + const cache = await this.getCache(agentContext) + this.removeExpiredItems(cache) + cache.delete(key) + + await this.persistCache(agentContext) + } + + private async getCache(agentContext: AgentContext) { + if (!this._cache) { + const cacheRecord = await this.fetchCacheRecord(agentContext) + this._cache = this.lruFromRecord(cacheRecord) + } + + return this._cache + } + + private lruFromRecord(cacheRecord: SingleContextLruCacheRecord) { + return new LRUMap(this.limit, cacheRecord.entries.entries()) + } + + private async fetchCacheRecord(agentContext: AgentContext) { + const cacheRepository = agentContext.dependencyManager.resolve(SingleContextLruCacheRepository) + let cacheRecord = await cacheRepository.findById(agentContext, CONTEXT_STORAGE_LRU_CACHE_ID) + + if (!cacheRecord) { + cacheRecord = new SingleContextLruCacheRecord({ + id: CONTEXT_STORAGE_LRU_CACHE_ID, + entries: new Map(), + }) + + try { + await cacheRepository.save(agentContext, cacheRecord) + } catch (error) { + // This addresses some race conditions issues where we first check if the record exists + // then we create one if it doesn't, but another process has created one in the meantime + // Although not the most elegant solution, it addresses the issues + if (error instanceof RecordDuplicateError) { + // the record already exists, which is our intended end state + // we can ignore this error and fetch the existing record + return cacheRepository.getById(agentContext, CONTEXT_STORAGE_LRU_CACHE_ID) + } else { + throw error + } + } + } + + return cacheRecord + } + + private removeExpiredItems(cache: LRUMap) { + cache.forEach((value, key) => { + if (value.expiresAt && Date.now() > value.expiresAt) { + cache.delete(key) + } + }) + } + + private async persistCache(agentContext: AgentContext) { + const cacheRepository = agentContext.dependencyManager.resolve(SingleContextLruCacheRepository) + const cache = await this.getCache(agentContext) + + await cacheRepository.update( + agentContext, + new SingleContextLruCacheRecord({ + entries: new Map(cache.toJSON().map(({ key, value }) => [key, value])), + id: CONTEXT_STORAGE_LRU_CACHE_ID, + }) + ) + } + + /** + * Asserts this class is not used with multiple agent context instances. + */ + private assertContextCorrelationId(agentContext: AgentContext) { + if (!this._contextCorrelationId) { + this._contextCorrelationId = agentContext.contextCorrelationId + } + + if (this._contextCorrelationId !== agentContext.contextCorrelationId) { + throw new CredoError( + 'SingleContextStorageLruCache can not be used with multiple agent context instances. Register a custom cache implementation in the CacheModule.' + ) + } + } +} diff --git a/packages/core/src/modules/cache/singleContextLruCache/__tests__/SingleContextStorageLruCache.test.ts b/packages/core/src/modules/cache/singleContextLruCache/__tests__/SingleContextStorageLruCache.test.ts new file mode 100644 index 0000000000..2251b9b854 --- /dev/null +++ b/packages/core/src/modules/cache/singleContextLruCache/__tests__/SingleContextStorageLruCache.test.ts @@ -0,0 +1,91 @@ +import { getAgentContext, mockFunction } from '../../../../../tests/helpers' +import { SingleContextLruCacheRecord } from '../SingleContextLruCacheRecord' +import { SingleContextLruCacheRepository } from '../SingleContextLruCacheRepository' +import { SingleContextStorageLruCache } from '../SingleContextStorageLruCache' + +jest.mock('../SingleContextLruCacheRepository') +const SingleContextLruCacheRepositoryMock = + SingleContextLruCacheRepository as jest.Mock + +const cacheRepository = new SingleContextLruCacheRepositoryMock() +const agentContext = getAgentContext({ + registerInstances: [[SingleContextLruCacheRepository, cacheRepository]], +}) + +describe('SingleContextLruCache', () => { + let cache: SingleContextStorageLruCache + + beforeEach(() => { + mockFunction(cacheRepository.findById).mockResolvedValue(null) + cache = new SingleContextStorageLruCache({ limit: 2 }) + }) + + it('should return the value from the persisted record', async () => { + const findMock = mockFunction(cacheRepository.findById).mockResolvedValue( + new SingleContextLruCacheRecord({ + id: 'CONTEXT_STORAGE_LRU_CACHE_ID', + entries: new Map([ + [ + 'test', + { + value: 'somevalue', + }, + ], + ]), + }) + ) + + expect(await cache.get(agentContext, 'doesnotexist')).toBeNull() + expect(await cache.get(agentContext, 'test')).toBe('somevalue') + expect(findMock).toHaveBeenCalledWith(agentContext, 'CONTEXT_STORAGE_LRU_CACHE_ID') + }) + + it('should set the value in the persisted record', async () => { + const updateMock = mockFunction(cacheRepository.update).mockResolvedValue() + + await cache.set(agentContext, 'test', 'somevalue') + const [[, cacheRecord]] = updateMock.mock.calls + + expect(cacheRecord.entries.size).toBe(1) + + const [[key, item]] = cacheRecord.entries.entries() + expect(key).toBe('test') + expect(item.value).toBe('somevalue') + + expect(await cache.get(agentContext, 'test')).toBe('somevalue') + }) + + it('should remove least recently used entries if entries are added that exceed the limit', async () => { + // Set first value in cache, resolves fine + await cache.set(agentContext, 'one', 'valueone') + expect(await cache.get(agentContext, 'one')).toBe('valueone') + + // Set two more entries in the cache. Third item + // exceeds limit, so first item gets removed + await cache.set(agentContext, 'two', 'valuetwo') + await cache.set(agentContext, 'three', 'valuethree') + expect(await cache.get(agentContext, 'one')).toBeNull() + expect(await cache.get(agentContext, 'two')).toBe('valuetwo') + expect(await cache.get(agentContext, 'three')).toBe('valuethree') + + // Get two from the cache, meaning three will be removed first now + // because it is not recently used + await cache.get(agentContext, 'two') + await cache.set(agentContext, 'four', 'valuefour') + expect(await cache.get(agentContext, 'three')).toBeNull() + expect(await cache.get(agentContext, 'two')).toBe('valuetwo') + }) + + it('should throw an error if used with multiple context correlation ids', async () => { + // No issue, first call with an agentContext + await cache.get(agentContext, 'test') + + const secondAgentContext = getAgentContext({ + contextCorrelationId: 'another', + }) + + expect(cache.get(secondAgentContext, 'test')).rejects.toThrowError( + 'SingleContextStorageLruCache can not be used with multiple agent context instances. Register a custom cache implementation in the CacheModule.' + ) + }) +}) diff --git a/packages/core/src/modules/cache/singleContextLruCache/index.ts b/packages/core/src/modules/cache/singleContextLruCache/index.ts new file mode 100644 index 0000000000..4d01549062 --- /dev/null +++ b/packages/core/src/modules/cache/singleContextLruCache/index.ts @@ -0,0 +1 @@ +export { SingleContextStorageLruCache, SingleContextStorageLruCacheOptions } from './SingleContextStorageLruCache' diff --git a/packages/core/src/modules/common/index.ts b/packages/core/src/modules/common/index.ts new file mode 100644 index 0000000000..58debe2da7 --- /dev/null +++ b/packages/core/src/modules/common/index.ts @@ -0,0 +1 @@ +export * from './messages/AckMessage' diff --git a/packages/core/src/modules/common/messages/AckMessage.ts b/packages/core/src/modules/common/messages/AckMessage.ts new file mode 100644 index 0000000000..933bfa7620 --- /dev/null +++ b/packages/core/src/modules/common/messages/AckMessage.ts @@ -0,0 +1,47 @@ +import { IsEnum } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +/** + * Ack message status types + */ +export enum AckStatus { + OK = 'OK', + PENDING = 'PENDING', +} + +export interface AckMessageOptions { + id?: string + threadId: string + status: AckStatus +} + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0015-acks/README.md#explicit-acks + */ +export class AckMessage extends AgentMessage { + /** + * Create new AckMessage instance. + * @param options + */ + public constructor(options: AckMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.status = options.status + + this.setThread({ + threadId: options.threadId, + }) + } + } + + @IsValidMessageType(AckMessage.type) + public readonly type: string = AckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/notification/1.0/ack') + + @IsEnum(AckStatus) + public status!: AckStatus +} diff --git a/packages/core/src/modules/connections/ConnectionEvents.ts b/packages/core/src/modules/connections/ConnectionEvents.ts new file mode 100644 index 0000000000..bd8f98989f --- /dev/null +++ b/packages/core/src/modules/connections/ConnectionEvents.ts @@ -0,0 +1,32 @@ +import type { DidExchangeState } from './models' +import type { ConnectionRecord } from './repository/ConnectionRecord' +import type { BaseEvent } from '../../agent/Events' + +export enum ConnectionEventTypes { + ConnectionStateChanged = 'ConnectionStateChanged', + ConnectionDidRotated = 'ConnectionDidRotated', +} + +export interface ConnectionStateChangedEvent extends BaseEvent { + type: typeof ConnectionEventTypes.ConnectionStateChanged + payload: { + connectionRecord: ConnectionRecord + previousState: DidExchangeState | null + } +} + +export interface ConnectionDidRotatedEvent extends BaseEvent { + type: typeof ConnectionEventTypes.ConnectionDidRotated + payload: { + connectionRecord: ConnectionRecord + + ourDid?: { + from: string + to: string + } + theirDid?: { + from: string + to: string + } + } +} diff --git a/packages/core/src/modules/connections/ConnectionsApi.ts b/packages/core/src/modules/connections/ConnectionsApi.ts new file mode 100644 index 0000000000..6251e5a9a9 --- /dev/null +++ b/packages/core/src/modules/connections/ConnectionsApi.ts @@ -0,0 +1,608 @@ +import type { ConnectionType } from './models' +import type { ConnectionRecord } from './repository/ConnectionRecord' +import type { Routing } from './services' +import type { Query, QueryOptions } from '../../storage/StorageService' +import type { OutOfBandRecord } from '../oob/repository' + +import { AgentContext } from '../../agent' +import { MessageHandlerRegistry } from '../../agent/MessageHandlerRegistry' +import { MessageSender } from '../../agent/MessageSender' +import { OutboundMessageContext } from '../../agent/models' +import { ReturnRouteTypes } from '../../decorators/transport/TransportDecorator' +import { CredoError } from '../../error' +import { injectable } from '../../plugins' +import { DidResolverService } from '../dids' +import { DidRepository } from '../dids/repository' +import { OutOfBandService } from '../oob/OutOfBandService' +import { RoutingService } from '../routing/services/RoutingService' +import { getMediationRecordForDidDocument } from '../routing/services/helpers' + +import { ConnectionsModuleConfig } from './ConnectionsModuleConfig' +import { DidExchangeProtocol } from './DidExchangeProtocol' +import { + AckMessageHandler, + ConnectionRequestHandler, + ConnectionResponseHandler, + DidExchangeCompleteHandler, + DidExchangeRequestHandler, + DidExchangeResponseHandler, + TrustPingMessageHandler, + TrustPingResponseMessageHandler, + ConnectionProblemReportHandler, + DidRotateHandler, + DidRotateAckHandler, + DidRotateProblemReportHandler, + HangupHandler, +} from './handlers' +import { HandshakeProtocol } from './models' +import { DidRotateService } from './services' +import { ConnectionService } from './services/ConnectionService' +import { TrustPingService } from './services/TrustPingService' + +export interface SendPingOptions { + responseRequested?: boolean + withReturnRouting?: boolean +} + +@injectable() +export class ConnectionsApi { + /** + * Configuration for the connections module + */ + public readonly config: ConnectionsModuleConfig + + private didExchangeProtocol: DidExchangeProtocol + private connectionService: ConnectionService + private didRotateService: DidRotateService + private outOfBandService: OutOfBandService + private messageSender: MessageSender + private trustPingService: TrustPingService + private routingService: RoutingService + private didRepository: DidRepository + private didResolverService: DidResolverService + private agentContext: AgentContext + + public constructor( + messageHandlerRegistry: MessageHandlerRegistry, + didExchangeProtocol: DidExchangeProtocol, + connectionService: ConnectionService, + didRotateService: DidRotateService, + outOfBandService: OutOfBandService, + trustPingService: TrustPingService, + routingService: RoutingService, + didRepository: DidRepository, + didResolverService: DidResolverService, + messageSender: MessageSender, + agentContext: AgentContext, + connectionsModuleConfig: ConnectionsModuleConfig + ) { + this.didExchangeProtocol = didExchangeProtocol + this.connectionService = connectionService + this.didRotateService = didRotateService + this.outOfBandService = outOfBandService + this.trustPingService = trustPingService + this.routingService = routingService + this.didRepository = didRepository + this.messageSender = messageSender + this.didResolverService = didResolverService + this.agentContext = agentContext + this.config = connectionsModuleConfig + + this.registerMessageHandlers(messageHandlerRegistry) + } + + public async acceptOutOfBandInvitation( + outOfBandRecord: OutOfBandRecord, + config: { + autoAcceptConnection?: boolean + label?: string + alias?: string + imageUrl?: string + protocol: HandshakeProtocol + routing?: Routing + ourDid?: string + } + ) { + const { protocol, label, alias, imageUrl, autoAcceptConnection, ourDid } = config + + if (ourDid && config.routing) { + throw new CredoError(`'routing' is disallowed when defining 'ourDid'`) + } + + // Only generate routing if ourDid hasn't been provided + let routing = config.routing + if (!routing && !ourDid) { + routing = await this.routingService.getRouting(this.agentContext, { mediatorId: outOfBandRecord.mediatorId }) + } + + let result + if (protocol === HandshakeProtocol.DidExchange) { + result = await this.didExchangeProtocol.createRequest(this.agentContext, outOfBandRecord, { + label, + alias, + routing, + autoAcceptConnection, + ourDid, + }) + } else if (protocol === HandshakeProtocol.Connections) { + if (ourDid) { + throw new CredoError('Using an externally defined did for connections protocol is unsupported') + } + // This is just to make TS happy, as we always generate routing if ourDid is not provided + // and ourDid is not supported for connection (see check above) + if (!routing) { + throw new CredoError('Routing is required for connections protocol') + } + + result = await this.connectionService.createRequest(this.agentContext, outOfBandRecord, { + label, + alias, + imageUrl, + routing, + autoAcceptConnection, + }) + } else { + throw new CredoError(`Unsupported handshake protocol ${protocol}.`) + } + + const { message, connectionRecord } = result + const outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connectionRecord, + outOfBand: outOfBandRecord, + }) + await this.messageSender.sendMessage(outboundMessageContext) + return connectionRecord + } + + /** + * Accept a connection request as inviter (by sending a connection response message) for the connection with the specified connection id. + * This is not needed when auto accepting of connection is enabled. + * + * @param connectionId the id of the connection for which to accept the request + * @returns connection record + */ + public async acceptRequest(connectionId: string): Promise { + const connectionRecord = await this.connectionService.findById(this.agentContext, connectionId) + if (!connectionRecord) { + throw new CredoError(`Connection record ${connectionId} not found.`) + } + if (!connectionRecord.outOfBandId) { + throw new CredoError(`Connection record ${connectionId} does not have out-of-band record.`) + } + + const outOfBandRecord = await this.outOfBandService.findById(this.agentContext, connectionRecord.outOfBandId) + if (!outOfBandRecord) { + throw new CredoError(`Out-of-band record ${connectionRecord.outOfBandId} not found.`) + } + + // We generate routing in two scenarios: + // 1. When the out-of-band invitation is reusable, as otherwise all connections use the same keys + // 2. When the out-of-band invitation has no inline services, as we don't want to generate a legacy did doc from a service did + const routing = + outOfBandRecord.reusable || outOfBandRecord.outOfBandInvitation.getInlineServices().length === 0 + ? await this.routingService.getRouting(this.agentContext) + : undefined + + let outboundMessageContext + if (connectionRecord.protocol === HandshakeProtocol.DidExchange) { + const message = await this.didExchangeProtocol.createResponse( + this.agentContext, + connectionRecord, + outOfBandRecord, + routing + ) + outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connectionRecord, + }) + } else { + // We generate routing in two scenarios: + // 1. When the out-of-band invitation is reusable, as otherwise all connections use the same keys + // 2. When the out-of-band invitation has no inline services, as we don't want to generate a legacy did doc from a service did + const routing = + outOfBandRecord.reusable || outOfBandRecord.outOfBandInvitation.getInlineServices().length === 0 + ? await this.routingService.getRouting(this.agentContext) + : undefined + + const { message } = await this.connectionService.createResponse( + this.agentContext, + connectionRecord, + outOfBandRecord, + routing + ) + outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connectionRecord, + }) + } + + await this.messageSender.sendMessage(outboundMessageContext) + return connectionRecord + } + + /** + * Accept a connection response as invitee (by sending a trust ping message) for the connection with the specified connection id. + * This is not needed when auto accepting of connection is enabled. + * + * @param connectionId the id of the connection for which to accept the response + * @returns connection record + */ + public async acceptResponse(connectionId: string): Promise { + const connectionRecord = await this.connectionService.getById(this.agentContext, connectionId) + + let outboundMessageContext + if (connectionRecord.protocol === HandshakeProtocol.DidExchange) { + if (!connectionRecord.outOfBandId) { + throw new CredoError(`Connection ${connectionRecord.id} does not have outOfBandId!`) + } + const outOfBandRecord = await this.outOfBandService.findById(this.agentContext, connectionRecord.outOfBandId) + if (!outOfBandRecord) { + throw new CredoError( + `OutOfBand record for connection ${connectionRecord.id} with outOfBandId ${connectionRecord.outOfBandId} not found!` + ) + } + const message = await this.didExchangeProtocol.createComplete( + this.agentContext, + connectionRecord, + outOfBandRecord + ) + // Disable return routing as we don't want to receive a response for this message over the same channel + // This has led to long timeouts as not all clients actually close an http socket if there is no response message + message.setReturnRouting(ReturnRouteTypes.none) + outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connectionRecord, + }) + } else { + const { message } = await this.connectionService.createTrustPing(this.agentContext, connectionRecord, { + responseRequested: false, + }) + // Disable return routing as we don't want to receive a response for this message over the same channel + // This has led to long timeouts as not all clients actually close an http socket if there is no response message + message.setReturnRouting(ReturnRouteTypes.none) + outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connectionRecord, + }) + } + + await this.messageSender.sendMessage(outboundMessageContext) + return connectionRecord + } + + /** + * Send a trust ping to an established connection + * + * @param connectionId the id of the connection for which to accept the response + * @param responseRequested do we want a response to our ping + * @param withReturnRouting do we want a response at the time of posting + * @returns TrustPingMessage + */ + public async sendPing( + connectionId: string, + { responseRequested = true, withReturnRouting = undefined }: SendPingOptions = {} + ) { + const connection = await this.getById(connectionId) + + const { message } = await this.connectionService.createTrustPing(this.agentContext, connection, { + responseRequested: responseRequested, + }) + + if (withReturnRouting === true) { + message.setReturnRouting(ReturnRouteTypes.all) + } + + // Disable return routing as we don't want to receive a response for this message over the same channel + // This has led to long timeouts as not all clients actually close an http socket if there is no response message + if (withReturnRouting === false) { + message.setReturnRouting(ReturnRouteTypes.none) + } + + await this.messageSender.sendMessage( + new OutboundMessageContext(message, { agentContext: this.agentContext, connection }) + ) + + return message + } + + /** + * Rotate the DID used for a given connection, notifying the other party immediately. + * + * If `toDid` is not specified, a new peer did will be created. Optionally, routing + * configuration can be set. + * + * Note: any did created or imported in agent wallet can be used as `toDid`, as long as + * there are valid DIDComm services in its DID Document. + * + * @param options connectionId and optional target did and routing configuration + * @returns object containing the new did + */ + public async rotate(options: { connectionId: string; toDid?: string; routing?: Routing }) { + const { connectionId, toDid } = options + const connection = await this.connectionService.getById(this.agentContext, connectionId) + + if (toDid && options.routing) { + throw new CredoError(`'routing' is disallowed when defining 'toDid'`) + } + + let routing = options.routing + if (!toDid && !routing) { + routing = await this.routingService.getRouting(this.agentContext, {}) + } + + const message = await this.didRotateService.createRotate(this.agentContext, { + connection, + toDid, + routing, + }) + + const outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + + return { newDid: message.toDid } + } + + /** + * Terminate a connection by sending a hang-up message to the other party. The connection record itself and any + * keys used for mediation will only be deleted if `deleteAfterHangup` flag is set. + * + * @param options connectionId + */ + public async hangup(options: { connectionId: string; deleteAfterHangup?: boolean }) { + const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + + const connectionBeforeHangup = connection.clone() + + // Create Hangup message and update did in connection record + const message = await this.didRotateService.createHangup(this.agentContext, { connection }) + + const outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connectionBeforeHangup, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + + // After hang-up message submission, delete connection if required + if (options.deleteAfterHangup) { + // First remove any recipient keys related to it + await this.removeRouting(connectionBeforeHangup) + + await this.deleteById(connection.id) + } + } + + public async returnWhenIsConnected(connectionId: string, options?: { timeoutMs: number }): Promise { + return this.connectionService.returnWhenIsConnected(this.agentContext, connectionId, options?.timeoutMs) + } + + /** + * Retrieve all connections records + * + * @returns List containing all connection records + */ + public getAll() { + return this.connectionService.getAll(this.agentContext) + } + + /** + * Retrieve all connections records by specified query params + * + * @returns List containing all connection records matching specified query paramaters + */ + public findAllByQuery(query: Query, queryOptions?: QueryOptions) { + return this.connectionService.findAllByQuery(this.agentContext, query, queryOptions) + } + + /** + * Allows for the addition of connectionType to the record. + * Either updates or creates an array of string connection types + * @param connectionId + * @param type + * @throws {RecordNotFoundError} If no record is found + */ + public async addConnectionType(connectionId: string, type: ConnectionType | string) { + const record = await this.getById(connectionId) + + await this.connectionService.addConnectionType(this.agentContext, record, type) + + return record + } + + /** + * Removes the given tag from the given record found by connectionId, if the tag exists otherwise does nothing + * @param connectionId + * @param type + * @throws {RecordNotFoundError} If no record is found + */ + public async removeConnectionType(connectionId: string, type: ConnectionType | string) { + const record = await this.getById(connectionId) + + await this.connectionService.removeConnectionType(this.agentContext, record, type) + + return record + } + + /** + * Gets the known connection types for the record matching the given connectionId + * @param connectionId + * @returns An array of known connection types or null if none exist + * @throws {RecordNotFoundError} If no record is found + */ + public async getConnectionTypes(connectionId: string) { + const record = await this.getById(connectionId) + + return this.connectionService.getConnectionTypes(record) + } + + /** + * + * @param connectionTypes An array of connection types to query for a match for + * @returns a promise of ab array of connection records + */ + public async findAllByConnectionTypes(connectionTypes: Array) { + return this.connectionService.findAllByConnectionTypes(this.agentContext, connectionTypes) + } + + /** + * Retrieve a connection record by id + * + * @param connectionId The connection record id + * @throws {RecordNotFoundError} If no record is found + * @return The connection record + * + */ + public getById(connectionId: string): Promise { + return this.connectionService.getById(this.agentContext, connectionId) + } + + /** + * Find a connection record by id + * + * @param connectionId the connection record id + * @returns The connection record or null if not found + */ + public findById(connectionId: string): Promise { + return this.connectionService.findById(this.agentContext, connectionId) + } + + /** + * Delete a connection record by id + * + * @param connectionId the connection record id + */ + public async deleteById(connectionId: string) { + const connection = await this.connectionService.getById(this.agentContext, connectionId) + + await this.removeRouting(connection) + + return this.connectionService.deleteById(this.agentContext, connectionId) + } + + private async removeRouting(connection: ConnectionRecord) { + if (connection.mediatorId && connection.did) { + const { didDocument } = await this.didResolverService.resolve(this.agentContext, connection.did) + + if (didDocument) { + await this.routingService.removeRouting(this.agentContext, { + recipientKeys: didDocument.recipientKeys, + mediatorId: connection.mediatorId, + }) + } + } + } + + /** + * Remove relationship of a connection with any previous did (either ours or theirs), preventing it from accepting + * messages from them. This is usually called when a DID Rotation flow has been succesful and we are sure that no + * more messages with older keys will arrive. + * + * It will remove routing keys from mediator if applicable. + * + * Note: this will not actually delete any DID from the wallet. + * + * @param connectionId + */ + public async removePreviousDids(options: { connectionId: string }) { + const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + + for (const previousDid of connection.previousDids) { + const did = await this.didResolverService.resolve(this.agentContext, previousDid) + if (!did.didDocument) continue + const mediatorRecord = await getMediationRecordForDidDocument(this.agentContext, did.didDocument) + + if (mediatorRecord) { + await this.routingService.removeRouting(this.agentContext, { + recipientKeys: did.didDocument.recipientKeys, + mediatorId: mediatorRecord.id, + }) + } + } + + connection.previousDids = [] + connection.previousTheirDids = [] + + await this.connectionService.update(this.agentContext, connection) + } + + public async findAllByOutOfBandId(outOfBandId: string) { + return this.connectionService.findAllByOutOfBandId(this.agentContext, outOfBandId) + } + + /** + * Retrieve a connection record by thread id + * + * @param threadId The thread id + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * @returns The connection record + */ + public getByThreadId(threadId: string): Promise { + return this.connectionService.getByThreadId(this.agentContext, threadId) + } + + public async findByDid(did: string): Promise { + return this.connectionService.findByTheirDid(this.agentContext, did) + } + + public async findByInvitationDid(invitationDid: string): Promise { + return this.connectionService.findByInvitationDid(this.agentContext, invitationDid) + } + + private registerMessageHandlers(messageHandlerRegistry: MessageHandlerRegistry) { + messageHandlerRegistry.registerMessageHandler( + new ConnectionRequestHandler( + this.connectionService, + this.outOfBandService, + this.routingService, + this.didRepository, + this.config + ) + ) + messageHandlerRegistry.registerMessageHandler( + new ConnectionResponseHandler(this.connectionService, this.outOfBandService, this.didResolverService, this.config) + ) + messageHandlerRegistry.registerMessageHandler(new AckMessageHandler(this.connectionService)) + messageHandlerRegistry.registerMessageHandler(new ConnectionProblemReportHandler(this.connectionService)) + messageHandlerRegistry.registerMessageHandler( + new TrustPingMessageHandler(this.trustPingService, this.connectionService) + ) + messageHandlerRegistry.registerMessageHandler(new TrustPingResponseMessageHandler(this.trustPingService)) + + messageHandlerRegistry.registerMessageHandler( + new DidExchangeRequestHandler( + this.didExchangeProtocol, + this.outOfBandService, + this.routingService, + this.didRepository, + this.config + ) + ) + + messageHandlerRegistry.registerMessageHandler( + new DidExchangeResponseHandler( + this.didExchangeProtocol, + this.outOfBandService, + this.connectionService, + this.didResolverService, + this.config + ) + ) + messageHandlerRegistry.registerMessageHandler( + new DidExchangeCompleteHandler(this.didExchangeProtocol, this.outOfBandService) + ) + + messageHandlerRegistry.registerMessageHandler(new DidRotateHandler(this.didRotateService, this.connectionService)) + + messageHandlerRegistry.registerMessageHandler(new DidRotateAckHandler(this.didRotateService)) + + messageHandlerRegistry.registerMessageHandler(new HangupHandler(this.didRotateService)) + + messageHandlerRegistry.registerMessageHandler(new DidRotateProblemReportHandler(this.didRotateService)) + } +} diff --git a/packages/core/src/modules/connections/ConnectionsModule.ts b/packages/core/src/modules/connections/ConnectionsModule.ts new file mode 100644 index 0000000000..b6cef0b7f6 --- /dev/null +++ b/packages/core/src/modules/connections/ConnectionsModule.ts @@ -0,0 +1,54 @@ +import type { ConnectionsModuleConfigOptions } from './ConnectionsModuleConfig' +import type { FeatureRegistry } from '../../agent/FeatureRegistry' +import type { DependencyManager, Module } from '../../plugins' + +import { Protocol } from '../../agent/models' + +import { ConnectionsApi } from './ConnectionsApi' +import { ConnectionsModuleConfig } from './ConnectionsModuleConfig' +import { DidExchangeProtocol } from './DidExchangeProtocol' +import { ConnectionRole, DidExchangeRole, DidRotateRole } from './models' +import { ConnectionRepository } from './repository' +import { ConnectionService, DidRotateService, TrustPingService } from './services' + +export class ConnectionsModule implements Module { + public readonly config: ConnectionsModuleConfig + public readonly api = ConnectionsApi + + public constructor(config?: ConnectionsModuleConfigOptions) { + this.config = new ConnectionsModuleConfig(config) + } + + /** + * Registers the dependencies of the connections module on the dependency manager. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Config + dependencyManager.registerInstance(ConnectionsModuleConfig, this.config) + + // Services + dependencyManager.registerSingleton(ConnectionService) + dependencyManager.registerSingleton(DidExchangeProtocol) + dependencyManager.registerSingleton(DidRotateService) + dependencyManager.registerSingleton(TrustPingService) + + // Repositories + dependencyManager.registerSingleton(ConnectionRepository) + + // Features + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/connections/1.0', + roles: [ConnectionRole.Invitee, ConnectionRole.Inviter], + }), + new Protocol({ + id: 'https://didcomm.org/didexchange/1.1', + roles: [DidExchangeRole.Requester, DidExchangeRole.Responder], + }), + new Protocol({ + id: 'https://didcomm.org/did-rotate/1.0', + roles: [DidRotateRole.RotatingParty, DidRotateRole.ObservingParty], + }) + ) + } +} diff --git a/packages/core/src/modules/connections/ConnectionsModuleConfig.ts b/packages/core/src/modules/connections/ConnectionsModuleConfig.ts new file mode 100644 index 0000000000..a978241c70 --- /dev/null +++ b/packages/core/src/modules/connections/ConnectionsModuleConfig.ts @@ -0,0 +1,80 @@ +import { PeerDidNumAlgo } from '../dids' + +/** + * ConnectionsModuleConfigOptions defines the interface for the options of the ConnectionsModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface ConnectionsModuleConfigOptions { + /** + * Whether to automatically accept connection messages. Applies to both the connection protocol (RFC 0160) + * and the DID exchange protocol (RFC 0023). + * + * Note: this setting does not apply to implicit invitation flows, which always need to be manually accepted + * using ConnectionStateChangedEvent + * + * @default false + */ + autoAcceptConnections?: boolean + + /** + * Peer did num algo to use in requests for DID exchange protocol (RFC 0023). It will be also used by default + * in responses in case that the request does not use a peer did. + * + * @default PeerDidNumAlgo.GenesisDoc + */ + peerNumAlgoForDidExchangeRequests?: PeerDidNumAlgo + + /** + * Peer did num algo to use for DID rotation (RFC 0794). + * + * @default PeerDidNumAlgo.ShortFormAndLongForm + */ + peerNumAlgoForDidRotation?: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc | PeerDidNumAlgo.ShortFormAndLongForm +} + +export class ConnectionsModuleConfig { + #autoAcceptConnections?: boolean + #peerNumAlgoForDidExchangeRequests?: PeerDidNumAlgo + #peerNumAlgoForDidRotation?: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc | PeerDidNumAlgo.ShortFormAndLongForm + + private options: ConnectionsModuleConfigOptions + + public constructor(options?: ConnectionsModuleConfigOptions) { + this.options = options ?? {} + this.#autoAcceptConnections = this.options.autoAcceptConnections + this.#peerNumAlgoForDidExchangeRequests = this.options.peerNumAlgoForDidExchangeRequests + this.#peerNumAlgoForDidRotation = this.options.peerNumAlgoForDidRotation + } + + /** See {@link ConnectionsModuleConfigOptions.autoAcceptConnections} */ + public get autoAcceptConnections() { + return this.#autoAcceptConnections ?? false + } + + /** See {@link ConnectionsModuleConfigOptions.autoAcceptConnections} */ + public set autoAcceptConnections(autoAcceptConnections: boolean) { + this.#autoAcceptConnections = autoAcceptConnections + } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidExchangeRequests} */ + public get peerNumAlgoForDidExchangeRequests() { + return this.#peerNumAlgoForDidExchangeRequests ?? PeerDidNumAlgo.GenesisDoc + } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidExchangeRequests} */ + public set peerNumAlgoForDidExchangeRequests(peerNumAlgoForDidExchangeRequests: PeerDidNumAlgo) { + this.#peerNumAlgoForDidExchangeRequests = peerNumAlgoForDidExchangeRequests + } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidRotation} */ + public get peerNumAlgoForDidRotation() { + return this.#peerNumAlgoForDidRotation ?? PeerDidNumAlgo.ShortFormAndLongForm + } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidRotation} */ + public set peerNumAlgoForDidRotation( + peerNumAlgoForDidRotation: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc | PeerDidNumAlgo.ShortFormAndLongForm + ) { + this.#peerNumAlgoForDidRotation = peerNumAlgoForDidRotation + } +} diff --git a/packages/core/src/modules/connections/DidExchangeProtocol.ts b/packages/core/src/modules/connections/DidExchangeProtocol.ts new file mode 100644 index 0000000000..a735cfca8a --- /dev/null +++ b/packages/core/src/modules/connections/DidExchangeProtocol.ts @@ -0,0 +1,676 @@ +import type { ConnectionRecord } from './repository' +import type { Routing } from './services/ConnectionService' +import type { AgentContext } from '../../agent' +import type { InboundMessageContext } from '../../agent/models/InboundMessageContext' +import type { ParsedMessageType } from '../../utils/messageType' +import type { ResolvedDidCommService } from '../didcomm' +import type { OutOfBandRecord } from '../oob/repository' + +import { InjectionSymbols } from '../../constants' +import { Key, KeyType } from '../../crypto' +import { JwsService } from '../../crypto/JwsService' +import { JwaSignatureAlgorithm } from '../../crypto/jose/jwa' +import { getJwkFromKey } from '../../crypto/jose/jwk' +import { Attachment, AttachmentData } from '../../decorators/attachment/Attachment' +import { CredoError } from '../../error' +import { Logger } from '../../logger' +import { inject, injectable } from '../../plugins' +import { TypedArrayEncoder, isDid, Buffer } from '../../utils' +import { JsonEncoder } from '../../utils/JsonEncoder' +import { JsonTransformer } from '../../utils/JsonTransformer' +import { base64ToBase64URL } from '../../utils/base64' +import { + DidDocument, + DidKey, + getNumAlgoFromPeerDid, + PeerDidNumAlgo, + DidsApi, + isValidPeerDid, + getAlternativeDidsForPeerDid, +} from '../dids' +import { getKeyFromVerificationMethod } from '../dids/domain/key-type' +import { tryParseDid } from '../dids/domain/parse' +import { didKeyToInstanceOfKey } from '../dids/helpers' +import { DidRepository } from '../dids/repository' +import { OutOfBandRole } from '../oob/domain/OutOfBandRole' +import { OutOfBandState } from '../oob/domain/OutOfBandState' +import { getMediationRecordForDidDocument } from '../routing/services/helpers' + +import { ConnectionsModuleConfig } from './ConnectionsModuleConfig' +import { DidExchangeStateMachine } from './DidExchangeStateMachine' +import { DidExchangeProblemReportError, DidExchangeProblemReportReason } from './errors' +import { DidExchangeRequestMessage, DidExchangeResponseMessage, DidExchangeCompleteMessage } from './messages' +import { DidExchangeRole, DidExchangeState, HandshakeProtocol } from './models' +import { ConnectionService } from './services' +import { createPeerDidFromServices, getDidDocumentForCreatedDid, routingToServices } from './services/helpers' + +interface DidExchangeRequestParams { + label?: string + alias?: string + goal?: string + goalCode?: string + routing?: Routing + autoAcceptConnection?: boolean + ourDid?: string +} + +@injectable() +export class DidExchangeProtocol { + private connectionService: ConnectionService + private jwsService: JwsService + private didRepository: DidRepository + private logger: Logger + + public constructor( + connectionService: ConnectionService, + didRepository: DidRepository, + jwsService: JwsService, + @inject(InjectionSymbols.Logger) logger: Logger + ) { + this.connectionService = connectionService + this.didRepository = didRepository + this.jwsService = jwsService + this.logger = logger + } + + public async createRequest( + agentContext: AgentContext, + outOfBandRecord: OutOfBandRecord, + params: DidExchangeRequestParams + ): Promise<{ message: DidExchangeRequestMessage; connectionRecord: ConnectionRecord }> { + this.logger.debug(`Create message ${DidExchangeRequestMessage.type.messageTypeUri} start`, { + outOfBandRecord, + params, + }) + const config = agentContext.dependencyManager.resolve(ConnectionsModuleConfig) + + const { outOfBandInvitation } = outOfBandRecord + const { alias, goal, goalCode, routing, autoAcceptConnection, ourDid: did } = params + // TODO: We should store only one did that we'll use to send the request message with success. + // We take just the first one for now. + const [invitationDid] = outOfBandInvitation.invitationDids + + // Create message + const label = params.label ?? agentContext.config.label + + let didDocument, mediatorId + + // If our did is specified, make sure we have all key material for it + if (did) { + didDocument = await getDidDocumentForCreatedDid(agentContext, did) + mediatorId = (await getMediationRecordForDidDocument(agentContext, didDocument))?.id + // Otherwise, create a did:peer based on the provided routing + } else { + if (!routing) throw new CredoError(`'routing' must be defined if 'ourDid' is not specified`) + + didDocument = await createPeerDidFromServices( + agentContext, + routingToServices(routing), + config.peerNumAlgoForDidExchangeRequests + ) + mediatorId = routing.mediatorId + } + + const parentThreadId = outOfBandRecord.outOfBandInvitation.id + + const message = new DidExchangeRequestMessage({ label, parentThreadId, did: didDocument.id, goal, goalCode }) + + // Create sign attachment containing didDoc + if (isValidPeerDid(didDocument.id) && getNumAlgoFromPeerDid(didDocument.id) === PeerDidNumAlgo.GenesisDoc) { + const didDocAttach = await this.createSignedAttachment( + agentContext, + didDocument.toJSON(), + didDocument.recipientKeys.map((key) => key.publicKeyBase58) + ) + message.didDoc = didDocAttach + } + + const connectionRecord = await this.connectionService.createConnection(agentContext, { + protocol: HandshakeProtocol.DidExchange, + role: DidExchangeRole.Requester, + alias, + state: DidExchangeState.InvitationReceived, + theirLabel: outOfBandInvitation.label, + mediatorId, + autoAcceptConnection: outOfBandRecord.autoAcceptConnection, + outOfBandId: outOfBandRecord.id, + invitationDid, + imageUrl: outOfBandInvitation.imageUrl, + }) + + DidExchangeStateMachine.assertCreateMessageState(DidExchangeRequestMessage.type, connectionRecord) + + connectionRecord.did = didDocument.id + connectionRecord.threadId = message.id + + if (autoAcceptConnection !== undefined || autoAcceptConnection !== null) { + connectionRecord.autoAcceptConnection = autoAcceptConnection + } + + await this.updateState(agentContext, DidExchangeRequestMessage.type, connectionRecord) + this.logger.debug(`Create message ${DidExchangeRequestMessage.type.messageTypeUri} end`, { + connectionRecord, + message, + }) + return { message, connectionRecord } + } + + public async processRequest( + messageContext: InboundMessageContext, + outOfBandRecord: OutOfBandRecord + ): Promise { + this.logger.debug(`Process message ${messageContext.message.type} start`, { + message: messageContext.message, + }) + + outOfBandRecord.assertRole(OutOfBandRole.Sender) + outOfBandRecord.assertState(OutOfBandState.AwaitResponse) + + // TODO check there is no connection record for particular oob record + + const { message, agentContext } = messageContext + + // Check corresponding invitation ID is the request's ~thread.pthid or pthid is a public did + // TODO Maybe we can do it in handler, but that actually does not make sense because we try to find oob by parent thread ID there. + const parentThreadId = message.thread?.parentThreadId + if ( + !parentThreadId || + (!tryParseDid(parentThreadId) && parentThreadId !== outOfBandRecord.getTags().invitationId) + ) { + throw new DidExchangeProblemReportError('Missing reference to invitation.', { + problemCode: DidExchangeProblemReportReason.RequestNotAccepted, + }) + } + + // If the responder wishes to continue the exchange, they will persist the received information in their wallet. + + // Get DID Document either from message (if it is a supported did:peer) or resolve it externally + const didDocument = await this.resolveDidDocument(agentContext, message) + + // A DID Record must be stored in order to allow for searching for its recipient keys when receiving a message + const didRecord = await this.didRepository.storeReceivedDid(messageContext.agentContext, { + did: didDocument.id, + // It is important to take the did document from the PeerDid class + // as it will have the id property + didDocument: + !isValidPeerDid(didDocument.id) || getNumAlgoFromPeerDid(message.did) === PeerDidNumAlgo.GenesisDoc + ? didDocument + : undefined, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + + // For did:peer, store any alternative dids (like short form did:peer:4), + // it may have in order to relate any message referencing it + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(didDocument.id) : undefined, + }, + }) + + this.logger.debug('Saved DID record', { + id: didRecord.id, + did: didRecord.did, + role: didRecord.role, + tags: didRecord.getTags(), + didDocument: 'omitted...', + }) + + const connectionRecord = await this.connectionService.createConnection(messageContext.agentContext, { + protocol: HandshakeProtocol.DidExchange, + role: DidExchangeRole.Responder, + state: DidExchangeState.RequestReceived, + alias: outOfBandRecord.alias, + theirDid: message.did, + theirLabel: message.label, + threadId: message.threadId, + mediatorId: outOfBandRecord.mediatorId, + autoAcceptConnection: outOfBandRecord.autoAcceptConnection, + outOfBandId: outOfBandRecord.id, + }) + + await this.updateState(messageContext.agentContext, DidExchangeRequestMessage.type, connectionRecord) + this.logger.debug(`Process message ${DidExchangeRequestMessage.type.messageTypeUri} end`, connectionRecord) + return connectionRecord + } + + public async createResponse( + agentContext: AgentContext, + connectionRecord: ConnectionRecord, + outOfBandRecord: OutOfBandRecord, + routing?: Routing + ): Promise { + this.logger.debug(`Create message ${DidExchangeResponseMessage.type.messageTypeUri} start`, connectionRecord) + DidExchangeStateMachine.assertCreateMessageState(DidExchangeResponseMessage.type, connectionRecord) + + const { threadId, theirDid } = connectionRecord + + const config = agentContext.dependencyManager.resolve(ConnectionsModuleConfig) + + if (!threadId) { + throw new CredoError('Missing threadId on connection record.') + } + + if (!theirDid) { + throw new CredoError('Missing theirDid on connection record.') + } + + let services: ResolvedDidCommService[] = [] + if (routing) { + services = routingToServices(routing) + } else if (outOfBandRecord.outOfBandInvitation.getInlineServices().length > 0) { + const inlineServices = outOfBandRecord.outOfBandInvitation.getInlineServices() + services = inlineServices.map((service) => ({ + id: service.id, + serviceEndpoint: service.serviceEndpoint, + recipientKeys: service.recipientKeys.map(didKeyToInstanceOfKey), + routingKeys: service.routingKeys?.map(didKeyToInstanceOfKey) ?? [], + })) + } else { + // We don't support using a did from the OOB invitation services currently, in this case we always pass routing to this method + throw new CredoError( + 'No routing provided, and no inline services found in out of band invitation. When using did services in out of band invitation, make sure to provide routing information for rotation.' + ) + } + + // Use the same num algo for response as received in request + const numAlgo = isValidPeerDid(theirDid) + ? getNumAlgoFromPeerDid(theirDid) + : config.peerNumAlgoForDidExchangeRequests + + const didDocument = await createPeerDidFromServices(agentContext, services, numAlgo) + const message = new DidExchangeResponseMessage({ did: didDocument.id, threadId }) + + if (numAlgo === PeerDidNumAlgo.GenesisDoc) { + message.didDoc = await this.createSignedAttachment( + agentContext, + didDocument.toJSON(), + Array.from( + new Set( + services + .map((s) => s.recipientKeys) + .reduce((acc, curr) => acc.concat(curr), []) + .map((key) => key.publicKeyBase58) + ) + ) + ) + } else { + // We assume any other case is a resolvable did (e.g. did:peer:2 or did:peer:4) + message.didRotate = await this.createSignedAttachment( + agentContext, + didDocument.id, + Array.from( + new Set( + services + .map((s) => s.recipientKeys) + .reduce((acc, curr) => acc.concat(curr), []) + .map((key) => key.publicKeyBase58) + ) + ) + ) + } + + connectionRecord.did = didDocument.id + + await this.updateState(agentContext, DidExchangeResponseMessage.type, connectionRecord) + this.logger.debug(`Create message ${DidExchangeResponseMessage.type.messageTypeUri} end`, { + connectionRecord, + message, + }) + return message + } + + public async processResponse( + messageContext: InboundMessageContext, + outOfBandRecord: OutOfBandRecord + ): Promise { + this.logger.debug(`Process message ${DidExchangeResponseMessage.type.messageTypeUri} start`, { + message: messageContext.message, + }) + + const { connection: connectionRecord, message, agentContext } = messageContext + + if (!connectionRecord) { + throw new CredoError('No connection record in message context.') + } + + DidExchangeStateMachine.assertProcessMessageState(DidExchangeResponseMessage.type, connectionRecord) + + if (!message.thread?.threadId || message.thread?.threadId !== connectionRecord.threadId) { + throw new DidExchangeProblemReportError('Invalid or missing thread ID.', { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + }) + } + + // Get DID Document either from message (if it is a did:peer) or resolve it externally + const didDocument = await this.resolveDidDocument( + agentContext, + message, + outOfBandRecord + .getTags() + .recipientKeyFingerprints.map((fingerprint) => Key.fromFingerprint(fingerprint).publicKeyBase58) + ) + + if (isValidPeerDid(didDocument.id)) { + const didRecord = await this.didRepository.storeReceivedDid(messageContext.agentContext, { + did: didDocument.id, + didDocument: getNumAlgoFromPeerDid(message.did) === PeerDidNumAlgo.GenesisDoc ? didDocument : undefined, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + + // For did:peer, store any alternative dids (like short form did:peer:4), + // it may have in order to relate any message referencing it + alternativeDids: getAlternativeDidsForPeerDid(didDocument.id), + }, + }) + + this.logger.debug('Saved DID record', { + id: didRecord.id, + did: didRecord.did, + role: didRecord.role, + tags: didRecord.getTags(), + didDocument: 'omitted...', + }) + } + + connectionRecord.theirDid = message.did + + await this.updateState(messageContext.agentContext, DidExchangeResponseMessage.type, connectionRecord) + this.logger.debug(`Process message ${DidExchangeResponseMessage.type.messageTypeUri} end`, connectionRecord) + return connectionRecord + } + + public async createComplete( + agentContext: AgentContext, + connectionRecord: ConnectionRecord, + outOfBandRecord: OutOfBandRecord + ): Promise { + this.logger.debug(`Create message ${DidExchangeCompleteMessage.type.messageTypeUri} start`, connectionRecord) + DidExchangeStateMachine.assertCreateMessageState(DidExchangeCompleteMessage.type, connectionRecord) + + const threadId = connectionRecord.threadId + const parentThreadId = outOfBandRecord.outOfBandInvitation.id + + if (!threadId) { + throw new CredoError(`Connection record ${connectionRecord.id} does not have 'threadId' attribute.`) + } + + if (!parentThreadId) { + throw new CredoError(`Connection record ${connectionRecord.id} does not have 'parentThreadId' attribute.`) + } + + const message = new DidExchangeCompleteMessage({ threadId, parentThreadId }) + + await this.updateState(agentContext, DidExchangeCompleteMessage.type, connectionRecord) + this.logger.debug(`Create message ${DidExchangeCompleteMessage.type.messageTypeUri} end`, { + connectionRecord, + message, + }) + return message + } + + public async processComplete( + messageContext: InboundMessageContext, + outOfBandRecord: OutOfBandRecord + ): Promise { + this.logger.debug(`Process message ${DidExchangeCompleteMessage.type.messageTypeUri} start`, { + message: messageContext.message, + }) + + const { connection: connectionRecord, message } = messageContext + + if (!connectionRecord) { + throw new CredoError('No connection record in message context.') + } + + DidExchangeStateMachine.assertProcessMessageState(DidExchangeCompleteMessage.type, connectionRecord) + + if (message.threadId !== connectionRecord.threadId) { + throw new DidExchangeProblemReportError('Invalid or missing thread ID.', { + problemCode: DidExchangeProblemReportReason.CompleteRejected, + }) + } + const pthid = message.thread?.parentThreadId + if (!pthid || pthid !== outOfBandRecord.outOfBandInvitation.id) { + throw new DidExchangeProblemReportError('Invalid or missing parent thread ID referencing to the invitation.', { + problemCode: DidExchangeProblemReportReason.CompleteRejected, + }) + } + + await this.updateState(messageContext.agentContext, DidExchangeCompleteMessage.type, connectionRecord) + this.logger.debug(`Process message ${DidExchangeCompleteMessage.type.messageTypeUri} end`, { connectionRecord }) + return connectionRecord + } + + private async updateState( + agentContext: AgentContext, + messageType: ParsedMessageType, + connectionRecord: ConnectionRecord + ) { + this.logger.debug(`Updating state`, { connectionRecord }) + const nextState = DidExchangeStateMachine.nextState(messageType, connectionRecord) + return this.connectionService.updateState(agentContext, connectionRecord, nextState) + } + + private async createSignedAttachment( + agentContext: AgentContext, + data: string | Record, + verkeys: string[] + ) { + const signedAttach = new Attachment({ + mimeType: typeof data === 'string' ? undefined : 'application/json', + data: new AttachmentData({ + base64: + typeof data === 'string' ? TypedArrayEncoder.toBase64URL(Buffer.from(data)) : JsonEncoder.toBase64(data), + }), + }) + + await Promise.all( + verkeys.map(async (verkey) => { + const key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) + const kid = new DidKey(key).did + const payload = typeof data === 'string' ? TypedArrayEncoder.fromString(data) : JsonEncoder.toBuffer(data) + + const jws = await this.jwsService.createJws(agentContext, { + payload, + key, + header: { + kid, + }, + protectedHeaderOptions: { + alg: JwaSignatureAlgorithm.EdDSA, + jwk: getJwkFromKey(key), + }, + }) + signedAttach.addJws(jws) + }) + ) + + return signedAttach + } + + /** + * Resolves a did document from a given `request` or `response` message, verifying its signature or did rotate + * signature in case it is taken from message attachment. + * + * @param message DID request or DID response message + * @param invitationKeys array containing keys from connection invitation that could be used for signing of DID document + * @returns verified DID document content from message attachment + */ + + private async resolveDidDocument( + agentContext: AgentContext, + message: DidExchangeRequestMessage | DidExchangeResponseMessage, + invitationKeysBase58: string[] = [] + ) { + // Not all agents use didRotate yet, some may still send a didDoc attach with various did types + // we should check if the didDoc attach is there and if not require that the didRotate be present + if (message.didDoc) { + return this.extractAttachedDidDocument(agentContext, message, invitationKeysBase58) + } else { + return this.extractResolvableDidDocument(agentContext, message, invitationKeysBase58) + } + } + + /** + * Extracts DID document from message (resolving it externally if required) and verifies did-rotate attachment signature + * if applicable + */ + private async extractResolvableDidDocument( + agentContext: AgentContext, + message: DidExchangeRequestMessage | DidExchangeResponseMessage, + invitationKeysBase58?: string[] + ) { + // Validate did-rotate attachment in case of DID Exchange response + if (message instanceof DidExchangeResponseMessage) { + const didRotateAttachment = message.didRotate + if (!didRotateAttachment) { + throw new DidExchangeProblemReportError( + 'Either a DID Rotate attachment or a didDoc attachment must be provided to make a secure connection', + { problemCode: DidExchangeProblemReportReason.ResponseNotAccepted } + ) + } + + const jws = didRotateAttachment.data.jws + + if (!jws) { + throw new DidExchangeProblemReportError('DID Rotate signature is missing.', { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + }) + } + + if (!didRotateAttachment.data.base64) { + throw new CredoError('DID Rotate attachment is missing base64 property for signed did.') + } + + // JWS payload must be base64url encoded + const base64UrlPayload = base64ToBase64URL(didRotateAttachment.data.base64) + const signedDid = TypedArrayEncoder.fromBase64(base64UrlPayload).toString() + + if (signedDid !== message.did) { + throw new CredoError( + `DID Rotate attachment's did ${message.did} does not correspond to message did ${message.did}` + ) + } + + const { isValid, signerKeys } = await this.jwsService.verifyJws(agentContext, { + jws: { + ...jws, + payload: base64UrlPayload, + }, + jwkResolver: ({ jws: { header } }) => { + if (typeof header.kid !== 'string' || !isDid(header.kid, 'key')) { + throw new CredoError('JWS header kid must be a did:key DID.') + } + + const didKey = DidKey.fromDid(header.kid) + return getJwkFromKey(didKey.key) + }, + }) + + if (!isValid || !signerKeys.every((key) => invitationKeysBase58?.includes(key.publicKeyBase58))) { + throw new DidExchangeProblemReportError( + `DID Rotate signature is invalid. isValid: ${isValid} signerKeys: ${JSON.stringify( + signerKeys + )} invitationKeys:${JSON.stringify(invitationKeysBase58)}`, + { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + } + ) + } + } + + // Now resolve the document related to the did (which can be either a public did or an inline did) + try { + return await agentContext.dependencyManager.resolve(DidsApi).resolveDidDocument(message.did) + } catch (error) { + const problemCode = + message instanceof DidExchangeRequestMessage + ? DidExchangeProblemReportReason.RequestNotAccepted + : DidExchangeProblemReportReason.ResponseNotAccepted + + throw new DidExchangeProblemReportError(error, { + problemCode, + }) + } + } + + /** + * Extracts DID document as is from request or response message attachment and verifies its signature. + * + * @param message DID request or DID response message + * @param invitationKeys array containing keys from connection invitation that could be used for signing of DID document + * @returns verified DID document content from message attachment + */ + private async extractAttachedDidDocument( + agentContext: AgentContext, + message: DidExchangeRequestMessage | DidExchangeResponseMessage, + invitationKeysBase58: string[] = [] + ): Promise { + if (!message.didDoc) { + const problemCode = + message instanceof DidExchangeRequestMessage + ? DidExchangeProblemReportReason.RequestNotAccepted + : DidExchangeProblemReportReason.ResponseNotAccepted + throw new DidExchangeProblemReportError('DID Document attachment is missing.', { problemCode }) + } + const didDocumentAttachment = message.didDoc + const jws = didDocumentAttachment.data.jws + + if (!jws) { + const problemCode = + message instanceof DidExchangeRequestMessage + ? DidExchangeProblemReportReason.RequestNotAccepted + : DidExchangeProblemReportReason.ResponseNotAccepted + throw new DidExchangeProblemReportError('DID Document signature is missing.', { problemCode }) + } + + if (!didDocumentAttachment.data.base64) { + throw new CredoError('DID Document attachment is missing base64 property for signed did document.') + } + + // JWS payload must be base64url encoded + const base64UrlPayload = base64ToBase64URL(didDocumentAttachment.data.base64) + + const { isValid, signerKeys } = await this.jwsService.verifyJws(agentContext, { + jws: { + ...jws, + payload: base64UrlPayload, + }, + jwkResolver: ({ jws: { header } }) => { + if (typeof header.kid !== 'string' || !isDid(header.kid, 'key')) { + throw new CredoError('JWS header kid must be a did:key DID.') + } + + const didKey = DidKey.fromDid(header.kid) + return getJwkFromKey(didKey.key) + }, + }) + + const json = JsonEncoder.fromBase64(didDocumentAttachment.data.base64) + const didDocument = JsonTransformer.fromJSON(json, DidDocument) + const didDocumentKeysBase58 = didDocument.authentication + ?.map((authentication) => { + const verificationMethod = + typeof authentication === 'string' + ? didDocument.dereferenceVerificationMethod(authentication) + : authentication + const key = getKeyFromVerificationMethod(verificationMethod) + return key.publicKeyBase58 + }) + .concat(invitationKeysBase58) + + this.logger.trace('JWS verification result', { isValid, signerKeys, didDocumentKeysBase58 }) + + if (!isValid || !signerKeys.every((key) => didDocumentKeysBase58?.includes(key.publicKeyBase58))) { + const problemCode = + message instanceof DidExchangeRequestMessage + ? DidExchangeProblemReportReason.RequestNotAccepted + : DidExchangeProblemReportReason.ResponseNotAccepted + throw new DidExchangeProblemReportError('DID Document signature is invalid.', { problemCode }) + } + + return didDocument + } +} diff --git a/packages/core/src/modules/connections/DidExchangeStateMachine.ts b/packages/core/src/modules/connections/DidExchangeStateMachine.ts new file mode 100644 index 0000000000..9a962ce77e --- /dev/null +++ b/packages/core/src/modules/connections/DidExchangeStateMachine.ts @@ -0,0 +1,88 @@ +import type { ConnectionRecord } from './repository' +import type { ParsedMessageType } from '../../utils/messageType' + +import { CredoError } from '../../error' +import { canHandleMessageType } from '../../utils/messageType' + +import { DidExchangeRequestMessage, DidExchangeResponseMessage, DidExchangeCompleteMessage } from './messages' +import { DidExchangeState, DidExchangeRole } from './models' + +export class DidExchangeStateMachine { + private static createMessageStateRules = [ + { + message: DidExchangeRequestMessage, + state: DidExchangeState.InvitationReceived, + role: DidExchangeRole.Requester, + nextState: DidExchangeState.RequestSent, + }, + { + message: DidExchangeResponseMessage, + state: DidExchangeState.RequestReceived, + role: DidExchangeRole.Responder, + nextState: DidExchangeState.ResponseSent, + }, + { + message: DidExchangeCompleteMessage, + state: DidExchangeState.ResponseReceived, + role: DidExchangeRole.Requester, + nextState: DidExchangeState.Completed, + }, + ] + + private static processMessageStateRules = [ + { + message: DidExchangeRequestMessage, + state: DidExchangeState.InvitationSent, + role: DidExchangeRole.Responder, + nextState: DidExchangeState.RequestReceived, + }, + { + message: DidExchangeResponseMessage, + state: DidExchangeState.RequestSent, + role: DidExchangeRole.Requester, + nextState: DidExchangeState.ResponseReceived, + }, + { + message: DidExchangeCompleteMessage, + state: DidExchangeState.ResponseSent, + role: DidExchangeRole.Responder, + nextState: DidExchangeState.Completed, + }, + ] + + public static assertCreateMessageState(messageType: ParsedMessageType, record: ConnectionRecord) { + const rule = this.createMessageStateRules.find((r) => canHandleMessageType(r.message, messageType)) + if (!rule) { + throw new CredoError(`Could not find create message rule for ${messageType}`) + } + if (rule.state !== record.state || rule.role !== record.role) { + throw new CredoError( + `Record with role ${record.role} is in invalid state ${record.state} to create ${messageType}. Expected state for role ${rule.role} is ${rule.state}.` + ) + } + } + + public static assertProcessMessageState(messageType: ParsedMessageType, record: ConnectionRecord) { + const rule = this.processMessageStateRules.find((r) => canHandleMessageType(r.message, messageType)) + if (!rule) { + throw new CredoError(`Could not find create message rule for ${messageType}`) + } + if (rule.state !== record.state || rule.role !== record.role) { + throw new CredoError( + `Record with role ${record.role} is in invalid state ${record.state} to process ${messageType.messageTypeUri}. Expected state for role ${rule.role} is ${rule.state}.` + ) + } + } + + public static nextState(messageType: ParsedMessageType, record: ConnectionRecord) { + const rule = this.createMessageStateRules + .concat(this.processMessageStateRules) + .find((r) => canHandleMessageType(r.message, messageType) && r.role === record.role) + if (!rule) { + throw new CredoError( + `Could not find create message rule for messageType ${messageType.messageTypeUri}, state ${record.state} and role ${record.role}` + ) + } + return rule.nextState + } +} diff --git a/packages/core/src/modules/connections/TrustPingEvents.ts b/packages/core/src/modules/connections/TrustPingEvents.ts new file mode 100644 index 0000000000..2b0fcf66c9 --- /dev/null +++ b/packages/core/src/modules/connections/TrustPingEvents.ts @@ -0,0 +1,24 @@ +import type { TrustPingMessage, TrustPingResponseMessage } from './messages' +import type { ConnectionRecord } from './repository/ConnectionRecord' +import type { BaseEvent } from '../../agent/Events' + +export enum TrustPingEventTypes { + TrustPingReceivedEvent = 'TrustPingReceivedEvent', + TrustPingResponseReceivedEvent = 'TrustPingResponseReceivedEvent', +} + +export interface TrustPingReceivedEvent extends BaseEvent { + type: typeof TrustPingEventTypes.TrustPingReceivedEvent + payload: { + connectionRecord: ConnectionRecord + message: TrustPingMessage + } +} + +export interface TrustPingResponseReceivedEvent extends BaseEvent { + type: typeof TrustPingEventTypes.TrustPingResponseReceivedEvent + payload: { + connectionRecord: ConnectionRecord + message: TrustPingResponseMessage + } +} diff --git a/packages/core/src/modules/connections/__tests__/ConnectionInvitationMessage.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionInvitationMessage.test.ts new file mode 100644 index 0000000000..188083309a --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/ConnectionInvitationMessage.test.ts @@ -0,0 +1,125 @@ +import { validateOrReject } from 'class-validator' +import { parseUrl } from 'query-string' + +import { Attachment } from '../../../decorators/attachment/Attachment' +import { ClassValidationError } from '../../../error/ClassValidationError' +import { JsonEncoder } from '../../../utils/JsonEncoder' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { ConnectionInvitationMessage } from '../messages/ConnectionInvitationMessage' + +describe('ConnectionInvitationMessage', () => { + it('should allow routingKeys to be left out of inline invitation', async () => { + const json = { + '@type': ConnectionInvitationMessage.type.messageTypeUri, + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'], + serviceEndpoint: 'https://example.com', + label: 'test', + } + const invitation = JsonTransformer.fromJSON(json, ConnectionInvitationMessage) + expect(invitation).toBeInstanceOf(ConnectionInvitationMessage) + }) + + it('should throw error if both did and inline keys / endpoint are missing', async () => { + const json = { + '@type': ConnectionInvitationMessage.type.messageTypeUri, + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + label: 'test', + } + + expect(() => JsonTransformer.fromJSON(json, ConnectionInvitationMessage)).toThrowError(ClassValidationError) + }) + + it('should replace legacy did:sov:BzCbsNYhMrjHiqZDTUASHg;spec prefix with https://didcomm.org in message type', async () => { + const json = { + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation', + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'], + serviceEndpoint: 'https://example.com', + label: 'test', + } + const invitation = JsonTransformer.fromJSON(json, ConnectionInvitationMessage) + + // Assert type + expect(invitation.type).toBe('https://didcomm.org/connections/1.0/invitation') + + // Assert validation also works with the transformation + expect(invitation).toBeInstanceOf(ConnectionInvitationMessage) + }) + + describe('toUrl', () => { + it('should correctly include the base64 encoded invitation in the url as the c_i query parameter', async () => { + const domain = 'https://example.com/ssi' + const json = { + '@type': 'https://didcomm.org/connections/1.0/invitation', + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'], + serviceEndpoint: 'https://example.com', + label: 'test', + } + const invitation = JsonTransformer.fromJSON(json, ConnectionInvitationMessage) + const invitationUrl = invitation.toUrl({ + domain, + }) + + expect(invitationUrl).toBe(`${domain}?c_i=${JsonEncoder.toBase64URL(json)}`) + }) + + it('should use did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation as type if useDidSovPrefixWhereAllowed is set to true', async () => { + const invitation = new ConnectionInvitationMessage({ + id: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'], + serviceEndpoint: 'https://example.com', + label: 'test', + imageUrl: 'test-image-path', + appendedAttachments: [ + new Attachment({ + id: 'test-attachment', + data: { + json: { + value: 'test', + }, + }, + }), + ], + }) + + const invitationUrl = invitation.toUrl({ + domain: 'https://example.com', + useDidSovPrefixWhereAllowed: true, + }) + + const parsedUrl = parseUrl(invitationUrl).query + const encodedInvitation = (parsedUrl['c_i'] ?? parsedUrl['d_m']) as string + + expect(JsonEncoder.fromBase64(encodedInvitation)['@type']).toBe( + 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation' + ) + }) + }) + + describe('fromUrl', () => { + it('should correctly convert a valid invitation url to a `ConnectionInvitationMessage` with `d_m` as parameter', () => { + const invitationUrl = + 'https://trinsic.studio/link/?d_m=eyJsYWJlbCI6InRlc3QiLCJpbWFnZVVybCI6Imh0dHBzOi8vdHJpbnNpY2FwaWFzc2V0cy5henVyZWVkZ2UubmV0L2ZpbGVzL2IyODhkMTE3LTNjMmMtNGFjNC05MzVhLWE1MDBkODQzYzFlOV9kMGYxN2I0OS0wNWQ5LTQ4ZDAtODJlMy1jNjg3MGI4MjNjMTUucG5nIiwic2VydmljZUVuZHBvaW50IjoiaHR0cHM6Ly9hcGkucG9ydGFsLnN0cmVldGNyZWQuaWQvYWdlbnQvTVZob1VaQjlHdUl6bVJzSTNIWUNuZHpBcXVKY1ZNdFUiLCJyb3V0aW5nS2V5cyI6WyJCaFZRdEZHdGJ4NzZhMm13Y3RQVkJuZWtLaG1iMTdtUHdFMktXWlVYTDFNaSJdLCJyZWNpcGllbnRLZXlzIjpbIkcyOVF6bXBlVXN0dUVHYzlXNzlYNnV2aUhTUTR6UlV2VWFFOHpXV2VZYjduIl0sIkBpZCI6IjgxYzZiNDUzLWNkMTUtNDQwMC04MWU5LTkwZTJjM2NhY2I1NCIsIkB0eXBlIjoiZGlkOnNvdjpCekNic05ZaE1yakhpcVpEVFVBU0hnO3NwZWMvY29ubmVjdGlvbnMvMS4wL2ludml0YXRpb24ifQ%3D%3D&orig=https://trinsic.studio/url/6dd56daf-e153-40dd-b849-2b345b6853f6' + + const invitation = ConnectionInvitationMessage.fromUrl(invitationUrl) + + expect(validateOrReject(invitation)).resolves.toBeUndefined() + }) + it('should correctly convert a valid invitation url to a `ConnectionInvitationMessage` with `c_i` as parameter', () => { + const invitationUrl = + 'https://example.com?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiZmM3ODFlMDItMjA1YS00NGUzLWE5ZTQtYjU1Y2U0OTE5YmVmIiwgInNlcnZpY2VFbmRwb2ludCI6ICJodHRwczovL2RpZGNvbW0uZmFiZXIuYWdlbnQuYW5pbW8uaWQiLCAibGFiZWwiOiAiQW5pbW8gRmFiZXIgQWdlbnQiLCAicmVjaXBpZW50S2V5cyI6IFsiR0hGczFQdFRabjdmYU5LRGVnMUFzU3B6QVAyQmpVckVjZlR2bjc3SnBRTUQiXX0=' + + const invitation = ConnectionInvitationMessage.fromUrl(invitationUrl) + + expect(validateOrReject(invitation)).resolves.toBeUndefined() + }) + + it('should throw error if url does not contain `c_i` or `d_m`', () => { + const invitationUrl = 'https://example.com?param=123' + + expect(() => ConnectionInvitationMessage.fromUrl(invitationUrl)).toThrowError() + }) + }) +}) diff --git a/packages/core/src/modules/connections/__tests__/ConnectionRequestMessage.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionRequestMessage.test.ts new file mode 100644 index 0000000000..91d9e11955 --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/ConnectionRequestMessage.test.ts @@ -0,0 +1,18 @@ +import { ClassValidationError } from '../../../error/ClassValidationError' +import { MessageValidator } from '../../../utils/MessageValidator' +import { ConnectionRequestMessage } from '../messages/ConnectionRequestMessage' + +describe('ConnectionRequestMessage', () => { + it('throws an error when the message does not contain a connection parameter', () => { + const connectionRequest = new ConnectionRequestMessage({ + did: 'did', + label: 'test-label', + }) + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete connectionRequest.connection + + expect(() => MessageValidator.validateSync(connectionRequest)).toThrowError(ClassValidationError) + }) +}) diff --git a/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts new file mode 100644 index 0000000000..39e46b91a9 --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts @@ -0,0 +1,1082 @@ +import type { AgentContext } from '../../../agent' +import type { Wallet } from '../../../wallet/Wallet' +import type { Routing } from '../services/ConnectionService' + +import { Subject } from 'rxjs' + +import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' +import { + getAgentConfig, + getAgentContext, + getMockConnection, + getMockOutOfBand, + mockFunction, +} from '../../../../tests/helpers' +import { AgentMessage } from '../../../agent/AgentMessage' +import { EventEmitter } from '../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import { Key, KeyType } from '../../../crypto' +import { signData, unpackAndVerifySignatureDecorator } from '../../../decorators/signature/SignatureDecoratorUtils' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { indyDidFromPublicKeyBase58 } from '../../../utils/did' +import { uuid } from '../../../utils/uuid' +import { AckMessage, AckStatus } from '../../common' +import { DidKey, IndyAgentService } from '../../dids' +import { DidDocumentRole } from '../../dids/domain/DidDocumentRole' +import { DidCommV1Service } from '../../dids/domain/service/DidCommV1Service' +import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' +import { DidRecord, DidRepository } from '../../dids/repository' +import { OutOfBandService } from '../../oob/OutOfBandService' +import { OutOfBandRole } from '../../oob/domain/OutOfBandRole' +import { OutOfBandState } from '../../oob/domain/OutOfBandState' +import { OutOfBandRepository } from '../../oob/repository/OutOfBandRepository' +import { ConnectionRequestMessage, ConnectionResponseMessage, TrustPingMessage } from '../messages' +import { + Connection, + DidDoc, + EmbeddedAuthentication, + Ed25119Sig2018, + DidExchangeRole, + DidExchangeState, + ReferencedAuthentication, + authenticationTypes, +} from '../models' +import { ConnectionRepository } from '../repository/ConnectionRepository' +import { ConnectionService } from '../services/ConnectionService' +import { convertToNewDidDocument } from '../services/helpers' + +jest.mock('../repository/ConnectionRepository') +jest.mock('../../oob/repository/OutOfBandRepository') +jest.mock('../../oob/OutOfBandService') +jest.mock('../../dids/repository/DidRepository') +const ConnectionRepositoryMock = ConnectionRepository as jest.Mock +const OutOfBandRepositoryMock = OutOfBandRepository as jest.Mock +const OutOfBandServiceMock = OutOfBandService as jest.Mock +const DidRepositoryMock = DidRepository as jest.Mock + +const connectionImageUrl = 'https://example.com/image.png' + +const agentConfig = getAgentConfig('ConnectionServiceTest', { + endpoints: ['http://agent.com:8080'], + connectionImageUrl, +}) + +const outOfBandRepository = new OutOfBandRepositoryMock() +const outOfBandService = new OutOfBandServiceMock() +const didRepository = new DidRepositoryMock() + +describe('ConnectionService', () => { + let wallet: Wallet + let connectionRepository: ConnectionRepository + + let connectionService: ConnectionService + let eventEmitter: EventEmitter + let myRouting: Routing + let agentContext: AgentContext + + beforeAll(async () => { + wallet = new InMemoryWallet() + agentContext = getAgentContext({ + wallet, + agentConfig, + registerInstances: [ + [OutOfBandRepository, outOfBandRepository], + [OutOfBandService, outOfBandService], + [DidRepository, didRepository], + ], + }) + await wallet.createAndOpen(agentConfig.walletConfig) + }) + + afterAll(async () => { + await wallet.delete() + }) + + beforeEach(async () => { + eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) + connectionRepository = new ConnectionRepositoryMock() + connectionService = new ConnectionService(agentConfig.logger, connectionRepository, didRepository, eventEmitter) + myRouting = { + recipientKey: Key.fromFingerprint('z6MkwFkSP4uv5PhhKJCGehtjuZedkotC7VF64xtMsxuM8R3W'), + endpoints: agentConfig.endpoints ?? [], + routingKeys: [], + mediatorId: 'fakeMediatorId', + } + + mockFunction(didRepository.getById).mockResolvedValue( + Promise.resolve( + new DidRecord({ + did: 'did:peer:123', + role: DidDocumentRole.Created, + }) + ) + ) + mockFunction(didRepository.findByQuery).mockResolvedValue(Promise.resolve([])) + }) + + describe('createRequest', () => { + it('returns a connection request message containing the information from the connection record', async () => { + expect.assertions(5) + + const outOfBand = getMockOutOfBand({ state: OutOfBandState.PrepareResponse }) + const config = { routing: myRouting } + + const { connectionRecord, message } = await connectionService.createRequest(agentContext, outOfBand, config) + + expect(connectionRecord.state).toBe(DidExchangeState.RequestSent) + expect(message.label).toBe(agentConfig.label) + expect(message.connection.did).toBe('XpwgBjsC2wh3eHcMW6ZRJT') + + const publicKey = new Ed25119Sig2018({ + id: `XpwgBjsC2wh3eHcMW6ZRJT#1`, + controller: 'XpwgBjsC2wh3eHcMW6ZRJT', + publicKeyBase58: 'HoVPnpfUjrDECoMZy8vu4U6dwEcLhbzjNwyS3gwLDCG8', + }) + + expect(message.connection.didDoc).toEqual( + new DidDoc({ + id: 'XpwgBjsC2wh3eHcMW6ZRJT', + publicKey: [publicKey], + authentication: [new ReferencedAuthentication(publicKey, authenticationTypes.Ed25519VerificationKey2018)], + + service: [ + new IndyAgentService({ + id: `XpwgBjsC2wh3eHcMW6ZRJT#IndyAgentService-1`, + serviceEndpoint: agentConfig.endpoints[0], + recipientKeys: ['HoVPnpfUjrDECoMZy8vu4U6dwEcLhbzjNwyS3gwLDCG8'], + routingKeys: [], + }), + ], + }) + ) + expect(message.imageUrl).toBe(connectionImageUrl) + }) + + it('returns a connection request message containing a custom label', async () => { + expect.assertions(1) + + const outOfBand = getMockOutOfBand({ state: OutOfBandState.PrepareResponse }) + const config = { label: 'Custom label', routing: myRouting } + + const { message } = await connectionService.createRequest(agentContext, outOfBand, config) + + expect(message.label).toBe('Custom label') + }) + + it('returns a connection record containing image url', async () => { + expect.assertions(1) + + const outOfBand = getMockOutOfBand({ state: OutOfBandState.PrepareResponse, imageUrl: connectionImageUrl }) + const config = { label: 'Custom label', routing: myRouting } + + const { connectionRecord } = await connectionService.createRequest(agentContext, outOfBand, config) + + expect(connectionRecord.imageUrl).toBe(connectionImageUrl) + }) + + it('returns a connection request message containing a custom image url', async () => { + expect.assertions(1) + + const outOfBand = getMockOutOfBand({ state: OutOfBandState.PrepareResponse }) + const config = { imageUrl: 'custom-image-url', routing: myRouting } + + const { message } = await connectionService.createRequest(agentContext, outOfBand, config) + + expect(message.imageUrl).toBe('custom-image-url') + }) + + it(`throws an error when out-of-band role is not ${OutOfBandRole.Receiver}`, async () => { + expect.assertions(1) + + const outOfBand = getMockOutOfBand({ role: OutOfBandRole.Sender, state: OutOfBandState.PrepareResponse }) + const config = { routing: myRouting } + + return expect(connectionService.createRequest(agentContext, outOfBand, config)).rejects.toThrowError( + `Invalid out-of-band record role ${OutOfBandRole.Sender}, expected is ${OutOfBandRole.Receiver}.` + ) + }) + + const invalidConnectionStates = [OutOfBandState.Initial, OutOfBandState.AwaitResponse, OutOfBandState.Done] + test.each(invalidConnectionStates)( + `throws an error when out-of-band state is %s and not ${OutOfBandState.PrepareResponse}`, + (state) => { + expect.assertions(1) + + const outOfBand = getMockOutOfBand({ state }) + const config = { routing: myRouting } + + return expect(connectionService.createRequest(agentContext, outOfBand, config)).rejects.toThrowError( + `Invalid out-of-band record state ${state}, valid states are: ${OutOfBandState.PrepareResponse}.` + ) + } + ) + }) + + describe('processRequest', () => { + it('returns a connection record containing the information from the connection request', async () => { + expect.assertions(5) + + const theirDid = 'their-did' + const theirKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) + const theirDidDoc = new DidDoc({ + id: theirDid, + publicKey: [], + authentication: [ + new EmbeddedAuthentication( + new Ed25119Sig2018({ + id: `${theirDid}#key-id`, + controller: theirDid, + publicKeyBase58: theirKey.publicKeyBase58, + }) + ), + ], + service: [ + new DidCommV1Service({ + id: `${theirDid};indy`, + serviceEndpoint: 'https://endpoint.com', + recipientKeys: [`${theirDid}#key-id`], + }), + ], + }) + + const connectionRequest = new ConnectionRequestMessage({ + did: theirDid, + didDoc: theirDidDoc, + label: 'test-label', + imageUrl: connectionImageUrl, + }) + + const messageContext = new InboundMessageContext(connectionRequest, { + agentContext, + senderKey: theirKey, + recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), + }) + + const outOfBand = getMockOutOfBand({ + mediatorId: 'fakeMediatorId', + role: OutOfBandRole.Sender, + state: OutOfBandState.AwaitResponse, + }) + const processedConnection = await connectionService.processRequest(messageContext, outOfBand) + + expect(processedConnection.state).toBe(DidExchangeState.RequestReceived) + expect(processedConnection.theirDid).toBe('did:peer:1zQmW2esSyEVGzrh3CFt1eQZUHEAb3Li1hyPudPhSoFevrFY') + expect(processedConnection.theirLabel).toBe('test-label') + expect(processedConnection.threadId).toBe(connectionRequest.id) + expect(processedConnection.imageUrl).toBe(connectionImageUrl) + }) + + it('returns a new connection record containing the information from the connection request when multiUseInvitation is enabled on the connection', async () => { + expect.assertions(8) + + const connectionRecord = getMockConnection({ + id: 'test', + state: DidExchangeState.InvitationSent, + role: DidExchangeRole.Responder, + }) + + const theirDid = 'their-did' + const theirKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) + const theirDidDoc = new DidDoc({ + id: theirDid, + publicKey: [], + authentication: [ + new EmbeddedAuthentication( + new Ed25119Sig2018({ + id: `${theirDid}#key-id`, + controller: theirDid, + publicKeyBase58: theirKey.publicKeyBase58, + }) + ), + ], + service: [ + new DidCommV1Service({ + id: `${theirDid};indy`, + serviceEndpoint: 'https://endpoint.com', + recipientKeys: [`${theirDid}#key-id`], + }), + ], + }) + + const connectionRequest = new ConnectionRequestMessage({ + did: theirDid, + didDoc: theirDidDoc, + label: 'test-label', + }) + + const messageContext = new InboundMessageContext(connectionRequest, { + agentContext, + connection: connectionRecord, + senderKey: theirKey, + recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), + }) + + const outOfBand = getMockOutOfBand({ + mediatorId: 'fakeMediatorId', + role: OutOfBandRole.Sender, + state: OutOfBandState.AwaitResponse, + }) + const processedConnection = await connectionService.processRequest(messageContext, outOfBand) + + expect(processedConnection.state).toBe(DidExchangeState.RequestReceived) + expect(processedConnection.theirDid).toBe('did:peer:1zQmW2esSyEVGzrh3CFt1eQZUHEAb3Li1hyPudPhSoFevrFY') + expect(processedConnection.theirLabel).toBe('test-label') + expect(processedConnection.threadId).toBe(connectionRequest.id) + + expect(connectionRepository.save).toHaveBeenCalledTimes(1) + expect(processedConnection.id).not.toBe(connectionRecord.id) + expect(connectionRecord.id).toBe('test') + expect(connectionRecord.state).toBe(DidExchangeState.InvitationSent) + }) + + it('throws an error when the message does not contain a did doc', async () => { + expect.assertions(1) + + const connectionRequest = new ConnectionRequestMessage({ + did: 'did', + label: 'test-label', + }) + + const messageContext = new InboundMessageContext(connectionRequest, { + agentContext, + recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), + senderKey: Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519), + }) + + const outOfBand = getMockOutOfBand({ role: OutOfBandRole.Sender, state: OutOfBandState.AwaitResponse }) + + return expect(connectionService.processRequest(messageContext, outOfBand)).rejects.toThrowError( + `Public DIDs are not supported yet` + ) + }) + + it(`throws an error when out-of-band role is not ${OutOfBandRole.Sender}`, async () => { + expect.assertions(1) + + const inboundMessage = new InboundMessageContext(jest.fn()(), { + agentContext, + recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), + senderKey: Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519), + }) + + const outOfBand = getMockOutOfBand({ role: OutOfBandRole.Receiver, state: OutOfBandState.AwaitResponse }) + + return expect(connectionService.processRequest(inboundMessage, outOfBand)).rejects.toThrowError( + `Invalid out-of-band record role ${OutOfBandRole.Receiver}, expected is ${OutOfBandRole.Sender}.` + ) + }) + + const invalidOutOfBandStates = [OutOfBandState.Initial, OutOfBandState.PrepareResponse, OutOfBandState.Done] + test.each(invalidOutOfBandStates)( + `throws an error when out-of-band state is %s and not ${OutOfBandState.AwaitResponse}`, + (state) => { + expect.assertions(1) + + const inboundMessage = new InboundMessageContext(jest.fn()(), { agentContext }) + const outOfBand = getMockOutOfBand({ role: OutOfBandRole.Sender, state }) + + return expect(connectionService.processRequest(inboundMessage, outOfBand)).rejects.toThrowError( + `Invalid out-of-band record state ${state}, valid states are: ${OutOfBandState.AwaitResponse}.` + ) + } + ) + }) + + describe('createResponse', () => { + it('returns a connection response message containing the information from the connection record', async () => { + expect.assertions(2) + + const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const did = indyDidFromPublicKeyBase58(key.publicKeyBase58) + + // Needed for signing connection~sig + const mockConnection = getMockConnection({ + state: DidExchangeState.RequestReceived, + role: DidExchangeRole.Responder, + tags: { + threadId: 'test', + }, + }) + + const recipientKeys = [new DidKey(key)] + const outOfBand = getMockOutOfBand({ recipientKeys: recipientKeys.map((did) => did.did) }) + + const publicKey = new Ed25119Sig2018({ + id: `${did}#1`, + controller: did, + publicKeyBase58: key.publicKeyBase58, + }) + const mockDidDoc = new DidDoc({ + id: did, + publicKey: [publicKey], + authentication: [new ReferencedAuthentication(publicKey, authenticationTypes.Ed25519VerificationKey2018)], + service: [ + new IndyAgentService({ + id: `${did}#IndyAgentService-1`, + serviceEndpoint: 'http://example.com', + recipientKeys: recipientKeys.map((did) => did.key.publicKeyBase58), + routingKeys: [], + }), + ], + }) + + const { message, connectionRecord: connectionRecord } = await connectionService.createResponse( + agentContext, + mockConnection, + outOfBand + ) + + const connection = new Connection({ + did, + didDoc: mockDidDoc, + }) + const plainConnection = JsonTransformer.toJSON(connection) + + expect(connectionRecord.state).toBe(DidExchangeState.ResponseSent) + expect(await unpackAndVerifySignatureDecorator(message.connectionSig, wallet)).toEqual(plainConnection) + }) + + it(`throws an error when connection role is ${DidExchangeRole.Requester} and not ${DidExchangeRole.Responder}`, async () => { + expect.assertions(1) + + const connection = getMockConnection({ + role: DidExchangeRole.Requester, + state: DidExchangeState.RequestReceived, + }) + const outOfBand = getMockOutOfBand() + return expect(connectionService.createResponse(agentContext, connection, outOfBand)).rejects.toThrowError( + `Connection record has invalid role ${DidExchangeRole.Requester}. Expected role ${DidExchangeRole.Responder}.` + ) + }) + + const invalidOutOfBandStates = [ + DidExchangeState.InvitationSent, + DidExchangeState.InvitationReceived, + DidExchangeState.RequestSent, + DidExchangeState.ResponseSent, + DidExchangeState.ResponseReceived, + DidExchangeState.Completed, + DidExchangeState.Abandoned, + DidExchangeState.Start, + ] + test.each(invalidOutOfBandStates)( + `throws an error when connection state is %s and not ${DidExchangeState.RequestReceived}`, + async (state) => { + expect.assertions(1) + + const connection = getMockConnection({ state }) + const outOfBand = getMockOutOfBand() + return expect(connectionService.createResponse(agentContext, connection, outOfBand)).rejects.toThrowError( + `Connection record is in invalid state ${state}. Valid states are: ${DidExchangeState.RequestReceived}.` + ) + } + ) + }) + + describe('processResponse', () => { + it('returns a connection record containing the information from the connection response', async () => { + expect.assertions(2) + + const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const did = indyDidFromPublicKeyBase58(key.publicKeyBase58) + + const theirKey = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const theirDid = indyDidFromPublicKeyBase58(key.publicKeyBase58) + + const connectionRecord = getMockConnection({ + did, + state: DidExchangeState.RequestSent, + role: DidExchangeRole.Requester, + }) + + const otherPartyConnection = new Connection({ + did: theirDid, + didDoc: new DidDoc({ + id: theirDid, + publicKey: [], + authentication: [ + new EmbeddedAuthentication( + new Ed25119Sig2018({ + id: `${theirDid}#key-id`, + controller: theirDid, + publicKeyBase58: theirKey.publicKeyBase58, + }) + ), + ], + service: [ + new DidCommV1Service({ + id: `${did};indy`, + serviceEndpoint: 'https://endpoint.com', + recipientKeys: [`${theirDid}#key-id`], + }), + ], + }), + }) + + const plainConnection = JsonTransformer.toJSON(otherPartyConnection) + const connectionSig = await signData(plainConnection, wallet, theirKey.publicKeyBase58) + + const connectionResponse = new ConnectionResponseMessage({ + threadId: uuid(), + connectionSig, + }) + + const outOfBandRecord = getMockOutOfBand({ + recipientKeys: [new DidKey(theirKey).did], + }) + const messageContext = new InboundMessageContext(connectionResponse, { + agentContext, + connection: connectionRecord, + senderKey: theirKey, + recipientKey: key, + }) + + const processedConnection = await connectionService.processResponse(messageContext, outOfBandRecord) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const peerDid = didDocumentJsonToNumAlgo1Did(convertToNewDidDocument(otherPartyConnection.didDoc!).toJSON()) + + expect(processedConnection.state).toBe(DidExchangeState.ResponseReceived) + expect(processedConnection.theirDid).toBe(peerDid) + }) + + it(`throws an error when connection role is ${DidExchangeRole.Responder} and not ${DidExchangeRole.Requester}`, async () => { + expect.assertions(1) + + const outOfBandRecord = getMockOutOfBand() + const connectionRecord = getMockConnection({ + role: DidExchangeRole.Responder, + state: DidExchangeState.RequestSent, + }) + const messageContext = new InboundMessageContext(jest.fn()(), { + agentContext, + connection: connectionRecord, + recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), + senderKey: Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519), + }) + + return expect(connectionService.processResponse(messageContext, outOfBandRecord)).rejects.toThrowError( + `Connection record has invalid role ${DidExchangeRole.Responder}. Expected role ${DidExchangeRole.Requester}.` + ) + }) + + it('throws an error when the connection sig is not signed with the same key as the recipient key from the invitation', async () => { + expect.assertions(1) + + const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const did = indyDidFromPublicKeyBase58(key.publicKeyBase58) + + const theirKey = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const theirDid = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const connectionRecord = getMockConnection({ + did, + role: DidExchangeRole.Requester, + state: DidExchangeState.RequestSent, + }) + + const otherPartyConnection = new Connection({ + did: theirDid, + didDoc: new DidDoc({ + id: theirDid, + publicKey: [], + authentication: [ + new EmbeddedAuthentication( + new Ed25119Sig2018({ + id: `${theirDid}#key-id`, + controller: theirDid, + publicKeyBase58: theirKey.publicKeyBase58, + }) + ), + ], + service: [ + new DidCommV1Service({ + id: `${did};indy`, + serviceEndpoint: 'https://endpoint.com', + recipientKeys: [`${theirDid}#key-id`], + }), + ], + }), + }) + const plainConnection = JsonTransformer.toJSON(otherPartyConnection) + const connectionSig = await signData(plainConnection, wallet, theirKey.publicKeyBase58) + + const connectionResponse = new ConnectionResponseMessage({ + threadId: uuid(), + connectionSig, + }) + + // Recipient key `verkey` is not the same as theirVerkey which was used to sign message, + // therefore it should cause a failure. + const outOfBandRecord = getMockOutOfBand({ + recipientKeys: [new DidKey(key).did], + }) + const messageContext = new InboundMessageContext(connectionResponse, { + agentContext, + connection: connectionRecord, + senderKey: theirKey, + recipientKey: key, + }) + + return expect(connectionService.processResponse(messageContext, outOfBandRecord)).rejects.toThrowError( + new RegExp( + 'Connection object in connection response message is not signed with same key as recipient key in invitation' + ) + ) + }) + + it('throws an error when the message does not contain a DID Document', async () => { + expect.assertions(1) + + const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const did = indyDidFromPublicKeyBase58(key.publicKeyBase58) + + const theirKey = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const theirDid = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const connectionRecord = getMockConnection({ + did, + state: DidExchangeState.RequestSent, + theirDid: undefined, + }) + + const otherPartyConnection = new Connection({ did: theirDid }) + const plainConnection = JsonTransformer.toJSON(otherPartyConnection) + const connectionSig = await signData(plainConnection, wallet, theirKey.publicKeyBase58) + + const connectionResponse = new ConnectionResponseMessage({ threadId: uuid(), connectionSig }) + + const outOfBandRecord = getMockOutOfBand({ recipientKeys: [new DidKey(theirKey).did] }) + const messageContext = new InboundMessageContext(connectionResponse, { + agentContext, + connection: connectionRecord, + recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), + senderKey: Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519), + }) + + return expect(connectionService.processResponse(messageContext, outOfBandRecord)).rejects.toThrowError( + `DID Document is missing.` + ) + }) + }) + + describe('createTrustPing', () => { + it('returns a trust ping message', async () => { + expect.assertions(2) + + const mockConnection = getMockConnection({ state: DidExchangeState.ResponseReceived }) + + const { message, connectionRecord: connectionRecord } = await connectionService.createTrustPing( + agentContext, + mockConnection + ) + + expect(connectionRecord.state).toBe(DidExchangeState.Completed) + expect(message).toEqual(expect.any(TrustPingMessage)) + }) + + const invalidConnectionStates = [ + DidExchangeState.InvitationSent, + DidExchangeState.InvitationReceived, + DidExchangeState.RequestSent, + DidExchangeState.RequestReceived, + DidExchangeState.ResponseSent, + DidExchangeState.Abandoned, + DidExchangeState.Start, + ] + test.each(invalidConnectionStates)( + `throws an error when connection state is %s and not ${DidExchangeState.ResponseReceived} or ${DidExchangeState.Completed}`, + (state) => { + expect.assertions(1) + const connection = getMockConnection({ state }) + + return expect(connectionService.createTrustPing(agentContext, connection)).rejects.toThrowError( + `Connection record is in invalid state ${state}. Valid states are: ${DidExchangeState.ResponseReceived}, ${DidExchangeState.Completed}.` + ) + } + ) + }) + + describe('processAck', () => { + it('throws an error when the message context does not have a connection', async () => { + expect.assertions(1) + + const ack = new AckMessage({ + status: AckStatus.OK, + threadId: 'thread-id', + }) + + const messageContext = new InboundMessageContext(ack, { agentContext }) + + return expect(connectionService.processAck(messageContext)).rejects.toThrowError( + 'Unable to process connection ack: connection for recipient key undefined not found' + ) + }) + + it('updates the state to Completed when the state is ResponseSent and role is Responder', async () => { + expect.assertions(1) + + const connection = getMockConnection({ + state: DidExchangeState.ResponseSent, + role: DidExchangeRole.Responder, + }) + + const ack = new AckMessage({ + status: AckStatus.OK, + threadId: 'thread-id', + }) + + const messageContext = new InboundMessageContext(ack, { agentContext, connection }) + + const updatedConnection = await connectionService.processAck(messageContext) + + expect(updatedConnection.state).toBe(DidExchangeState.Completed) + }) + + it('does not update the state when the state is not ResponseSent or the role is not Responder', async () => { + expect.assertions(1) + + const connection = getMockConnection({ + state: DidExchangeState.ResponseReceived, + role: DidExchangeRole.Requester, + }) + + const ack = new AckMessage({ + status: AckStatus.OK, + threadId: 'thread-id', + }) + + const messageContext = new InboundMessageContext(ack, { agentContext, connection }) + + const updatedConnection = await connectionService.processAck(messageContext) + + expect(updatedConnection.state).toBe(DidExchangeState.ResponseReceived) + }) + }) + + describe('assertConnectionOrOutOfBandExchange', () => { + it('should throw an error when a expectedConnectionId is present, but no connection is present in the messageContext', async () => { + expect.assertions(1) + + const messageContext = new InboundMessageContext(new AgentMessage(), { + agentContext, + }) + + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + expectedConnectionId: '123', + }) + ).rejects.toThrow('Expected incoming message to be from connection 123 but no connection found.') + }) + + it('should throw an error when a expectedConnectionId is present, but does not match with connection id present in the messageContext', async () => { + expect.assertions(1) + + const messageContext = new InboundMessageContext(new AgentMessage(), { + agentContext, + connection: getMockConnection({ state: DidExchangeState.InvitationReceived, id: 'something' }), + }) + + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + expectedConnectionId: 'something-else', + }) + ).rejects.toThrow('Expected incoming message to be from connection something-else but connection is something.') + }) + + it('should not throw an error when a connection record with state complete is present in the messageContext', async () => { + expect.assertions(1) + + const messageContext = new InboundMessageContext(new AgentMessage(), { + agentContext, + connection: getMockConnection({ state: DidExchangeState.Completed }), + }) + + await expect(connectionService.assertConnectionOrOutOfBandExchange(messageContext)).resolves.not.toThrow() + }) + + it('should throw an error when a connection record is present and state not complete in the messageContext', async () => { + expect.assertions(1) + + const messageContext = new InboundMessageContext(new AgentMessage(), { + agentContext, + connection: getMockConnection({ state: DidExchangeState.InvitationReceived }), + }) + + await expect(connectionService.assertConnectionOrOutOfBandExchange(messageContext)).rejects.toThrowError( + 'Connection record is not ready to be used' + ) + }) + + it('should not throw an error when no connection record is present in the messageContext and no additional data, but the message has a ~service decorator', async () => { + expect.assertions(1) + + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(null) + + const message = new AgentMessage() + message.setService({ + recipientKeys: [], + serviceEndpoint: '', + routingKeys: [], + }) + const messageContext = new InboundMessageContext(message, { agentContext }) + + await expect(connectionService.assertConnectionOrOutOfBandExchange(messageContext)).resolves.not.toThrow() + }) + + it('should not throw when a fully valid connection-less input is passed', async () => { + expect.assertions(1) + + const recipientKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) + const senderKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) + + const lastSentMessage = new AgentMessage() + lastSentMessage.setService({ + recipientKeys: [recipientKey.publicKeyBase58], + serviceEndpoint: '', + routingKeys: [], + }) + + const lastReceivedMessage = new AgentMessage() + lastReceivedMessage.setService({ + recipientKeys: [senderKey.publicKeyBase58], + serviceEndpoint: '', + routingKeys: [], + }) + + const message = new AgentMessage() + message.setService({ + recipientKeys: [senderKey.publicKeyBase58], + serviceEndpoint: '', + routingKeys: [], + }) + const messageContext = new InboundMessageContext(message, { agentContext, recipientKey, senderKey }) + + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, + }) + ).resolves.not.toThrow() + }) + + it('should throw an error when lastSentMessage is present, but recipientVerkey is not ', async () => { + expect.assertions(1) + + const lastSentMessage = new AgentMessage() + lastSentMessage.setService({ + recipientKeys: [], + serviceEndpoint: '', + routingKeys: [], + }) + + const message = new AgentMessage() + message.setService({ + recipientKeys: [], + serviceEndpoint: '', + routingKeys: [], + }) + const messageContext = new InboundMessageContext(message, { agentContext }) + + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastSentMessage, + }) + ).rejects.toThrowError( + 'Incoming message must have recipientKey and senderKey (so cannot be AuthCrypt or unpacked) if there are lastSentMessage or lastReceivedMessage.' + ) + }) + + it('should throw an error when lastSentMessage and recipientKey are present, but recipient key is not present in recipientKeys of previously sent message ~service decorator', async () => { + expect.assertions(1) + + const recipientKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) + const senderKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) + + const lastSentMessage = new AgentMessage() + lastSentMessage.setService({ + recipientKeys: ['anotherKey'], + serviceEndpoint: '', + routingKeys: [], + }) + + const message = new AgentMessage() + message.setService({ + recipientKeys: [], + serviceEndpoint: '', + routingKeys: [], + }) + const messageContext = new InboundMessageContext(message, { agentContext, recipientKey, senderKey }) + + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastSentMessage, + }) + ).rejects.toThrowError('Recipient key 8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K not found in our service') + }) + + it('should throw an error when lastReceivedMessage is present, but senderVerkey is not ', async () => { + expect.assertions(1) + + const lastReceivedMessage = new AgentMessage() + lastReceivedMessage.setService({ + recipientKeys: [], + serviceEndpoint: '', + routingKeys: [], + }) + + const message = new AgentMessage() + const messageContext = new InboundMessageContext(message, { agentContext }) + + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + }) + ).rejects.toThrowError( + 'No keys on our side to use for encrypting messages, and previous messages found (in which case our keys MUST also be present).' + ) + }) + + it('should throw an error when lastReceivedMessage and senderKey are present, but sender key is not present in recipientKeys of previously received message ~service decorator', async () => { + expect.assertions(1) + + const senderKey = 'senderKey' + + const lastReceivedMessage = new AgentMessage() + lastReceivedMessage.setService({ + recipientKeys: ['anotherKey'], + serviceEndpoint: '', + routingKeys: [], + }) + + const lastSentMessage = new AgentMessage() + lastSentMessage.setService({ + recipientKeys: [senderKey], + serviceEndpoint: '', + routingKeys: [], + }) + + const message = new AgentMessage() + const messageContext = new InboundMessageContext(message, { + agentContext, + senderKey: Key.fromPublicKeyBase58('randomKey', KeyType.Ed25519), + recipientKey: Key.fromPublicKeyBase58(senderKey, KeyType.Ed25519), + }) + + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, + }) + ).rejects.toThrowError('Sender key randomKey not found in their service') + }) + }) + + describe('repository methods', () => { + it('getById should return value from connectionRepository.getById', async () => { + const expected = getMockConnection() + mockFunction(connectionRepository.getById).mockReturnValue(Promise.resolve(expected)) + const result = await connectionService.getById(agentContext, expected.id) + expect(connectionRepository.getById).toBeCalledWith(agentContext, expected.id) + + expect(result).toBe(expected) + }) + + it('getByThreadId should return value from connectionRepository.getSingleByQuery', async () => { + const expected = getMockConnection() + mockFunction(connectionRepository.getByThreadId).mockReturnValue(Promise.resolve(expected)) + const result = await connectionService.getByThreadId(agentContext, 'threadId') + expect(connectionRepository.getByThreadId).toBeCalledWith(agentContext, 'threadId') + + expect(result).toBe(expected) + }) + + it('findById should return value from connectionRepository.findById', async () => { + const expected = getMockConnection() + mockFunction(connectionRepository.findById).mockReturnValue(Promise.resolve(expected)) + const result = await connectionService.findById(agentContext, expected.id) + expect(connectionRepository.findById).toBeCalledWith(agentContext, expected.id) + + expect(result).toBe(expected) + }) + + it('getAll should return value from connectionRepository.getAll', async () => { + const expected = [getMockConnection(), getMockConnection()] + + mockFunction(connectionRepository.getAll).mockReturnValue(Promise.resolve(expected)) + const result = await connectionService.getAll(agentContext) + expect(connectionRepository.getAll).toBeCalledWith(agentContext) + + expect(result).toEqual(expect.arrayContaining(expected)) + }) + + it('findAllByQuery should return value from connectionRepository.findByQuery', async () => { + const expected = [getMockConnection(), getMockConnection()] + + mockFunction(connectionRepository.findByQuery).mockReturnValue(Promise.resolve(expected)) + const result = await connectionService.findAllByQuery( + agentContext, + { + state: DidExchangeState.InvitationReceived, + }, + undefined + ) + expect(connectionRepository.findByQuery).toBeCalledWith( + agentContext, + { + state: DidExchangeState.InvitationReceived, + }, + undefined + ) + + expect(result).toEqual(expect.arrayContaining(expected)) + }) + }) + + describe('connectionType', () => { + it('addConnectionType', async () => { + const connection = getMockConnection() + + await connectionService.addConnectionType(agentContext, connection, 'type-1') + let connectionTypes = await connectionService.getConnectionTypes(connection) + expect(connectionTypes).toMatchObject(['type-1']) + + await connectionService.addConnectionType(agentContext, connection, 'type-2') + await connectionService.addConnectionType(agentContext, connection, 'type-3') + + connectionTypes = await connectionService.getConnectionTypes(connection) + expect(connectionTypes.sort()).toMatchObject(['type-1', 'type-2', 'type-3'].sort()) + }) + + it('removeConnectionType - existing type', async () => { + const connection = getMockConnection() + connection.connectionTypes = ['type-1', 'type-2', 'type-3'] + let connectionTypes = await connectionService.getConnectionTypes(connection) + expect(connectionTypes.sort()).toMatchObject(['type-1', 'type-2', 'type-3'].sort()) + + await connectionService.removeConnectionType(agentContext, connection, 'type-2') + connectionTypes = await connectionService.getConnectionTypes(connection) + expect(connectionTypes.sort()).toMatchObject(['type-1', 'type-3'].sort()) + }) + + it('removeConnectionType - type not existent', async () => { + const connection = getMockConnection() + connection.connectionTypes = ['type-1', 'type-2', 'type-3'] + let connectionTypes = await connectionService.getConnectionTypes(connection) + expect(connectionTypes).toMatchObject(['type-1', 'type-2', 'type-3']) + + await connectionService.removeConnectionType(agentContext, connection, 'type-4') + connectionTypes = await connectionService.getConnectionTypes(connection) + expect(connectionTypes.sort()).toMatchObject(['type-1', 'type-2', 'type-3'].sort()) + }) + + it('removeConnectionType - no previous types', async () => { + const connection = getMockConnection() + + let connectionTypes = await connectionService.getConnectionTypes(connection) + expect(connectionTypes).toMatchObject([]) + + await connectionService.removeConnectionType(agentContext, connection, 'type-4') + connectionTypes = await connectionService.getConnectionTypes(connection) + expect(connectionTypes).toMatchObject([]) + }) + }) +}) diff --git a/packages/core/src/modules/connections/__tests__/ConnectionsModule.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionsModule.test.ts new file mode 100644 index 0000000000..5d026182dc --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/ConnectionsModule.test.ts @@ -0,0 +1,35 @@ +import { FeatureRegistry } from '../../../agent/FeatureRegistry' +import { DependencyManager } from '../../../plugins/DependencyManager' +import { ConnectionsModule } from '../ConnectionsModule' +import { ConnectionsModuleConfig } from '../ConnectionsModuleConfig' +import { DidExchangeProtocol } from '../DidExchangeProtocol' +import { ConnectionRepository } from '../repository' +import { ConnectionService, TrustPingService } from '../services' +import { DidRotateService } from '../services/DidRotateService' + +jest.mock('../../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +jest.mock('../../../agent/FeatureRegistry') +const FeatureRegistryMock = FeatureRegistry as jest.Mock + +const featureRegistry = new FeatureRegistryMock() + +describe('ConnectionsModule', () => { + test('registers dependencies on the dependency manager', () => { + const connectionsModule = new ConnectionsModule() + connectionsModule.register(dependencyManager, featureRegistry) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(ConnectionsModuleConfig, connectionsModule.config) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(5) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(ConnectionService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidExchangeProtocol) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TrustPingService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidRotateService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(ConnectionRepository) + }) +}) diff --git a/packages/core/src/modules/connections/__tests__/ConnectionsModuleConfig.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionsModuleConfig.test.ts new file mode 100644 index 0000000000..bc4c0b29bb --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/ConnectionsModuleConfig.test.ts @@ -0,0 +1,17 @@ +import { ConnectionsModuleConfig } from '../ConnectionsModuleConfig' + +describe('ConnectionsModuleConfig', () => { + test('sets default values', () => { + const config = new ConnectionsModuleConfig() + + expect(config.autoAcceptConnections).toBe(false) + }) + + test('sets values', () => { + const config = new ConnectionsModuleConfig({ + autoAcceptConnections: true, + }) + + expect(config.autoAcceptConnections).toBe(true) + }) +}) diff --git a/packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts b/packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts new file mode 100644 index 0000000000..0741c5abf7 --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts @@ -0,0 +1,100 @@ +import type { AgentContext } from '../../../agent' +import type { + DidRegistrar, + DidResolver, + DidDocument, + DidCreateOptions, + DidCreateResult, + DidUpdateResult, + DidDeactivateResult, + DidResolutionResult, +} from '../../dids' + +import { DidRecord, DidDocumentRole, DidRepository } from '../../dids' + +export class InMemoryDidRegistry implements DidRegistrar, DidResolver { + public readonly supportedMethods = ['inmemory'] + + public readonly allowsCaching = false + + private dids: Record = {} + + public async create(agentContext: AgentContext, options: DidCreateOptions): Promise { + const { did, didDocument } = options + + if (!did || !didDocument) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'InMemoryDidRegistrar requires to specify both did and didDocument', + }, + } + } + + this.dids[did] = didDocument + + // Save the did so we know we created it and can use it for didcomm + const didRecord = new DidRecord({ + did: didDocument.id, + role: DidDocumentRole.Created, + didDocument, + }) + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + await didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: didDocument.id, + didDocument, + }, + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:inmemory not implemented yet`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:inmemory not implemented yet`, + }, + } + } + + public async resolve(agentContext: AgentContext, did: string): Promise { + const didDocument = this.dids[did] + + if (!didDocument) { + return { + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}'`, + }, + } + } + + return { + didDocument, + didDocumentMetadata: {}, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } +} diff --git a/packages/core/src/modules/connections/__tests__/connection-manual.test.ts b/packages/core/src/modules/connections/__tests__/connection-manual.test.ts new file mode 100644 index 0000000000..fe519c8950 --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/connection-manual.test.ts @@ -0,0 +1,147 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { ConnectionStateChangedEvent } from '../ConnectionEvents' + +import { firstValueFrom } from 'rxjs' +import { filter, first, map, timeout } from 'rxjs/operators' + +import { setupSubjectTransports } from '../../../../tests' +import { getInMemoryAgentOptions } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { ConnectionEventTypes } from '../ConnectionEvents' +import { ConnectionsModule } from '../ConnectionsModule' +import { DidExchangeState } from '../models' + +function waitForRequest(agent: Agent, theirLabel: string) { + return firstValueFrom( + agent.events.observable(ConnectionEventTypes.ConnectionStateChanged).pipe( + map((event) => event.payload.connectionRecord), + // Wait for request received + filter( + (connectionRecord) => + connectionRecord.state === DidExchangeState.RequestReceived && connectionRecord.theirLabel === theirLabel + ), + first(), + timeout(5000) + ) + ) +} + +function waitForResponse(agent: Agent, connectionId: string) { + return firstValueFrom( + agent.events.observable(ConnectionEventTypes.ConnectionStateChanged).pipe( + // Wait for response received + map((event) => event.payload.connectionRecord), + filter( + (connectionRecord) => + connectionRecord.state === DidExchangeState.ResponseReceived && connectionRecord.id === connectionId + ), + first(), + timeout(5000) + ) + ) +} + +describe('Manual Connection Flow', () => { + // This test was added to reproduce a bug where all connections based on a reusable invitation would use the same keys + // This was only present in the manual flow, which is almost never used. + it('can connect multiple times using the same reusable invitation without manually using the connections api', async () => { + const aliceAgentOptions = getInMemoryAgentOptions( + 'Manual Connection Flow Alice', + { + label: 'alice', + endpoints: ['rxjs:alice'], + }, + { + connections: new ConnectionsModule({ + autoAcceptConnections: false, + }), + } + ) + const bobAgentOptions = getInMemoryAgentOptions( + 'Manual Connection Flow Bob', + { + label: 'bob', + endpoints: ['rxjs:bob'], + }, + { + connections: new ConnectionsModule({ + autoAcceptConnections: false, + }), + } + ) + const faberAgentOptions = getInMemoryAgentOptions( + 'Manual Connection Flow Faber', + { + endpoints: ['rxjs:faber'], + }, + { + connections: new ConnectionsModule({ + autoAcceptConnections: false, + }), + } + ) + + const aliceAgent = new Agent(aliceAgentOptions) + const bobAgent = new Agent(bobAgentOptions) + const faberAgent = new Agent(faberAgentOptions) + + setupSubjectTransports([aliceAgent, bobAgent, faberAgent]) + await aliceAgent.initialize() + await bobAgent.initialize() + await faberAgent.initialize() + + const faberOutOfBandRecord = await faberAgent.oob.createInvitation({ + autoAcceptConnection: false, + multiUseInvitation: true, + }) + + const waitForAliceRequest = waitForRequest(faberAgent, 'alice') + const waitForBobRequest = waitForRequest(faberAgent, 'bob') + + let { connectionRecord: aliceConnectionRecord } = await aliceAgent.oob.receiveInvitation( + faberOutOfBandRecord.outOfBandInvitation, + { + autoAcceptInvitation: true, + autoAcceptConnection: false, + } + ) + + let { connectionRecord: bobConnectionRecord } = await bobAgent.oob.receiveInvitation( + faberOutOfBandRecord.outOfBandInvitation, + { + autoAcceptInvitation: true, + autoAcceptConnection: false, + } + ) + + let faberAliceConnectionRecord = await waitForAliceRequest + let faberBobConnectionRecord = await waitForBobRequest + + const waitForAliceResponse = waitForResponse(aliceAgent, aliceConnectionRecord!.id) + const waitForBobResponse = waitForResponse(bobAgent, bobConnectionRecord!.id) + + await faberAgent.connections.acceptRequest(faberAliceConnectionRecord.id) + await faberAgent.connections.acceptRequest(faberBobConnectionRecord.id) + + aliceConnectionRecord = await waitForAliceResponse + await aliceAgent.connections.acceptResponse(aliceConnectionRecord!.id) + + bobConnectionRecord = await waitForBobResponse + await bobAgent.connections.acceptResponse(bobConnectionRecord!.id) + + aliceConnectionRecord = await aliceAgent.connections.returnWhenIsConnected(aliceConnectionRecord!.id) + bobConnectionRecord = await bobAgent.connections.returnWhenIsConnected(bobConnectionRecord!.id) + faberAliceConnectionRecord = await faberAgent.connections.returnWhenIsConnected(faberAliceConnectionRecord!.id) + faberBobConnectionRecord = await faberAgent.connections.returnWhenIsConnected(faberBobConnectionRecord!.id) + + expect(aliceConnectionRecord).toBeConnectedWith(faberAliceConnectionRecord) + expect(bobConnectionRecord).toBeConnectedWith(faberBobConnectionRecord) + + await aliceAgent.wallet.delete() + await aliceAgent.shutdown() + await bobAgent.wallet.delete() + await bobAgent.shutdown() + await faberAgent.wallet.delete() + await faberAgent.shutdown() + }) +}) diff --git a/packages/core/src/modules/connections/__tests__/did-rotate.test.ts b/packages/core/src/modules/connections/__tests__/did-rotate.test.ts new file mode 100644 index 0000000000..ee791adcd4 --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/did-rotate.test.ts @@ -0,0 +1,404 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import { ReplaySubject, first, firstValueFrom, timeout } from 'rxjs' + +import { MessageSender } from '../../..//agent/MessageSender' +import { setupSubjectTransports, testLogger } from '../../../../tests' +import { + getInMemoryAgentOptions, + makeConnection, + waitForAgentMessageProcessedEvent, + waitForBasicMessage, + waitForDidRotate, +} from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { getOutboundMessageContext } from '../../../agent/getOutboundMessageContext' +import { RecordNotFoundError } from '../../../error' +import { uuid } from '../../../utils/uuid' +import { BasicMessage } from '../../basic-messages' +import { createPeerDidDocumentFromServices } from '../../dids' +import { ConnectionsModule } from '../ConnectionsModule' +import { DidRotateProblemReportMessage, HangupMessage, DidRotateAckMessage } from '../messages' +import { ConnectionRecord } from '../repository' + +import { InMemoryDidRegistry } from './InMemoryDidRegistry' + +// This is the most common flow +describe('Rotation E2E tests', () => { + let aliceAgent: Agent + let bobAgent: Agent + let aliceBobConnection: ConnectionRecord | undefined + let bobAliceConnection: ConnectionRecord | undefined + + beforeEach(async () => { + const aliceAgentOptions = getInMemoryAgentOptions( + 'DID Rotate Alice', + { + label: 'alice', + endpoints: ['rxjs:alice'], + logger: testLogger, + }, + { + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + } + ) + const bobAgentOptions = getInMemoryAgentOptions( + 'DID Rotate Bob', + { + label: 'bob', + endpoints: ['rxjs:bob'], + logger: testLogger, + }, + { + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + } + ) + + aliceAgent = new Agent(aliceAgentOptions) + bobAgent = new Agent(bobAgentOptions) + + setupSubjectTransports([aliceAgent, bobAgent]) + await aliceAgent.initialize() + await bobAgent.initialize() + ;[aliceBobConnection, bobAliceConnection] = await makeConnection(aliceAgent, bobAgent) + }) + + afterEach(async () => { + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + await bobAgent.shutdown() + await bobAgent.wallet.delete() + }) + + describe('Rotation from did:peer:1 to did:peer:4', () => { + test('Rotate succesfully and send messages to new did afterwards', async () => { + const oldDid = aliceBobConnection!.did + expect(bobAliceConnection!.theirDid).toEqual(oldDid) + + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + // Do did rotate + const { newDid } = await aliceAgent.connections.rotate({ connectionId: aliceBobConnection!.id }) + + // Wait for acknowledge + await waitForAgentMessageProcessedEvent(aliceAgent, { messageType: DidRotateAckMessage.type.messageTypeUri }) + + // Check that new did is taken into account by both parties + const newAliceBobConnection = await aliceAgent.connections.getById(aliceBobConnection!.id) + const newBobAliceConnection = await bobAgent.connections.getById(bobAliceConnection!.id) + + expect(newAliceBobConnection.did).toEqual(newDid) + expect(newBobAliceConnection.theirDid).toEqual(newDid) + + // And also they store it into previous dids array + expect(newAliceBobConnection.previousDids).toContain(oldDid) + expect(newBobAliceConnection.previousTheirDids).toContain(oldDid) + + // Send message to new did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello new did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello new did', connectionId: aliceBobConnection!.id }) + }) + + test('Rotate succesfully and send messages to previous did afterwards', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + const messageToPreviousDid = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message to previous did' }), + connectionRecord: bobAliceConnection, + }) + + // Do did rotate + await aliceAgent.connections.rotate({ connectionId: aliceBobConnection!.id }) + + // Wait for acknowledge + await waitForAgentMessageProcessedEvent(aliceAgent, { messageType: DidRotateAckMessage.type.messageTypeUri }) + + // Send message to previous did + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageToPreviousDid) + + await waitForBasicMessage(aliceAgent, { + content: 'Message to previous did', + connectionId: aliceBobConnection!.id, + }) + }) + }) + + describe('Rotation specifying did and routing externally', () => { + test('Rotate succesfully and send messages to new did afterwards', async () => { + const oldDid = aliceBobConnection!.did + expect(bobAliceConnection!.theirDid).toEqual(oldDid) + + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + // Create a new external did + + // Make a common in-memory did registry for both agents + const didRegistry = new InMemoryDidRegistry() + aliceAgent.dids.config.addRegistrar(didRegistry) + aliceAgent.dids.config.addResolver(didRegistry) + bobAgent.dids.config.addRegistrar(didRegistry) + bobAgent.dids.config.addResolver(didRegistry) + + const didRouting = await aliceAgent.mediationRecipient.getRouting({}) + const did = `did:inmemory:${uuid()}` + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ]) + didDocument.id = did + + await aliceAgent.dids.create({ + did, + didDocument, + }) + + // Do did rotate + const { newDid } = await aliceAgent.connections.rotate({ + connectionId: aliceBobConnection!.id, + toDid: did, + }) + + // Wait for acknowledge + await waitForAgentMessageProcessedEvent(aliceAgent, { messageType: DidRotateAckMessage.type.messageTypeUri }) + + // Check that new did is taken into account by both parties + const newAliceBobConnection = await aliceAgent.connections.getById(aliceBobConnection!.id) + const newBobAliceConnection = await bobAgent.connections.getById(bobAliceConnection!.id) + + expect(newAliceBobConnection.did).toEqual(newDid) + expect(newBobAliceConnection.theirDid).toEqual(newDid) + + // And also they store it into previous dids array + expect(newAliceBobConnection.previousDids).toContain(oldDid) + expect(newBobAliceConnection.previousTheirDids).toContain(oldDid) + + // Send message to new did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello new did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello new did', connectionId: aliceBobConnection!.id }) + }) + + test('Rotate succesfully and send messages to previous did afterwards', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + const messageToPreviousDid = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message to previous did' }), + connectionRecord: bobAliceConnection, + }) + + // Create a new external did + + // Make a common in-memory did registry for both agents + const didRegistry = new InMemoryDidRegistry() + aliceAgent.dids.config.addRegistrar(didRegistry) + aliceAgent.dids.config.addResolver(didRegistry) + bobAgent.dids.config.addRegistrar(didRegistry) + bobAgent.dids.config.addResolver(didRegistry) + + const didRouting = await aliceAgent.mediationRecipient.getRouting({}) + const did = `did:inmemory:${uuid()}` + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ]) + didDocument.id = did + + await aliceAgent.dids.create({ + did, + didDocument, + }) + + const waitForAllDidRotate = Promise.all([waitForDidRotate(aliceAgent, {}), waitForDidRotate(bobAgent, {})]) + + // Do did rotate + await aliceAgent.connections.rotate({ connectionId: aliceBobConnection!.id, toDid: did }) + + // Wait for acknowledge + await waitForAgentMessageProcessedEvent(aliceAgent, { messageType: DidRotateAckMessage.type.messageTypeUri }) + const [firstRotate, secondRotate] = await waitForAllDidRotate + + const preRotateDid = aliceBobConnection!.did + expect(firstRotate).toEqual({ + connectionRecord: expect.any(ConnectionRecord), + ourDid: { + from: preRotateDid, + to: did, + }, + theirDid: undefined, + }) + + expect(secondRotate).toEqual({ + connectionRecord: expect.any(ConnectionRecord), + ourDid: undefined, + theirDid: { + from: preRotateDid, + to: did, + }, + }) + + // Send message to previous did + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageToPreviousDid) + + await waitForBasicMessage(aliceAgent, { + content: 'Message to previous did', + connectionId: aliceBobConnection!.id, + }) + }) + + test('Rotate failed and send messages to previous did afterwards', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + const messageToPreviousDid = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message to previous did' }), + connectionRecord: bobAliceConnection, + }) + + // Create a new external did + + // Use custom registry only for Alice agent, in order to force an error on Bob side + const didRegistry = new InMemoryDidRegistry() + aliceAgent.dids.config.addRegistrar(didRegistry) + aliceAgent.dids.config.addResolver(didRegistry) + + const didRouting = await aliceAgent.mediationRecipient.getRouting({}) + const did = `did:inmemory:${uuid()}` + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ]) + didDocument.id = did + + await aliceAgent.dids.create({ + did, + didDocument, + }) + + // Do did rotate + await aliceAgent.connections.rotate({ connectionId: aliceBobConnection!.id, toDid: did }) + + // Wait for a problem report + await waitForAgentMessageProcessedEvent(aliceAgent, { + messageType: DidRotateProblemReportMessage.type.messageTypeUri, + }) + + // Send message to previous did + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageToPreviousDid) + + await waitForBasicMessage(aliceAgent, { + content: 'Message to previous did', + connectionId: aliceBobConnection!.id, + }) + + // Send message to stored did (should be the previous one) + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Message after did rotation failure') + + await waitForBasicMessage(aliceAgent, { + content: 'Message after did rotation failure', + connectionId: aliceBobConnection!.id, + }) + }) + }) + + describe('Hangup', () => { + test('Hangup without record deletion', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + // Store an outbound context so we can attempt to send a message even if the connection is terminated. + // A bit hacky, but may happen in some cases where message retry mechanisms are being used + const messageBeforeHangup = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message before hangup' }), + connectionRecord: bobAliceConnection!.clone(), + }) + + await aliceAgent.connections.hangup({ connectionId: aliceBobConnection!.id }) + + // Wait for hangup + await waitForAgentMessageProcessedEvent(bobAgent, { + messageType: HangupMessage.type.messageTypeUri, + }) + + // If Bob attempts to send a message to Alice after they received the hangup, framework should reject it + expect(bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Message after hangup')).rejects.toThrowError() + + // If Bob sends a message afterwards, Alice should still be able to receive it + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageBeforeHangup) + + await waitForBasicMessage(aliceAgent, { + content: 'Message before hangup', + connectionId: aliceBobConnection!.id, + }) + }) + + test('Hangup and delete connection record', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + // Store an outbound context so we can attempt to send a message even if the connection is terminated. + // A bit hacky, but may happen in some cases where message retry mechanisms are being used + const messageBeforeHangup = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message before hangup' }), + connectionRecord: bobAliceConnection!.clone(), + }) + + await aliceAgent.connections.hangup({ connectionId: aliceBobConnection!.id, deleteAfterHangup: true }) + + // Verify that alice connection has been effectively deleted + expect(aliceAgent.connections.getById(aliceBobConnection!.id)).rejects.toThrow(RecordNotFoundError) + + // Wait for hangup + await waitForAgentMessageProcessedEvent(bobAgent, { + messageType: HangupMessage.type.messageTypeUri, + }) + + // If Bob sends a message afterwards, Alice should not receive it since the connection has been deleted + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageBeforeHangup) + + // An error is thrown by Alice agent and, after inspecting all basic messages, it cannot be found + // TODO: Update as soon as agent sends error events upon reception of messages + const observable = aliceAgent.events.observable('AgentReceiveMessageError') + const subject = new ReplaySubject(1) + observable.pipe(first(), timeout({ first: 10000 })).subscribe(subject) + await firstValueFrom(subject) + + const aliceBasicMessages = await aliceAgent.basicMessages.findAllByQuery({}) + expect(aliceBasicMessages.find((message) => message.content === 'Message before hangup')).toBeUndefined() + }) + }) +}) diff --git a/packages/core/src/modules/connections/__tests__/didexchange-numalgo.test.ts b/packages/core/src/modules/connections/__tests__/didexchange-numalgo.test.ts new file mode 100644 index 0000000000..3edcc48d0f --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/didexchange-numalgo.test.ts @@ -0,0 +1,190 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { ConnectionStateChangedEvent } from '../ConnectionEvents' + +import { firstValueFrom } from 'rxjs' +import { filter, first, map, timeout } from 'rxjs/operators' + +import { setupSubjectTransports } from '../../../../tests' +import { getInMemoryAgentOptions } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { uuid } from '../../../utils/uuid' +import { DidsModule, PeerDidNumAlgo, createPeerDidDocumentFromServices } from '../../dids' +import { ConnectionEventTypes } from '../ConnectionEvents' +import { ConnectionsModule } from '../ConnectionsModule' +import { DidExchangeState } from '../models' + +import { InMemoryDidRegistry } from './InMemoryDidRegistry' + +function waitForRequest(agent: Agent, theirLabel: string) { + return firstValueFrom( + agent.events.observable(ConnectionEventTypes.ConnectionStateChanged).pipe( + map((event) => event.payload.connectionRecord), + // Wait for request received + filter( + (connectionRecord) => + connectionRecord.state === DidExchangeState.RequestReceived && connectionRecord.theirLabel === theirLabel + ), + first(), + timeout(5000) + ) + ) +} + +function waitForResponse(agent: Agent, connectionId: string) { + return firstValueFrom( + agent.events.observable(ConnectionEventTypes.ConnectionStateChanged).pipe( + // Wait for response received + map((event) => event.payload.connectionRecord), + filter( + (connectionRecord) => + connectionRecord.state === DidExchangeState.ResponseReceived && connectionRecord.id === connectionId + ), + first(), + timeout(5000) + ) + ) +} + +describe('Did Exchange numalgo settings', () => { + test('Connect using default setting (numalgo 1)', async () => { + await didExchangeNumAlgoBaseTest({}) + }) + + test('Connect using default setting for requester and numalgo 2 for responder', async () => { + await didExchangeNumAlgoBaseTest({ responderNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc }) + }) + + test('Connect using numalgo 2 for requester and default setting for responder', async () => { + await didExchangeNumAlgoBaseTest({ requesterNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc }) + }) + + test('Connect using numalgo 2 for both requester and responder', async () => { + await didExchangeNumAlgoBaseTest({ + requesterNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + responderNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + }) + }) + + test('Connect using default setting for requester and numalgo 4 for responder', async () => { + await didExchangeNumAlgoBaseTest({ responderNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm }) + }) + + test('Connect using numalgo 4 for requester and default setting for responder', async () => { + await didExchangeNumAlgoBaseTest({ requesterNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm }) + }) + + test('Connect using numalgo 4 for both requester and responder', async () => { + await didExchangeNumAlgoBaseTest({ + requesterNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm, + responderNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm, + }) + }) + + test('Connect using an externally defined did for the requested', async () => { + await didExchangeNumAlgoBaseTest({ + createExternalDidForRequester: true, + }) + }) +}) + +async function didExchangeNumAlgoBaseTest(options: { + requesterNumAlgoSetting?: PeerDidNumAlgo + responderNumAlgoSetting?: PeerDidNumAlgo + createExternalDidForRequester?: boolean +}) { + // Make a common in-memory did registry for both agents + const didRegistry = new InMemoryDidRegistry() + + const aliceAgentOptions = getInMemoryAgentOptions( + 'DID Exchange numalgo settings Alice', + { + label: 'alice', + endpoints: ['rxjs:alice'], + }, + { + connections: new ConnectionsModule({ + autoAcceptConnections: false, + peerNumAlgoForDidExchangeRequests: options.requesterNumAlgoSetting, + }), + dids: new DidsModule({ registrars: [didRegistry], resolvers: [didRegistry] }), + } + ) + const faberAgentOptions = getInMemoryAgentOptions( + 'DID Exchange numalgo settings Alice', + { + endpoints: ['rxjs:faber'], + }, + { + connections: new ConnectionsModule({ + autoAcceptConnections: false, + peerNumAlgoForDidExchangeRequests: options.responderNumAlgoSetting, + }), + dids: new DidsModule({ registrars: [didRegistry], resolvers: [didRegistry] }), + } + ) + + const aliceAgent = new Agent(aliceAgentOptions) + const faberAgent = new Agent(faberAgentOptions) + + setupSubjectTransports([aliceAgent, faberAgent]) + await aliceAgent.initialize() + await faberAgent.initialize() + + const faberOutOfBandRecord = await faberAgent.oob.createInvitation({ + autoAcceptConnection: false, + multiUseInvitation: false, + }) + + const waitForAliceRequest = waitForRequest(faberAgent, 'alice') + + let ourDid, routing + if (options.createExternalDidForRequester) { + // Create did externally + const didRouting = await aliceAgent.mediationRecipient.getRouting({}) + ourDid = `did:inmemory:${uuid()}` + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ]) + didDocument.id = ourDid + + await aliceAgent.dids.create({ + did: ourDid, + didDocument, + }) + } + + let { connectionRecord: aliceConnectionRecord } = await aliceAgent.oob.receiveInvitation( + faberOutOfBandRecord.outOfBandInvitation, + { + autoAcceptInvitation: true, + autoAcceptConnection: false, + routing, + ourDid, + } + ) + + let faberAliceConnectionRecord = await waitForAliceRequest + + const waitForAliceResponse = waitForResponse(aliceAgent, aliceConnectionRecord!.id) + + await faberAgent.connections.acceptRequest(faberAliceConnectionRecord.id) + + aliceConnectionRecord = await waitForAliceResponse + await aliceAgent.connections.acceptResponse(aliceConnectionRecord!.id) + + aliceConnectionRecord = await aliceAgent.connections.returnWhenIsConnected(aliceConnectionRecord!.id) + faberAliceConnectionRecord = await faberAgent.connections.returnWhenIsConnected(faberAliceConnectionRecord!.id) + + expect(aliceConnectionRecord).toBeConnectedWith(faberAliceConnectionRecord) + + await aliceAgent.wallet.delete() + await aliceAgent.shutdown() + + await faberAgent.wallet.delete() + await faberAgent.shutdown() +} diff --git a/packages/core/src/modules/connections/__tests__/helpers.test.ts b/packages/core/src/modules/connections/__tests__/helpers.test.ts new file mode 100644 index 0000000000..ae144cc34d --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/helpers.test.ts @@ -0,0 +1,173 @@ +import { DidCommV1Service, IndyAgentService, VerificationMethod } from '../../dids' +import { + DidDoc, + Ed25119Sig2018, + EddsaSaSigSecp256k1, + EmbeddedAuthentication, + ReferencedAuthentication, + RsaSig2018, +} from '../models' +import { convertToNewDidDocument } from '../services/helpers' + +const key = new Ed25119Sig2018({ + id: 'did:sov:SKJVx2kn373FNgvff1SbJo#4', + controller: 'did:sov:SKJVx2kn373FNgvff1SbJo', + publicKeyBase58: 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d', +}) +const didDoc = new DidDoc({ + authentication: [ + new ReferencedAuthentication(key, 'Ed25519SignatureAuthentication2018'), + new EmbeddedAuthentication( + new Ed25119Sig2018({ + id: '#8', + controller: 'did:sov:SKJVx2kn373FNgvff1SbJo', + publicKeyBase58: '5UQ3drtEMMQXaLLmEywbciW92jZaQgRYgfuzXfonV8iz', + }) + ), + ], + id: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKey: [ + key, + new RsaSig2018({ + id: '#3', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC X...', + }), + new EddsaSaSigSecp256k1({ + id: '#6', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyHex: '-----BEGIN PUBLIC A...', + }), + ], + service: [ + new IndyAgentService({ + id: 'did:sov:SKJVx2kn373FNgvff1SbJo#service-1', + serviceEndpoint: 'did:sov:SKJVx2kn373FNgvff1SbJo', + recipientKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + routingKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + priority: 5, + }), + new DidCommV1Service({ + id: '#service-2', + serviceEndpoint: 'https://agent.com', + recipientKeys: ['did:sov:SKJVx2kn373FNgvff1SbJo#4', '#8'], + routingKeys: [ + 'did:key:z6MktFXxTu8tHkoE1Jtqj4ApYEg1c44qmU1p7kq7QZXBtJv1#z6MktFXxTu8tHkoE1Jtqj4ApYEg1c44qmU1p7kq7QZXBtJv1', + ], + priority: 2, + }), + ], +}) + +describe('convertToNewDidDocument', () => { + test('create a new DidDocument and with authentication, publicKey and service from DidDoc', () => { + const oldDocument = didDoc + const newDocument = convertToNewDidDocument(oldDocument) + + expect(newDocument.authentication).toEqual(['#EoGusetS', '#5UQ3drtE']) + + expect(newDocument.verificationMethod).toEqual([ + new VerificationMethod({ + id: '#5UQ3drtE', + type: 'Ed25519VerificationKey2018', + controller: '#id', + publicKeyBase58: '5UQ3drtEMMQXaLLmEywbciW92jZaQgRYgfuzXfonV8iz', + }), + new VerificationMethod({ + id: '#EoGusetS', + type: 'Ed25519VerificationKey2018', + controller: '#id', + publicKeyBase58: 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d', + }), + ]) + + expect(newDocument.service).toEqual([ + new IndyAgentService({ + id: '#service-1', + serviceEndpoint: 'did:sov:SKJVx2kn373FNgvff1SbJo', + recipientKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + routingKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + priority: 5, + }), + new DidCommV1Service({ + id: '#service-2', + serviceEndpoint: 'https://agent.com', + recipientKeys: ['#EoGusetS', '#5UQ3drtE'], + routingKeys: [ + 'did:key:z6MktFXxTu8tHkoE1Jtqj4ApYEg1c44qmU1p7kq7QZXBtJv1#z6MktFXxTu8tHkoE1Jtqj4ApYEg1c44qmU1p7kq7QZXBtJv1', + ], + priority: 2, + }), + ]) + }) + + test('will use ; as an id mark instead of # if the # is missing in a service id', () => { + const oldDocument = new DidDoc({ + id: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + authentication: [], + publicKey: [], + service: [ + new IndyAgentService({ + id: 'did:sov:SKJVx2kn373FNgvff1SbJo;service-1', + serviceEndpoint: 'did:sov:SKJVx2kn373FNgvff1SbJo', + recipientKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + routingKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + priority: 5, + }), + ], + }) + const newDocument = convertToNewDidDocument(oldDocument) + + expect(newDocument.service).toEqual([ + new IndyAgentService({ + id: '#service-1', + serviceEndpoint: 'did:sov:SKJVx2kn373FNgvff1SbJo', + recipientKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + routingKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + priority: 5, + }), + ]) + }) + + test('will only split on the first ; or # and leave the other ones in place as id values', () => { + const oldDocument = new DidDoc({ + id: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + authentication: [], + publicKey: [], + service: [ + new IndyAgentService({ + id: 'did:sov:SKJVx2kn373FNgvff1SbJo;service-1;something-extra', + serviceEndpoint: 'did:sov:SKJVx2kn373FNgvff1SbJo', + recipientKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + routingKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + priority: 6, + }), + new IndyAgentService({ + id: 'did:sov:SKJVx2kn373FNgvff1SbJo#service-2#something-extra', + serviceEndpoint: 'did:sov:SKJVx2kn373FNgvff1SbJo', + recipientKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + routingKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + priority: 5, + }), + ], + }) + const newDocument = convertToNewDidDocument(oldDocument) + + expect(newDocument.service).toEqual([ + new IndyAgentService({ + id: '#service-1;something-extra', + serviceEndpoint: 'did:sov:SKJVx2kn373FNgvff1SbJo', + recipientKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + routingKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + priority: 6, + }), + new IndyAgentService({ + id: '#service-2#something-extra', + serviceEndpoint: 'did:sov:SKJVx2kn373FNgvff1SbJo', + recipientKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + routingKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + priority: 5, + }), + ]) + }) +}) diff --git a/packages/core/src/modules/connections/errors/ConnectionProblemReportError.ts b/packages/core/src/modules/connections/errors/ConnectionProblemReportError.ts new file mode 100644 index 0000000000..f96f98261d --- /dev/null +++ b/packages/core/src/modules/connections/errors/ConnectionProblemReportError.ts @@ -0,0 +1,22 @@ +import type { ConnectionProblemReportReason } from './ConnectionProblemReportReason' +import type { ProblemReportErrorOptions } from '../../problem-reports' + +import { ProblemReportError } from '../../problem-reports' +import { ConnectionProblemReportMessage } from '../messages' + +interface ConnectionProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: ConnectionProblemReportReason +} +export class ConnectionProblemReportError extends ProblemReportError { + public problemReport: ConnectionProblemReportMessage + + public constructor(public message: string, { problemCode }: ConnectionProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new ConnectionProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/core/src/modules/connections/errors/ConnectionProblemReportReason.ts b/packages/core/src/modules/connections/errors/ConnectionProblemReportReason.ts new file mode 100644 index 0000000000..06f81b83c3 --- /dev/null +++ b/packages/core/src/modules/connections/errors/ConnectionProblemReportReason.ts @@ -0,0 +1,11 @@ +/** + * Connection error code in RFC 160. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0160-connection-protocol/README.md#errors + */ +export enum ConnectionProblemReportReason { + RequestNotAccepted = 'request_not_accepted', + RequestProcessingError = 'request_processing_error', + ResponseNotAccepted = 'response_not_accepted', + ResponseProcessingError = 'response_processing_error', +} diff --git a/packages/core/src/modules/connections/errors/DidExchangeProblemReportError.ts b/packages/core/src/modules/connections/errors/DidExchangeProblemReportError.ts new file mode 100644 index 0000000000..6e8d9a9925 --- /dev/null +++ b/packages/core/src/modules/connections/errors/DidExchangeProblemReportError.ts @@ -0,0 +1,22 @@ +import type { DidExchangeProblemReportReason } from './DidExchangeProblemReportReason' +import type { ProblemReportErrorOptions } from '../../problem-reports' + +import { ProblemReportError } from '../../problem-reports' +import { DidExchangeProblemReportMessage } from '../messages' + +interface DidExchangeProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: DidExchangeProblemReportReason +} +export class DidExchangeProblemReportError extends ProblemReportError { + public problemReport: DidExchangeProblemReportMessage + + public constructor(public message: string, { problemCode }: DidExchangeProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new DidExchangeProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/core/src/modules/connections/errors/DidExchangeProblemReportReason.ts b/packages/core/src/modules/connections/errors/DidExchangeProblemReportReason.ts new file mode 100644 index 0000000000..540cc9923c --- /dev/null +++ b/packages/core/src/modules/connections/errors/DidExchangeProblemReportReason.ts @@ -0,0 +1,12 @@ +/** + * Connection error code in RFC 0023. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0023-did-exchange/README.md#errors + */ +export enum DidExchangeProblemReportReason { + RequestNotAccepted = 'request_not_accepted', + RequestProcessingError = 'request_processing_error', + ResponseNotAccepted = 'response_not_accepted', + ResponseProcessingError = 'response_processing_error', + CompleteRejected = 'complete_rejected', +} diff --git a/packages/core/src/modules/connections/errors/index.ts b/packages/core/src/modules/connections/errors/index.ts new file mode 100644 index 0000000000..c745a4cdde --- /dev/null +++ b/packages/core/src/modules/connections/errors/index.ts @@ -0,0 +1,4 @@ +export * from './ConnectionProblemReportError' +export * from './ConnectionProblemReportReason' +export * from './DidExchangeProblemReportError' +export * from './DidExchangeProblemReportReason' diff --git a/packages/core/src/modules/connections/handlers/AckMessageHandler.ts b/packages/core/src/modules/connections/handlers/AckMessageHandler.ts new file mode 100644 index 0000000000..e9c8fafc38 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/AckMessageHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { ConnectionService } from '../services/ConnectionService' + +import { AckMessage } from '../../common' + +export class AckMessageHandler implements MessageHandler { + private connectionService: ConnectionService + public supportedMessages = [AckMessage] + + public constructor(connectionService: ConnectionService) { + this.connectionService = connectionService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + await this.connectionService.processAck(inboundMessage) + } +} diff --git a/packages/core/src/modules/connections/handlers/ConnectionProblemReportHandler.ts b/packages/core/src/modules/connections/handlers/ConnectionProblemReportHandler.ts new file mode 100644 index 0000000000..5f464a531c --- /dev/null +++ b/packages/core/src/modules/connections/handlers/ConnectionProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { ConnectionService } from '../services' + +import { ConnectionProblemReportMessage } from '../messages' + +export class ConnectionProblemReportHandler implements MessageHandler { + private connectionService: ConnectionService + public supportedMessages = [ConnectionProblemReportMessage] + + public constructor(connectionService: ConnectionService) { + this.connectionService = connectionService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.connectionService.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/connections/handlers/ConnectionRequestHandler.ts b/packages/core/src/modules/connections/handlers/ConnectionRequestHandler.ts new file mode 100644 index 0000000000..b70ec36ab7 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/ConnectionRequestHandler.ts @@ -0,0 +1,102 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRepository } from '../../dids/repository' +import type { OutOfBandService } from '../../oob/OutOfBandService' +import type { RoutingService } from '../../routing/services/RoutingService' +import type { ConnectionsModuleConfig } from '../ConnectionsModuleConfig' +import type { ConnectionService } from '../services/ConnectionService' + +import { TransportService } from '../../../agent/TransportService' +import { OutboundMessageContext } from '../../../agent/models' +import { CredoError } from '../../../error/CredoError' +import { tryParseDid } from '../../dids/domain/parse' +import { ConnectionRequestMessage } from '../messages' +import { HandshakeProtocol } from '../models' + +export class ConnectionRequestHandler implements MessageHandler { + private connectionService: ConnectionService + private outOfBandService: OutOfBandService + private routingService: RoutingService + private didRepository: DidRepository + private connectionsModuleConfig: ConnectionsModuleConfig + public supportedMessages = [ConnectionRequestMessage] + + public constructor( + connectionService: ConnectionService, + outOfBandService: OutOfBandService, + routingService: RoutingService, + didRepository: DidRepository, + connectionsModuleConfig: ConnectionsModuleConfig + ) { + this.connectionService = connectionService + this.outOfBandService = outOfBandService + this.routingService = routingService + this.didRepository = didRepository + this.connectionsModuleConfig = connectionsModuleConfig + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const { agentContext, connection, recipientKey, senderKey, message, sessionId } = messageContext + + if (!recipientKey || !senderKey) { + throw new CredoError('Unable to process connection request without senderVerkey or recipientKey') + } + + const parentThreadId = message.thread?.parentThreadId + + const outOfBandRecord = + parentThreadId && tryParseDid(parentThreadId) + ? await this.outOfBandService.createFromImplicitInvitation(agentContext, { + did: parentThreadId, + threadId: message.threadId, + recipientKey, + handshakeProtocols: [HandshakeProtocol.Connections], + }) + : await this.outOfBandService.findCreatedByRecipientKey(agentContext, recipientKey) + + if (!outOfBandRecord) { + throw new CredoError(`Out-of-band record for recipient key ${recipientKey.fingerprint} was not found.`) + } + + if (connection && !outOfBandRecord.reusable) { + throw new CredoError(`Connection record for non-reusable out-of-band ${outOfBandRecord.id} already exists.`) + } + + const receivedDidRecord = await this.didRepository.findReceivedDidByRecipientKey(agentContext, senderKey) + if (receivedDidRecord) { + throw new CredoError(`A received did record for sender key ${senderKey.fingerprint} already exists.`) + } + + const connectionRecord = await this.connectionService.processRequest(messageContext, outOfBandRecord) + + // Associate the new connection with the session created for the inbound message + if (sessionId) { + const transportService = agentContext.dependencyManager.resolve(TransportService) + transportService.setConnectionIdForSession(sessionId, connectionRecord.id) + } + + if (connectionRecord?.autoAcceptConnection ?? this.connectionsModuleConfig.autoAcceptConnections) { + // TODO: Allow rotation of keys used in the invitation for new ones not only when out-of-band is reusable or + // when there are no inline services in the invitation + + // We generate routing in two scenarios: + // 1. When the out-of-band invitation is reusable, as otherwise all connections use the same keys + // 2. When the out-of-band invitation has no inline services, as we don't want to generate a legacy did doc from a service did + const routing = + outOfBandRecord.reusable || outOfBandRecord.outOfBandInvitation.getInlineServices().length === 0 + ? await this.routingService.getRouting(agentContext) + : undefined + + const { message } = await this.connectionService.createResponse( + agentContext, + connectionRecord, + outOfBandRecord, + routing + ) + return new OutboundMessageContext(message, { + agentContext, + connection: connectionRecord, + outOfBand: outOfBandRecord, + }) + } + } +} diff --git a/packages/core/src/modules/connections/handlers/ConnectionResponseHandler.ts b/packages/core/src/modules/connections/handlers/ConnectionResponseHandler.ts new file mode 100644 index 0000000000..fd3ec2ac29 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/ConnectionResponseHandler.ts @@ -0,0 +1,93 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidResolverService } from '../../dids' +import type { OutOfBandService } from '../../oob/OutOfBandService' +import type { ConnectionsModuleConfig } from '../ConnectionsModuleConfig' +import type { ConnectionService } from '../services/ConnectionService' + +import { OutboundMessageContext } from '../../../agent/models' +import { ReturnRouteTypes } from '../../../decorators/transport/TransportDecorator' +import { CredoError } from '../../../error' +import { ConnectionResponseMessage } from '../messages' +import { DidExchangeRole } from '../models' + +export class ConnectionResponseHandler implements MessageHandler { + private connectionService: ConnectionService + private outOfBandService: OutOfBandService + private didResolverService: DidResolverService + private connectionsModuleConfig: ConnectionsModuleConfig + + public supportedMessages = [ConnectionResponseMessage] + + public constructor( + connectionService: ConnectionService, + outOfBandService: OutOfBandService, + didResolverService: DidResolverService, + connectionsModuleConfig: ConnectionsModuleConfig + ) { + this.connectionService = connectionService + this.outOfBandService = outOfBandService + this.didResolverService = didResolverService + this.connectionsModuleConfig = connectionsModuleConfig + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const { recipientKey, senderKey, message } = messageContext + + if (!recipientKey || !senderKey) { + throw new CredoError('Unable to process connection response without senderKey or recipientKey') + } + + // Query by both role and thread id to allow connecting to self + const connectionRecord = await this.connectionService.getByRoleAndThreadId( + messageContext.agentContext, + DidExchangeRole.Requester, + message.threadId + ) + if (!connectionRecord) { + throw new CredoError(`Connection for thread ID ${message.threadId} not found!`) + } + + if (!connectionRecord.did) { + throw new CredoError(`Connection record ${connectionRecord.id} has no 'did'`) + } + + const ourDidDocument = await this.didResolverService.resolveDidDocument( + messageContext.agentContext, + connectionRecord.did + ) + if (!ourDidDocument) { + throw new CredoError(`Did document for did ${connectionRecord.did} was not resolved!`) + } + + // Validate if recipient key is included in recipient keys of the did document resolved by + // connection record did + if (!ourDidDocument.recipientKeys.find((key) => key.fingerprint === recipientKey.fingerprint)) { + throw new CredoError(`Recipient key ${recipientKey.fingerprint} not found in did document recipient keys.`) + } + + const outOfBandRecord = + connectionRecord.outOfBandId && + (await this.outOfBandService.findById(messageContext.agentContext, connectionRecord.outOfBandId)) + + if (!outOfBandRecord) { + throw new CredoError(`Out-of-band record ${connectionRecord.outOfBandId} was not found.`) + } + + messageContext.connection = connectionRecord + const connection = await this.connectionService.processResponse(messageContext, outOfBandRecord) + + // TODO: should we only send ping message in case of autoAcceptConnection or always? + // In AATH we have a separate step to send the ping. So for now we'll only do it + // if auto accept is enable + if (connection.autoAcceptConnection ?? this.connectionsModuleConfig.autoAcceptConnections) { + const { message } = await this.connectionService.createTrustPing(messageContext.agentContext, connection, { + responseRequested: false, + }) + + // Disable return routing as we don't want to receive a response for this message over the same channel + // This has led to long timeouts as not all clients actually close an http socket if there is no response message + message.setReturnRouting(ReturnRouteTypes.none) + return new OutboundMessageContext(message, { agentContext: messageContext.agentContext, connection }) + } + } +} diff --git a/packages/core/src/modules/connections/handlers/DidExchangeCompleteHandler.ts b/packages/core/src/modules/connections/handlers/DidExchangeCompleteHandler.ts new file mode 100644 index 0000000000..3b83f51112 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidExchangeCompleteHandler.ts @@ -0,0 +1,55 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { OutOfBandService } from '../../oob/OutOfBandService' +import type { DidExchangeProtocol } from '../DidExchangeProtocol' + +import { CredoError } from '../../../error' +import { tryParseDid } from '../../dids/domain/parse' +import { OutOfBandState } from '../../oob/domain/OutOfBandState' +import { DidExchangeCompleteMessage } from '../messages' +import { HandshakeProtocol } from '../models' + +export class DidExchangeCompleteHandler implements MessageHandler { + private didExchangeProtocol: DidExchangeProtocol + private outOfBandService: OutOfBandService + public supportedMessages = [DidExchangeCompleteMessage] + + public constructor(didExchangeProtocol: DidExchangeProtocol, outOfBandService: OutOfBandService) { + this.didExchangeProtocol = didExchangeProtocol + this.outOfBandService = outOfBandService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const { connection: connectionRecord } = messageContext + + if (!connectionRecord) { + throw new CredoError(`Connection is missing in message context`) + } + + const { protocol } = connectionRecord + if (protocol !== HandshakeProtocol.DidExchange) { + throw new CredoError( + `Connection record protocol is ${protocol} but handler supports only ${HandshakeProtocol.DidExchange}.` + ) + } + + const { message } = messageContext + const parentThreadId = message.thread?.parentThreadId + if (!parentThreadId) { + throw new CredoError(`Message does not contain pthid attribute`) + } + const outOfBandRecord = await this.outOfBandService.findByCreatedInvitationId( + messageContext.agentContext, + parentThreadId, + tryParseDid(parentThreadId) ? message.threadId : undefined + ) + + if (!outOfBandRecord) { + throw new CredoError(`OutOfBand record for message ID ${message.thread?.parentThreadId} not found!`) + } + + if (!outOfBandRecord.reusable) { + await this.outOfBandService.updateState(messageContext.agentContext, outOfBandRecord, OutOfBandState.Done) + } + await this.didExchangeProtocol.processComplete(messageContext, outOfBandRecord) + } +} diff --git a/packages/core/src/modules/connections/handlers/DidExchangeRequestHandler.ts b/packages/core/src/modules/connections/handlers/DidExchangeRequestHandler.ts new file mode 100644 index 0000000000..fcd8429540 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidExchangeRequestHandler.ts @@ -0,0 +1,111 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRepository } from '../../dids/repository' +import type { OutOfBandService } from '../../oob/OutOfBandService' +import type { RoutingService } from '../../routing/services/RoutingService' +import type { ConnectionsModuleConfig } from '../ConnectionsModuleConfig' +import type { DidExchangeProtocol } from '../DidExchangeProtocol' + +import { TransportService } from '../../../agent/TransportService' +import { OutboundMessageContext } from '../../../agent/models' +import { CredoError } from '../../../error/CredoError' +import { tryParseDid } from '../../dids/domain/parse' +import { OutOfBandState } from '../../oob/domain/OutOfBandState' +import { DidExchangeRequestMessage } from '../messages' +import { HandshakeProtocol } from '../models' + +export class DidExchangeRequestHandler implements MessageHandler { + private didExchangeProtocol: DidExchangeProtocol + private outOfBandService: OutOfBandService + private routingService: RoutingService + private didRepository: DidRepository + private connectionsModuleConfig: ConnectionsModuleConfig + public supportedMessages = [DidExchangeRequestMessage] + + public constructor( + didExchangeProtocol: DidExchangeProtocol, + outOfBandService: OutOfBandService, + routingService: RoutingService, + didRepository: DidRepository, + connectionsModuleConfig: ConnectionsModuleConfig + ) { + this.didExchangeProtocol = didExchangeProtocol + this.outOfBandService = outOfBandService + this.routingService = routingService + this.didRepository = didRepository + this.connectionsModuleConfig = connectionsModuleConfig + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const { agentContext, recipientKey, senderKey, message, connection, sessionId } = messageContext + + if (!recipientKey || !senderKey) { + throw new CredoError('Unable to process connection request without senderKey or recipientKey') + } + + const parentThreadId = message.thread?.parentThreadId + + if (!parentThreadId) { + throw new CredoError(`Message does not contain 'pthid' attribute`) + } + + const outOfBandRecord = tryParseDid(parentThreadId) + ? await this.outOfBandService.createFromImplicitInvitation(agentContext, { + did: parentThreadId, + threadId: message.threadId, + recipientKey, + handshakeProtocols: [HandshakeProtocol.DidExchange], + }) + : await this.outOfBandService.findByCreatedInvitationId(agentContext, parentThreadId) + if (!outOfBandRecord) { + throw new CredoError(`OutOfBand record for message ID ${parentThreadId} not found!`) + } + + if (connection && !outOfBandRecord.reusable) { + throw new CredoError(`Connection record for non-reusable out-of-band ${outOfBandRecord.id} already exists.`) + } + + const receivedDidRecord = await this.didRepository.findReceivedDidByRecipientKey(agentContext, senderKey) + if (receivedDidRecord) { + throw new CredoError(`A received did record for sender key ${senderKey.fingerprint} already exists.`) + } + + // TODO Shouldn't we check also if the keys match the keys from oob invitation services? + + if (outOfBandRecord.state === OutOfBandState.Done) { + throw new CredoError('Out-of-band record has been already processed and it does not accept any new requests') + } + + const connectionRecord = await this.didExchangeProtocol.processRequest(messageContext, outOfBandRecord) + + // Associate the new connection with the session created for the inbound message + if (sessionId) { + const transportService = agentContext.dependencyManager.resolve(TransportService) + transportService.setConnectionIdForSession(sessionId, connectionRecord.id) + } + + if (connectionRecord.autoAcceptConnection ?? this.connectionsModuleConfig.autoAcceptConnections) { + // TODO We should add an option to not pass routing and therefore do not rotate keys and use the keys from the invitation + // TODO: Allow rotation of keys used in the invitation for new ones not only when out-of-band is reusable + + // We generate routing in two scenarios: + // 1. When the out-of-band invitation is reusable, as otherwise all connections use the same keys + // 2. When the out-of-band invitation has no inline services, as we don't want to generate a legacy did doc from a service did + const routing = + outOfBandRecord.reusable || outOfBandRecord.outOfBandInvitation.getInlineServices().length === 0 + ? await this.routingService.getRouting(agentContext) + : undefined + + const message = await this.didExchangeProtocol.createResponse( + agentContext, + connectionRecord, + outOfBandRecord, + routing + ) + return new OutboundMessageContext(message, { + agentContext, + connection: connectionRecord, + outOfBand: outOfBandRecord, + }) + } + } +} diff --git a/packages/core/src/modules/connections/handlers/DidExchangeResponseHandler.ts b/packages/core/src/modules/connections/handlers/DidExchangeResponseHandler.ts new file mode 100644 index 0000000000..24b0ba892e --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidExchangeResponseHandler.ts @@ -0,0 +1,113 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidResolverService } from '../../dids' +import type { OutOfBandService } from '../../oob/OutOfBandService' +import type { ConnectionsModuleConfig } from '../ConnectionsModuleConfig' +import type { DidExchangeProtocol } from '../DidExchangeProtocol' +import type { ConnectionService } from '../services' + +import { OutboundMessageContext } from '../../../agent/models' +import { ReturnRouteTypes } from '../../../decorators/transport/TransportDecorator' +import { CredoError } from '../../../error' +import { OutOfBandState } from '../../oob/domain/OutOfBandState' +import { DidExchangeResponseMessage } from '../messages' +import { DidExchangeRole, HandshakeProtocol } from '../models' + +export class DidExchangeResponseHandler implements MessageHandler { + private didExchangeProtocol: DidExchangeProtocol + private outOfBandService: OutOfBandService + private connectionService: ConnectionService + private didResolverService: DidResolverService + private connectionsModuleConfig: ConnectionsModuleConfig + public supportedMessages = [DidExchangeResponseMessage] + + public constructor( + didExchangeProtocol: DidExchangeProtocol, + outOfBandService: OutOfBandService, + connectionService: ConnectionService, + didResolverService: DidResolverService, + connectionsModuleConfig: ConnectionsModuleConfig + ) { + this.didExchangeProtocol = didExchangeProtocol + this.outOfBandService = outOfBandService + this.connectionService = connectionService + this.didResolverService = didResolverService + this.connectionsModuleConfig = connectionsModuleConfig + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const { agentContext, recipientKey, senderKey, message } = messageContext + + if (!recipientKey || !senderKey) { + throw new CredoError('Unable to process connection response without sender key or recipient key') + } + + const connectionRecord = await this.connectionService.getByRoleAndThreadId( + agentContext, + DidExchangeRole.Requester, + message.threadId + ) + if (!connectionRecord) { + throw new CredoError(`Connection for thread ID ${message.threadId} not found!`) + } + + if (!connectionRecord.did) { + throw new CredoError(`Connection record ${connectionRecord.id} has no 'did'`) + } + + const ourDidDocument = await this.didResolverService.resolveDidDocument(agentContext, connectionRecord.did) + if (!ourDidDocument) { + throw new CredoError(`Did document for did ${connectionRecord.did} was not resolved`) + } + + // Validate if recipient key is included in recipient keys of the did document resolved by + // connection record did + if (!ourDidDocument.recipientKeys.find((key) => key.fingerprint === recipientKey.fingerprint)) { + throw new CredoError(`Recipient key ${recipientKey.fingerprint} not found in did document recipient keys.`) + } + + const { protocol } = connectionRecord + if (protocol !== HandshakeProtocol.DidExchange) { + throw new CredoError( + `Connection record protocol is ${protocol} but handler supports only ${HandshakeProtocol.DidExchange}.` + ) + } + + if (!connectionRecord.outOfBandId) { + throw new CredoError(`Connection ${connectionRecord.id} does not have outOfBandId!`) + } + + const outOfBandRecord = await this.outOfBandService.findById(agentContext, connectionRecord.outOfBandId) + + if (!outOfBandRecord) { + throw new CredoError( + `OutOfBand record for connection ${connectionRecord.id} with outOfBandId ${connectionRecord.outOfBandId} not found!` + ) + } + + // TODO + // + // A connection request message is the only case when I can use the connection record found + // only based on recipient key without checking that `theirKey` is equal to sender key. + // + // The question is if we should do it here in this way or rather somewhere else to keep + // responsibility of all handlers aligned. + // + messageContext.connection = connectionRecord + const connection = await this.didExchangeProtocol.processResponse(messageContext, outOfBandRecord) + + // TODO: should we only send complete message in case of autoAcceptConnection or always? + // In AATH we have a separate step to send the complete. So for now we'll only do it + // if auto accept is enabled + if (connection.autoAcceptConnection ?? this.connectionsModuleConfig.autoAcceptConnections) { + const message = await this.didExchangeProtocol.createComplete(agentContext, connection, outOfBandRecord) + // Disable return routing as we don't want to receive a response for this message over the same channel + // This has led to long timeouts as not all clients actually close an http socket if there is no response message + message.setReturnRouting(ReturnRouteTypes.none) + + if (!outOfBandRecord.reusable) { + await this.outOfBandService.updateState(agentContext, outOfBandRecord, OutOfBandState.Done) + } + return new OutboundMessageContext(message, { agentContext, connection }) + } + } +} diff --git a/packages/core/src/modules/connections/handlers/DidRotateAckHandler.ts b/packages/core/src/modules/connections/handlers/DidRotateAckHandler.ts new file mode 100644 index 0000000000..ec05c71e8e --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidRotateAckHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRotateService } from '../services' + +import { DidRotateAckMessage } from '../messages' + +export class DidRotateAckHandler implements MessageHandler { + private didRotateService: DidRotateService + public supportedMessages = [DidRotateAckMessage] + + public constructor(didRotateService: DidRotateService) { + this.didRotateService = didRotateService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + await this.didRotateService.processRotateAck(inboundMessage) + } +} diff --git a/packages/core/src/modules/connections/handlers/DidRotateHandler.ts b/packages/core/src/modules/connections/handlers/DidRotateHandler.ts new file mode 100644 index 0000000000..458f5bdb86 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidRotateHandler.ts @@ -0,0 +1,26 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRotateService } from '../services' +import type { ConnectionService } from '../services/ConnectionService' + +import { CredoError } from '../../../error' +import { DidRotateMessage } from '../messages' + +export class DidRotateHandler implements MessageHandler { + private didRotateService: DidRotateService + private connectionService: ConnectionService + public supportedMessages = [DidRotateMessage] + + public constructor(didRotateService: DidRotateService, connectionService: ConnectionService) { + this.didRotateService = didRotateService + this.connectionService = connectionService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const { connection, recipientKey } = messageContext + if (!connection) { + throw new CredoError(`Connection for verkey ${recipientKey?.fingerprint} not found!`) + } + + return this.didRotateService.processRotate(messageContext) + } +} diff --git a/packages/core/src/modules/connections/handlers/DidRotateProblemReportHandler.ts b/packages/core/src/modules/connections/handlers/DidRotateProblemReportHandler.ts new file mode 100644 index 0000000000..2f68e748bd --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidRotateProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRotateService } from '../services' + +import { DidRotateProblemReportMessage } from '../messages' + +export class DidRotateProblemReportHandler implements MessageHandler { + private didRotateService: DidRotateService + public supportedMessages = [DidRotateProblemReportMessage] + + public constructor(didRotateService: DidRotateService) { + this.didRotateService = didRotateService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.didRotateService.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/connections/handlers/HangupHandler.ts b/packages/core/src/modules/connections/handlers/HangupHandler.ts new file mode 100644 index 0000000000..5e66ee2944 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/HangupHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRotateService } from '../services' + +import { HangupMessage } from '../messages' + +export class HangupHandler implements MessageHandler { + private didRotateService: DidRotateService + public supportedMessages = [HangupMessage] + + public constructor(didRotateService: DidRotateService) { + this.didRotateService = didRotateService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + await this.didRotateService.processHangup(inboundMessage) + } +} diff --git a/packages/core/src/modules/connections/handlers/TrustPingMessageHandler.ts b/packages/core/src/modules/connections/handlers/TrustPingMessageHandler.ts new file mode 100644 index 0000000000..37b36d1929 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/TrustPingMessageHandler.ts @@ -0,0 +1,33 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { ConnectionService } from '../services/ConnectionService' +import type { TrustPingService } from '../services/TrustPingService' + +import { CredoError } from '../../../error' +import { TrustPingMessage } from '../messages' +import { DidExchangeState } from '../models' + +export class TrustPingMessageHandler implements MessageHandler { + private trustPingService: TrustPingService + private connectionService: ConnectionService + public supportedMessages = [TrustPingMessage] + + public constructor(trustPingService: TrustPingService, connectionService: ConnectionService) { + this.trustPingService = trustPingService + this.connectionService = connectionService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const { connection, recipientKey } = messageContext + if (!connection) { + throw new CredoError(`Connection for verkey ${recipientKey?.fingerprint} not found!`) + } + + // TODO: This is better addressed in a middleware of some kind because + // any message can transition the state to complete, not just an ack or trust ping + if (connection.state === DidExchangeState.ResponseSent) { + await this.connectionService.updateState(messageContext.agentContext, connection, DidExchangeState.Completed) + } + + return this.trustPingService.processPing(messageContext, connection) + } +} diff --git a/packages/core/src/modules/connections/handlers/TrustPingResponseMessageHandler.ts b/packages/core/src/modules/connections/handlers/TrustPingResponseMessageHandler.ts new file mode 100644 index 0000000000..27e0bff533 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/TrustPingResponseMessageHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { TrustPingService } from '../services/TrustPingService' + +import { TrustPingResponseMessage } from '../messages' + +export class TrustPingResponseMessageHandler implements MessageHandler { + private trustPingService: TrustPingService + public supportedMessages = [TrustPingResponseMessage] + + public constructor(trustPingService: TrustPingService) { + this.trustPingService = trustPingService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + return this.trustPingService.processPingResponse(inboundMessage) + } +} diff --git a/packages/core/src/modules/connections/handlers/index.ts b/packages/core/src/modules/connections/handlers/index.ts new file mode 100644 index 0000000000..0aeb955bdc --- /dev/null +++ b/packages/core/src/modules/connections/handlers/index.ts @@ -0,0 +1,13 @@ +export * from './AckMessageHandler' +export * from './ConnectionRequestHandler' +export * from './ConnectionResponseHandler' +export * from './TrustPingMessageHandler' +export * from './TrustPingResponseMessageHandler' +export * from './DidExchangeRequestHandler' +export * from './DidExchangeResponseHandler' +export * from './DidExchangeCompleteHandler' +export * from './ConnectionProblemReportHandler' +export * from './DidRotateHandler' +export * from './DidRotateAckHandler' +export * from './DidRotateProblemReportHandler' +export * from './HangupHandler' diff --git a/packages/core/src/modules/connections/index.ts b/packages/core/src/modules/connections/index.ts new file mode 100644 index 0000000000..e9dd5862d9 --- /dev/null +++ b/packages/core/src/modules/connections/index.ts @@ -0,0 +1,10 @@ +export * from './messages' +export * from './models' +export * from './repository' +export * from './services' +export * from './ConnectionEvents' +export * from './TrustPingEvents' +export * from './ConnectionsApi' +export * from './DidExchangeProtocol' +export * from './ConnectionsModuleConfig' +export * from './ConnectionsModule' diff --git a/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts b/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts new file mode 100644 index 0000000000..420a1ea8f3 --- /dev/null +++ b/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts @@ -0,0 +1,157 @@ +import type { Attachment } from '../../../decorators/attachment/Attachment' + +import { Transform } from 'class-transformer' +import { ArrayNotEmpty, IsArray, IsOptional, IsString, IsUrl, ValidateIf } from 'class-validator' +import { parseUrl } from 'query-string' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { CredoError } from '../../../error' +import { JsonEncoder } from '../../../utils/JsonEncoder' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { IsValidMessageType, parseMessageType, replaceLegacyDidSovPrefix } from '../../../utils/messageType' + +export interface BaseInvitationOptions { + id?: string + label: string + imageUrl?: string + appendedAttachments?: Attachment[] +} + +export interface InlineInvitationOptions { + recipientKeys: string[] + serviceEndpoint: string + routingKeys?: string[] +} + +export interface DIDInvitationOptions { + did: string +} + +/** + * Message to invite another agent to create a connection + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0160-connection-protocol/README.md#0-invitation-to-connect + */ +export class ConnectionInvitationMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + + /** + * Create new ConnectionInvitationMessage instance. + * @param options + */ + public constructor(options: BaseInvitationOptions & (DIDInvitationOptions | InlineInvitationOptions)) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.label = options.label + this.imageUrl = options.imageUrl + this.appendedAttachments = options.appendedAttachments + + if (isDidInvitation(options)) { + this.did = options.did + } else { + this.recipientKeys = options.recipientKeys + this.serviceEndpoint = options.serviceEndpoint + this.routingKeys = options.routingKeys + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (options.did && (options.recipientKeys || options.routingKeys || options.serviceEndpoint)) { + throw new CredoError( + 'either the did or the recipientKeys/serviceEndpoint/routingKeys must be set, but not both' + ) + } + } + } + + @IsValidMessageType(ConnectionInvitationMessage.type) + @Transform(({ value }) => replaceLegacyDidSovPrefix(value), { + toClassOnly: true, + }) + public readonly type = ConnectionInvitationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/connections/1.0/invitation') + + @IsString() + public label!: string + + @IsString() + @ValidateIf((o: ConnectionInvitationMessage) => o.recipientKeys === undefined) + public did?: string + + @IsString({ + each: true, + }) + @IsArray() + @ValidateIf((o: ConnectionInvitationMessage) => o.did === undefined) + @ArrayNotEmpty() + public recipientKeys?: string[] + + @IsString() + @ValidateIf((o: ConnectionInvitationMessage) => o.did === undefined) + public serviceEndpoint?: string + + @IsString({ + each: true, + }) + @ValidateIf((o: ConnectionInvitationMessage) => o.did === undefined) + @IsOptional() + public routingKeys?: string[] + + @IsOptional() + @IsUrl() + public imageUrl?: string + + /** + * Create an invitation url from this instance + * + * @param domain domain name to use for invitation url + * @returns invitation url with base64 encoded invitation + */ + public toUrl({ + domain, + useDidSovPrefixWhereAllowed = false, + }: { + domain: string + useDidSovPrefixWhereAllowed?: boolean + }) { + const invitationJson = this.toJSON({ useDidSovPrefixWhereAllowed }) + + const encodedInvitation = JsonEncoder.toBase64URL(invitationJson) + const invitationUrl = `${domain}?c_i=${encodedInvitation}` + + return invitationUrl + } + + /** + * Create a `ConnectionInvitationMessage` instance from the `c_i` or `d_m` parameter of an URL + * + * @param invitationUrl invitation url containing c_i or d_m parameter + * + * @throws Error when the url can not be decoded to JSON, or decoded message is not a valid 'ConnectionInvitationMessage' + */ + public static fromUrl(invitationUrl: string) { + const parsedUrl = parseUrl(invitationUrl).query + const encodedInvitation = parsedUrl['c_i'] ?? parsedUrl['d_m'] + if (typeof encodedInvitation === 'string') { + const invitationJson = JsonEncoder.fromBase64(encodedInvitation) + const invitation = JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage) + + return invitation + } else { + throw new CredoError('InvitationUrl is invalid. Needs to be encoded with either c_i, d_m, or oob') + } + } +} + +/** + * Check whether an invitation is a `DIDInvitationData` object + * + * @param invitation invitation object + */ +function isDidInvitation( + invitation: InlineInvitationOptions | DIDInvitationOptions +): invitation is DIDInvitationOptions { + return (invitation as DIDInvitationOptions).did !== undefined +} diff --git a/packages/core/src/modules/connections/messages/ConnectionProblemReportMessage.ts b/packages/core/src/modules/connections/messages/ConnectionProblemReportMessage.ts new file mode 100644 index 0000000000..d6d84dcb0f --- /dev/null +++ b/packages/core/src/modules/connections/messages/ConnectionProblemReportMessage.ts @@ -0,0 +1,25 @@ +import type { ProblemReportMessageOptions } from '../../problem-reports/messages/ProblemReportMessage' + +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { ProblemReportMessage } from '../../problem-reports/messages/ProblemReportMessage' + +export type ConnectionProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class ConnectionProblemReportMessage extends ProblemReportMessage { + public readonly allowDidSovPrefix = true + + /** + * Create new ConnectionProblemReportMessage instance. + * @param options + */ + public constructor(options: ConnectionProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(ConnectionProblemReportMessage.type) + public readonly type = ConnectionProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/connection/1.0/problem-report') +} diff --git a/packages/core/src/modules/connections/messages/ConnectionRequestMessage.ts b/packages/core/src/modules/connections/messages/ConnectionRequestMessage.ts new file mode 100644 index 0000000000..46a33fa221 --- /dev/null +++ b/packages/core/src/modules/connections/messages/ConnectionRequestMessage.ts @@ -0,0 +1,60 @@ +import type { DidDoc } from '../models' + +import { Type } from 'class-transformer' +import { IsInstance, IsOptional, IsString, IsUrl, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { Connection } from '../models' + +export interface ConnectionRequestMessageOptions { + id?: string + label: string + did: string + didDoc?: DidDoc + imageUrl?: string +} + +/** + * Message to communicate the DID document to the other agent when creating a connection + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0160-connection-protocol/README.md#1-connection-request + */ +export class ConnectionRequestMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + + /** + * Create new ConnectionRequestMessage instance. + * @param options + */ + public constructor(options: ConnectionRequestMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.label = options.label + this.imageUrl = options.imageUrl + + this.connection = new Connection({ + did: options.did, + didDoc: options.didDoc, + }) + } + } + + @IsValidMessageType(ConnectionRequestMessage.type) + public readonly type = ConnectionRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/connections/1.0/request') + + @IsString() + public label!: string + + @Type(() => Connection) + @ValidateNested() + @IsInstance(Connection) + public connection!: Connection + + @IsOptional() + @IsUrl() + public imageUrl?: string +} diff --git a/packages/core/src/modules/connections/messages/ConnectionResponseMessage.ts b/packages/core/src/modules/connections/messages/ConnectionResponseMessage.ts new file mode 100644 index 0000000000..08e22a166b --- /dev/null +++ b/packages/core/src/modules/connections/messages/ConnectionResponseMessage.ts @@ -0,0 +1,46 @@ +import { Type, Expose } from 'class-transformer' +import { IsInstance, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { SignatureDecorator } from '../../../decorators/signature/SignatureDecorator' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface ConnectionResponseMessageOptions { + id?: string + threadId: string + connectionSig: SignatureDecorator +} + +/** + * Message part of connection protocol used to complete the connection + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0160-connection-protocol/README.md#2-connection-response + */ +export class ConnectionResponseMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + + /** + * Create new ConnectionResponseMessage instance. + * @param options + */ + public constructor(options: ConnectionResponseMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.connectionSig = options.connectionSig + + this.setThread({ threadId: options.threadId }) + } + } + + @IsValidMessageType(ConnectionResponseMessage.type) + public readonly type = ConnectionResponseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/connections/1.0/response') + + @Type(() => SignatureDecorator) + @ValidateNested() + @IsInstance(SignatureDecorator) + @Expose({ name: 'connection~sig' }) + public connectionSig!: SignatureDecorator +} diff --git a/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts new file mode 100644 index 0000000000..754f049a71 --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts @@ -0,0 +1,30 @@ +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface DidExchangeCompleteMessageOptions { + id?: string + threadId: string + parentThreadId: string +} + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0023-did-exchange/README.md#3-exchange-complete + */ +export class DidExchangeCompleteMessage extends AgentMessage { + public constructor(options: DidExchangeCompleteMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + + this.setThread({ + threadId: options.threadId, + parentThreadId: options.parentThreadId, + }) + } + } + + @IsValidMessageType(DidExchangeCompleteMessage.type) + public readonly type = DidExchangeCompleteMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/complete') +} diff --git a/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts new file mode 100644 index 0000000000..3f948aa768 --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts @@ -0,0 +1,19 @@ +import type { ProblemReportMessageOptions } from '../../problem-reports/messages/ProblemReportMessage' + +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { ProblemReportMessage } from '../../problem-reports/messages/ProblemReportMessage' + +export type DidExchangeProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class DidExchangeProblemReportMessage extends ProblemReportMessage { + public constructor(options: DidExchangeProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(DidExchangeProblemReportMessage.type) + public readonly type = DidExchangeProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/problem-report') +} diff --git a/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts new file mode 100644 index 0000000000..353ac079a2 --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts @@ -0,0 +1,65 @@ +import { Expose, Type } from 'class-transformer' +import { IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { Attachment } from '../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface DidExchangeRequestMessageOptions { + id?: string + parentThreadId: string + label: string + goalCode?: string + goal?: string + did: string +} + +/** + * Message to communicate the DID document to the other agent when creating a connection + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0023-did-exchange/README.md#1-exchange-request + */ +export class DidExchangeRequestMessage extends AgentMessage { + /** + * Create new DidExchangeRequestMessage instance. + * @param options + */ + public constructor(options: DidExchangeRequestMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.label = options.label + this.goalCode = options.goalCode + this.goal = options.goal + this.did = options.did + + this.setThread({ + parentThreadId: options.parentThreadId, + }) + } + } + + @IsValidMessageType(DidExchangeRequestMessage.type) + public readonly type = DidExchangeRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/request') + + @IsString() + public readonly label?: string + + @Expose({ name: 'goal_code' }) + @IsOptional() + public readonly goalCode?: string + + @IsString() + @IsOptional() + public readonly goal?: string + + @IsString() + public readonly did!: string + + @Expose({ name: 'did_doc~attach' }) + @Type(() => Attachment) + @ValidateNested() + public didDoc?: Attachment +} diff --git a/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts new file mode 100644 index 0000000000..fe62a6393a --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts @@ -0,0 +1,55 @@ +import { Type, Expose } from 'class-transformer' +import { IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { Attachment } from '../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface DidExchangeResponseMessageOptions { + id?: string + threadId: string + did: string +} + +/** + * Message part of connection protocol used to complete the connection + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0023-did-exchange/README.md#2-exchange-response + */ +export class DidExchangeResponseMessage extends AgentMessage { + /** + * Create new DidExchangeResponseMessage instance. + * @param options + */ + public constructor(options: DidExchangeResponseMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.did = options.did + + this.setThread({ + threadId: options.threadId, + }) + } + } + + @IsValidMessageType(DidExchangeResponseMessage.type) + public readonly type = DidExchangeResponseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/response') + + @IsString() + public readonly did!: string + + @Expose({ name: 'did_doc~attach' }) + @IsOptional() + @Type(() => Attachment) + @ValidateNested() + public didDoc?: Attachment + + @Expose({ name: 'did_rotate~attach' }) + @IsOptional() + @Type(() => Attachment) + @ValidateNested() + public didRotate?: Attachment +} diff --git a/packages/core/src/modules/connections/messages/DidRotateAckMessage.ts b/packages/core/src/modules/connections/messages/DidRotateAckMessage.ts new file mode 100644 index 0000000000..363c7b1e45 --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidRotateAckMessage.ts @@ -0,0 +1,20 @@ +import type { AckMessageOptions } from '../../common/messages/AckMessage' + +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { AckMessage } from '../../common/messages/AckMessage' + +export type DidRotateAckMessageOptions = AckMessageOptions + +export class DidRotateAckMessage extends AckMessage { + /** + * Create new CredentialAckMessage instance. + * @param options + */ + public constructor(options: DidRotateAckMessageOptions) { + super(options) + } + + @IsValidMessageType(DidRotateAckMessage.type) + public readonly type = DidRotateAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/did-rotate/1.0/ack') +} diff --git a/packages/core/src/modules/connections/messages/DidRotateMessage.ts b/packages/core/src/modules/connections/messages/DidRotateMessage.ts new file mode 100644 index 0000000000..a33db94a71 --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidRotateMessage.ts @@ -0,0 +1,38 @@ +import { Expose } from 'class-transformer' +import { IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface DidRotateMessageOptions { + id?: string + toDid: string +} + +/** + * Message to communicate the DID a party wish to rotate to. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0794-did-rotate#rotate + */ +export class DidRotateMessage extends AgentMessage { + /** + * Create new RotateMessage instance. + * @param options + */ + public constructor(options: DidRotateMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.toDid = options.toDid + } + } + + @IsValidMessageType(DidRotateMessage.type) + public readonly type = DidRotateMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/did-rotate/1.0/rotate') + + @Expose({ name: 'to_did' }) + @IsString() + public readonly toDid!: string +} diff --git a/packages/core/src/modules/connections/messages/DidRotateProblemReportMessage.ts b/packages/core/src/modules/connections/messages/DidRotateProblemReportMessage.ts new file mode 100644 index 0000000000..2eaf0c027d --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidRotateProblemReportMessage.ts @@ -0,0 +1,19 @@ +import type { ProblemReportMessageOptions } from '../../problem-reports/messages/ProblemReportMessage' + +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { ProblemReportMessage } from '../../problem-reports/messages/ProblemReportMessage' + +export type DidRotateProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class DidRotateProblemReportMessage extends ProblemReportMessage { + public constructor(options: DidRotateProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(DidRotateProblemReportMessage.type) + public readonly type = DidRotateProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/did-rotate/1.0/problem-report') +} diff --git a/packages/core/src/modules/connections/messages/HangupMessage.ts b/packages/core/src/modules/connections/messages/HangupMessage.ts new file mode 100644 index 0000000000..b9ab2bd510 --- /dev/null +++ b/packages/core/src/modules/connections/messages/HangupMessage.ts @@ -0,0 +1,30 @@ +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface HangupMessageOptions { + id?: string +} + +/** + * This message is sent by the rotating_party to inform the observing_party that they are done + * with the relationship and will no longer be responding. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0794-did-rotate#hangup + */ +export class HangupMessage extends AgentMessage { + /** + * Create new HangupMessage instance. + * @param options + */ + public constructor(options: HangupMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + } + } + + @IsValidMessageType(HangupMessage.type) + public readonly type = HangupMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/did-rotate/1.0/hangup') +} diff --git a/packages/core/src/modules/connections/messages/TrustPingMessage.ts b/packages/core/src/modules/connections/messages/TrustPingMessage.ts new file mode 100644 index 0000000000..a8719f0b34 --- /dev/null +++ b/packages/core/src/modules/connections/messages/TrustPingMessage.ts @@ -0,0 +1,58 @@ +import type { TimingDecorator } from '../../../decorators/timing/TimingDecorator' + +import { Expose } from 'class-transformer' +import { IsString, IsBoolean, IsOptional } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface TrustPingMessageOptions { + comment?: string + id?: string + responseRequested?: boolean + timing?: Pick +} + +/** + * Message to initiate trust ping interaction + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0048-trust-ping/README.md#messages + */ +export class TrustPingMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + + /** + * Create new TrustPingMessage instance. + * responseRequested will be true if not passed + * @param options + */ + public constructor(options: TrustPingMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.comment = options.comment + this.responseRequested = options.responseRequested !== undefined ? options.responseRequested : true + + if (options.timing) { + this.setTiming({ + outTime: options.timing.outTime, + expiresTime: options.timing.expiresTime, + delayMilli: options.timing.delayMilli, + }) + } + } + } + + @IsValidMessageType(TrustPingMessage.type) + public readonly type = TrustPingMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/trust_ping/1.0/ping') + + @IsString() + @IsOptional() + public comment?: string + + @IsBoolean() + @Expose({ name: 'response_requested' }) + public responseRequested = true +} diff --git a/packages/core/src/modules/connections/messages/TrustPingResponseMessage.ts b/packages/core/src/modules/connections/messages/TrustPingResponseMessage.ts new file mode 100644 index 0000000000..9fa465a836 --- /dev/null +++ b/packages/core/src/modules/connections/messages/TrustPingResponseMessage.ts @@ -0,0 +1,55 @@ +import type { TimingDecorator } from '../../../decorators/timing/TimingDecorator' + +import { IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface TrustPingResponseMessageOptions { + comment?: string + id?: string + threadId: string + timing?: Pick +} + +/** + * Message to respond to a trust ping message + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0048-trust-ping/README.md#messages + */ +export class TrustPingResponseMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + + /** + * Create new TrustPingResponseMessage instance. + * responseRequested will be true if not passed + * @param options + */ + public constructor(options: TrustPingResponseMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.comment = options.comment + + this.setThread({ + threadId: options.threadId, + }) + + if (options.timing) { + this.setTiming({ + inTime: options.timing.inTime, + outTime: options.timing.outTime, + }) + } + } + } + + @IsValidMessageType(TrustPingResponseMessage.type) + public readonly type = TrustPingResponseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/trust_ping/1.0/ping_response') + + @IsString() + @IsOptional() + public comment?: string +} diff --git a/packages/core/src/modules/connections/messages/index.ts b/packages/core/src/modules/connections/messages/index.ts new file mode 100644 index 0000000000..1a3acdf7c8 --- /dev/null +++ b/packages/core/src/modules/connections/messages/index.ts @@ -0,0 +1,14 @@ +export * from './ConnectionInvitationMessage' +export * from './ConnectionRequestMessage' +export * from './ConnectionResponseMessage' +export * from './TrustPingMessage' +export * from './TrustPingResponseMessage' +export * from './ConnectionProblemReportMessage' +export * from './DidExchangeRequestMessage' +export * from './DidExchangeResponseMessage' +export * from './DidExchangeCompleteMessage' +export * from './DidExchangeProblemReportMessage' +export * from './DidRotateProblemReportMessage' +export * from './DidRotateMessage' +export * from './DidRotateAckMessage' +export * from './HangupMessage' diff --git a/packages/core/src/modules/connections/models/Connection.ts b/packages/core/src/modules/connections/models/Connection.ts new file mode 100644 index 0000000000..63bcd4e2d6 --- /dev/null +++ b/packages/core/src/modules/connections/models/Connection.ts @@ -0,0 +1,29 @@ +import { Expose, Type } from 'class-transformer' +import { IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { DidDoc } from './did/DidDoc' + +export interface ConnectionOptions { + did: string + didDoc?: DidDoc +} + +export class Connection { + public constructor(options: ConnectionOptions) { + if (options) { + this.did = options.did + this.didDoc = options.didDoc + } + } + + @IsString() + @Expose({ name: 'DID' }) + public did!: string + + @Expose({ name: 'DIDDoc' }) + @Type(() => DidDoc) + @ValidateNested() + @IsInstance(DidDoc) + @IsOptional() + public didDoc?: DidDoc +} diff --git a/packages/core/src/modules/connections/models/ConnectionRole.ts b/packages/core/src/modules/connections/models/ConnectionRole.ts new file mode 100644 index 0000000000..e0703e840a --- /dev/null +++ b/packages/core/src/modules/connections/models/ConnectionRole.ts @@ -0,0 +1,4 @@ +export enum ConnectionRole { + Inviter = 'inviter', + Invitee = 'invitee', +} diff --git a/packages/core/src/modules/connections/models/ConnectionState.ts b/packages/core/src/modules/connections/models/ConnectionState.ts new file mode 100644 index 0000000000..dbc35cbfda --- /dev/null +++ b/packages/core/src/modules/connections/models/ConnectionState.ts @@ -0,0 +1,30 @@ +import { DidExchangeState } from './DidExchangeState' + +/** + * Connection states as defined in RFC 0160. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0160-connection-protocol/README.md#states + */ +export enum ConnectionState { + Null = 'null', + Invited = 'invited', + Requested = 'requested', + Responded = 'responded', + Complete = 'complete', +} + +export function rfc0160StateFromDidExchangeState(didExchangeState: DidExchangeState) { + const stateMapping = { + [DidExchangeState.Start]: ConnectionState.Null, + [DidExchangeState.Abandoned]: ConnectionState.Null, + [DidExchangeState.InvitationReceived]: ConnectionState.Invited, + [DidExchangeState.InvitationSent]: ConnectionState.Invited, + [DidExchangeState.RequestReceived]: ConnectionState.Requested, + [DidExchangeState.RequestSent]: ConnectionState.Requested, + [DidExchangeState.ResponseReceived]: ConnectionState.Responded, + [DidExchangeState.ResponseSent]: ConnectionState.Responded, + [DidExchangeState.Completed]: ConnectionState.Complete, + } + + return stateMapping[didExchangeState] +} diff --git a/packages/core/src/modules/connections/models/ConnectionType.ts b/packages/core/src/modules/connections/models/ConnectionType.ts new file mode 100644 index 0000000000..85e6a5dbf9 --- /dev/null +++ b/packages/core/src/modules/connections/models/ConnectionType.ts @@ -0,0 +1,3 @@ +export enum ConnectionType { + Mediator = 'mediator', +} diff --git a/packages/core/src/modules/connections/models/DidExchangeRole.ts b/packages/core/src/modules/connections/models/DidExchangeRole.ts new file mode 100644 index 0000000000..bc5c3939d4 --- /dev/null +++ b/packages/core/src/modules/connections/models/DidExchangeRole.ts @@ -0,0 +1,4 @@ +export enum DidExchangeRole { + Requester = 'requester', + Responder = 'responder', +} diff --git a/packages/core/src/modules/connections/models/DidExchangeState.ts b/packages/core/src/modules/connections/models/DidExchangeState.ts new file mode 100644 index 0000000000..9614b81da2 --- /dev/null +++ b/packages/core/src/modules/connections/models/DidExchangeState.ts @@ -0,0 +1,16 @@ +/** + * Connection states as defined in RFC 0023. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0023-did-exchange/README.md#state-machine-tables + */ +export enum DidExchangeState { + Start = 'start', + InvitationSent = 'invitation-sent', + InvitationReceived = 'invitation-received', + RequestSent = 'request-sent', + RequestReceived = 'request-received', + ResponseSent = 'response-sent', + ResponseReceived = 'response-received', + Abandoned = 'abandoned', + Completed = 'completed', +} diff --git a/packages/core/src/modules/connections/models/DidRotateRole.ts b/packages/core/src/modules/connections/models/DidRotateRole.ts new file mode 100644 index 0000000000..310c124ed4 --- /dev/null +++ b/packages/core/src/modules/connections/models/DidRotateRole.ts @@ -0,0 +1,4 @@ +export enum DidRotateRole { + RotatingParty = 'rotating_party', + ObservingParty = 'observing_party', +} diff --git a/packages/core/src/modules/connections/models/HandshakeProtocol.ts b/packages/core/src/modules/connections/models/HandshakeProtocol.ts new file mode 100644 index 0000000000..8a084b05bb --- /dev/null +++ b/packages/core/src/modules/connections/models/HandshakeProtocol.ts @@ -0,0 +1,8 @@ +/** + * Enum values should be sorted based on order of preference. Values will be + * included in this order when creating out of band invitations. + */ +export enum HandshakeProtocol { + DidExchange = 'https://didcomm.org/didexchange/1.x', + Connections = 'https://didcomm.org/connections/1.x', +} diff --git a/packages/core/src/modules/connections/models/InvitationDetails.ts b/packages/core/src/modules/connections/models/InvitationDetails.ts new file mode 100644 index 0000000000..bd270240a5 --- /dev/null +++ b/packages/core/src/modules/connections/models/InvitationDetails.ts @@ -0,0 +1,6 @@ +export interface InvitationDetails { + label: string + recipientKeys: string[] + serviceEndpoint: string + routingKeys: string[] +} diff --git a/packages/core/src/modules/connections/models/__tests__/ConnectionState.test.ts b/packages/core/src/modules/connections/models/__tests__/ConnectionState.test.ts new file mode 100644 index 0000000000..e18a96ca43 --- /dev/null +++ b/packages/core/src/modules/connections/models/__tests__/ConnectionState.test.ts @@ -0,0 +1,30 @@ +import { ConnectionState, rfc0160StateFromDidExchangeState } from '../ConnectionState' +import { DidExchangeState } from '../DidExchangeState' + +describe('ConnectionState', () => { + test('state matches Connection 1.0 (RFC 0160) state value', () => { + expect(ConnectionState.Null).toBe('null') + expect(ConnectionState.Invited).toBe('invited') + expect(ConnectionState.Requested).toBe('requested') + expect(ConnectionState.Responded).toBe('responded') + expect(ConnectionState.Complete).toBe('complete') + }) + + describe('rfc0160StateFromDidExchangeState', () => { + it('should return the connection state for all did exchanges states', () => { + expect(rfc0160StateFromDidExchangeState(DidExchangeState.Abandoned)).toEqual(ConnectionState.Null) + expect(rfc0160StateFromDidExchangeState(DidExchangeState.Start)).toEqual(ConnectionState.Null) + + expect(rfc0160StateFromDidExchangeState(DidExchangeState.InvitationReceived)).toEqual(ConnectionState.Invited) + expect(rfc0160StateFromDidExchangeState(DidExchangeState.InvitationSent)).toEqual(ConnectionState.Invited) + + expect(rfc0160StateFromDidExchangeState(DidExchangeState.RequestReceived)).toEqual(ConnectionState.Requested) + expect(rfc0160StateFromDidExchangeState(DidExchangeState.RequestSent)).toEqual(ConnectionState.Requested) + + expect(rfc0160StateFromDidExchangeState(DidExchangeState.ResponseReceived)).toEqual(ConnectionState.Responded) + expect(rfc0160StateFromDidExchangeState(DidExchangeState.ResponseReceived)).toEqual(ConnectionState.Responded) + + expect(rfc0160StateFromDidExchangeState(DidExchangeState.Completed)).toEqual(ConnectionState.Complete) + }) + }) +}) diff --git a/packages/core/src/modules/connections/models/did/DidDoc.ts b/packages/core/src/modules/connections/models/did/DidDoc.ts new file mode 100644 index 0000000000..d40a4aaeca --- /dev/null +++ b/packages/core/src/modules/connections/models/did/DidDoc.ts @@ -0,0 +1,89 @@ +import type { Authentication } from './authentication' +import type { PublicKey } from './publicKey' +import type { DidDocumentService } from '../../../dids/domain/service' + +import { Expose } from 'class-transformer' +import { Equals, IsArray, IsString, ValidateNested } from 'class-validator' + +import { ServiceTransformer, DidCommV1Service, IndyAgentService } from '../../../dids/domain/service' + +import { AuthenticationTransformer } from './authentication' +import { PublicKeyTransformer } from './publicKey' + +type DidDocOptions = Pick + +export class DidDoc { + @Expose({ name: '@context' }) + @Equals('https://w3id.org/did/v1') + public context = 'https://w3id.org/did/v1' + + @IsString() + public id!: string + + @IsArray() + @ValidateNested() + @PublicKeyTransformer() + public publicKey: PublicKey[] = [] + + @IsArray() + @ValidateNested() + @ServiceTransformer() + public service: DidDocumentService[] = [] + + @IsArray() + @ValidateNested() + @AuthenticationTransformer() + public authentication: Authentication[] = [] + + public constructor(options: DidDocOptions) { + if (options) { + this.id = options.id + this.publicKey = options.publicKey + this.service = options.service + this.authentication = options.authentication + } + } + + /** + * Gets the matching public key for a given key id + * + * @param id fully qualified key id + */ + public getPublicKey(id: string): PublicKey | undefined { + return this.publicKey.find((item) => item.id === id) + } + + /** + * Returns all of the service endpoints matching the given type. + * + * @param type The type of service(s) to query. + */ + public getServicesByType(type: string): S[] { + return this.service.filter((service) => service.type === type) as S[] + } + + /** + * Returns all of the service endpoints matching the given class + * + * @param classType The class to query services. + */ + public getServicesByClassType( + classType: new (...args: never[]) => S + ): S[] { + return this.service.filter((service) => service instanceof classType) as S[] + } + + /** + * Get all DIDComm services ordered by priority descending. This means the highest + * priority will be the first entry. + */ + public get didCommServices(): Array { + const didCommServiceTypes = [IndyAgentService.type, DidCommV1Service.type] + const services = this.service.filter((service) => didCommServiceTypes.includes(service.type)) as Array< + IndyAgentService | DidCommV1Service + > + + // Sort services based on indicated priority + return services.sort((a, b) => a.priority - b.priority) + } +} diff --git a/packages/core/src/modules/connections/models/did/__tests__/Authentication.test.ts b/packages/core/src/modules/connections/models/did/__tests__/Authentication.test.ts new file mode 100644 index 0000000000..5cd81e82c9 --- /dev/null +++ b/packages/core/src/modules/connections/models/did/__tests__/Authentication.test.ts @@ -0,0 +1,163 @@ +import type { Authentication } from '../authentication' + +import { instanceToPlain, plainToInstance } from 'class-transformer' + +import { AuthenticationTransformer, ReferencedAuthentication, EmbeddedAuthentication } from '../authentication' +import { PublicKey, RsaSig2018 } from '../publicKey' + +describe('Did | Authentication', () => { + describe('EmbeddedAuthentication', () => { + it('should correctly transform ReferencedAuthentication class to Json', async () => { + const publicKey = new RsaSig2018({ + controller: 'test', + publicKeyPem: 'test', + id: 'test#1', + }) + + const referencedAuthentication = new ReferencedAuthentication(publicKey, 'RsaSignatureAuthentication2018') + const transformed = instanceToPlain(referencedAuthentication) + + expect(transformed).toMatchObject({ + type: 'RsaSignatureAuthentication2018', + publicKey: 'test#1', + }) + }) + }) + + describe('AuthenticationTransformer', () => { + class AuthenticationTransformerTest { + public publicKey: PublicKey[] = [] + + @AuthenticationTransformer() + public authentication: Authentication[] = [] + } + + it("should use generic 'publicKey' type when no matching public key type class is present", async () => { + const embeddedAuthenticationJson = { + controller: 'did:sov:1123123', + id: 'did:sov:1123123#1', + type: 'RandomType', + publicKeyPem: '-----BEGIN PUBLIC X...', + } + + const referencedAuthenticationJson = { + type: 'RandomType', + publicKey: 'did:sov:1123123#1', + } + + const authenticationWrapperJson = { + publicKey: [embeddedAuthenticationJson], + authentication: [referencedAuthenticationJson, embeddedAuthenticationJson], + } + const authenticationWrapper = plainToInstance(AuthenticationTransformerTest, authenticationWrapperJson) + + expect(authenticationWrapper.authentication.length).toBe(2) + + const [referencedAuthentication, embeddedAuthentication] = authenticationWrapper.authentication as [ + ReferencedAuthentication, + EmbeddedAuthentication + ] + expect(referencedAuthentication.publicKey).toBeInstanceOf(PublicKey) + expect(embeddedAuthentication.publicKey).toBeInstanceOf(PublicKey) + }) + + it("should transform Json to ReferencedAuthentication class when the 'publicKey' key is present on the authentication object", async () => { + const publicKeyJson = { + controller: 'did:sov:1123123', + id: 'did:sov:1123123#1', + type: 'RsaVerificationKey2018', + publicKeyPem: '-----BEGIN PUBLIC X...', + } + const referencedAuthenticationJson = { + type: 'RsaSignatureAuthentication2018', + publicKey: 'did:sov:1123123#1', + } + + const authenticationWrapperJson = { + publicKey: [publicKeyJson], + authentication: [referencedAuthenticationJson], + } + const authenticationWrapper = plainToInstance(AuthenticationTransformerTest, authenticationWrapperJson) + + expect(authenticationWrapper.authentication.length).toBe(1) + + const firstAuth = authenticationWrapper.authentication[0] as ReferencedAuthentication + expect(firstAuth).toBeInstanceOf(ReferencedAuthentication) + expect(firstAuth.publicKey).toBeInstanceOf(RsaSig2018) + expect(firstAuth.type).toBe(referencedAuthenticationJson.type) + }) + + it("should throw an error when the 'publicKey' is present, but no publicKey entry exists with the corresponding id", async () => { + const referencedAuthenticationJson = { + type: 'RsaVerificationKey2018', + publicKey: 'did:sov:1123123#1', + } + + const authenticationWrapperJson = { + publicKey: [], + authentication: [referencedAuthenticationJson], + } + + expect(() => plainToInstance(AuthenticationTransformerTest, authenticationWrapperJson)).toThrowError( + `Invalid public key referenced ${referencedAuthenticationJson.publicKey}` + ) + }) + + it("should transform Json to EmbeddedAuthentication class when the 'publicKey' key is not present on the authentication object", async () => { + const publicKeyJson = { + controller: 'did:sov:1123123', + id: 'did:sov:1123123#1', + type: 'RsaVerificationKey2018', + publicKeyPem: '-----BEGIN PUBLIC X...', + } + + const authenticationWrapperJson = { + authentication: [publicKeyJson], + } + const authenticationWrapper = plainToInstance(AuthenticationTransformerTest, authenticationWrapperJson) + + expect(authenticationWrapper.authentication.length).toBe(1) + + const firstAuth = authenticationWrapper.authentication[0] as EmbeddedAuthentication + expect(firstAuth).toBeInstanceOf(EmbeddedAuthentication) + expect(firstAuth.publicKey).toBeInstanceOf(RsaSig2018) + expect(firstAuth.publicKey.value).toBe(publicKeyJson.publicKeyPem) + }) + + it('should transform EmbeddedAuthentication and ReferencedAuthentication class to Json', async () => { + const authenticationWrapper = new AuthenticationTransformerTest() + authenticationWrapper.authentication = [ + new EmbeddedAuthentication( + new RsaSig2018({ + controller: 'test', + publicKeyPem: 'test', + id: 'test#1', + }) + ), + new ReferencedAuthentication( + new RsaSig2018({ + controller: 'test', + publicKeyPem: 'test', + id: 'test#1', + }), + 'RsaSignatureAuthentication2018' + ), + ] + + expect(authenticationWrapper.authentication.length).toBe(2) + const [embeddedJson, referencedJson] = instanceToPlain(authenticationWrapper).authentication + + expect(embeddedJson).toMatchObject({ + controller: 'test', + publicKeyPem: 'test', + id: 'test#1', + type: 'RsaVerificationKey2018', + }) + + expect(referencedJson).toMatchObject({ + type: 'RsaSignatureAuthentication2018', + publicKey: 'test#1', + }) + }) + }) +}) diff --git a/packages/core/src/modules/connections/models/did/__tests__/DidDoc.test.ts b/packages/core/src/modules/connections/models/did/__tests__/DidDoc.test.ts new file mode 100644 index 0000000000..59e301acaa --- /dev/null +++ b/packages/core/src/modules/connections/models/did/__tests__/DidDoc.test.ts @@ -0,0 +1,198 @@ +import { instanceToPlain, plainToInstance } from 'class-transformer' + +import { DidCommV1Service, DidDocumentService, IndyAgentService } from '../../../../dids' +import { DidDoc } from '../DidDoc' +import { ReferencedAuthentication, EmbeddedAuthentication } from '../authentication' +import { Ed25119Sig2018, EddsaSaSigSecp256k1, RsaSig2018 } from '../publicKey' + +import diddoc from './diddoc.json' + +const didDoc = new DidDoc({ + authentication: [ + new ReferencedAuthentication( + new RsaSig2018({ + id: '3', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC X...', + }), + 'RsaSignatureAuthentication2018' + ), + new EmbeddedAuthentication( + new EddsaSaSigSecp256k1({ + id: '6', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyHex: '-----BEGIN PUBLIC A...', + }) + ), + ], + id: 'test-id', + publicKey: [ + new RsaSig2018({ + id: '3', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC X...', + }), + new Ed25119Sig2018({ + id: 'did:sov:LjgpST2rjsoxYegQDRm7EL#4', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyBase58: '-----BEGIN PUBLIC 9...', + }), + new EddsaSaSigSecp256k1({ + id: '6', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyHex: '-----BEGIN PUBLIC A...', + }), + ], + service: [ + new DidDocumentService({ + id: '0', + type: 'Mediator', + serviceEndpoint: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + }), + new IndyAgentService({ + id: '6', + serviceEndpoint: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + recipientKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], + routingKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], + priority: 5, + }), + new DidCommV1Service({ + id: '7', + serviceEndpoint: 'https://agent.com/did-comm', + recipientKeys: ['DADEajsDSaksLng9h'], + routingKeys: ['DADEajsDSaksLng9h'], + priority: 10, + }), + ], +}) + +// Test adopted from ACA-Py +// TODO: add more tests +describe('Did | DidDoc', () => { + it('should correctly transforms Json to DidDoc class', () => { + const didDoc = plainToInstance(DidDoc, diddoc) + + // Check array length of all items + expect(didDoc.publicKey.length).toBe(diddoc.publicKey.length) + expect(didDoc.service.length).toBe(diddoc.service.length) + expect(didDoc.authentication.length).toBe(diddoc.authentication.length) + + // Check other properties + expect(didDoc.id).toBe(diddoc.id) + expect(didDoc.context).toBe(diddoc['@context']) + + // Check publicKey + expect(didDoc.publicKey[0]).toBeInstanceOf(RsaSig2018) + expect(didDoc.publicKey[1]).toBeInstanceOf(Ed25119Sig2018) + expect(didDoc.publicKey[2]).toBeInstanceOf(EddsaSaSigSecp256k1) + + // Check Service + expect(didDoc.service[0]).toBeInstanceOf(DidDocumentService) + expect(didDoc.service[1]).toBeInstanceOf(IndyAgentService) + expect(didDoc.service[2]).toBeInstanceOf(DidCommV1Service) + + // Check Authentication + expect(didDoc.authentication[0]).toBeInstanceOf(ReferencedAuthentication) + expect(didDoc.authentication[1]).toBeInstanceOf(EmbeddedAuthentication) + }) + + it('should correctly transforms DidDoc class to Json', () => { + const json = instanceToPlain(didDoc) + + // Check array length of all items + expect(json.publicKey.length).toBe(didDoc.publicKey.length) + expect(json.service.length).toBe(didDoc.service.length) + expect(json.authentication.length).toBe(didDoc.authentication.length) + + // Check other properties + expect(json.id).toBe(didDoc.id) + expect(json['@context']).toBe(didDoc.context) + + // Check publicKey + expect(json.publicKey[0]).toMatchObject({ + id: '3', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC X...', + }) + expect(json.publicKey[1]).toMatchObject({ + id: 'did:sov:LjgpST2rjsoxYegQDRm7EL#4', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyBase58: '-----BEGIN PUBLIC 9...', + }) + expect(json.publicKey[2]).toMatchObject({ + id: '6', + type: 'Secp256k1VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyHex: '-----BEGIN PUBLIC A...', + }) + + // Check Service + expect(json.service[0]).toMatchObject({ + id: '0', + type: 'Mediator', + serviceEndpoint: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + }) + expect(json.service[1]).toMatchObject({ + id: '6', + type: 'IndyAgent', + serviceEndpoint: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + recipientKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], + routingKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], + priority: 5, + }) + expect(json.service[2]).toMatchObject({ + id: '7', + type: 'did-communication', + serviceEndpoint: 'https://agent.com/did-comm', + recipientKeys: ['DADEajsDSaksLng9h'], + routingKeys: ['DADEajsDSaksLng9h'], + priority: 10, + }) + + // Check Authentication + expect(json.authentication[0]).toMatchObject({ + type: 'RsaSignatureAuthentication2018', + publicKey: '3', + }) + expect(json.authentication[1]).toMatchObject({ + id: '6', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + type: 'Secp256k1VerificationKey2018', + publicKeyHex: '-----BEGIN PUBLIC A...', + }) + }) + + describe('getPublicKey', () => { + it('return the public key with the specified id', async () => { + expect(didDoc.getPublicKey('3')).toEqual(didDoc.publicKey.find((item) => item.id === '3')) + }) + }) + + describe('getServicesByType', () => { + it('returns all services with specified type', async () => { + expect(didDoc.getServicesByType('IndyAgent')).toEqual( + didDoc.service.filter((service) => service.type === 'IndyAgent') + ) + }) + }) + + describe('getServicesByClassType', () => { + it('returns all services with specified class', async () => { + expect(didDoc.getServicesByClassType(IndyAgentService)).toEqual( + didDoc.service.filter((service) => service instanceof IndyAgentService) + ) + }) + }) + + describe('didCommServices', () => { + it('returns all IndyAgentService and DidCommService instances', async () => { + expect(didDoc.didCommServices).toEqual(expect.arrayContaining([didDoc.service[1], didDoc.service[2]])) + }) + + it('returns all IndyAgentService and DidCommService instances sorted by priority', async () => { + expect(didDoc.didCommServices).toEqual([didDoc.service[1], didDoc.service[2]]) + }) + }) +}) diff --git a/packages/core/src/modules/connections/models/did/__tests__/PublicKey.test.ts b/packages/core/src/modules/connections/models/did/__tests__/PublicKey.test.ts new file mode 100644 index 0000000000..7d735337a4 --- /dev/null +++ b/packages/core/src/modules/connections/models/did/__tests__/PublicKey.test.ts @@ -0,0 +1,149 @@ +import type { ClassConstructor } from 'class-transformer' + +import { instanceToPlain, plainToInstance } from 'class-transformer' + +import { + PublicKeyTransformer, + PublicKey, + publicKeyTypes, + EddsaSaSigSecp256k1, + Ed25119Sig2018, + RsaSig2018, +} from '../publicKey' + +const publicKeysJson = [ + { + class: RsaSig2018, + valueKey: 'publicKeyPem', + json: { + id: '3', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC X...', + }, + }, + { + class: Ed25119Sig2018, + valueKey: 'publicKeyBase58', + json: { + id: '4', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyBase58: '-----BEGIN PUBLIC X...', + }, + }, + { + class: EddsaSaSigSecp256k1, + valueKey: 'publicKeyHex', + json: { + id: 'did:sov:LjgpST2rjsoxYegQDRm7EL#5', + type: 'Secp256k1VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyHex: '-----BEGIN PUBLIC X...', + }, + }, +] + +describe('Did | PublicKey', () => { + it('should correctly transform Json to PublicKey class', async () => { + const json = { + id: 'did:sov:LjgpST2rjsoxYegQDRm7EL#5', + type: 'RandomType', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + } + + const service = plainToInstance(PublicKey, json) + expect(service.id).toBe(json.id) + expect(service.type).toBe(json.type) + expect(service.controller).toBe(json.controller) + }) + + it('should correctly transform PublicKey class to Json', async () => { + const json = { + id: 'did:sov:LjgpST2rjsoxYegQDRm7EL#5', + type: 'RandomType', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + } + const publicKey = new PublicKey({ + ...json, + }) + const transformed = instanceToPlain(publicKey) + expect(transformed).toEqual(json) + }) + + const publicKeyJsonToClassTests: [string, ClassConstructor, Record, string][] = + publicKeysJson.map((pk) => [pk.class.name, pk.class, pk.json, pk.valueKey]) + test.each(publicKeyJsonToClassTests)( + 'should correctly transform Json to %s class', + async (_, publicKeyClass, json, valueKey) => { + const publicKey = plainToInstance(publicKeyClass, json) + + expect(publicKey.id).toBe(json.id) + expect(publicKey.type).toBe(json.type) + expect(publicKey.controller).toBe(json.controller) + expect(publicKey.value).toBe(json[valueKey]) + } + ) + + const publicKeyClassToJsonTests: [string, PublicKey, Record, string][] = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + publicKeysJson.map((pk) => [pk.class.name, new pk.class({ ...(pk.json as any) }), pk.json, pk.valueKey]) + + test.each(publicKeyClassToJsonTests)( + 'should correctly transform %s class to Json', + async (_, publicKey, json, valueKey) => { + const publicKeyJson = instanceToPlain(publicKey) + + expect(publicKey.value).toBe(json[valueKey]) + expect(publicKeyJson).toMatchObject(json) + } + ) + + describe('PublicKeyTransformer', () => { + class PublicKeyTransformerTest { + @PublicKeyTransformer() + public publicKey: PublicKey[] = [] + } + + it("should transform Json to default PublicKey class when the 'type' key is not present in 'publicKeyTypes'", async () => { + const publicKeyJson = { + id: '3', + type: 'RsaVerificationKey2018--unknown', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC X...', + } + + const publicKeyWrapperJson = { + publicKey: [publicKeyJson], + } + const publicKeyWrapper = plainToInstance(PublicKeyTransformerTest, publicKeyWrapperJson) + + expect(publicKeyWrapper.publicKey.length).toBe(1) + + const firstPublicKey = publicKeyWrapper.publicKey[0] + expect(firstPublicKey).toBeInstanceOf(PublicKey) + expect(firstPublicKey.id).toBe(publicKeyJson.id) + expect(firstPublicKey.type).toBe(publicKeyJson.type) + expect(firstPublicKey.controller).toBe(publicKeyJson.controller) + expect(firstPublicKey.value).toBeUndefined() + }) + + it("should transform Json to corresponding class when the 'type' key is present in 'publicKeyTypes'", async () => { + const publicKeyArray = publicKeysJson.map((pk) => pk.json) + + const publicKeyWrapperJson = { + publicKey: publicKeyArray, + } + const publicKeyWrapper = plainToInstance(PublicKeyTransformerTest, publicKeyWrapperJson) + + expect(publicKeyWrapper.publicKey.length).toBe(publicKeyArray.length) + + for (let i = 0; i < publicKeyArray.length; i++) { + const publicKeyJson = publicKeyArray[i] + const publicKey = publicKeyWrapper.publicKey[i] + + expect(publicKey).toBeInstanceOf(publicKeyTypes[publicKeyJson.type]) + } + }) + }) +}) diff --git a/src/lib/modules/connections/models/did/__tests__/diddoc.json b/packages/core/src/modules/connections/models/did/__tests__/diddoc.json similarity index 76% rename from src/lib/modules/connections/models/did/__tests__/diddoc.json rename to packages/core/src/modules/connections/models/did/__tests__/diddoc.json index 18d3a81dc9..65355743ac 100644 --- a/src/lib/modules/connections/models/did/__tests__/diddoc.json +++ b/packages/core/src/modules/connections/models/did/__tests__/diddoc.json @@ -1,9 +1,9 @@ { - "@context": "https: //w3id.org/did/v1", + "@context": "https://w3id.org/did/v1", "id": "did:sov:LjgpST2rjsoxYegQDRm7EL", "publicKey": [ { - "id": "3", + "id": "#3", "type": "RsaVerificationKey2018", "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", "publicKeyPem": "-----BEGIN PUBLIC X..." @@ -15,7 +15,7 @@ "publicKeyBase58": "-----BEGIN PUBLIC 9..." }, { - "id": "6", + "id": "#6", "type": "Secp256k1VerificationKey2018", "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", "publicKeyHex": "-----BEGIN PUBLIC A..." @@ -28,21 +28,29 @@ "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" }, { - "id": "6", + "id": "#6", "type": "IndyAgent", "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], "priority": 5 + }, + { + "id": "#7", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["DADEajsDSaksLng9h"], + "routingKeys": ["DADEajsDSaksLng9h"], + "priority": 10 } ], "authentication": [ { "type": "RsaSignatureAuthentication2018", - "publicKey": "3" + "publicKey": "#3" }, { - "id": "6", + "id": "#6", "type": "RsaVerificationKey2018", "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", "publicKeyPem": "-----BEGIN PUBLIC A..." diff --git a/packages/core/src/modules/connections/models/did/authentication/Authentication.ts b/packages/core/src/modules/connections/models/did/authentication/Authentication.ts new file mode 100644 index 0000000000..41ff0bc5d5 --- /dev/null +++ b/packages/core/src/modules/connections/models/did/authentication/Authentication.ts @@ -0,0 +1,5 @@ +import type { PublicKey } from '../publicKey/PublicKey' + +export abstract class Authentication { + public abstract publicKey: PublicKey +} diff --git a/packages/core/src/modules/connections/models/did/authentication/EmbeddedAuthentication.ts b/packages/core/src/modules/connections/models/did/authentication/EmbeddedAuthentication.ts new file mode 100644 index 0000000000..12edf9b1b7 --- /dev/null +++ b/packages/core/src/modules/connections/models/did/authentication/EmbeddedAuthentication.ts @@ -0,0 +1,18 @@ +import { IsInstance, IsNotEmpty, ValidateNested } from 'class-validator' + +import { PublicKey } from '../publicKey/PublicKey' + +import { Authentication } from './Authentication' + +export class EmbeddedAuthentication extends Authentication { + @IsNotEmpty() + @ValidateNested() + @IsInstance(PublicKey) + public publicKey!: PublicKey + + public constructor(publicKey: PublicKey) { + super() + + this.publicKey = publicKey + } +} diff --git a/packages/core/src/modules/connections/models/did/authentication/ReferencedAuthentication.ts b/packages/core/src/modules/connections/models/did/authentication/ReferencedAuthentication.ts new file mode 100644 index 0000000000..a4ee64aea8 --- /dev/null +++ b/packages/core/src/modules/connections/models/did/authentication/ReferencedAuthentication.ts @@ -0,0 +1,23 @@ +import { Transform } from 'class-transformer' +import { IsString } from 'class-validator' + +import { PublicKey } from '../publicKey/PublicKey' + +import { Authentication } from './Authentication' + +export class ReferencedAuthentication extends Authentication { + public constructor(publicKey: PublicKey, type: string) { + super() + + this.publicKey = publicKey + this.type = type + } + + @IsString() + public type!: string + + @Transform(({ value }: { value: PublicKey }) => value.id, { + toPlainOnly: true, + }) + public publicKey!: PublicKey +} diff --git a/packages/core/src/modules/connections/models/did/authentication/index.ts b/packages/core/src/modules/connections/models/did/authentication/index.ts new file mode 100644 index 0000000000..989e4d12ef --- /dev/null +++ b/packages/core/src/modules/connections/models/did/authentication/index.ts @@ -0,0 +1,69 @@ +import type { ClassConstructor } from 'class-transformer' + +import { Transform, TransformationType, plainToInstance, instanceToPlain } from 'class-transformer' + +import { CredoError } from '../../../../../error' +import { PublicKey, publicKeyTypes } from '../publicKey' + +import { Authentication } from './Authentication' +import { EmbeddedAuthentication } from './EmbeddedAuthentication' +import { ReferencedAuthentication } from './ReferencedAuthentication' + +export const authenticationTypes = { + RsaVerificationKey2018: 'RsaSignatureAuthentication2018', + Ed25519VerificationKey2018: 'Ed25519SignatureAuthentication2018', + Secp256k1VerificationKey2018: 'Secp256k1SignatureAuthenticationKey2018', +} + +/** + * Decorator that transforms authentication json to corresponding class instances. See {@link authenticationTypes} + * + * @example + * class Example { + * AuthenticationTransformer() + * private authentication: Authentication + * } + */ +export function AuthenticationTransformer() { + return Transform( + ({ + value, + obj, + type, + }: { + value: { type: string; publicKey?: string | PublicKey }[] + obj: { publicKey: { id: string; type: string }[] } + type: TransformationType + }) => { + // TODO: PLAIN_TO_PLAIN + + if (type === TransformationType.PLAIN_TO_CLASS) { + return value.map((auth) => { + // referenced public key + if (auth.publicKey) { + //referenced + const publicKeyJson = obj.publicKey.find((publicKey) => publicKey.id === auth.publicKey) + + if (!publicKeyJson) { + throw new CredoError(`Invalid public key referenced ${auth.publicKey}`) + } + + // Referenced keys use other types than embedded keys. + const publicKeyClass = (publicKeyTypes[publicKeyJson.type] ?? PublicKey) as ClassConstructor + const publicKey = plainToInstance(publicKeyClass, publicKeyJson) + return new ReferencedAuthentication(publicKey, auth.type) + } else { + // embedded + const publicKeyClass = (publicKeyTypes[auth.type] ?? PublicKey) as ClassConstructor + const publicKey = plainToInstance(publicKeyClass, auth) + return new EmbeddedAuthentication(publicKey) + } + }) + } else { + return value.map((auth) => (auth instanceof EmbeddedAuthentication ? instanceToPlain(auth.publicKey) : auth)) + } + } + ) +} + +export { Authentication, EmbeddedAuthentication, ReferencedAuthentication } diff --git a/packages/core/src/modules/connections/models/did/index.ts b/packages/core/src/modules/connections/models/did/index.ts new file mode 100644 index 0000000000..f832050609 --- /dev/null +++ b/packages/core/src/modules/connections/models/did/index.ts @@ -0,0 +1,3 @@ +export * from './DidDoc' +export * from './publicKey' +export * from './authentication' diff --git a/packages/core/src/modules/connections/models/did/publicKey/Ed25119Sig2018.ts b/packages/core/src/modules/connections/models/did/publicKey/Ed25119Sig2018.ts new file mode 100644 index 0000000000..7077a5ac25 --- /dev/null +++ b/packages/core/src/modules/connections/models/did/publicKey/Ed25119Sig2018.ts @@ -0,0 +1,21 @@ +import { Expose } from 'class-transformer' +import { Equals, IsString } from 'class-validator' + +import { PublicKey } from './PublicKey' + +export class Ed25119Sig2018 extends PublicKey { + public constructor(options: { id: string; controller: string; publicKeyBase58: string }) { + super({ ...options, type: 'Ed25519VerificationKey2018' }) + + if (options) { + this.value = options.publicKeyBase58 + } + } + + @Equals('Ed25519VerificationKey2018') + public type = 'Ed25519VerificationKey2018' as const + + @Expose({ name: 'publicKeyBase58' }) + @IsString() + public value!: string +} diff --git a/packages/core/src/modules/connections/models/did/publicKey/EddsaSaSigSecp256k1.ts b/packages/core/src/modules/connections/models/did/publicKey/EddsaSaSigSecp256k1.ts new file mode 100644 index 0000000000..f9c2eee646 --- /dev/null +++ b/packages/core/src/modules/connections/models/did/publicKey/EddsaSaSigSecp256k1.ts @@ -0,0 +1,21 @@ +import { Expose } from 'class-transformer' +import { Equals, IsString } from 'class-validator' + +import { PublicKey } from './PublicKey' + +export class EddsaSaSigSecp256k1 extends PublicKey { + public constructor(options: { id: string; controller: string; publicKeyHex: string }) { + super({ ...options, type: 'Secp256k1VerificationKey2018' }) + + if (options) { + this.value = options.publicKeyHex + } + } + + @Equals('Secp256k1VerificationKey2018') + public type = 'Secp256k1VerificationKey2018' as const + + @Expose({ name: 'publicKeyHex' }) + @IsString() + public value!: string +} diff --git a/packages/core/src/modules/connections/models/did/publicKey/PublicKey.ts b/packages/core/src/modules/connections/models/did/publicKey/PublicKey.ts new file mode 100644 index 0000000000..43ecfcc05f --- /dev/null +++ b/packages/core/src/modules/connections/models/did/publicKey/PublicKey.ts @@ -0,0 +1,25 @@ +import { IsOptional, IsString } from 'class-validator' + +export class PublicKey { + public constructor(options: { id: string; controller: string; type: string; value?: string }) { + if (options) { + this.id = options.id + this.controller = options.controller + this.type = options.type + this.value = options.value + } + } + + @IsString() + public id!: string + + @IsString() + public controller!: string + + @IsString() + public type!: string + + @IsString() + @IsOptional() + public value?: string +} diff --git a/packages/core/src/modules/connections/models/did/publicKey/RsaSig2018.ts b/packages/core/src/modules/connections/models/did/publicKey/RsaSig2018.ts new file mode 100644 index 0000000000..fdc106b443 --- /dev/null +++ b/packages/core/src/modules/connections/models/did/publicKey/RsaSig2018.ts @@ -0,0 +1,21 @@ +import { Expose } from 'class-transformer' +import { Equals, IsString } from 'class-validator' + +import { PublicKey } from './PublicKey' + +export class RsaSig2018 extends PublicKey { + public constructor(options: { id: string; controller: string; publicKeyPem: string }) { + super({ ...options, type: 'RsaVerificationKey2018' }) + + if (options) { + this.value = options.publicKeyPem + } + } + + @Equals('RsaVerificationKey2018') + public type = 'RsaVerificationKey2018' as const + + @Expose({ name: 'publicKeyPem' }) + @IsString() + public value!: string +} diff --git a/packages/core/src/modules/connections/models/did/publicKey/index.ts b/packages/core/src/modules/connections/models/did/publicKey/index.ts new file mode 100644 index 0000000000..72a2105e7e --- /dev/null +++ b/packages/core/src/modules/connections/models/did/publicKey/index.ts @@ -0,0 +1,41 @@ +import type { ClassConstructor } from 'class-transformer' + +import { Transform, plainToInstance } from 'class-transformer' + +import { Ed25119Sig2018 } from './Ed25119Sig2018' +import { EddsaSaSigSecp256k1 } from './EddsaSaSigSecp256k1' +import { PublicKey } from './PublicKey' +import { RsaSig2018 } from './RsaSig2018' + +export const publicKeyTypes: { [key: string]: unknown | undefined } = { + RsaVerificationKey2018: RsaSig2018, + Ed25519VerificationKey2018: Ed25119Sig2018, + Secp256k1VerificationKey2018: EddsaSaSigSecp256k1, +} + +/** + * Decorator that transforms public key json to corresonding class instances. See {@link publicKeyTypes} + * + * @example + * class Example { + * @PublicKeyTransformer() + * private publicKey: PublicKey + * } + */ +export function PublicKeyTransformer() { + return Transform( + ({ value }: { value: { type: string }[] }) => { + return value.map((publicKeyJson) => { + const publicKeyClass = (publicKeyTypes[publicKeyJson.type] ?? PublicKey) as ClassConstructor + const publicKey = plainToInstance(publicKeyClass, publicKeyJson) + + return publicKey + }) + }, + { + toClassOnly: true, + } + ) +} + +export { Ed25119Sig2018, PublicKey, EddsaSaSigSecp256k1, RsaSig2018 } diff --git a/packages/core/src/modules/connections/models/index.ts b/packages/core/src/modules/connections/models/index.ts new file mode 100644 index 0000000000..72a4635768 --- /dev/null +++ b/packages/core/src/modules/connections/models/index.ts @@ -0,0 +1,9 @@ +export * from './Connection' +export * from './ConnectionRole' +export * from './ConnectionState' +export * from './DidExchangeState' +export * from './DidExchangeRole' +export * from './DidRotateRole' +export * from './HandshakeProtocol' +export * from './did' +export * from './ConnectionType' diff --git a/packages/core/src/modules/connections/repository/ConnectionMetadataTypes.ts b/packages/core/src/modules/connections/repository/ConnectionMetadataTypes.ts new file mode 100644 index 0000000000..f16ea2d043 --- /dev/null +++ b/packages/core/src/modules/connections/repository/ConnectionMetadataTypes.ts @@ -0,0 +1,15 @@ +export enum ConnectionMetadataKeys { + UseDidKeysForProtocol = '_internal/useDidKeysForProtocol', + DidRotate = '_internal/didRotate', +} + +export type ConnectionMetadata = { + [ConnectionMetadataKeys.UseDidKeysForProtocol]: { + [protocolUri: string]: boolean + } + [ConnectionMetadataKeys.DidRotate]: { + did: string + threadId: string + mediatorId?: string + } +} diff --git a/packages/core/src/modules/connections/repository/ConnectionRecord.ts b/packages/core/src/modules/connections/repository/ConnectionRecord.ts new file mode 100644 index 0000000000..beb7154be4 --- /dev/null +++ b/packages/core/src/modules/connections/repository/ConnectionRecord.ts @@ -0,0 +1,170 @@ +import type { ConnectionMetadata } from './ConnectionMetadataTypes' +import type { TagsBase } from '../../../storage/BaseRecord' +import type { ConnectionType } from '../models/ConnectionType' + +import { Transform } from 'class-transformer' + +import { CredoError } from '../../../error' +import { BaseRecord } from '../../../storage/BaseRecord' +import { uuid } from '../../../utils/uuid' +import { rfc0160StateFromDidExchangeState, DidExchangeRole, DidExchangeState, HandshakeProtocol } from '../models' + +export interface ConnectionRecordProps { + id?: string + createdAt?: Date + did?: string + theirDid?: string + theirLabel?: string + state: DidExchangeState + role: DidExchangeRole + alias?: string + autoAcceptConnection?: boolean + threadId?: string + tags?: CustomConnectionTags + imageUrl?: string + mediatorId?: string + errorMessage?: string + protocol?: HandshakeProtocol + outOfBandId?: string + invitationDid?: string + connectionTypes?: Array + previousDids?: Array + previousTheirDids?: Array +} + +export type CustomConnectionTags = TagsBase +export type DefaultConnectionTags = { + state: DidExchangeState + role: DidExchangeRole + threadId?: string + mediatorId?: string + did?: string + theirDid?: string + outOfBandId?: string + invitationDid?: string + connectionTypes?: Array + previousDids?: Array + previousTheirDids?: Array +} + +export class ConnectionRecord extends BaseRecord { + public state!: DidExchangeState + public role!: DidExchangeRole + + public did?: string + + public theirDid?: string + public theirLabel?: string + + public alias?: string + public autoAcceptConnection?: boolean + public imageUrl?: string + + public threadId?: string + public mediatorId?: string + public errorMessage?: string + + // We used to store connection record using major.minor version, but we now + // only store the major version, storing .x for the minor version. We have this + // transformation so we don't have to migrate the data in the database. + @Transform( + ({ value }) => { + if (!value || typeof value !== 'string' || value.endsWith('.x')) return value + return value.split('.').slice(0, -1).join('.') + '.x' + }, + + { toClassOnly: true } + ) + public protocol?: HandshakeProtocol + public outOfBandId?: string + public invitationDid?: string + + public connectionTypes: string[] = [] + public previousDids: string[] = [] + public previousTheirDids: string[] = [] + + public static readonly type = 'ConnectionRecord' + public readonly type = ConnectionRecord.type + + public constructor(props: ConnectionRecordProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.did = props.did + this.invitationDid = props.invitationDid + this.theirDid = props.theirDid + this.theirLabel = props.theirLabel + this.state = props.state + this.role = props.role + this.alias = props.alias + this.autoAcceptConnection = props.autoAcceptConnection + this._tags = props.tags ?? {} + this.threadId = props.threadId + this.imageUrl = props.imageUrl + this.mediatorId = props.mediatorId + this.errorMessage = props.errorMessage + this.protocol = props.protocol + this.outOfBandId = props.outOfBandId + this.connectionTypes = props.connectionTypes ?? [] + this.previousDids = props.previousDids ?? [] + this.previousTheirDids = props.previousTheirDids ?? [] + } + } + + public getTags(): DefaultConnectionTags & CustomConnectionTags { + return { + ...this._tags, + state: this.state, + role: this.role, + threadId: this.threadId, + mediatorId: this.mediatorId, + did: this.did, + theirDid: this.theirDid, + outOfBandId: this.outOfBandId, + invitationDid: this.invitationDid, + connectionTypes: this.connectionTypes, + previousDids: this.previousDids, + previousTheirDids: this.previousTheirDids, + } + } + + public get isRequester() { + return this.role === DidExchangeRole.Requester + } + + public get rfc0160State() { + return rfc0160StateFromDidExchangeState(this.state) + } + + public get isReady() { + return this.state && [DidExchangeState.Completed, DidExchangeState.ResponseSent].includes(this.state) + } + + public assertReady() { + if (!this.isReady) { + throw new CredoError( + `Connection record is not ready to be used. Expected ${DidExchangeState.ResponseSent}, ${DidExchangeState.ResponseReceived} or ${DidExchangeState.Completed}, found invalid state ${this.state}` + ) + } + } + + public assertState(expectedStates: DidExchangeState | DidExchangeState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new CredoError( + `Connection record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.` + ) + } + } + + public assertRole(expectedRole: DidExchangeRole) { + if (this.role !== expectedRole) { + throw new CredoError(`Connection record has invalid role ${this.role}. Expected role ${expectedRole}.`) + } + } +} diff --git a/packages/core/src/modules/connections/repository/ConnectionRepository.ts b/packages/core/src/modules/connections/repository/ConnectionRepository.ts new file mode 100644 index 0000000000..158c75e7a8 --- /dev/null +++ b/packages/core/src/modules/connections/repository/ConnectionRepository.ts @@ -0,0 +1,45 @@ +import type { AgentContext } from '../../../agent' +import type { DidExchangeRole } from '../models' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { injectable, inject } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { ConnectionRecord } from './ConnectionRecord' + +@injectable() +export class ConnectionRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(ConnectionRecord, storageService, eventEmitter) + } + + public async findByDids(agentContext: AgentContext, { ourDid, theirDid }: { ourDid: string; theirDid: string }) { + return this.findSingleByQuery(agentContext, { + $or: [ + { + did: ourDid, + theirDid, + }, + { did: ourDid, previousTheirDids: [theirDid] }, + { previousDids: [ourDid], theirDid }, + ], + }) + } + + public getByThreadId(agentContext: AgentContext, threadId: string): Promise { + return this.getSingleByQuery(agentContext, { threadId }) + } + + public getByRoleAndThreadId( + agentContext: AgentContext, + role: DidExchangeRole, + threadId: string + ): Promise { + return this.getSingleByQuery(agentContext, { threadId, role }) + } +} diff --git a/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts b/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts new file mode 100644 index 0000000000..a2be2ebf53 --- /dev/null +++ b/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts @@ -0,0 +1,56 @@ +import { JsonTransformer } from '../../../../utils' +import { DidExchangeRole, DidExchangeState, HandshakeProtocol } from '../../models' +import { ConnectionRecord } from '../ConnectionRecord' + +describe('ConnectionRecord', () => { + describe('getTags', () => { + it('should return default tags', () => { + const connectionRecord = new ConnectionRecord({ + state: DidExchangeState.Completed, + role: DidExchangeRole.Requester, + threadId: 'a-thread-id', + mediatorId: 'a-mediator-id', + did: 'a-did', + theirDid: 'a-their-did', + outOfBandId: 'a-out-of-band-id', + invitationDid: 'a-invitation-did', + }) + + expect(connectionRecord.getTags()).toEqual({ + state: DidExchangeState.Completed, + role: DidExchangeRole.Requester, + threadId: 'a-thread-id', + mediatorId: 'a-mediator-id', + did: 'a-did', + theirDid: 'a-their-did', + outOfBandId: 'a-out-of-band-id', + invitationDid: 'a-invitation-did', + connectionTypes: [], + previousDids: [], + previousTheirDids: [], + }) + }) + }) + + it('should transform handshake protocol with minor version to .x', () => { + const connectionRecord = JsonTransformer.fromJSON( + { + protocol: 'https://didcomm.org/didexchange/1.0', + }, + ConnectionRecord + ) + + expect(connectionRecord.protocol).toEqual(HandshakeProtocol.DidExchange) + }) + + it('should not transform handshake protocol when minor version is .x', () => { + const connectionRecord = JsonTransformer.fromJSON( + { + protocol: 'https://didcomm.org/didexchange/1.x', + }, + ConnectionRecord + ) + + expect(connectionRecord.protocol).toEqual(HandshakeProtocol.DidExchange) + }) +}) diff --git a/packages/core/src/modules/connections/repository/index.ts b/packages/core/src/modules/connections/repository/index.ts new file mode 100644 index 0000000000..6e5db358af --- /dev/null +++ b/packages/core/src/modules/connections/repository/index.ts @@ -0,0 +1,2 @@ +export * from './ConnectionRecord' +export * from './ConnectionRepository' diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts new file mode 100644 index 0000000000..8979489ee0 --- /dev/null +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -0,0 +1,950 @@ +import type { AgentContext } from '../../../agent' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { Query, QueryOptions } from '../../../storage/StorageService' +import type { AckMessage } from '../../common' +import type { OutOfBandDidCommService } from '../../oob/domain/OutOfBandDidCommService' +import type { OutOfBandRecord } from '../../oob/repository' +import type { ConnectionStateChangedEvent } from '../ConnectionEvents' +import type { ConnectionProblemReportMessage } from '../messages' +import type { ConnectionType } from '../models' +import type { ConnectionRecordProps } from '../repository/ConnectionRecord' + +import { firstValueFrom, ReplaySubject } from 'rxjs' +import { first, map, timeout } from 'rxjs/operators' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { filterContextCorrelationId } from '../../../agent/Events' +import { InjectionSymbols } from '../../../constants' +import { Key } from '../../../crypto' +import { signData, unpackAndVerifySignatureDecorator } from '../../../decorators/signature/SignatureDecoratorUtils' +import { CredoError } from '../../../error' +import { Logger } from '../../../logger' +import { inject, injectable } from '../../../plugins' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { indyDidFromPublicKeyBase58 } from '../../../utils/did' +import { DidKey, IndyAgentService } from '../../dids' +import { DidDocumentRole } from '../../dids/domain/DidDocumentRole' +import { didKeyToVerkey } from '../../dids/helpers' +import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' +import { DidRecord, DidRepository } from '../../dids/repository' +import { DidRecordMetadataKeys } from '../../dids/repository/didRecordMetadataTypes' +import { OutOfBandService } from '../../oob/OutOfBandService' +import { OutOfBandRole } from '../../oob/domain/OutOfBandRole' +import { OutOfBandState } from '../../oob/domain/OutOfBandState' +import { InvitationType } from '../../oob/messages' +import { OutOfBandRepository } from '../../oob/repository' +import { OutOfBandRecordMetadataKeys } from '../../oob/repository/outOfBandRecordMetadataTypes' +import { ConnectionEventTypes } from '../ConnectionEvents' +import { ConnectionProblemReportError, ConnectionProblemReportReason } from '../errors' +import { ConnectionRequestMessage, ConnectionResponseMessage, TrustPingMessage } from '../messages' +import { + authenticationTypes, + Connection, + DidDoc, + DidExchangeRole, + DidExchangeState, + Ed25119Sig2018, + HandshakeProtocol, + ReferencedAuthentication, +} from '../models' +import { ConnectionRecord } from '../repository/ConnectionRecord' +import { ConnectionRepository } from '../repository/ConnectionRepository' + +import { assertNoCreatedDidExistsForKeys, convertToNewDidDocument } from './helpers' + +export interface ConnectionRequestParams { + label?: string + imageUrl?: string + alias?: string + routing: Routing + autoAcceptConnection?: boolean +} + +@injectable() +export class ConnectionService { + private connectionRepository: ConnectionRepository + private didRepository: DidRepository + private eventEmitter: EventEmitter + private logger: Logger + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + connectionRepository: ConnectionRepository, + didRepository: DidRepository, + eventEmitter: EventEmitter + ) { + this.connectionRepository = connectionRepository + this.didRepository = didRepository + this.eventEmitter = eventEmitter + this.logger = logger + } + + /** + * Create a connection request message for a given out-of-band. + * + * @param outOfBandRecord out-of-band record for which to create a connection request + * @param config config for creation of connection request + * @returns outbound message containing connection request + */ + public async createRequest( + agentContext: AgentContext, + outOfBandRecord: OutOfBandRecord, + config: ConnectionRequestParams + ): Promise> { + this.logger.debug(`Create message ${ConnectionRequestMessage.type.messageTypeUri} start`, outOfBandRecord) + outOfBandRecord.assertRole(OutOfBandRole.Receiver) + outOfBandRecord.assertState(OutOfBandState.PrepareResponse) + + // TODO check there is no connection record for particular oob record + + const { outOfBandInvitation } = outOfBandRecord + + const { mediatorId } = config.routing + const didDoc = this.createDidDoc(config.routing) + + // TODO: We should store only one did that we'll use to send the request message with success. + // We take just the first one for now. + const [invitationDid] = outOfBandInvitation.invitationDids + + const { did: peerDid } = await this.createDid(agentContext, { + role: DidDocumentRole.Created, + didDoc, + }) + + const { label, imageUrl } = config + const connectionRequest = new ConnectionRequestMessage({ + label: label ?? agentContext.config.label, + did: didDoc.id, + didDoc, + imageUrl: imageUrl ?? agentContext.config.connectionImageUrl, + }) + + connectionRequest.setThread({ + threadId: connectionRequest.threadId, + parentThreadId: outOfBandRecord.outOfBandInvitation.id, + }) + + const connectionRecord = await this.createConnection(agentContext, { + protocol: HandshakeProtocol.Connections, + role: DidExchangeRole.Requester, + state: DidExchangeState.InvitationReceived, + theirLabel: outOfBandInvitation.label, + alias: config?.alias, + did: peerDid, + mediatorId, + autoAcceptConnection: config?.autoAcceptConnection, + outOfBandId: outOfBandRecord.id, + invitationDid, + imageUrl: outOfBandInvitation.imageUrl, + threadId: connectionRequest.threadId, + }) + + await this.updateState(agentContext, connectionRecord, DidExchangeState.RequestSent) + + return { + connectionRecord, + message: connectionRequest, + } + } + + public async processRequest( + messageContext: InboundMessageContext, + outOfBandRecord: OutOfBandRecord + ): Promise { + this.logger.debug(`Process message ${ConnectionRequestMessage.type.messageTypeUri} start`, { + message: messageContext.message, + }) + outOfBandRecord.assertRole(OutOfBandRole.Sender) + outOfBandRecord.assertState(OutOfBandState.AwaitResponse) + + // TODO check there is no connection record for particular oob record + + const { message } = messageContext + if (!message.connection.didDoc) { + throw new ConnectionProblemReportError('Public DIDs are not supported yet', { + problemCode: ConnectionProblemReportReason.RequestNotAccepted, + }) + } + + const { did: peerDid } = await this.createDid(messageContext.agentContext, { + role: DidDocumentRole.Received, + didDoc: message.connection.didDoc, + }) + + const connectionRecord = await this.createConnection(messageContext.agentContext, { + protocol: HandshakeProtocol.Connections, + role: DidExchangeRole.Responder, + state: DidExchangeState.RequestReceived, + alias: outOfBandRecord.alias, + theirLabel: message.label, + imageUrl: message.imageUrl, + outOfBandId: outOfBandRecord.id, + theirDid: peerDid, + threadId: message.threadId, + mediatorId: outOfBandRecord.mediatorId, + autoAcceptConnection: outOfBandRecord.autoAcceptConnection, + }) + + await this.connectionRepository.update(messageContext.agentContext, connectionRecord) + this.emitStateChangedEvent(messageContext.agentContext, connectionRecord, null) + + this.logger.debug(`Process message ${ConnectionRequestMessage.type.messageTypeUri} end`, connectionRecord) + return connectionRecord + } + + /** + * Create a connection response message for the connection with the specified connection id. + * + * @param connectionRecord the connection for which to create a connection response + * @returns outbound message containing connection response + */ + public async createResponse( + agentContext: AgentContext, + connectionRecord: ConnectionRecord, + outOfBandRecord: OutOfBandRecord, + routing?: Routing + ): Promise> { + this.logger.debug(`Create message ${ConnectionResponseMessage.type.messageTypeUri} start`, connectionRecord) + connectionRecord.assertState(DidExchangeState.RequestReceived) + connectionRecord.assertRole(DidExchangeRole.Responder) + + let didDoc: DidDoc + if (routing) { + didDoc = this.createDidDoc(routing) + } else if (outOfBandRecord.outOfBandInvitation.getInlineServices().length > 0) { + didDoc = this.createDidDocFromOutOfBandDidCommServices(outOfBandRecord.outOfBandInvitation.getInlineServices()) + } else { + // We don't support using a did from the OOB invitation services currently, in this case we always pass routing to this method + throw new CredoError( + 'No routing provided, and no inline services found in out of band invitation. When using did services in out of band invitation, make sure to provide routing information for rotation.' + ) + } + + const { did: peerDid } = await this.createDid(agentContext, { + role: DidDocumentRole.Created, + didDoc, + }) + + const connection = new Connection({ + did: didDoc.id, + didDoc, + }) + + const connectionJson = JsonTransformer.toJSON(connection) + + if (!connectionRecord.threadId) { + throw new CredoError(`Connection record with id ${connectionRecord.id} does not have a thread id`) + } + + const signingKey = Key.fromFingerprint(outOfBandRecord.getTags().recipientKeyFingerprints[0]).publicKeyBase58 + + const connectionResponse = new ConnectionResponseMessage({ + threadId: connectionRecord.threadId, + connectionSig: await signData(connectionJson, agentContext.wallet, signingKey), + }) + + connectionRecord.did = peerDid + await this.updateState(agentContext, connectionRecord, DidExchangeState.ResponseSent) + + this.logger.debug(`Create message ${ConnectionResponseMessage.type.messageTypeUri} end`, { + connectionRecord, + message: connectionResponse, + }) + return { + connectionRecord, + message: connectionResponse, + } + } + + /** + * Process a received connection response message. This will not accept the connection request + * or send a connection acknowledgement message. It will only update the existing connection record + * with all the new information from the connection response message. Use {@link ConnectionService.createTrustPing} + * after calling this function to create a trust ping message. + * + * @param messageContext the message context containing a connection response message + * @returns updated connection record + */ + public async processResponse( + messageContext: InboundMessageContext, + outOfBandRecord: OutOfBandRecord + ): Promise { + this.logger.debug(`Process message ${ConnectionResponseMessage.type.messageTypeUri} start`, { + message: messageContext.message, + }) + const { connection: connectionRecord, message, recipientKey, senderKey } = messageContext + + if (!recipientKey || !senderKey) { + throw new CredoError('Unable to process connection request without senderKey or recipientKey') + } + + if (!connectionRecord) { + throw new CredoError('No connection record in message context.') + } + + connectionRecord.assertState(DidExchangeState.RequestSent) + connectionRecord.assertRole(DidExchangeRole.Requester) + + let connectionJson = null + try { + connectionJson = await unpackAndVerifySignatureDecorator( + message.connectionSig, + messageContext.agentContext.wallet + ) + } catch (error) { + if (error instanceof CredoError) { + throw new ConnectionProblemReportError(error.message, { + problemCode: ConnectionProblemReportReason.ResponseProcessingError, + }) + } + throw error + } + + const connection = JsonTransformer.fromJSON(connectionJson, Connection) + + // Per the Connection RFC we must check if the key used to sign the connection~sig is the same key + // as the recipient key(s) in the connection invitation message + const signerVerkey = message.connectionSig.signer + + const invitationKey = Key.fromFingerprint(outOfBandRecord.getTags().recipientKeyFingerprints[0]).publicKeyBase58 + + if (signerVerkey !== invitationKey) { + throw new ConnectionProblemReportError( + `Connection object in connection response message is not signed with same key as recipient key in invitation expected='${invitationKey}' received='${signerVerkey}'`, + { problemCode: ConnectionProblemReportReason.ResponseNotAccepted } + ) + } + + if (!connection.didDoc) { + throw new CredoError('DID Document is missing.') + } + + const { did: peerDid } = await this.createDid(messageContext.agentContext, { + role: DidDocumentRole.Received, + didDoc: connection.didDoc, + }) + + connectionRecord.theirDid = peerDid + connectionRecord.threadId = message.threadId + + await this.updateState(messageContext.agentContext, connectionRecord, DidExchangeState.ResponseReceived) + return connectionRecord + } + + /** + * Create a trust ping message for the connection with the specified connection id. + * + * By default a trust ping message should elicit a response. If this is not desired the + * `config.responseRequested` property can be set to `false`. + * + * @param connectionRecord the connection for which to create a trust ping message + * @param config the config for the trust ping message + * @returns outbound message containing trust ping message + */ + public async createTrustPing( + agentContext: AgentContext, + connectionRecord: ConnectionRecord, + config: { responseRequested?: boolean; comment?: string } = {} + ): Promise> { + connectionRecord.assertState([DidExchangeState.ResponseReceived, DidExchangeState.Completed]) + + // TODO: + // - create ack message + // - maybe this shouldn't be in the connection service? + const trustPing = new TrustPingMessage(config) + + // Only update connection record and emit an event if the state is not already 'Complete' + if (connectionRecord.state !== DidExchangeState.Completed) { + await this.updateState(agentContext, connectionRecord, DidExchangeState.Completed) + } + + return { + connectionRecord, + message: trustPing, + } + } + + /** + * Process a received ack message. This will update the state of the connection + * to Completed if this is not already the case. + * + * @param messageContext the message context containing an ack message + * @returns updated connection record + */ + public async processAck(messageContext: InboundMessageContext): Promise { + const { connection, recipientKey } = messageContext + + if (!connection) { + throw new CredoError( + `Unable to process connection ack: connection for recipient key ${recipientKey?.fingerprint} not found` + ) + } + + // TODO: This is better addressed in a middleware of some kind because + // any message can transition the state to complete, not just an ack or trust ping + if (connection.state === DidExchangeState.ResponseSent && connection.role === DidExchangeRole.Responder) { + await this.updateState(messageContext.agentContext, connection, DidExchangeState.Completed) + } + + return connection + } + + /** + * Process a received {@link ProblemReportMessage}. + * + * @param messageContext The message context containing a connection problem report message + * @returns connection record associated with the connection problem report message + * + */ + public async processProblemReport( + messageContext: InboundMessageContext + ): Promise { + const { message: connectionProblemReportMessage, recipientKey, senderKey } = messageContext + + this.logger.debug(`Processing connection problem report for verkey ${recipientKey?.fingerprint}`) + + if (!recipientKey) { + throw new CredoError('Unable to process connection problem report without recipientKey') + } + + const ourDidRecord = await this.didRepository.findCreatedDidByRecipientKey( + messageContext.agentContext, + recipientKey + ) + if (!ourDidRecord) { + throw new CredoError( + `Unable to process connection problem report: created did record for recipient key ${recipientKey.fingerprint} not found` + ) + } + + const connectionRecord = await this.findByOurDid(messageContext.agentContext, ourDidRecord.did) + if (!connectionRecord) { + throw new CredoError( + `Unable to process connection problem report: connection for recipient key ${recipientKey.fingerprint} not found` + ) + } + + const theirDidRecord = + connectionRecord.theirDid && + (await this.didRepository.findReceivedDid(messageContext.agentContext, connectionRecord.theirDid)) + if (!theirDidRecord) { + throw new CredoError(`Received did record for did ${connectionRecord.theirDid} not found.`) + } + + if (senderKey) { + if (!theirDidRecord?.getTags().recipientKeyFingerprints?.includes(senderKey.fingerprint)) { + throw new CredoError("Sender key doesn't match key of connection record") + } + } + + connectionRecord.errorMessage = `${connectionProblemReportMessage.description.code} : ${connectionProblemReportMessage.description.en}` + await this.update(messageContext.agentContext, connectionRecord) + + // Marking connection as abandoned in case of problem report from issuer agent + // TODO: Can be conditionally abandoned - Like if another user is scanning already used connection invite where issuer will send invite-already-used problem code. + await this.updateState(messageContext.agentContext, connectionRecord, DidExchangeState.Abandoned) + + return connectionRecord + } + + /** + * Assert that an inbound message either has a connection associated with it, + * or has everything correctly set up for connection-less exchange (optionally with out of band) + * + * @param messageContext - the inbound message context + */ + public async assertConnectionOrOutOfBandExchange( + messageContext: InboundMessageContext, + { + lastSentMessage, + lastReceivedMessage, + expectedConnectionId, + }: { + lastSentMessage?: AgentMessage | null + lastReceivedMessage?: AgentMessage | null + expectedConnectionId?: string + } = {} + ) { + const { connection, message } = messageContext + + if (expectedConnectionId && !connection) { + throw new CredoError( + `Expected incoming message to be from connection ${expectedConnectionId} but no connection found.` + ) + } + if (expectedConnectionId && connection?.id !== expectedConnectionId) { + throw new CredoError( + `Expected incoming message to be from connection ${expectedConnectionId} but connection is ${connection?.id}.` + ) + } + + // Check if we have a ready connection. Verification is already done somewhere else. Return + if (connection) { + connection.assertReady() + this.logger.debug(`Processing message with id ${message.id} and connection id ${connection.id}`, { + type: message.type, + }) + } else { + this.logger.debug(`Processing connection-less message with id ${message.id}`, { + type: message.type, + }) + + const recipientKey = messageContext.recipientKey && messageContext.recipientKey.publicKeyBase58 + const senderKey = messageContext.senderKey && messageContext.senderKey.publicKeyBase58 + + // set theirService to the value of lastReceivedMessage.service + let theirService = + messageContext.message?.service?.resolvedDidCommService ?? lastReceivedMessage?.service?.resolvedDidCommService + let ourService = lastSentMessage?.service?.resolvedDidCommService + + // 1. check if there's an oob record associated. + const outOfBandRepository = messageContext.agentContext.dependencyManager.resolve(OutOfBandRepository) + const outOfBandService = messageContext.agentContext.dependencyManager.resolve(OutOfBandService) + const outOfBandRecord = await outOfBandRepository.findSingleByQuery(messageContext.agentContext, { + invitationRequestsThreadIds: [message.threadId], + }) + + // If we have an out of band record, we can extract the service for our/the other party from the oob record + if (outOfBandRecord?.role === OutOfBandRole.Sender) { + ourService = await outOfBandService.getResolvedServiceForOutOfBandServices( + messageContext.agentContext, + outOfBandRecord.outOfBandInvitation.getServices() + ) + } else if (outOfBandRecord?.role === OutOfBandRole.Receiver) { + theirService = await outOfBandService.getResolvedServiceForOutOfBandServices( + messageContext.agentContext, + outOfBandRecord.outOfBandInvitation.getServices() + ) + } + + // theirService can be null when we receive an oob invitation and process the message. + // In this case there MUST be an oob record, otherwise there is no way for us to reply + // to the message + if (!theirService && !outOfBandRecord) { + throw new CredoError( + 'No service for incoming connection-less message and no associated out of band record found.' + ) + } + + // ourService can be null when we receive an oob invitation or legacy connectionless message and process the message. + // In this case lastSentMessage and lastReceivedMessage MUST be null, because there shouldn't be any previous exchange + if (!ourService && (lastReceivedMessage || lastSentMessage)) { + throw new CredoError( + 'No keys on our side to use for encrypting messages, and previous messages found (in which case our keys MUST also be present).' + ) + } + + // If the message is unpacked or AuthCrypt, there cannot be any previous exchange (this must be the first message). + // All exchange after the first unpacked oob exchange MUST be encrypted. + if ((!senderKey || !recipientKey) && (lastSentMessage || lastReceivedMessage)) { + throw new CredoError( + 'Incoming message must have recipientKey and senderKey (so cannot be AuthCrypt or unpacked) if there are lastSentMessage or lastReceivedMessage.' + ) + } + + // Check if recipientKey is in ourService + if (recipientKey && ourService) { + const recipientKeyFound = ourService.recipientKeys.some((key) => key.publicKeyBase58 === recipientKey) + if (!recipientKeyFound) { + throw new CredoError(`Recipient key ${recipientKey} not found in our service`) + } + } + + // Check if senderKey is in theirService + if (senderKey && theirService) { + const senderKeyFound = theirService.recipientKeys.some((key) => key.publicKeyBase58 === senderKey) + if (!senderKeyFound) { + throw new CredoError(`Sender key ${senderKey} not found in their service.`) + } + } + } + } + + /** + * If knownConnectionId is passed, it will compare the incoming connection id with the knownConnectionId, and skip the other validation. + * + * If no known connection id is passed, it asserts that the incoming message is in response to an attached request message to an out of band invitation. + * If is the case, and the state of the out of band record is still await response, the state will be updated to done + * + */ + public async matchIncomingMessageToRequestMessageInOutOfBandExchange( + messageContext: InboundMessageContext, + { expectedConnectionId }: { expectedConnectionId?: string } + ) { + if (expectedConnectionId && messageContext.connection?.id !== expectedConnectionId) { + throw new CredoError( + `Expecting incoming message to have connection ${expectedConnectionId}, but incoming connection is ${ + messageContext.connection?.id ?? 'undefined' + }` + ) + } + + const outOfBandRepository = messageContext.agentContext.dependencyManager.resolve(OutOfBandRepository) + const outOfBandInvitationId = messageContext.message.thread?.parentThreadId + + // Find the out of band record that is associated with this request + const outOfBandRecord = await outOfBandRepository.findSingleByQuery(messageContext.agentContext, { + invitationId: outOfBandInvitationId, + role: OutOfBandRole.Sender, + invitationRequestsThreadIds: [messageContext.message.threadId], + }) + + // There is no out of band record + if (!outOfBandRecord) { + throw new CredoError( + `No out of band record found for credential request message with thread ${messageContext.message.threadId}, out of band invitation id ${outOfBandInvitationId} and role ${OutOfBandRole.Sender}` + ) + } + + const legacyInvitationMetadata = outOfBandRecord.metadata.get(OutOfBandRecordMetadataKeys.LegacyInvitation) + + // If the original invitation was a legacy connectionless invitation, it's okay if the message does not have a pthid. + if ( + legacyInvitationMetadata?.legacyInvitationType !== InvitationType.Connectionless && + outOfBandRecord.outOfBandInvitation.id !== outOfBandInvitationId + ) { + throw new CredoError( + 'Response messages to out of band invitation requests MUST have a parent thread id that matches the out of band invitation id.' + ) + } + + // This should not happen, as it is not allowed to create reusable out of band invitations with attached messages + // But should that implementation change, we at least cover it here. + if (outOfBandRecord.reusable) { + throw new CredoError('Receiving messages in response to reusable out of band invitations is not supported.') + } + + if (outOfBandRecord.state === OutOfBandState.Done) { + if (!messageContext.connection) { + throw new CredoError( + "Can't find connection associated with incoming message, while out of band state is done. State must be await response if no connection has been created" + ) + } + if (messageContext.connection.outOfBandId !== outOfBandRecord.id) { + throw new CredoError( + 'Connection associated with incoming message is not associated with the out of band invitation containing the attached message.' + ) + } + + // We're good to go. Connection was created and points to the correct out of band record. And the message is in response to an attached request message from the oob invitation. + } else if (outOfBandRecord.state === OutOfBandState.AwaitResponse) { + // We're good to go. Waiting for a response. And the message is in response to an attached request message from the oob invitation. + + // Now that we have received the first response message to our out of band invitation, we mark the out of band record as done + outOfBandRecord.state = OutOfBandState.Done + await outOfBandRepository.update(messageContext.agentContext, outOfBandRecord) + } else { + throw new CredoError(`Out of band record is in incorrect state ${outOfBandRecord.state}`) + } + } + + public async updateState(agentContext: AgentContext, connectionRecord: ConnectionRecord, newState: DidExchangeState) { + const previousState = connectionRecord.state + connectionRecord.state = newState + await this.connectionRepository.update(agentContext, connectionRecord) + + this.emitStateChangedEvent(agentContext, connectionRecord, previousState) + } + + private emitStateChangedEvent( + agentContext: AgentContext, + connectionRecord: ConnectionRecord, + previousState: DidExchangeState | null + ) { + this.eventEmitter.emit(agentContext, { + type: ConnectionEventTypes.ConnectionStateChanged, + payload: { + // Connection record in event should be static + connectionRecord: connectionRecord.clone(), + previousState, + }, + }) + } + + public update(agentContext: AgentContext, connectionRecord: ConnectionRecord) { + return this.connectionRepository.update(agentContext, connectionRecord) + } + + /** + * Retrieve all connections records + * + * @returns List containing all connection records + */ + public getAll(agentContext: AgentContext) { + return this.connectionRepository.getAll(agentContext) + } + + /** + * Retrieve a connection record by id + * + * @param connectionId The connection record id + * @throws {RecordNotFoundError} If no record is found + * @return The connection record + * + */ + public getById(agentContext: AgentContext, connectionId: string): Promise { + return this.connectionRepository.getById(agentContext, connectionId) + } + + /** + * Find a connection record by id + * + * @param connectionId the connection record id + * @returns The connection record or null if not found + */ + public findById(agentContext: AgentContext, connectionId: string): Promise { + return this.connectionRepository.findById(agentContext, connectionId) + } + + /** + * Delete a connection record by id + * + * @param connectionId the connection record id + */ + public async deleteById(agentContext: AgentContext, connectionId: string) { + const connectionRecord = await this.getById(agentContext, connectionId) + return this.connectionRepository.delete(agentContext, connectionRecord) + } + + public async findByDids(agentContext: AgentContext, query: { ourDid: string; theirDid: string }) { + return this.connectionRepository.findByDids(agentContext, query) + } + + /** + * Retrieve a connection record by thread id + * + * @param threadId The thread id + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * @returns The connection record + */ + public async getByThreadId(agentContext: AgentContext, threadId: string): Promise { + return this.connectionRepository.getByThreadId(agentContext, threadId) + } + + public async getByRoleAndThreadId(agentContext: AgentContext, role: DidExchangeRole, threadId: string) { + return this.connectionRepository.getByRoleAndThreadId(agentContext, role, threadId) + } + + public async findByTheirDid(agentContext: AgentContext, theirDid: string): Promise { + return this.connectionRepository.findSingleByQuery(agentContext, { theirDid }) + } + + public async findByOurDid(agentContext: AgentContext, ourDid: string): Promise { + return this.connectionRepository.findSingleByQuery(agentContext, { did: ourDid }) + } + + public async findAllByOutOfBandId(agentContext: AgentContext, outOfBandId: string) { + return this.connectionRepository.findByQuery(agentContext, { outOfBandId }) + } + + public async findAllByConnectionTypes(agentContext: AgentContext, connectionTypes: Array) { + return this.connectionRepository.findByQuery(agentContext, { connectionTypes }) + } + + public async findByInvitationDid(agentContext: AgentContext, invitationDid: string) { + return this.connectionRepository.findByQuery(agentContext, { invitationDid }) + } + + public async findByKeys( + agentContext: AgentContext, + { senderKey, recipientKey }: { senderKey: Key; recipientKey: Key } + ) { + const theirDidRecord = await this.didRepository.findReceivedDidByRecipientKey(agentContext, senderKey) + if (theirDidRecord) { + const ourDidRecord = await this.didRepository.findCreatedDidByRecipientKey(agentContext, recipientKey) + if (ourDidRecord) { + const connectionRecord = await this.findByDids(agentContext, { + ourDid: ourDidRecord.did, + theirDid: theirDidRecord.did, + }) + if (connectionRecord && connectionRecord.isReady) return connectionRecord + } + } + + this.logger.debug( + `No connection record found for encrypted message with recipient key ${recipientKey.fingerprint} and sender key ${senderKey.fingerprint}` + ) + + return null + } + + public async findAllByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ): Promise { + return this.connectionRepository.findByQuery(agentContext, query, queryOptions) + } + + public async createConnection(agentContext: AgentContext, options: ConnectionRecordProps): Promise { + const connectionRecord = new ConnectionRecord(options) + await this.connectionRepository.save(agentContext, connectionRecord) + return connectionRecord + } + + public async addConnectionType(agentContext: AgentContext, connectionRecord: ConnectionRecord, type: string) { + const connectionTypes = connectionRecord.connectionTypes || [] + connectionRecord.connectionTypes = [type, ...connectionTypes] + await this.update(agentContext, connectionRecord) + } + + public async removeConnectionType(agentContext: AgentContext, connectionRecord: ConnectionRecord, type: string) { + connectionRecord.connectionTypes = connectionRecord.connectionTypes.filter((value) => value !== type) + await this.update(agentContext, connectionRecord) + } + + public async getConnectionTypes(connectionRecord: ConnectionRecord) { + return connectionRecord.connectionTypes || [] + } + + private async createDid(agentContext: AgentContext, { role, didDoc }: { role: DidDocumentRole; didDoc: DidDoc }) { + // Convert the legacy did doc to a new did document + const didDocument = convertToNewDidDocument(didDoc) + + // Assert that the keys we are going to use for creating a did document haven't already been used in another did document + if (role === DidDocumentRole.Created) { + await assertNoCreatedDidExistsForKeys(agentContext, didDocument.recipientKeys) + } + + const peerDid = didDocumentJsonToNumAlgo1Did(didDocument.toJSON()) + didDocument.id = peerDid + const didRecord = new DidRecord({ + did: peerDid, + role, + didDocument, + }) + + // Store the unqualified did with the legacy did document in the metadata + // Can be removed at a later stage if we know for sure we don't need it anymore + didRecord.metadata.set(DidRecordMetadataKeys.LegacyDid, { + unqualifiedDid: didDoc.id, + didDocumentString: JsonTransformer.serialize(didDoc), + }) + + this.logger.debug('Saving DID record', { + id: didRecord.id, + did: didRecord.did, + role: didRecord.role, + tags: didRecord.getTags(), + didDocument: 'omitted...', + }) + + await this.didRepository.save(agentContext, didRecord) + this.logger.debug('Did record created.', didRecord) + return { did: peerDid, didDocument } + } + + private createDidDoc(routing: Routing) { + const indyDid = indyDidFromPublicKeyBase58(routing.recipientKey.publicKeyBase58) + + const publicKey = new Ed25119Sig2018({ + id: `${indyDid}#1`, + controller: indyDid, + publicKeyBase58: routing.recipientKey.publicKeyBase58, + }) + + const auth = new ReferencedAuthentication(publicKey, authenticationTypes.Ed25519VerificationKey2018) + + // IndyAgentService is old service type + const services = routing.endpoints.map( + (endpoint, index) => + new IndyAgentService({ + id: `${indyDid}#IndyAgentService-${index + 1}`, + serviceEndpoint: endpoint, + recipientKeys: [routing.recipientKey.publicKeyBase58], + routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), + // Order of endpoint determines priority + priority: index, + }) + ) + + return new DidDoc({ + id: indyDid, + authentication: [auth], + service: services, + publicKey: [publicKey], + }) + } + + private createDidDocFromOutOfBandDidCommServices(services: OutOfBandDidCommService[]) { + const [recipientDidKey] = services[0].recipientKeys + + const recipientKey = DidKey.fromDid(recipientDidKey).key + const did = indyDidFromPublicKeyBase58(recipientKey.publicKeyBase58) + + const publicKey = new Ed25119Sig2018({ + id: `${did}#1`, + controller: did, + publicKeyBase58: recipientKey.publicKeyBase58, + }) + + const auth = new ReferencedAuthentication(publicKey, authenticationTypes.Ed25519VerificationKey2018) + + // IndyAgentService is old service type + const service = services.map( + (service, index) => + new IndyAgentService({ + id: `${did}#IndyAgentService-${index + 1}`, + serviceEndpoint: service.serviceEndpoint, + recipientKeys: [recipientKey.publicKeyBase58], + routingKeys: service.routingKeys?.map(didKeyToVerkey), + priority: index, + }) + ) + + return new DidDoc({ + id: did, + authentication: [auth], + service, + publicKey: [publicKey], + }) + } + + public async returnWhenIsConnected( + agentContext: AgentContext, + connectionId: string, + timeoutMs = 20000 + ): Promise { + const isConnected = (connection: ConnectionRecord) => { + return connection.id === connectionId && connection.state === DidExchangeState.Completed + } + + const observable = this.eventEmitter.observable( + ConnectionEventTypes.ConnectionStateChanged + ) + const subject = new ReplaySubject(1) + + observable + .pipe( + filterContextCorrelationId(agentContext.contextCorrelationId), + map((e) => e.payload.connectionRecord), + first(isConnected), // Do not wait for longer than specified timeout + timeout({ + first: timeoutMs, + meta: 'ConnectionService.returnWhenIsConnected', + }) + ) + .subscribe(subject) + + const connection = await this.getById(agentContext, connectionId) + if (isConnected(connection)) { + subject.next(connection) + } + + return firstValueFrom(subject) + } +} + +export interface Routing { + endpoints: string[] + recipientKey: Key + routingKeys: Key[] + mediatorId?: string +} + +export interface ConnectionProtocolMsgReturnType { + message: MessageType + connectionRecord: ConnectionRecord +} diff --git a/packages/core/src/modules/connections/services/DidRotateService.ts b/packages/core/src/modules/connections/services/DidRotateService.ts new file mode 100644 index 0000000000..e797b19b02 --- /dev/null +++ b/packages/core/src/modules/connections/services/DidRotateService.ts @@ -0,0 +1,319 @@ +import type { Routing } from './ConnectionService' +import type { AgentContext } from '../../../agent' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { ConnectionDidRotatedEvent } from '../ConnectionEvents' +import type { ConnectionRecord } from '../repository/ConnectionRecord' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { OutboundMessageContext } from '../../../agent/models' +import { InjectionSymbols } from '../../../constants' +import { CredoError } from '../../../error' +import { Logger } from '../../../logger' +import { inject, injectable } from '../../../plugins' +import { AckStatus } from '../../common' +import { + DidRepository, + DidResolverService, + PeerDidNumAlgo, + getAlternativeDidsForPeerDid, + getNumAlgoFromPeerDid, + isValidPeerDid, +} from '../../dids' +import { getMediationRecordForDidDocument } from '../../routing/services/helpers' +import { ConnectionEventTypes } from '../ConnectionEvents' +import { ConnectionsModuleConfig } from '../ConnectionsModuleConfig' +import { DidRotateMessage, DidRotateAckMessage, DidRotateProblemReportMessage, HangupMessage } from '../messages' +import { ConnectionMetadataKeys } from '../repository/ConnectionMetadataTypes' + +import { ConnectionService } from './ConnectionService' +import { createPeerDidFromServices, getDidDocumentForCreatedDid, routingToServices } from './helpers' + +@injectable() +export class DidRotateService { + private didResolverService: DidResolverService + private logger: Logger + private eventEmitter: EventEmitter + + public constructor( + didResolverService: DidResolverService, + @inject(InjectionSymbols.Logger) logger: Logger, + eventEmitter: EventEmitter + ) { + this.didResolverService = didResolverService + this.logger = logger + this.eventEmitter = eventEmitter + } + + public async createRotate( + agentContext: AgentContext, + options: { connection: ConnectionRecord; toDid?: string; routing?: Routing } + ) { + const { connection, toDid, routing } = options + + const config = agentContext.dependencyManager.resolve(ConnectionsModuleConfig) + + // Do not allow to receive concurrent did rotation flows + const didRotateMetadata = connection.metadata.get(ConnectionMetadataKeys.DidRotate) + + if (didRotateMetadata) { + throw new CredoError(`There is already an existing opened did rotation flow for connection id ${connection.id}`) + } + + let didDocument, mediatorId + // If did is specified, make sure we have all key material for it + if (toDid) { + didDocument = await getDidDocumentForCreatedDid(agentContext, toDid) + mediatorId = (await getMediationRecordForDidDocument(agentContext, didDocument))?.id + + // Otherwise, create a did:peer based on the provided routing + } else { + if (!routing) { + throw new CredoError('Routing configuration must be defined when rotating to a new peer did') + } + + didDocument = await createPeerDidFromServices( + agentContext, + routingToServices(routing), + config.peerNumAlgoForDidRotation + ) + mediatorId = routing.mediatorId + } + + const message = new DidRotateMessage({ toDid: didDocument.id }) + + // We set new info into connection metadata for further 'sealing' it once we receive an acknowledge + // All messages sent in-between will be using previous connection information + connection.metadata.set(ConnectionMetadataKeys.DidRotate, { + threadId: message.threadId, + did: didDocument.id, + mediatorId, + }) + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + + return message + } + + public async createHangup(agentContext: AgentContext, options: { connection: ConnectionRecord }) { + const { connection } = options + + const message = new HangupMessage({}) + + // Remove did to indicate termination status for this connection + if (connection.did) { + connection.previousDids = [...connection.previousDids, connection.did] + } + + connection.did = undefined + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + + return message + } + + /** + * Process a Hangup message and mark connection's theirDid as undefined so it is effectively terminated. + * Connection Record itself is not deleted (TODO: config parameter to automatically do so) + * + * Its previous did will be stored in record in order to be able to recognize any message received + * afterwards. + * + * @param messageContext + */ + public async processHangup(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + const { agentContext } = messageContext + + if (connection.theirDid) { + connection.previousTheirDids = [...connection.previousTheirDids, connection.theirDid] + } + + connection.theirDid = undefined + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + } + + /** + * Process an incoming DID Rotate message and update connection if success. Any acknowledge + * or problem report will be sent to the prior DID, so the created context will take former + * connection record data + * + * @param param + * @param connection + * @returns + */ + public async processRotate(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + const { message, agentContext } = messageContext + + // Check and store their new did + const newDid = message.toDid + + // DID Rotation not supported for peer:1 dids, as we need explicit did document information + if (isValidPeerDid(newDid) && getNumAlgoFromPeerDid(newDid) === PeerDidNumAlgo.GenesisDoc) { + this.logger.error(`Unable to resolve DID Document for '${newDid}`) + + const response = new DidRotateProblemReportMessage({ + description: { en: 'DID Method Unsupported', code: 'e.did.method_unsupported' }, + }) + return new OutboundMessageContext(response, { agentContext, connection }) + } + + const didDocument = (await this.didResolverService.resolve(agentContext, newDid)).didDocument + + // Cannot resolve did + if (!didDocument) { + this.logger.error(`Unable to resolve DID Document for '${newDid}`) + + const response = new DidRotateProblemReportMessage({ + description: { en: 'DID Unresolvable', code: 'e.did.unresolvable' }, + }) + return new OutboundMessageContext(response, { agentContext, connection }) + } + + // Did is resolved but no compatible DIDComm services found + if (!didDocument.didCommServices) { + const response = new DidRotateProblemReportMessage({ + description: { en: 'DID Document Unsupported', code: 'e.did.doc_unsupported' }, + }) + return new OutboundMessageContext(response, { agentContext, connection }) + } + + // Send acknowledge to previous did and persist new did. Previous did will be stored in connection record in + // order to still accept messages from it + const outboundMessageContext = new OutboundMessageContext( + new DidRotateAckMessage({ + threadId: message.threadId, + status: AckStatus.OK, + }), + { agentContext, connection: connection.clone() } + ) + + // Store received did and update connection for further message processing + await agentContext.dependencyManager.resolve(DidRepository).storeReceivedDid(agentContext, { + did: didDocument.id, + didDocument, + tags: { + // For did:peer, store any alternative dids (like short form did:peer:4), + // it may have in order to relate any message referencing it + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(didDocument.id) : undefined, + }, + }) + + if (connection.theirDid) { + connection.previousTheirDids = [...connection.previousTheirDids, connection.theirDid] + } + + const previousTheirDid = connection.theirDid + connection.theirDid = newDid + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + this.emitDidRotatedEvent(agentContext, connection, { + previousTheirDid, + }) + + return outboundMessageContext + } + + public async processRotateAck(inboundMessage: InboundMessageContext) { + const { agentContext, message } = inboundMessage + + const connection = inboundMessage.assertReadyConnection() + + // Update connection info based on metadata set when creating the rotate message + const didRotateMetadata = connection.metadata.get(ConnectionMetadataKeys.DidRotate) + + if (!didRotateMetadata) { + throw new CredoError(`No did rotation data found for connection with id '${connection.id}'`) + } + + if (didRotateMetadata.threadId !== message.threadId) { + throw new CredoError( + `Existing did rotation flow thread id '${didRotateMetadata.threadId} does not match incoming message'` + ) + } + + // Store previous did in order to still accept out-of-order messages that arrived later using it + if (connection.did) connection.previousDids = [...connection.previousDids, connection.did] + + const previousOurDid = connection.did + connection.did = didRotateMetadata.did + connection.mediatorId = didRotateMetadata.mediatorId + connection.metadata.delete(ConnectionMetadataKeys.DidRotate) + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + this.emitDidRotatedEvent(agentContext, connection, { + previousOurDid, + }) + } + + /** + * Process a problem report related to did rotate protocol, by simply deleting any temporary metadata. + * + * No specific event is thrown other than generic message processing + * + * @param messageContext + */ + public async processProblemReport( + messageContext: InboundMessageContext + ): Promise { + const { message, agentContext } = messageContext + + const connection = messageContext.assertReadyConnection() + + this.logger.debug(`Processing problem report with id ${message.id}`) + + // Delete any existing did rotation metadata in order to 'reset' the connection + const didRotateMetadata = connection.metadata.get(ConnectionMetadataKeys.DidRotate) + + if (!didRotateMetadata) { + throw new CredoError(`No did rotation data found for connection with id '${connection.id}'`) + } + + connection.metadata.delete(ConnectionMetadataKeys.DidRotate) + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + } + + public async clearDidRotationData(agentContext: AgentContext, connection: ConnectionRecord) { + const didRotateMetadata = connection.metadata.get(ConnectionMetadataKeys.DidRotate) + + if (!didRotateMetadata) { + throw new CredoError(`No did rotation data found for connection with id '${connection.id}'`) + } + + connection.metadata.delete(ConnectionMetadataKeys.DidRotate) + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + } + + private emitDidRotatedEvent( + agentContext: AgentContext, + connectionRecord: ConnectionRecord, + { previousOurDid, previousTheirDid }: { previousOurDid?: string; previousTheirDid?: string } + ) { + this.eventEmitter.emit(agentContext, { + type: ConnectionEventTypes.ConnectionDidRotated, + payload: { + // Connection record in event should be static + connectionRecord: connectionRecord.clone(), + + ourDid: + previousOurDid && connectionRecord.did + ? { + from: previousOurDid, + to: connectionRecord.did, + } + : undefined, + + theirDid: + previousTheirDid && connectionRecord.theirDid + ? { + from: previousTheirDid, + to: connectionRecord.theirDid, + } + : undefined, + }, + }) + } +} diff --git a/packages/core/src/modules/connections/services/TrustPingService.ts b/packages/core/src/modules/connections/services/TrustPingService.ts new file mode 100644 index 0000000000..5236a2c83d --- /dev/null +++ b/packages/core/src/modules/connections/services/TrustPingService.ts @@ -0,0 +1,51 @@ +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { TrustPingReceivedEvent, TrustPingResponseReceivedEvent } from '../TrustPingEvents' +import type { TrustPingMessage } from '../messages' +import type { ConnectionRecord } from '../repository/ConnectionRecord' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { OutboundMessageContext } from '../../../agent/models' +import { injectable } from '../../../plugins' +import { TrustPingEventTypes } from '../TrustPingEvents' +import { TrustPingResponseMessage } from '../messages' + +@injectable() +export class TrustPingService { + private eventEmitter: EventEmitter + + public constructor(eventEmitter: EventEmitter) { + this.eventEmitter = eventEmitter + } + + public processPing({ message, agentContext }: InboundMessageContext, connection: ConnectionRecord) { + this.eventEmitter.emit(agentContext, { + type: TrustPingEventTypes.TrustPingReceivedEvent, + payload: { + connectionRecord: connection, + message: message, + }, + }) + + if (message.responseRequested) { + const response = new TrustPingResponseMessage({ + threadId: message.threadId, + }) + + return new OutboundMessageContext(response, { agentContext, connection }) + } + } + + public processPingResponse(inboundMessage: InboundMessageContext) { + const { agentContext, message } = inboundMessage + + const connection = inboundMessage.assertReadyConnection() + + this.eventEmitter.emit(agentContext, { + type: TrustPingEventTypes.TrustPingResponseReceivedEvent, + payload: { + connectionRecord: connection, + message: message, + }, + }) + } +} diff --git a/packages/core/src/modules/connections/services/helpers.ts b/packages/core/src/modules/connections/services/helpers.ts new file mode 100644 index 0000000000..0b9efbac74 --- /dev/null +++ b/packages/core/src/modules/connections/services/helpers.ts @@ -0,0 +1,200 @@ +import type { Routing } from './ConnectionService' +import type { AgentContext } from '../../../agent' +import type { ResolvedDidCommService } from '../../didcomm' +import type { DidDocument, PeerDidNumAlgo } from '../../dids' +import type { DidDoc, PublicKey } from '../models' + +import { Key, KeyType } from '../../../crypto' +import { CredoError } from '../../../error' +import { + IndyAgentService, + DidCommV1Service, + DidDocumentBuilder, + getEd25519VerificationKey2018, + DidRepository, + DidsApi, + createPeerDidDocumentFromServices, + DidDocumentRole, +} from '../../dids' +import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' +import { EmbeddedAuthentication } from '../models' + +export function convertToNewDidDocument(didDoc: DidDoc): DidDocument { + const didDocumentBuilder = new DidDocumentBuilder('') + + const oldIdNewIdMapping: { [key: string]: string } = {} + + didDoc.authentication.forEach((auth) => { + const { publicKey: pk } = auth + + // did:peer did documents can only use referenced keys. + if (pk.type === 'Ed25519VerificationKey2018' && pk.value) { + const ed25519VerificationMethod = convertPublicKeyToVerificationMethod(pk) + + const oldKeyId = normalizeId(pk.id) + oldIdNewIdMapping[oldKeyId] = ed25519VerificationMethod.id + didDocumentBuilder.addAuthentication(ed25519VerificationMethod.id) + + // Only the auth is embedded, we also need to add the key to the verificationMethod + // for referenced authentication this should already be the case + if (auth instanceof EmbeddedAuthentication) { + didDocumentBuilder.addVerificationMethod(ed25519VerificationMethod) + } + } + }) + + didDoc.publicKey.forEach((pk) => { + if (pk.type === 'Ed25519VerificationKey2018' && pk.value) { + const ed25519VerificationMethod = convertPublicKeyToVerificationMethod(pk) + + const oldKeyId = normalizeId(pk.id) + oldIdNewIdMapping[oldKeyId] = ed25519VerificationMethod.id + didDocumentBuilder.addVerificationMethod(ed25519VerificationMethod) + } + }) + + // FIXME: we reverse the didCommServices here, as the previous implementation was wrong + // and we need to keep the same order to not break the did creation process. + // When we implement the migration to did:peer:2 and did:peer:3 according to the + // RFCs we can change it. + didDoc.didCommServices.reverse().forEach((service) => { + const serviceId = normalizeId(service.id) + + // For didcommv1, we need to replace the old id with the new ones + if (service instanceof DidCommV1Service) { + const recipientKeys = service.recipientKeys.map((keyId) => { + const oldKeyId = normalizeId(keyId) + return oldIdNewIdMapping[oldKeyId] + }) + + service = new DidCommV1Service({ + id: serviceId, + recipientKeys, + serviceEndpoint: service.serviceEndpoint, + routingKeys: service.routingKeys, + accept: service.accept, + priority: service.priority, + }) + } else if (service instanceof IndyAgentService) { + service = new IndyAgentService({ + id: serviceId, + recipientKeys: service.recipientKeys, + serviceEndpoint: service.serviceEndpoint, + routingKeys: service.routingKeys, + priority: service.priority, + }) + } + + didDocumentBuilder.addService(service) + }) + + const didDocument = didDocumentBuilder.build() + + const peerDid = didDocumentJsonToNumAlgo1Did(didDocument.toJSON()) + didDocument.id = peerDid + + return didDocument +} + +function normalizeId(fullId: string): `#${string}` { + // Some old dids use `;` as the delimiter for the id. If we can't find a `#` + // and a `;` exists, we will parse everything after `;` as the id. + if (!fullId.includes('#') && fullId.includes(';')) { + const [, ...ids] = fullId.split(';') + + return `#${ids.join(';')}` + } + + const [, ...ids] = fullId.split('#') + return `#${ids.length ? ids.join('#') : fullId}` +} + +function convertPublicKeyToVerificationMethod(publicKey: PublicKey) { + if (!publicKey.value) { + throw new CredoError(`Public key ${publicKey.id} does not have value property`) + } + const publicKeyBase58 = publicKey.value + const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519) + return getEd25519VerificationKey2018({ + id: `#${publicKeyBase58.slice(0, 8)}`, + key: ed25519Key, + controller: '#id', + }) +} + +export function routingToServices(routing: Routing): ResolvedDidCommService[] { + return routing.endpoints.map((endpoint, index) => ({ + id: `#inline-${index}`, + serviceEndpoint: endpoint, + recipientKeys: [routing.recipientKey], + routingKeys: routing.routingKeys, + })) +} + +export async function getDidDocumentForCreatedDid(agentContext: AgentContext, did: string) { + const didRecord = await agentContext.dependencyManager.resolve(DidRepository).findCreatedDid(agentContext, did) + + if (!didRecord?.didDocument) { + throw new CredoError(`Could not get DidDocument for created did ${did}`) + } + return didRecord.didDocument +} + +/** + * Asserts that the keys we are going to use for creating a did document haven't already been used in another did document + * Due to how DIDComm v1 works (only reference the key not the did in encrypted message) we can't have multiple dids containing + * the same key as we won't know which did (and thus which connection) a message is intended for. + */ +export async function assertNoCreatedDidExistsForKeys(agentContext: AgentContext, recipientKeys: Key[]) { + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + const recipientKeyFingerprints = recipientKeys.map((key) => key.fingerprint) + + const didsForServices = await didRepository.findByQuery(agentContext, { + role: DidDocumentRole.Created, + + // We want an $or query so we query for each key individually, not one did document + // containing exactly the same keys as the did document we are trying to create + $or: recipientKeyFingerprints.map((fingerprint) => ({ + recipientKeyFingerprints: [fingerprint], + })), + }) + + if (didsForServices.length > 0) { + const allDidRecipientKeys = didsForServices.flatMap((did) => did.getTags().recipientKeyFingerprints ?? []) + const matchingFingerprints = allDidRecipientKeys.filter((f) => recipientKeyFingerprints.includes(f)) + throw new CredoError( + `A did already exists for some of the keys in the provided services. DIDComm v1 uses key based routing, and therefore it is not allowed to re-use the same key in multiple did documents for DIDComm. If you use the same 'routing' object for multiple invitations, instead provide an 'invitationDid' to the create invitation method. The following fingerprints are already in use: ${matchingFingerprints.join( + ',' + )}` + ) + } +} + +export async function createPeerDidFromServices( + agentContext: AgentContext, + services: ResolvedDidCommService[], + numAlgo: PeerDidNumAlgo +) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + + // Create did document without the id property + const didDocument = createPeerDidDocumentFromServices(services) + + // Assert that the keys we are going to use for creating a did document haven't already been used in another did document + await assertNoCreatedDidExistsForKeys(agentContext, didDocument.recipientKeys) + + // Register did:peer document. This will generate the id property and save it to a did record + const result = await didsApi.create({ + method: 'peer', + didDocument, + options: { + numAlgo, + }, + }) + + if (result.didState?.state !== 'finished') { + throw new CredoError(`Did document creation failed: ${JSON.stringify(result.didState)}`) + } + + return result.didState.didDocument +} diff --git a/packages/core/src/modules/connections/services/index.ts b/packages/core/src/modules/connections/services/index.ts new file mode 100644 index 0000000000..db9fe13ac4 --- /dev/null +++ b/packages/core/src/modules/connections/services/index.ts @@ -0,0 +1,3 @@ +export * from './ConnectionService' +export * from './DidRotateService' +export * from './TrustPingService' diff --git a/packages/core/src/modules/credentials/CredentialEvents.ts b/packages/core/src/modules/credentials/CredentialEvents.ts new file mode 100644 index 0000000000..2a60193d51 --- /dev/null +++ b/packages/core/src/modules/credentials/CredentialEvents.ts @@ -0,0 +1,22 @@ +import type { CredentialState } from './models/CredentialState' +import type { CredentialExchangeRecord } from './repository/CredentialExchangeRecord' +import type { BaseEvent } from '../../agent/Events' + +export enum CredentialEventTypes { + CredentialStateChanged = 'CredentialStateChanged', + RevocationNotificationReceived = 'RevocationNotificationReceived', +} +export interface CredentialStateChangedEvent extends BaseEvent { + type: typeof CredentialEventTypes.CredentialStateChanged + payload: { + credentialRecord: CredentialExchangeRecord + previousState: CredentialState | null + } +} + +export interface RevocationNotificationReceivedEvent extends BaseEvent { + type: typeof CredentialEventTypes.RevocationNotificationReceived + payload: { + credentialRecord: CredentialExchangeRecord + } +} diff --git a/packages/core/src/modules/credentials/CredentialsApi.ts b/packages/core/src/modules/credentials/CredentialsApi.ts new file mode 100644 index 0000000000..9eef044a8c --- /dev/null +++ b/packages/core/src/modules/credentials/CredentialsApi.ts @@ -0,0 +1,714 @@ +import type { + AcceptCredentialOptions, + AcceptCredentialOfferOptions, + AcceptCredentialProposalOptions, + AcceptCredentialRequestOptions, + CreateCredentialOfferOptions, + FindCredentialMessageReturn, + FindCredentialOfferMessageReturn, + FindCredentialProposalMessageReturn, + FindCredentialRequestMessageReturn, + GetCredentialFormatDataReturn, + NegotiateCredentialOfferOptions, + NegotiateCredentialProposalOptions, + OfferCredentialOptions, + ProposeCredentialOptions, + SendCredentialProblemReportOptions, + DeleteCredentialOptions, + SendRevocationNotificationOptions, + DeclineCredentialOfferOptions, +} from './CredentialsApiOptions' +import type { CredentialProtocol } from './protocol/CredentialProtocol' +import type { CredentialFormatsFromProtocols } from './protocol/CredentialProtocolOptions' +import type { CredentialExchangeRecord } from './repository/CredentialExchangeRecord' +import type { AgentMessage } from '../../agent/AgentMessage' +import type { Query, QueryOptions } from '../../storage/StorageService' + +import { AgentContext } from '../../agent' +import { MessageSender } from '../../agent/MessageSender' +import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' +import { InjectionSymbols } from '../../constants' +import { CredoError } from '../../error' +import { Logger } from '../../logger' +import { inject, injectable } from '../../plugins' +import { DidCommMessageRepository } from '../../storage/didcomm/DidCommMessageRepository' +import { ConnectionService } from '../connections/services' +import { RoutingService } from '../routing/services/RoutingService' + +import { CredentialsModuleConfig } from './CredentialsModuleConfig' +import { CredentialState } from './models/CredentialState' +import { RevocationNotificationService } from './protocol/revocation-notification/services' +import { CredentialRepository } from './repository/CredentialRepository' + +export interface CredentialsApi { + // Propose Credential methods + proposeCredential(options: ProposeCredentialOptions): Promise + acceptProposal(options: AcceptCredentialProposalOptions): Promise + negotiateProposal(options: NegotiateCredentialProposalOptions): Promise + + // Offer Credential Methods + offerCredential(options: OfferCredentialOptions): Promise + acceptOffer(options: AcceptCredentialOfferOptions): Promise + declineOffer(credentialRecordId: string, options?: DeclineCredentialOfferOptions): Promise + negotiateOffer(options: NegotiateCredentialOfferOptions): Promise + + // Request Credential Methods + // This is for beginning the exchange with a request (no proposal or offer). Only possible + // (currently) with W3C. We will not implement this in phase I + + // when the issuer accepts the request he issues the credential to the holder + acceptRequest(options: AcceptCredentialRequestOptions): Promise + + // Issue Credential Methods + acceptCredential(options: AcceptCredentialOptions): Promise + + // Revoke Credential Methods + sendRevocationNotification(options: SendRevocationNotificationOptions): Promise + + // out of band + createOffer(options: CreateCredentialOfferOptions): Promise<{ + message: AgentMessage + credentialRecord: CredentialExchangeRecord + }> + + sendProblemReport(options: SendCredentialProblemReportOptions): Promise + + // Record Methods + getAll(): Promise + findAllByQuery( + query: Query, + queryOptions?: QueryOptions + ): Promise + getById(credentialRecordId: string): Promise + findById(credentialRecordId: string): Promise + deleteById(credentialRecordId: string, options?: DeleteCredentialOptions): Promise + update(credentialRecord: CredentialExchangeRecord): Promise + getFormatData(credentialRecordId: string): Promise>> + + // DidComm Message Records + findProposalMessage(credentialExchangeId: string): Promise> + findOfferMessage(credentialExchangeId: string): Promise> + findRequestMessage(credentialExchangeId: string): Promise> + findCredentialMessage(credentialExchangeId: string): Promise> +} + +@injectable() +export class CredentialsApi implements CredentialsApi { + /** + * Configuration for the credentials module + */ + public readonly config: CredentialsModuleConfig + + private connectionService: ConnectionService + private messageSender: MessageSender + private credentialRepository: CredentialRepository + private agentContext: AgentContext + private didCommMessageRepository: DidCommMessageRepository + private revocationNotificationService: RevocationNotificationService + private routingService: RoutingService + private logger: Logger + + public constructor( + messageSender: MessageSender, + connectionService: ConnectionService, + agentContext: AgentContext, + @inject(InjectionSymbols.Logger) logger: Logger, + credentialRepository: CredentialRepository, + mediationRecipientService: RoutingService, + didCommMessageRepository: DidCommMessageRepository, + revocationNotificationService: RevocationNotificationService, + config: CredentialsModuleConfig + ) { + this.messageSender = messageSender + this.connectionService = connectionService + this.credentialRepository = credentialRepository + this.routingService = mediationRecipientService + this.agentContext = agentContext + this.didCommMessageRepository = didCommMessageRepository + this.revocationNotificationService = revocationNotificationService + this.logger = logger + this.config = config + } + + private getProtocol(protocolVersion: PVT): CredentialProtocol { + const credentialProtocol = this.config.credentialProtocols.find((protocol) => protocol.version === protocolVersion) + + if (!credentialProtocol) { + throw new CredoError(`No credential protocol registered for protocol version ${protocolVersion}`) + } + + return credentialProtocol + } + + /** + * Initiate a new credential exchange as holder by sending a credential proposal message + * to the connection with the specified connection id. + * + * @param options configuration to use for the proposal + * @returns Credential exchange record associated with the sent proposal message + */ + + public async proposeCredential(options: ProposeCredentialOptions): Promise { + const protocol = this.getProtocol(options.protocolVersion) + + const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) + + // Assert + connectionRecord.assertReady() + + // will get back a credential record -> map to Credential Exchange Record + const { credentialRecord, message } = await protocol.createProposal(this.agentContext, { + connectionRecord, + credentialFormats: options.credentialFormats, + comment: options.comment, + autoAcceptCredential: options.autoAcceptCredential, + goalCode: options.goalCode, + goal: options.goal, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: credentialRecord, + connectionRecord, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + return credentialRecord + } + + /** + * Accept a credential proposal as issuer (by sending a credential offer message) to the connection + * associated with the credential record. + * + * @param options config object for accepting the proposal + * @returns Credential exchange record associated with the credential offer + * + */ + public async acceptProposal(options: AcceptCredentialProposalOptions): Promise { + const credentialRecord = await this.getById(options.credentialRecordId) + + if (!credentialRecord.connectionId) { + throw new CredoError( + `No connectionId found for credential record '${credentialRecord.id}'. Connection-less issuance does not support credential proposal or negotiation.` + ) + } + + // with version we can get the protocol + const protocol = this.getProtocol(credentialRecord.protocolVersion) + const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + + // Assert + connectionRecord.assertReady() + + // will get back a credential record -> map to Credential Exchange Record + const { message } = await protocol.acceptProposal(this.agentContext, { + credentialRecord, + credentialFormats: options.credentialFormats, + comment: options.comment, + autoAcceptCredential: options.autoAcceptCredential, + goalCode: options.goalCode, + goal: options.goal, + }) + + // send the message + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: credentialRecord, + connectionRecord, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return credentialRecord + } + + /** + * Negotiate a credential proposal as issuer (by sending a credential offer message) to the connection + * associated with the credential record. + * + * @param options configuration for the offer see {@link NegotiateCredentialProposalOptions} + * @returns Credential exchange record associated with the credential offer + * + */ + public async negotiateProposal(options: NegotiateCredentialProposalOptions): Promise { + const credentialRecord = await this.getById(options.credentialRecordId) + + if (!credentialRecord.connectionId) { + throw new CredoError( + `No connection id for credential record ${credentialRecord.id} not found. Connection-less issuance does not support negotiation` + ) + } + + // with version we can get the Service + const protocol = this.getProtocol(credentialRecord.protocolVersion) + + const { message } = await protocol.negotiateProposal(this.agentContext, { + credentialRecord, + credentialFormats: options.credentialFormats, + comment: options.comment, + autoAcceptCredential: options.autoAcceptCredential, + goalCode: options.goalCode, + goal: options.goal, + }) + + const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: credentialRecord, + connectionRecord, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return credentialRecord + } + + /** + * Initiate a new credential exchange as issuer by sending a credential offer message + * to the connection with the specified connection id. + * + * @param options config options for the credential offer + * @returns Credential exchange record associated with the sent credential offer message + */ + public async offerCredential(options: OfferCredentialOptions): Promise { + const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) + const protocol = this.getProtocol(options.protocolVersion) + + this.logger.debug(`Got a credentialProtocol object for version ${options.protocolVersion}`) + + const { message, credentialRecord } = await protocol.createOffer(this.agentContext, { + credentialFormats: options.credentialFormats, + autoAcceptCredential: options.autoAcceptCredential, + comment: options.comment, + connectionRecord, + goalCode: options.goalCode, + goal: options.goal, + }) + + this.logger.debug('Offer Message successfully created; message= ', message) + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: credentialRecord, + connectionRecord, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return credentialRecord + } + + /** + * Accept a credential offer as holder (by sending a credential request message) to the connection + * associated with the credential record. + * + * @param options The object containing config options of the offer to be accepted + * @returns Object containing offer associated credential record + */ + public async acceptOffer(options: AcceptCredentialOfferOptions): Promise { + const credentialRecord = await this.getById(options.credentialRecordId) + + const protocol = this.getProtocol(credentialRecord.protocolVersion) + + this.logger.debug(`Got a credentialProtocol object for this version; version = ${protocol.version}`) + const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) + if (!offerMessage) { + throw new CredoError(`No offer message found for credential record with id '${credentialRecord.id}'`) + } + + // Use connection if present + const connectionRecord = credentialRecord.connectionId + ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + : undefined + connectionRecord?.assertReady() + + const { message } = await protocol.acceptOffer(this.agentContext, { + credentialRecord, + credentialFormats: options.credentialFormats, + comment: options.comment, + autoAcceptCredential: options.autoAcceptCredential, + goalCode: options.goalCode, + goal: options.goal, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: credentialRecord, + lastReceivedMessage: offerMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return credentialRecord + } + + public async declineOffer( + credentialRecordId: string, + options?: DeclineCredentialOfferOptions + ): Promise { + const credentialRecord = await this.getById(credentialRecordId) + credentialRecord.assertState(CredentialState.OfferReceived) + + // with version we can get the Service + const protocol = this.getProtocol(credentialRecord.protocolVersion) + if (options?.sendProblemReport) { + await this.sendProblemReport({ + credentialRecordId, + description: options.problemReportDescription ?? 'Offer declined', + }) + } + + await protocol.updateState(this.agentContext, credentialRecord, CredentialState.Declined) + + return credentialRecord + } + + public async negotiateOffer(options: NegotiateCredentialOfferOptions): Promise { + const credentialRecord = await this.getById(options.credentialRecordId) + + if (!credentialRecord.connectionId) { + throw new CredoError( + `No connection id for credential record ${credentialRecord.id} not found. Connection-less issuance does not support negotiation` + ) + } + + const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + + // Assert + connectionRecord.assertReady() + + const protocol = this.getProtocol(credentialRecord.protocolVersion) + const { message } = await protocol.negotiateOffer(this.agentContext, { + credentialFormats: options.credentialFormats, + credentialRecord, + comment: options.comment, + autoAcceptCredential: options.autoAcceptCredential, + goalCode: options.goalCode, + goal: options.goal, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: credentialRecord, + connectionRecord, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return credentialRecord + } + + /** + * Initiate a new credential exchange as issuer by creating a credential offer + * not bound to any connection. The offer must be delivered out-of-band to the holder + * @param options The credential options to use for the offer + * @returns The credential record and credential offer message + */ + public async createOffer(options: CreateCredentialOfferOptions): Promise<{ + message: AgentMessage + credentialRecord: CredentialExchangeRecord + }> { + const protocol = this.getProtocol(options.protocolVersion) + + this.logger.debug(`Got a credentialProtocol object for version ${options.protocolVersion}`) + const { message, credentialRecord } = await protocol.createOffer(this.agentContext, { + credentialFormats: options.credentialFormats, + comment: options.comment, + autoAcceptCredential: options.autoAcceptCredential, + goalCode: options.goalCode, + goal: options.goal, + }) + + this.logger.debug('Offer Message successfully created', { message }) + + return { message, credentialRecord } + } + + /** + * Accept a credential request as holder (by sending a credential request message) to the connection + * associated with the credential record. + * + * @param options The object containing config options of the request + * @returns CredentialExchangeRecord updated with information pertaining to this request + */ + public async acceptRequest(options: AcceptCredentialRequestOptions): Promise { + const credentialRecord = await this.getById(options.credentialRecordId) + + // with version we can get the Service + const protocol = this.getProtocol(credentialRecord.protocolVersion) + + this.logger.debug(`Got a credentialProtocol object for version ${credentialRecord.protocolVersion}`) + + // Use connection if present + const connectionRecord = credentialRecord.connectionId + ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + : undefined + connectionRecord?.assertReady() + + const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) + if (!requestMessage) { + throw new CredoError(`No request message found for credential record with id '${credentialRecord.id}'`) + } + const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) + if (!offerMessage) { + throw new CredoError(`No offer message found for credential record with id '${credentialRecord.id}'`) + } + + const { message } = await protocol.acceptRequest(this.agentContext, { + credentialRecord, + credentialFormats: options.credentialFormats, + comment: options.comment, + autoAcceptCredential: options.autoAcceptCredential, + }) + this.logger.debug('We have a credential message (sending outbound): ', message) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: credentialRecord, + lastReceivedMessage: requestMessage, + lastSentMessage: offerMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return credentialRecord + } + + /** + * Accept a credential as holder (by sending a credential acknowledgement message) to the connection + * associated with the credential record. + * + * @param credentialRecordId The id of the credential record for which to accept the credential + * @returns credential exchange record associated with the sent credential acknowledgement message + * + */ + public async acceptCredential(options: AcceptCredentialOptions): Promise { + const credentialRecord = await this.getById(options.credentialRecordId) + + // with version we can get the Service + const protocol = this.getProtocol(credentialRecord.protocolVersion) + + this.logger.debug(`Got a credentialProtocol object for version ${credentialRecord.protocolVersion}`) + + // Use connection if present + const connectionRecord = credentialRecord.connectionId + ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + : undefined + connectionRecord?.assertReady() + + const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) + if (!requestMessage) { + throw new CredoError(`No request message found for credential record with id '${credentialRecord.id}'`) + } + const credentialMessage = await protocol.findCredentialMessage(this.agentContext, credentialRecord.id) + if (!credentialMessage) { + throw new CredoError(`No credential message found for credential record with id '${credentialRecord.id}'`) + } + + const { message } = await protocol.acceptCredential(this.agentContext, { + credentialRecord, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: credentialRecord, + lastReceivedMessage: credentialMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return credentialRecord + } + + /** + * Send a revocation notification for a credential exchange record. Currently Revocation Notification V2 protocol is supported + * + * @param credentialRecordId The id of the credential record for which to send revocation notification + */ + public async sendRevocationNotification(options: SendRevocationNotificationOptions): Promise { + const { credentialRecordId, revocationId, revocationFormat, comment, requestAck } = options + + const credentialRecord = await this.getById(credentialRecordId) + + const { message } = await this.revocationNotificationService.v2CreateRevocationNotification({ + credentialId: revocationId, + revocationFormat, + comment, + requestAck, + }) + const protocol = this.getProtocol(credentialRecord.protocolVersion) + + const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) + if (!requestMessage) { + throw new CredoError(`No request message found for credential record with id '${credentialRecord.id}'`) + } + + const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) + if (!offerMessage) { + throw new CredoError(`No offer message found for credential record with id '${credentialRecord.id}'`) + } + + // Use connection if present + const connectionRecord = credentialRecord.connectionId + ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + : undefined + connectionRecord?.assertReady() + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: credentialRecord, + }) + await this.messageSender.sendMessage(outboundMessageContext) + } + + /** + * Send problem report message for a credential record + * @param credentialRecordId The id of the credential record for which to send problem report + * @returns credential record associated with the credential problem report message + */ + public async sendProblemReport(options: SendCredentialProblemReportOptions) { + const credentialRecord = await this.getById(options.credentialRecordId) + + const protocol = this.getProtocol(credentialRecord.protocolVersion) + + const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) + + const { message: problemReport } = await protocol.createProblemReport(this.agentContext, { + description: options.description, + credentialRecord, + }) + + // Use connection if present + const connectionRecord = credentialRecord.connectionId + ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + : undefined + connectionRecord?.assertReady() + + // If there's no connection (so connection-less, we require the state to be offer received) + if (!connectionRecord) { + credentialRecord.assertState(CredentialState.OfferReceived) + + if (!offerMessage) { + throw new CredoError(`No offer message found for credential record with id '${credentialRecord.id}'`) + } + } + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message: problemReport, + connectionRecord, + associatedRecord: credentialRecord, + lastReceivedMessage: offerMessage ?? undefined, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return credentialRecord + } + + public async getFormatData( + credentialRecordId: string + ): Promise>> { + const credentialRecord = await this.getById(credentialRecordId) + const protocol = this.getProtocol(credentialRecord.protocolVersion) + + return protocol.getFormatData(this.agentContext, credentialRecordId) + } + + /** + * Retrieve a credential record by id + * + * @param credentialRecordId The credential record id + * @throws {RecordNotFoundError} If no record is found + * @return The credential record + * + */ + public getById(credentialRecordId: string): Promise { + return this.credentialRepository.getById(this.agentContext, credentialRecordId) + } + + /** + * Retrieve all credential records + * + * @returns List containing all credential records + */ + public getAll(): Promise { + return this.credentialRepository.getAll(this.agentContext) + } + + /** + * Retrieve all credential records by specified query params + * + * @returns List containing all credential records matching specified query paramaters + */ + public findAllByQuery(query: Query) { + return this.credentialRepository.findByQuery(this.agentContext, query) + } + + /** + * Find a credential record by id + * + * @param credentialRecordId the credential record id + * @returns The credential record or null if not found + */ + public findById(credentialRecordId: string): Promise { + return this.credentialRepository.findById(this.agentContext, credentialRecordId) + } + + /** + * Delete a credential record by id, also calls service to delete from wallet + * + * @param credentialId the credential record id + * @param options the delete credential options for the delete operation + */ + public async deleteById(credentialId: string, options?: DeleteCredentialOptions) { + const credentialRecord = await this.getById(credentialId) + const protocol = this.getProtocol(credentialRecord.protocolVersion) + return protocol.delete(this.agentContext, credentialRecord, options) + } + + async deleteByIdLd(credentialId: string, options?: DeleteCredentialOptions) { + const credentialRecord = await this.getById(credentialId) + const protocol = await this.getServiceForCredentialExchangeId(credentialId) + return protocol.delete(this.agentContext, credentialRecord, options) +} + + /** + * Update a credential exchange record + * + * @param credentialRecord the credential exchange record + */ + public async update(credentialRecord: CredentialExchangeRecord): Promise { + await this.credentialRepository.update(this.agentContext, credentialRecord) + } + + public async findProposalMessage(credentialExchangeId: string): Promise> { + const protocol = await this.getServiceForCredentialExchangeId(credentialExchangeId) + + return protocol.findProposalMessage( + this.agentContext, + credentialExchangeId + ) as FindCredentialProposalMessageReturn + } + + public async findOfferMessage(credentialExchangeId: string): Promise> { + const protocol = await this.getServiceForCredentialExchangeId(credentialExchangeId) + + return protocol.findOfferMessage(this.agentContext, credentialExchangeId) as FindCredentialOfferMessageReturn + } + + public async findRequestMessage(credentialExchangeId: string): Promise> { + const protocol = await this.getServiceForCredentialExchangeId(credentialExchangeId) + + return protocol.findRequestMessage( + this.agentContext, + credentialExchangeId + ) as FindCredentialRequestMessageReturn + } + + public async findCredentialMessage(credentialExchangeId: string): Promise> { + const protocol = await this.getServiceForCredentialExchangeId(credentialExchangeId) + + return protocol.findCredentialMessage(this.agentContext, credentialExchangeId) as FindCredentialMessageReturn + } + + private async getServiceForCredentialExchangeId(credentialExchangeId: string) { + const credentialExchangeRecord = await this.getById(credentialExchangeId) + + return this.getProtocol(credentialExchangeRecord.protocolVersion) + } +} diff --git a/packages/core/src/modules/credentials/CredentialsApiOptions.ts b/packages/core/src/modules/credentials/CredentialsApiOptions.ts new file mode 100644 index 0000000000..98d3f4ace3 --- /dev/null +++ b/packages/core/src/modules/credentials/CredentialsApiOptions.ts @@ -0,0 +1,173 @@ +import type { CredentialFormatPayload } from './formats' +import type { AutoAcceptCredential } from './models' +import type { CredentialProtocol } from './protocol/CredentialProtocol' +import type { + CredentialFormatsFromProtocols, + DeleteCredentialOptions, + GetCredentialFormatDataReturn, +} from './protocol/CredentialProtocolOptions' + +// re-export GetCredentialFormatDataReturn type from protocol, as it is also used in the api +export type { GetCredentialFormatDataReturn, DeleteCredentialOptions } + +export type FindCredentialProposalMessageReturn = ReturnType< + CPs[number]['findProposalMessage'] +> +export type FindCredentialOfferMessageReturn = ReturnType< + CPs[number]['findOfferMessage'] +> +export type FindCredentialRequestMessageReturn = ReturnType< + CPs[number]['findRequestMessage'] +> +export type FindCredentialMessageReturn = ReturnType< + CPs[number]['findCredentialMessage'] +> + +/** + * Get the supported protocol versions based on the provided credential protocols. + */ +export type CredentialProtocolVersionType = + CPs[number]['version'] + +interface BaseOptions { + autoAcceptCredential?: AutoAcceptCredential + comment?: string + + /** + * Will be ignored for v1 protocol as it is not supported + */ + goalCode?: string + + /** + * Will be ignored for v1 protocol as it is not supported + */ + goal?: string +} + +/** + * Interface for CredentialsApi.proposeCredential. Will send a proposal. + */ +export interface ProposeCredentialOptions extends BaseOptions { + connectionId: string + protocolVersion: CredentialProtocolVersionType + credentialFormats: CredentialFormatPayload, 'createProposal'> +} + +/** + * Interface for CredentialsApi.acceptProposal. Will send an offer + * + * credentialFormats is optional because this is an accept method + */ +export interface AcceptCredentialProposalOptions + extends BaseOptions { + credentialRecordId: string + credentialFormats?: CredentialFormatPayload, 'acceptProposal'> +} + +/** + * Interface for CredentialsApi.negotiateProposal. Will send an offer + */ +export interface NegotiateCredentialProposalOptions + extends BaseOptions { + credentialRecordId: string + credentialFormats: CredentialFormatPayload, 'createOffer'> +} + +/** + * Interface for CredentialsApi.createOffer. Will create an out of band offer + */ +export interface CreateCredentialOfferOptions + extends BaseOptions { + protocolVersion: CredentialProtocolVersionType + credentialFormats: CredentialFormatPayload, 'createOffer'> +} + +/** + * Interface for CredentialsApi.offerCredential. Extends CreateCredentialOfferOptions, will send an offer + */ +export interface OfferCredentialOptions + extends BaseOptions, + CreateCredentialOfferOptions { + connectionId: string +} + +/** + * Interface for CredentialsApi.acceptOffer. Will send a request + * + * credentialFormats is optional because this is an accept method + */ +export interface AcceptCredentialOfferOptions + extends BaseOptions { + credentialRecordId: string + credentialFormats?: CredentialFormatPayload, 'acceptOffer'> +} + +/** + * Interface for CredentialsApi.negotiateOffer. Will send a proposal. + */ +export interface NegotiateCredentialOfferOptions + extends BaseOptions { + credentialRecordId: string + credentialFormats: CredentialFormatPayload, 'createProposal'> +} + +/** + * Interface for CredentialsApi.acceptRequest. Will send a credential + * + * credentialFormats is optional because this is an accept method + */ +export interface AcceptCredentialRequestOptions + extends BaseOptions { + credentialRecordId: string + credentialFormats?: CredentialFormatPayload, 'acceptRequest'> + autoAcceptCredential?: AutoAcceptCredential + comment?: string +} + +/** + * Interface for CredentialsApi.acceptCredential. Will send an ack message + */ +export interface AcceptCredentialOptions { + credentialRecordId: string +} + +/** + * Interface for CredentialsApi.sendRevocationNotification. Will send a revoke message + */ +export interface SendRevocationNotificationOptions { + credentialRecordId: string + revocationId: string // TODO: Get from record? + revocationFormat: string // TODO: Get from record? + comment?: string + requestAck?: boolean +} + +/** + * Interface for CredentialsApi.sendProblemReport. Will send a problem-report message + */ +export interface SendCredentialProblemReportOptions { + credentialRecordId: string + description: string +} + +/** + * Interface for CredentialsApi.declineOffer. Decline a received credential offer and optionally send a problem-report message to Issuer. + */ +export interface DeclineCredentialOfferOptions { + // TODO: in next major release, move the id to this object as well + // for consistency with the proofs api + // credentialRecordId: string + + /** + * Whether to send a problem-report message to the issuer as part + * of declining the credential offer + */ + sendProblemReport?: boolean + + /** + * Description to include in the problem-report message + * Only used if `sendProblemReport` is set to `true`. + * @default "Offer declined" + */ + problemReportDescription?: string +} diff --git a/packages/core/src/modules/credentials/CredentialsModule.ts b/packages/core/src/modules/credentials/CredentialsModule.ts new file mode 100644 index 0000000000..043a5bde5a --- /dev/null +++ b/packages/core/src/modules/credentials/CredentialsModule.ts @@ -0,0 +1,74 @@ +import type { CredentialsModuleConfigOptions } from './CredentialsModuleConfig' +import type { CredentialProtocol } from './protocol/CredentialProtocol' +import type { FeatureRegistry } from '../../agent/FeatureRegistry' +import type { ApiModule, DependencyManager } from '../../plugins' +import type { Constructor } from '../../utils/mixins' +import type { Optional } from '../../utils/type' + +import { Protocol } from '../../agent/models' + +import { CredentialsApi } from './CredentialsApi' +import { CredentialsModuleConfig } from './CredentialsModuleConfig' +import { RevocationNotificationService } from './protocol/revocation-notification/services' +import { V2CredentialProtocol } from './protocol/v2' +import { CredentialRepository } from './repository' + +/** + * Default credentialProtocols that will be registered if the `credentialProtocols` property is not configured. + */ +export type DefaultCredentialProtocols = [] + +// CredentialsModuleOptions makes the credentialProtocols property optional from the config, as it will set it when not provided. +export type CredentialsModuleOptions = Optional< + CredentialsModuleConfigOptions, + 'credentialProtocols' +> + +export class CredentialsModule + implements ApiModule +{ + public readonly config: CredentialsModuleConfig + + // Infer Api type from the config + public readonly api: Constructor> = CredentialsApi + + public constructor(config?: CredentialsModuleOptions) { + this.config = new CredentialsModuleConfig({ + ...config, + // NOTE: the credentialProtocols defaults are set in the CredentialsModule rather than the CredentialsModuleConfig to + // avoid dependency cycles. + credentialProtocols: config?.credentialProtocols ?? [new V2CredentialProtocol({ credentialFormats: [] })], + }) as CredentialsModuleConfig + } + + /** + * Registers the dependencies of the credentials module on the dependency manager. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Config + dependencyManager.registerInstance(CredentialsModuleConfig, this.config) + + // Services + dependencyManager.registerSingleton(RevocationNotificationService) + + // Repositories + dependencyManager.registerSingleton(CredentialRepository) + + // Features + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/revocation_notification/1.0', + roles: ['holder'], + }), + new Protocol({ + id: 'https://didcomm.org/revocation_notification/2.0', + roles: ['holder'], + }) + ) + + // Protocol needs to register feature registry items and handlers + for (const credentialProtocol of this.config.credentialProtocols) { + credentialProtocol.register(dependencyManager, featureRegistry) + } + } +} diff --git a/packages/core/src/modules/credentials/CredentialsModuleConfig.ts b/packages/core/src/modules/credentials/CredentialsModuleConfig.ts new file mode 100644 index 0000000000..e6d23909ed --- /dev/null +++ b/packages/core/src/modules/credentials/CredentialsModuleConfig.ts @@ -0,0 +1,47 @@ +import type { CredentialProtocol } from './protocol/CredentialProtocol' + +import { AutoAcceptCredential } from './models' + +/** + * CredentialsModuleConfigOptions defines the interface for the options of the CredentialsModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface CredentialsModuleConfigOptions { + /** + * Whether to automatically accept credential messages. Applies to all issue credential protocol versions. + * + * @default {@link AutoAcceptCredential.Never} + */ + autoAcceptCredentials?: AutoAcceptCredential + + /** + * Credential protocols to make available to the credentials module. Only one credential protocol should be registered for each credential + * protocol version. + * + * When not provided, the `V2CredentialProtocol` is registered by default. + * + * @default + * ``` + * [V2CredentialProtocol] + * ``` + */ + credentialProtocols: CredentialProtocols +} + +export class CredentialsModuleConfig { + private options: CredentialsModuleConfigOptions + + public constructor(options: CredentialsModuleConfigOptions) { + this.options = options + } + + /** See {@link CredentialsModuleConfigOptions.autoAcceptCredentials} */ + public get autoAcceptCredentials() { + return this.options.autoAcceptCredentials ?? AutoAcceptCredential.Never + } + + /** See {@link CredentialsModuleConfigOptions.credentialProtocols} */ + public get credentialProtocols() { + return this.options.credentialProtocols + } +} diff --git a/packages/core/src/modules/credentials/__tests__/CredentialsModule.test.ts b/packages/core/src/modules/credentials/__tests__/CredentialsModule.test.ts new file mode 100644 index 0000000000..5b5d4eb0d1 --- /dev/null +++ b/packages/core/src/modules/credentials/__tests__/CredentialsModule.test.ts @@ -0,0 +1,73 @@ +import type { CredentialProtocol } from '../protocol/CredentialProtocol' + +import { FeatureRegistry } from '../../../agent/FeatureRegistry' +import { Protocol } from '../../../agent/models/features/Protocol' +import { DependencyManager } from '../../../plugins/DependencyManager' +import { CredentialsModule } from '../CredentialsModule' +import { CredentialsModuleConfig } from '../CredentialsModuleConfig' +import { V2CredentialProtocol } from '../protocol' +import { RevocationNotificationService } from '../protocol/revocation-notification/services' +import { CredentialRepository } from '../repository' + +jest.mock('../../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +jest.mock('../../../agent/FeatureRegistry') +const FeatureRegistryMock = FeatureRegistry as jest.Mock + +const featureRegistry = new FeatureRegistryMock() + +describe('CredentialsModule', () => { + test('registers dependencies on the dependency manager', () => { + const credentialsModule = new CredentialsModule({ + credentialProtocols: [], + }) + credentialsModule.register(dependencyManager, featureRegistry) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(CredentialsModuleConfig, credentialsModule.config) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(RevocationNotificationService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(CredentialRepository) + + expect(featureRegistry.register).toHaveBeenCalledTimes(1) + expect(featureRegistry.register).toHaveBeenCalledWith( + new Protocol({ + id: 'https://didcomm.org/revocation_notification/1.0', + roles: ['holder'], + }), + new Protocol({ + id: 'https://didcomm.org/revocation_notification/2.0', + roles: ['holder'], + }) + ) + }) + + test('registers V2CredentialProtocol if no credentialProtocols are configured', () => { + const credentialsModule = new CredentialsModule() + + expect(credentialsModule.config.credentialProtocols).toEqual([expect.any(V2CredentialProtocol)]) + }) + + test('calls register on the provided CredentialProtocols', () => { + const registerMock = jest.fn() + const credentialProtocol = { + register: registerMock, + } as unknown as CredentialProtocol + + const credentialsModule = new CredentialsModule({ + credentialProtocols: [credentialProtocol], + }) + + expect(credentialsModule.config.credentialProtocols).toEqual([credentialProtocol]) + + credentialsModule.register(dependencyManager, featureRegistry) + + expect(registerMock).toHaveBeenCalledTimes(1) + expect(registerMock).toHaveBeenCalledWith(dependencyManager, featureRegistry) + }) +}) diff --git a/packages/core/src/modules/credentials/__tests__/CredentialsModuleConfig.test.ts b/packages/core/src/modules/credentials/__tests__/CredentialsModuleConfig.test.ts new file mode 100644 index 0000000000..23a0e1c6e6 --- /dev/null +++ b/packages/core/src/modules/credentials/__tests__/CredentialsModuleConfig.test.ts @@ -0,0 +1,26 @@ +import type { CredentialProtocol } from '../protocol/CredentialProtocol' + +import { CredentialsModuleConfig } from '../CredentialsModuleConfig' +import { AutoAcceptCredential } from '../models' + +describe('CredentialsModuleConfig', () => { + test('sets default values', () => { + const config = new CredentialsModuleConfig({ + credentialProtocols: [], + }) + + expect(config.autoAcceptCredentials).toBe(AutoAcceptCredential.Never) + expect(config.credentialProtocols).toEqual([]) + }) + + test('sets values', () => { + const credentialProtocol = jest.fn() as unknown as CredentialProtocol + const config = new CredentialsModuleConfig({ + autoAcceptCredentials: AutoAcceptCredential.Always, + credentialProtocols: [credentialProtocol], + }) + + expect(config.autoAcceptCredentials).toBe(AutoAcceptCredential.Always) + expect(config.credentialProtocols).toEqual([credentialProtocol]) + }) +}) diff --git a/packages/core/src/modules/credentials/__tests__/fixtures.ts b/packages/core/src/modules/credentials/__tests__/fixtures.ts new file mode 100644 index 0000000000..057ae8d4e1 --- /dev/null +++ b/packages/core/src/modules/credentials/__tests__/fixtures.ts @@ -0,0 +1,21 @@ +export const credReq = { + prover_did: 'did:sov:Y8iyDrCHfUpBY2jkd7Utfx', + cred_def_id: 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:51:TAG', + blinded_ms: { + u: '110610123432332476473375007487247709218419524765032439076208019871743569018252586850427838771931221771227203551775289761586009084292284314207436640231052129266015503401118322009304919643287710408379757802540667358968471419257863330969561198349637578063688118910240720917456714103872180385172499545967921817473077820161374967377407759331556210823439440478684915287345759439215952485377081630435110911287494666818169608863639467996786227107447757434904894305851282532340335379056077475867151483520074334113239997171746478579695337411744772387197598863836759115206573022265599781958164663366458791934494773405738216913411', + ur: null, + hidden_attributes: ['master_secret'], + committed_attributes: {}, + }, + blinded_ms_correctness_proof: { + c: '74166567145664716669042749172899862913175746842119925863709522367997555162535', + v_dash_cap: + '1891661791592401364793544973569850112519453874155294114300886230795255714579603892516573573155105241417827172655027285062713792077137917614458690245067502490043126222829248919183676387904671567784621260696991226361605344734978904242726352512061421137336169348863177667958333777571812458318894495425085637370715152338807798447174855274779220884193480392221426666786386198680359381546692118689959879385498358879593493608080913336396532253364578927496868954362997951935977034507467171417802640352406191044080192001188762610962085274270807255753335099171457405366335155255038768918649029766176047384127483587155470131765852176320591348954350985301805080951657475246349277435569952922829946940821962356900415616036024524136', + m_caps: { + master_secret: + '32296179824587808657350024608644011637567680645343910724911461554002267640642014452361757388185386803499726200537448417105380225841945137943648126052207146380258164316458003146028', + }, + r_caps: {}, + }, + nonce: '784158051402761459123237', +} diff --git a/packages/core/src/modules/credentials/formats/CredentialFormat.ts b/packages/core/src/modules/credentials/formats/CredentialFormat.ts new file mode 100644 index 0000000000..add9c51212 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/CredentialFormat.ts @@ -0,0 +1,44 @@ +/** + * Get the payload for a specific method from a list of CredentialFormat interfaces and a method + * + * @example + * ``` + * + * type CreateOfferCredentialFormats = CredentialFormatPayload<[IndyCredentialFormat, JsonLdCredentialFormat], 'createOffer'> + * + * // equal to + * type CreateOfferCredentialFormats = { + * indy: { + * // ... params for indy create offer ... + * }, + * jsonld: { + * // ... params for jsonld create offer ... + * } + * } + * ``` + */ +export type CredentialFormatPayload< + CFs extends CredentialFormat[], + M extends keyof CredentialFormat['credentialFormats'] +> = { + [CredentialFormat in CFs[number] as CredentialFormat['formatKey']]?: CredentialFormat['credentialFormats'][M] +} + +export interface CredentialFormat { + formatKey: string // e.g. 'credentialManifest', cannot be shared between different formats + credentialRecordType: string // e.g. 'w3c', can be shared between multiple formats + credentialFormats: { + createProposal: unknown + acceptProposal: unknown + createOffer: unknown + acceptOffer: unknown + createRequest: unknown + acceptRequest: unknown + } + formatData: { + proposal: unknown + offer: unknown + request: unknown + credential: unknown + } +} diff --git a/packages/core/src/modules/credentials/formats/CredentialFormatService.ts b/packages/core/src/modules/credentials/formats/CredentialFormatService.ts new file mode 100644 index 0000000000..ac1ffde0a9 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/CredentialFormatService.ts @@ -0,0 +1,82 @@ +import type { CredentialFormat } from './CredentialFormat' +import type { + CredentialFormatCreateProposalOptions, + CredentialFormatCreateProposalReturn, + CredentialFormatProcessOptions, + CredentialFormatCreateOfferOptions, + CredentialFormatCreateOfferReturn, + CredentialFormatCreateRequestOptions, + CredentialFormatCreateReturn, + CredentialFormatAcceptRequestOptions, + CredentialFormatAcceptOfferOptions, + CredentialFormatAcceptProposalOptions, + CredentialFormatAutoRespondCredentialOptions, + CredentialFormatAutoRespondOfferOptions, + CredentialFormatAutoRespondProposalOptions, + CredentialFormatAutoRespondRequestOptions, + CredentialFormatProcessCredentialOptions, +} from './CredentialFormatServiceOptions' +import type { AgentContext } from '../../../agent' + +export interface CredentialFormatService { + formatKey: CF['formatKey'] + credentialRecordType: CF['credentialRecordType'] + + // proposal methods + createProposal( + agentContext: AgentContext, + options: CredentialFormatCreateProposalOptions + ): Promise + processProposal(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise + acceptProposal( + agentContext: AgentContext, + options: CredentialFormatAcceptProposalOptions + ): Promise + + // offer methods + createOffer( + agentContext: AgentContext, + options: CredentialFormatCreateOfferOptions + ): Promise + processOffer(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise + acceptOffer( + agentContext: AgentContext, + options: CredentialFormatAcceptOfferOptions + ): Promise + + // request methods + createRequest( + agentContext: AgentContext, + options: CredentialFormatCreateRequestOptions + ): Promise + processRequest(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise + acceptRequest( + agentContext: AgentContext, + options: CredentialFormatAcceptRequestOptions + ): Promise + + // credential methods + processCredential(agentContext: AgentContext, options: CredentialFormatProcessCredentialOptions): Promise + + // auto accept methods + shouldAutoRespondToProposal( + agentContext: AgentContext, + options: CredentialFormatAutoRespondProposalOptions + ): Promise + shouldAutoRespondToOffer( + agentContext: AgentContext, + options: CredentialFormatAutoRespondOfferOptions + ): Promise + shouldAutoRespondToRequest( + agentContext: AgentContext, + options: CredentialFormatAutoRespondRequestOptions + ): Promise + shouldAutoRespondToCredential( + agentContext: AgentContext, + options: CredentialFormatAutoRespondCredentialOptions + ): Promise + + deleteCredentialById(agentContext: AgentContext, credentialId: string): Promise + + supportsFormat(formatIdentifier: string): boolean +} diff --git a/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts b/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts new file mode 100644 index 0000000000..1a70479450 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts @@ -0,0 +1,138 @@ +import type { CredentialFormat, CredentialFormatPayload } from './CredentialFormat' +import type { CredentialFormatService } from './CredentialFormatService' +import type { Attachment } from '../../../decorators/attachment/Attachment' +import type { CredentialFormatSpec } from '../models/CredentialFormatSpec' +import type { CredentialPreviewAttributeOptions } from '../models/CredentialPreviewAttribute' +import type { CredentialExchangeRecord } from '../repository/CredentialExchangeRecord' + +/** + * Infer the {@link CredentialFormat} based on a {@link CredentialFormatService}. + * + * It does this by extracting the `CredentialFormat` generic from the `CredentialFormatService`. + * + * @example + * ``` + * // TheCredentialFormat is now equal to IndyCredentialFormat + * type TheCredentialFormat = ExtractCredentialFormat + * ``` + * + * Because the `IndyCredentialFormatService` is defined as follows: + * ``` + * class IndyCredentialFormatService implements CredentialFormatService { + * } + * ``` + */ +export type ExtractCredentialFormat = Type extends CredentialFormatService + ? CredentialFormat + : never + +/** + * Infer an array of {@link CredentialFormat} types based on an array of {@link CredentialFormatService} types. + * + * This is based on {@link ExtractCredentialFormat}, but allows to handle arrays. + */ +export type ExtractCredentialFormats = { + [CF in keyof CFs]: ExtractCredentialFormat +} + +/** + * Base return type for all methods that create an attachment format. + * + * It requires an attachment and a format to be returned. + */ +export interface CredentialFormatCreateReturn { + format: CredentialFormatSpec + attachment: Attachment + appendAttachments?: Attachment[] +} + +/** + * Base return type for all credential process methods. + */ +export interface CredentialFormatProcessOptions { + attachment: Attachment + credentialRecord: CredentialExchangeRecord +} + +export interface CredentialFormatProcessCredentialOptions extends CredentialFormatProcessOptions { + offerAttachment: Attachment + requestAttachment: Attachment + requestAppendAttachments?: Attachment[] +} + +export interface CredentialFormatCreateProposalOptions { + credentialRecord: CredentialExchangeRecord + credentialFormats: CredentialFormatPayload<[CF], 'createProposal'> + attachmentId?: string +} + +export interface CredentialFormatAcceptProposalOptions { + credentialRecord: CredentialExchangeRecord + credentialFormats?: CredentialFormatPayload<[CF], 'acceptProposal'> + attachmentId?: string + + proposalAttachment: Attachment +} + +export interface CredentialFormatCreateProposalReturn extends CredentialFormatCreateReturn { + previewAttributes?: CredentialPreviewAttributeOptions[] +} + +export interface CredentialFormatCreateOfferOptions { + credentialRecord: CredentialExchangeRecord + credentialFormats: CredentialFormatPayload<[CF], 'createOffer'> + attachmentId?: string +} + +export interface CredentialFormatAcceptOfferOptions { + credentialRecord: CredentialExchangeRecord + credentialFormats?: CredentialFormatPayload<[CF], 'acceptOffer'> + attachmentId?: string + offerAttachment: Attachment +} + +export interface CredentialFormatCreateOfferReturn extends CredentialFormatCreateReturn { + previewAttributes?: CredentialPreviewAttributeOptions[] +} + +export interface CredentialFormatCreateRequestOptions { + credentialRecord: CredentialExchangeRecord + credentialFormats: CredentialFormatPayload<[CF], 'createRequest'> +} + +export interface CredentialFormatAcceptRequestOptions { + credentialRecord: CredentialExchangeRecord + credentialFormats?: CredentialFormatPayload<[CF], 'acceptRequest'> + attachmentId?: string + offerAttachment?: Attachment + requestAttachment: Attachment + requestAppendAttachments?: Attachment[] +} + +// Auto accept method interfaces +export interface CredentialFormatAutoRespondProposalOptions { + credentialRecord: CredentialExchangeRecord + proposalAttachment: Attachment + offerAttachment: Attachment +} + +export interface CredentialFormatAutoRespondOfferOptions { + credentialRecord: CredentialExchangeRecord + proposalAttachment: Attachment + offerAttachment: Attachment +} + +export interface CredentialFormatAutoRespondRequestOptions { + credentialRecord: CredentialExchangeRecord + proposalAttachment?: Attachment + offerAttachment: Attachment + requestAttachment: Attachment +} + +export interface CredentialFormatAutoRespondCredentialOptions { + credentialRecord: CredentialExchangeRecord + proposalAttachment?: Attachment + offerAttachment?: Attachment + requestAttachment: Attachment + credentialAttachment: Attachment +} diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts new file mode 100644 index 0000000000..d8be4784b7 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts @@ -0,0 +1,64 @@ +import type { + AnonCredsLinkSecretCredentialRequestOptions as AnonCredsLinkSecretAcceptOfferOptions, + DataIntegrityCredential, + DataIntegrityCredentialOffer, + DataIntegrityCredentialRequest, + DidCommSignedAttachmentCredentialRequestOptions as DidCommSignedAttachmentAcceptOfferOptions, + W3C_VC_DATA_MODEL_VERSION, +} from './dataIntegrityExchange' +import type { CredentialFormat, JsonObject } from '../../../..' +import type { W3cCredential } from '../../../vc' + +export interface AnonCredsLinkSecretCreateOfferOptions { + credentialDefinitionId: string + revocationRegistryDefinitionId?: string + revocationRegistryIndex?: number +} + +export interface DidCommSignedAttachmentCreateOfferOptions { + didMethodsSupported?: string[] + algsSupported?: string[] +} + +export interface DataIntegrityAcceptOfferFormat { + dataModelVersion?: W3C_VC_DATA_MODEL_VERSION + didCommSignedAttachment?: DidCommSignedAttachmentAcceptOfferOptions + anonCredsLinkSecret?: AnonCredsLinkSecretAcceptOfferOptions +} + +/** + * This defines the module payload for calling CredentialsApi.offerCredential + */ +export interface DataIntegrityOfferCredentialFormat { + credential: W3cCredential | JsonObject + bindingRequired: boolean + anonCredsLinkSecretBinding?: AnonCredsLinkSecretCreateOfferOptions + didCommSignedAttachmentBinding?: DidCommSignedAttachmentCreateOfferOptions +} + +/** + * This defines the module payload for calling CredentialsApi.acceptRequest + */ +export interface DataIntegrityAcceptRequestFormat { + credentialSubjectId?: string + issuerVerificationMethod?: string +} + +export interface DataIntegrityCredentialFormat extends CredentialFormat { + formatKey: 'dataIntegrity' + credentialRecordType: 'w3c' + credentialFormats: { + createProposal: never + acceptProposal: never + createOffer: DataIntegrityOfferCredentialFormat + acceptOffer: DataIntegrityAcceptOfferFormat + createRequest: never // cannot start from createRequest + acceptRequest: DataIntegrityAcceptRequestFormat + } + formatData: { + proposal: never + offer: DataIntegrityCredentialOffer + request: DataIntegrityCredentialRequest + credential: DataIntegrityCredential + } +} diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts new file mode 100644 index 0000000000..c57124b270 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts @@ -0,0 +1,179 @@ +import { Expose, Type } from 'class-transformer' +import { ArrayNotEmpty, IsBoolean, IsEnum, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { JsonObject } from '../../../../types' +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { W3cCredential } from '../../../vc' + +const SUPPORTED_W3C_VC_DATA_MODEL_VERSIONS = ['1.1', '2.0'] as const +export type W3C_VC_DATA_MODEL_VERSION = (typeof SUPPORTED_W3C_VC_DATA_MODEL_VERSIONS)[number] + +export interface AnonCredsLinkSecretBindingMethodOptions { + credentialDefinitionId: string + nonce: string + keyCorrectnessProof: Record +} + +// This binding method is intended to be used in combination with a credential containing an AnonCreds proof. +export class AnonCredsLinkSecretBindingMethod { + public constructor(options: AnonCredsLinkSecretBindingMethodOptions) { + if (options) { + this.credentialDefinitionId = options.credentialDefinitionId + this.nonce = options.nonce + this.keyCorrectnessProof = options.keyCorrectnessProof + } + } + + @IsString() + @Expose({ name: 'cred_def_id' }) + public credentialDefinitionId!: string + + @IsString() + public nonce!: string + + @Expose({ name: 'key_correctness_proof' }) + public keyCorrectnessProof!: Record +} + +export interface DidCommSignedAttachmentBindingMethodOptions { + algSupported: string[] + didMethodsSupported: string[] + nonce: string +} + +export class DidCommSignedAttachmentBindingMethod { + public constructor(options: DidCommSignedAttachmentBindingMethodOptions) { + if (options) { + this.algsSupported = options.algSupported + this.didMethodsSupported = options.didMethodsSupported + this.nonce = options.nonce + } + } + + @IsString({ each: true }) + @Expose({ name: 'algs_supported' }) + public algsSupported!: string[] + + @IsString({ each: true }) + @Expose({ name: 'did_methods_supported' }) + public didMethodsSupported!: string[] + + @IsString() + public nonce!: string +} + +export interface DataIntegrityBindingMethodsOptions { + anonCredsLinkSecret?: AnonCredsLinkSecretBindingMethod + didcommSignedAttachment?: DidCommSignedAttachmentBindingMethod +} + +export class DataIntegrityBindingMethods { + public constructor(options: DataIntegrityBindingMethodsOptions) { + if (options) { + this.anoncredsLinkSecret = options.anonCredsLinkSecret + this.didcommSignedAttachment = options.didcommSignedAttachment + } + } + + @IsOptional() + @ValidateNested() + @Type(() => AnonCredsLinkSecretBindingMethod) + @Expose({ name: 'anoncreds_link_secret' }) + public anoncredsLinkSecret?: AnonCredsLinkSecretBindingMethod + + @IsOptional() + @ValidateNested() + @Type(() => DidCommSignedAttachmentBindingMethod) + @Expose({ name: 'didcomm_signed_attachment' }) + public didcommSignedAttachment?: DidCommSignedAttachmentBindingMethod +} + +export interface DataIntegrityCredentialOfferOptions { + dataModelVersionsSupported: W3C_VC_DATA_MODEL_VERSION[] + bindingRequired?: boolean + bindingMethod?: DataIntegrityBindingMethods + credential: W3cCredential | JsonObject +} + +export class DataIntegrityCredentialOffer { + public constructor(options: DataIntegrityCredentialOfferOptions) { + if (options) { + this.credential = + options.credential instanceof W3cCredential ? JsonTransformer.toJSON(options.credential) : options.credential + this.bindingRequired = options.bindingRequired + this.bindingMethod = options.bindingMethod + this.dataModelVersionsSupported = options.dataModelVersionsSupported + } + } + + // List of strings indicating the supported VC Data Model versions. + // The list MUST contain at least one value. The values MUST be a valid data model version. Current supported values include 1.1 and 2.0. + @ArrayNotEmpty() + @IsEnum(SUPPORTED_W3C_VC_DATA_MODEL_VERSIONS, { each: true }) + @Expose({ name: 'data_model_versions_supported' }) + public dataModelVersionsSupported!: W3C_VC_DATA_MODEL_VERSION[] + + // Boolean indicating whether the credential MUST be bound to the holder. If omitted, the credential is not required to be bound to the holder. + // If set to true, the credential MUST be bound to the holder using at least one of the binding methods defined in binding_method. + @IsOptional() + @IsBoolean() + @Expose({ name: 'binding_required' }) + public bindingRequired?: boolean + + // Required if binding_required is true. + // Object containing key-value pairs of binding methods supported by the issuer to bind the credential to a holder. + // If the value is omitted, this indicates the issuer does not support any binding methods for issuance of the credential. + @IsOptional() + @ValidateNested() + @Type(() => DataIntegrityBindingMethods) + @Expose({ name: 'binding_method' }) + public bindingMethod?: DataIntegrityBindingMethods + + // The credential should be compliant with the VC Data Model. + // The credential MUST NOT contain any proofs. + // Some properties MAY be omitted if they will only be available at time of issuance, such as issuanceDate, issuer, credentialSubject.id, credentialStatus, credentialStatus.id. + // The credential MUST be conformant with one of the data model versions indicated in data_model_versions_supported. + @Expose({ name: 'credential' }) + public credential!: JsonObject +} + +export interface AnonCredsLinkSecretDataIntegrityBindingProof { + cred_def_id: string + entropy: string + blinded_ms: Record + blinded_ms_correctness_proof: Record + nonce: string +} + +export interface DidCommSignedAttachmentDataIntegrityBindingProof { + // The id of the appended attachment included in the request message that contains the signed attachment. + attachment_id: string +} + +export interface DataIntegrityCredentialRequestBindingProof { + anoncreds_link_secret?: AnonCredsLinkSecretDataIntegrityBindingProof + didcomm_signed_attachment?: DidCommSignedAttachmentDataIntegrityBindingProof +} + +export interface DataIntegrityCredentialRequest { + // The data model version of the credential to be issued. The value MUST be a valid data model version and match one of the values from the data_model_versions_supported offer. + data_model_version: W3C_VC_DATA_MODEL_VERSION + // Required if binding_required is true in the offer. + // Object containing key-value pairs of proofs for the binding to the holder. + // The keys MUST match keys of the binding_method object from the offer. + // See Binding Methods for a registry of default binding methods supported as part of this RFC. + binding_proof?: DataIntegrityCredentialRequestBindingProof +} + +export interface AnonCredsLinkSecretCredentialRequestOptions { + linkSecretId?: string +} + +export interface DidCommSignedAttachmentCredentialRequestOptions { + kid: string + alg?: string +} + +export interface DataIntegrityCredential { + credential: JsonObject +} diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts new file mode 100644 index 0000000000..7d053b9f0f --- /dev/null +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts @@ -0,0 +1,2 @@ +export * from './DataIntegrityCredentialFormat' +export * from './dataIntegrityExchange' diff --git a/packages/core/src/modules/credentials/formats/index.ts b/packages/core/src/modules/credentials/formats/index.ts new file mode 100644 index 0000000000..38d2aaa5a1 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/index.ts @@ -0,0 +1,5 @@ +export * from './CredentialFormatService' +export * from './CredentialFormatServiceOptions' +export * from './CredentialFormat' +export * from './jsonld' +export * from './dataIntegrity' diff --git a/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialDetail.ts b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialDetail.ts new file mode 100644 index 0000000000..734fc6366c --- /dev/null +++ b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialDetail.ts @@ -0,0 +1,30 @@ +import { Expose, Type } from 'class-transformer' + +import { W3cCredential } from '../../../vc/models/credential/W3cCredential' + +import { JsonLdCredentialDetailOptions } from './JsonLdCredentialDetailOptions' + +export interface JsonLdCredentialDetailInputOptions { + credential: W3cCredential + options: JsonLdCredentialDetailOptions +} + +/** + * Class providing validation for the V2 json ld credential as per RFC0593 (used to sign credentials) + * + */ +export class JsonLdCredentialDetail { + public constructor(options: JsonLdCredentialDetailInputOptions) { + if (options) { + this.credential = options.credential + this.options = options.options + } + } + + @Type(() => W3cCredential) + public credential!: W3cCredential + + @Expose({ name: 'options' }) + @Type(() => JsonLdCredentialDetailOptions) + public options!: JsonLdCredentialDetailOptions +} diff --git a/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialDetailOptions.ts b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialDetailOptions.ts new file mode 100644 index 0000000000..bbc33c06c6 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialDetailOptions.ts @@ -0,0 +1,59 @@ +import { IsObject, IsOptional, IsString } from 'class-validator' + +export interface JsonLdCredentialDetailCredentialStatusOptions { + type: string +} + +export class JsonLdCredentialDetailCredentialStatus { + public constructor(options: JsonLdCredentialDetailCredentialStatusOptions) { + if (options) { + this.type = options.type + } + } + @IsString() + public type!: string +} + +export interface JsonLdCredentialDetailOptionsOptions { + proofPurpose: string + created?: string + domain?: string + challenge?: string + credentialStatus?: JsonLdCredentialDetailCredentialStatus + proofType: string +} + +export class JsonLdCredentialDetailOptions { + public constructor(options: JsonLdCredentialDetailOptionsOptions) { + if (options) { + this.proofPurpose = options.proofPurpose + this.created = options.created + this.domain = options.domain + this.challenge = options.challenge + this.credentialStatus = options.credentialStatus + this.proofType = options.proofType + } + } + + @IsString() + public proofPurpose!: string + + @IsString() + @IsOptional() + public created?: string + + @IsString() + @IsOptional() + public domain?: string + + @IsString() + @IsOptional() + public challenge?: string + + @IsString() + public proofType!: string + + @IsOptional() + @IsObject() + public credentialStatus?: JsonLdCredentialDetailCredentialStatus +} diff --git a/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormat.ts b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormat.ts new file mode 100644 index 0000000000..298961370a --- /dev/null +++ b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormat.ts @@ -0,0 +1,101 @@ +import type { JsonObject } from '../../../../types' +import type { SingleOrArray } from '../../../../utils' +import type { W3cIssuerOptions } from '../../../vc/models/credential/W3cIssuer' +import type { CredentialFormat } from '../CredentialFormat' + +export interface JsonCredential { + '@context': Array | JsonObject + id?: string + type: Array + issuer: string | W3cIssuerOptions + issuanceDate: string + expirationDate?: string + credentialSubject: SingleOrArray + [key: string]: unknown +} + +/** + * Format for creating a jsonld proposal, offer or request. + */ +export interface JsonLdCredentialDetailFormat { + credential: JsonCredential + options: { + proofPurpose: string + proofType: string + } +} + +// use empty object in the acceptXXX jsonld format interface so we indicate that +// the jsonld format service needs to be invoked +type EmptyObject = Record + +/** + * Format for accepting a jsonld credential request. Optionally allows the verification + * method to use to sign the credential. + */ +export interface JsonLdAcceptRequestFormat { + verificationMethod?: string +} + +export interface JsonLdCredentialFormat extends CredentialFormat { + formatKey: 'jsonld' + credentialRecordType: 'w3c' + credentialFormats: { + createProposal: JsonLdCredentialDetailFormat + acceptProposal: EmptyObject + createOffer: JsonLdCredentialDetailFormat + acceptOffer: EmptyObject + createRequest: JsonLdCredentialDetailFormat + acceptRequest: JsonLdAcceptRequestFormat + } + formatData: { + proposal: JsonLdFormatDataCredentialDetail + offer: JsonLdFormatDataCredentialDetail + request: JsonLdFormatDataCredentialDetail + credential: JsonLdFormatDataVerifiableCredential + } +} + +/** + * Represents a signed verifiable credential. Only meant to be used for credential + * format data interfaces. + */ +export interface JsonLdFormatDataVerifiableCredential extends JsonCredential { + proof: { + type: string + proofPurpose: string + verificationMethod: string + created: string + domain?: string + challenge?: string + jws?: string + proofValue?: string + nonce?: string + [key: string]: unknown + } +} + +/** + * Represents the jsonld credential detail. Only meant to be used for credential + * format data interfaces. + */ +export interface JsonLdFormatDataCredentialDetail { + credential: JsonCredential + options: JsonLdFormatDataCredentialDetailOptions +} + +/** + * Represents the jsonld credential detail options. Only meant to be used for credential + * format data interfaces. + */ +export interface JsonLdFormatDataCredentialDetailOptions { + proofPurpose: string + proofType: string + created?: string + domain?: string + challenge?: string + credentialStatus?: { + type: string + [key: string]: unknown + } +} diff --git a/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.ts b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.ts new file mode 100644 index 0000000000..3829d4b7cb --- /dev/null +++ b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.ts @@ -0,0 +1,447 @@ +import type { + JsonLdCredentialFormat, + JsonCredential, + JsonLdFormatDataCredentialDetail, + JsonLdFormatDataVerifiableCredential, +} from './JsonLdCredentialFormat' +import type { AgentContext } from '../../../../agent' +import type { CredentialFormatService } from '../CredentialFormatService' +import type { + CredentialFormatAcceptOfferOptions, + CredentialFormatAcceptProposalOptions, + CredentialFormatAcceptRequestOptions, + CredentialFormatAutoRespondOfferOptions, + CredentialFormatAutoRespondProposalOptions, + CredentialFormatAutoRespondRequestOptions, + CredentialFormatCreateOfferOptions, + CredentialFormatCreateOfferReturn, + CredentialFormatCreateProposalOptions, + CredentialFormatCreateProposalReturn, + CredentialFormatCreateRequestOptions, + CredentialFormatCreateReturn, + CredentialFormatProcessCredentialOptions, + CredentialFormatProcessOptions, + CredentialFormatAutoRespondCredentialOptions, +} from '../CredentialFormatServiceOptions' + +import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' +import { CredoError } from '../../../../error' +import { JsonEncoder, areObjectsEqual } from '../../../../utils' +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { findVerificationMethodByKeyType } from '../../../dids/domain/DidDocument' +import { DidResolverService } from '../../../dids/services/DidResolverService' +import { ClaimFormat, W3cCredential, W3cCredentialService, W3cJsonLdVerifiableCredential } from '../../../vc' +import { W3cJsonLdCredentialService } from '../../../vc/data-integrity/W3cJsonLdCredentialService' +import { CredentialFormatSpec } from '../../models/CredentialFormatSpec' + +import { JsonLdCredentialDetail } from './JsonLdCredentialDetail' + +const JSONLD_VC_DETAIL = 'aries/ld-proof-vc-detail@v1.0' +const JSONLD_VC = 'aries/ld-proof-vc@v1.0' + +export class JsonLdCredentialFormatService implements CredentialFormatService { + public readonly formatKey = 'jsonld' as const + public readonly credentialRecordType = 'w3c' as const + + /** + * Create a {@link AttachmentFormats} object dependent on the message type. + * + * @param options The object containing all the options for the proposed credential + * @returns object containing associated attachment, formats and filtersAttach elements + * + */ + public async createProposal( + agentContext: AgentContext, + { credentialFormats }: CredentialFormatCreateProposalOptions + ): Promise { + const format = new CredentialFormatSpec({ + format: JSONLD_VC_DETAIL, + }) + + const jsonLdFormat = credentialFormats.jsonld + if (!jsonLdFormat) { + throw new CredoError('Missing jsonld payload in createProposal') + } + + // this does the validation + JsonTransformer.fromJSON(jsonLdFormat.credential, JsonLdCredentialDetail) + + // jsonLdFormat is now of type JsonLdFormatDataCredentialDetail + const attachment = this.getFormatData(jsonLdFormat, format.attachmentId) + return { format, attachment } + } + + /** + * Method called on reception of a propose credential message + * @param options the options needed to accept the proposal + */ + public async processProposal( + agentContext: AgentContext, + { attachment }: CredentialFormatProcessOptions + ): Promise { + const credProposalJson = attachment.getDataAsJson() + + if (!credProposalJson) { + throw new CredoError('Missing jsonld credential proposal data payload') + } + + // validation is done in here + JsonTransformer.fromJSON(credProposalJson, JsonLdCredentialDetail) + } + + public async acceptProposal( + agentContext: AgentContext, + { attachmentId, proposalAttachment }: CredentialFormatAcceptProposalOptions + ): Promise { + // if the offer has an attachment Id use that, otherwise the generated id of the formats object + const format = new CredentialFormatSpec({ + attachmentId, + format: JSONLD_VC_DETAIL, + }) + + const credentialProposal = proposalAttachment.getDataAsJson() + JsonTransformer.fromJSON(credentialProposal, JsonLdCredentialDetail) + + const offerData = credentialProposal + + const attachment = this.getFormatData(offerData, format.attachmentId) + + return { format, attachment } + } + + /** + * Create a {@link AttachmentFormats} object dependent on the message type. + * + * @param options The object containing all the options for the credential offer + * @returns object containing associated attachment, formats and offersAttach elements + * + */ + public async createOffer( + agentContext: AgentContext, + { credentialFormats, attachmentId }: CredentialFormatCreateOfferOptions + ): Promise { + // if the offer has an attachment Id use that, otherwise the generated id of the formats object + const format = new CredentialFormatSpec({ + attachmentId, + format: JSONLD_VC_DETAIL, + }) + + const jsonLdFormat = credentialFormats?.jsonld + if (!jsonLdFormat) { + throw new CredoError('Missing jsonld payload in createOffer') + } + + // validate + JsonTransformer.fromJSON(jsonLdFormat.credential, JsonLdCredentialDetail) + + const attachment = this.getFormatData(jsonLdFormat, format.attachmentId) + + return { format, attachment } + } + + public async processOffer(agentContext: AgentContext, { attachment }: CredentialFormatProcessOptions) { + const credentialOfferJson = attachment.getDataAsJson() + + if (!credentialOfferJson) { + throw new CredoError('Missing jsonld credential offer data payload') + } + + JsonTransformer.fromJSON(credentialOfferJson, JsonLdCredentialDetail) + } + + public async acceptOffer( + agentContext: AgentContext, + { attachmentId, offerAttachment }: CredentialFormatAcceptOfferOptions + ): Promise { + const credentialOffer = offerAttachment.getDataAsJson() + + // validate + JsonTransformer.fromJSON(credentialOffer, JsonLdCredentialDetail) + + const format = new CredentialFormatSpec({ + attachmentId, + format: JSONLD_VC_DETAIL, + }) + + const attachment = this.getFormatData(credentialOffer, format.attachmentId) + return { format, attachment } + } + + /** + * Create a credential attachment format for a credential request. + * + * @param options The object containing all the options for the credential request is derived + * @returns object containing associated attachment, formats and requestAttach elements + * + */ + public async createRequest( + agentContext: AgentContext, + { credentialFormats }: CredentialFormatCreateRequestOptions + ): Promise { + const jsonLdFormat = credentialFormats?.jsonld + + const format = new CredentialFormatSpec({ + format: JSONLD_VC_DETAIL, + }) + + if (!jsonLdFormat) { + throw new CredoError('Missing jsonld payload in createRequest') + } + + // this does the validation + JsonTransformer.fromJSON(jsonLdFormat.credential, JsonLdCredentialDetail) + + const attachment = this.getFormatData(jsonLdFormat, format.attachmentId) + + return { format, attachment } + } + + public async processRequest( + agentContext: AgentContext, + { attachment }: CredentialFormatProcessOptions + ): Promise { + const requestJson = attachment.getDataAsJson() + + if (!requestJson) { + throw new CredoError('Missing jsonld credential request data payload') + } + + // validate + JsonTransformer.fromJSON(requestJson, JsonLdCredentialDetail) + } + + public async acceptRequest( + agentContext: AgentContext, + { credentialFormats, attachmentId, requestAttachment }: CredentialFormatAcceptRequestOptions + ): Promise { + const w3cJsonLdCredentialService = agentContext.dependencyManager.resolve(W3cJsonLdCredentialService) + + // sign credential here. credential to be signed is received as the request attachment + // (attachment in the request message from holder to issuer) + const credentialRequest = requestAttachment.getDataAsJson() + + const verificationMethod = + credentialFormats?.jsonld?.verificationMethod ?? + (await this.deriveVerificationMethod(agentContext, credentialRequest.credential, credentialRequest)) + + if (!verificationMethod) { + throw new CredoError('Missing verification method in credential data') + } + const format = new CredentialFormatSpec({ + attachmentId, + format: JSONLD_VC, + }) + + const options = credentialRequest.options + + // Get a list of fields found in the options that are not supported at the moment + const unsupportedFields = ['challenge', 'domain', 'credentialStatus', 'created'] as const + const foundFields = unsupportedFields.filter((field) => options[field] !== undefined) + + if (foundFields.length > 0) { + throw new CredoError(`Some fields are not currently supported in credential options: ${foundFields.join(', ')}`) + } + + const credential = JsonTransformer.fromJSON(credentialRequest.credential, W3cCredential) + + const verifiableCredential = await w3cJsonLdCredentialService.signCredential(agentContext, { + format: ClaimFormat.LdpVc, + credential, + proofType: credentialRequest.options.proofType, + verificationMethod: verificationMethod, + }) + + const attachment = this.getFormatData(JsonTransformer.toJSON(verifiableCredential), format.attachmentId) + return { format, attachment } + } + + /** + * Derive a verification method using the issuer from the given verifiable credential + * @param credentialAsJson the verifiable credential we want to sign + * @return the verification method derived from this credential and its associated issuer did, keys etc. + */ + private async deriveVerificationMethod( + agentContext: AgentContext, + credentialAsJson: JsonCredential, + credentialRequest: JsonLdFormatDataCredentialDetail + ): Promise { + const didResolver = agentContext.dependencyManager.resolve(DidResolverService) + const w3cJsonLdCredentialService = agentContext.dependencyManager.resolve(W3cJsonLdCredentialService) + + const credential = JsonTransformer.fromJSON(credentialAsJson, W3cCredential) + + // extract issuer from vc (can be string or Issuer) + let issuerDid = credential.issuer + + if (typeof issuerDid !== 'string') { + issuerDid = issuerDid.id + } + // this will throw an error if the issuer did is invalid + const issuerDidDocument = await didResolver.resolveDidDocument(agentContext, issuerDid) + + // find first key which matches proof type + const proofType = credentialRequest.options.proofType + + // actually gets the key type(s) + const keyType = w3cJsonLdCredentialService.getVerificationMethodTypesByProofType(proofType) + + if (!keyType || keyType.length === 0) { + throw new CredoError(`No Key Type found for proofType ${proofType}`) + } + + const verificationMethod = await findVerificationMethodByKeyType(keyType[0], issuerDidDocument) + if (!verificationMethod) { + throw new CredoError(`Missing verification method for key type ${keyType}`) + } + + return verificationMethod.id + } + /** + * Processes an incoming credential - retrieve metadata, retrieve payload and store it in the Indy wallet + * @param options the issue credential message wrapped inside this object + * @param credentialRecord the credential exchange record for this credential + */ + public async processCredential( + agentContext: AgentContext, + { credentialRecord, attachment, requestAttachment }: CredentialFormatProcessCredentialOptions + ): Promise { + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + + const credentialAsJson = attachment.getDataAsJson() + const credential = JsonTransformer.fromJSON(credentialAsJson, W3cJsonLdVerifiableCredential) + const requestAsJson = requestAttachment.getDataAsJson() + + // Verify the credential request matches the credential + this.verifyReceivedCredentialMatchesRequest(credential, requestAsJson) + + // verify signatures of the credential + const result = await w3cCredentialService.verifyCredential(agentContext, { credential }) + if (result && !result.isValid) { + throw new CredoError(`Failed to validate credential, error = ${result.error}`) + } + + const verifiableCredential = await w3cCredentialService.storeCredential(agentContext, { + credential, + }) + + credentialRecord.credentials.push({ + credentialRecordType: this.credentialRecordType, + credentialRecordId: verifiableCredential.id, + }) + } + + private verifyReceivedCredentialMatchesRequest( + credential: W3cJsonLdVerifiableCredential, + request: JsonLdFormatDataCredentialDetail + ): void { + const jsonCredential = JsonTransformer.toJSON(credential) + delete jsonCredential.proof + + if (Array.isArray(credential.proof)) { + throw new CredoError('Credential proof arrays are not supported') + } + + if (request.options.created && credential.proof.created !== request.options.created) { + throw new CredoError('Received credential proof created does not match created from credential request') + } + + if (credential.proof.domain !== request.options.domain) { + throw new CredoError('Received credential proof domain does not match domain from credential request') + } + + if (credential.proof.challenge !== request.options.challenge) { + throw new CredoError('Received credential proof challenge does not match challenge from credential request') + } + + if (credential.proof.type !== request.options.proofType) { + throw new CredoError('Received credential proof type does not match proof type from credential request') + } + + if (credential.proof.proofPurpose !== request.options.proofPurpose) { + throw new CredoError('Received credential proof purpose does not match proof purpose from credential request') + } + + // Check whether the received credential (minus the proof) matches the credential request + if (!areObjectsEqual(jsonCredential, request.credential)) { + throw new CredoError('Received credential does not match credential request') + } + + // TODO: add check for the credentialStatus once this is supported in Credo + } + + public supportsFormat(format: string): boolean { + const supportedFormats = [JSONLD_VC_DETAIL, JSONLD_VC] + + return supportedFormats.includes(format) + } + + public async deleteCredentialById(): Promise { + throw new Error('Not implemented.') + } + + public areCredentialsEqual = (message1: Attachment, message2: Attachment): boolean => { + const obj1 = message1.getDataAsJson() + const obj2 = message2.getDataAsJson() + + return areObjectsEqual(obj1, obj2) + } + + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + { offerAttachment, proposalAttachment }: CredentialFormatAutoRespondProposalOptions + ) { + return this.areCredentialsEqual(proposalAttachment, offerAttachment) + } + + public async shouldAutoRespondToOffer( + agentContext: AgentContext, + { offerAttachment, proposalAttachment }: CredentialFormatAutoRespondOfferOptions + ) { + return this.areCredentialsEqual(proposalAttachment, offerAttachment) + } + + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + { offerAttachment, requestAttachment }: CredentialFormatAutoRespondRequestOptions + ) { + return this.areCredentialsEqual(offerAttachment, requestAttachment) + } + + public async shouldAutoRespondToCredential( + agentContext: AgentContext, + { requestAttachment, credentialAttachment }: CredentialFormatAutoRespondCredentialOptions + ) { + const credentialJson = credentialAttachment.getDataAsJson() + const w3cCredential = JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential) + const request = requestAttachment.getDataAsJson() + + try { + // This check is also done in the processCredential method, but we do it here as well + // to be certain we don't skip the check + this.verifyReceivedCredentialMatchesRequest(w3cCredential, request) + + return true + } catch (error) { + return false + } + } + + /** + * Returns an object of type {@link Attachment} for use in credential exchange messages. + * It looks up the correct format identifier and encodes the data as a base64 attachment. + * + * @param data The data to include in the attach object + * @param id the attach id from the formats component of the message + */ + private getFormatData(data: unknown, id: string): Attachment { + const attachment = new Attachment({ + id, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(data), + }), + }) + + return attachment + } +} diff --git a/packages/core/src/modules/credentials/formats/jsonld/__tests__/JsonLdCredentialFormatService.test.ts b/packages/core/src/modules/credentials/formats/jsonld/__tests__/JsonLdCredentialFormatService.test.ts new file mode 100644 index 0000000000..18e9885c04 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/jsonld/__tests__/JsonLdCredentialFormatService.test.ts @@ -0,0 +1,577 @@ +import type { CredentialFormatService } from '../..' +import type { AgentContext } from '../../../../../agent' +import type { CredentialPreviewAttribute } from '../../../models/CredentialPreviewAttribute' +import type { CustomCredentialTags } from '../../../repository/CredentialExchangeRecord' +import type { JsonCredential, JsonLdCredentialFormat, JsonLdCredentialDetailFormat } from '../JsonLdCredentialFormat' + +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' +import { JsonTransformer } from '../../../../../utils' +import { JsonEncoder } from '../../../../../utils/JsonEncoder' +import { DidDocument } from '../../../../dids' +import { DidResolverService } from '../../../../dids/services/DidResolverService' +import { + CREDENTIALS_CONTEXT_V1_URL, + W3cCredentialRecord, + W3cJsonLdVerifiableCredential, + W3cCredentialService, +} from '../../../../vc' +import { W3cJsonLdCredentialService } from '../../../../vc/data-integrity/W3cJsonLdCredentialService' +import { Ed25519Signature2018Fixtures } from '../../../../vc/data-integrity/__tests__/fixtures' +import { CredentialState, CredentialRole } from '../../../models' +import { V2CredentialPreview } from '../../../protocol/v2/messages' +import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import { JsonLdCredentialFormatService } from '../JsonLdCredentialFormatService' + +jest.mock('../../../../vc/W3cCredentialService') +jest.mock('../../../../vc/data-integrity/W3cJsonLdCredentialService') +jest.mock('../../../../dids/services/DidResolverService') + +const W3cCredentialServiceMock = W3cCredentialService as jest.Mock +const W3cJsonLdCredentialServiceMock = W3cJsonLdCredentialService as jest.Mock +const DidResolverServiceMock = DidResolverService as jest.Mock + +const didDocument = JsonTransformer.fromJSON( + { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + verificationMethod: [ + { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + publicKeyBase58: '3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx', + }, + ], + authentication: [ + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + ], + assertionMethod: [ + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + ], + keyAgreement: [ + { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6LSbkodSr6SU2trs8VUgnrnWtSm7BAPG245ggrBmSrxbv1R', + type: 'X25519KeyAgreementKey2019', + controller: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + publicKeyBase58: '5dTvYHaNaB7mk7iA9LqCJEHG2dGZQsvoi8WGzDRtYEf', + }, + ], + }, + DidDocument +) + +const vcJson = { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + credentialSubject: { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED.credentialSubject, + alumniOf: 'oops', + }, +} + +const vc = JsonTransformer.fromJSON(vcJson, W3cJsonLdVerifiableCredential) + +const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', +}) + +const offerAttachment = new Attachment({ + mimeType: 'application/json', + data: new AttachmentData({ + base64: + 'eyJzY2hlbWFfaWQiOiJhYWEiLCJjcmVkX2RlZl9pZCI6IlRoN01wVGFSWlZSWW5QaWFiZHM4MVk6MzpDTDoxNzpUQUciLCJub25jZSI6Im5vbmNlIiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7fX0', + }), +}) + +const credentialAttachment = new Attachment({ + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(vcJson), + }), +}) + +// A record is deserialized to JSON when it's stored into the storage. We want to simulate this behaviour for `offer` +// object to test our service would behave correctly. We use type assertion for `offer` attribute to `any`. +const mockCredentialRecord = ({ + state, + role, + threadId, + connectionId, + tags, + id, + credentialAttributes, +}: { + state?: CredentialState + role?: CredentialRole + tags?: CustomCredentialTags + threadId?: string + connectionId?: string + id?: string + credentialAttributes?: CredentialPreviewAttribute[] +} = {}) => { + const credentialRecord = new CredentialExchangeRecord({ + id, + credentialAttributes: credentialAttributes || credentialPreview.attributes, + state: state || CredentialState.OfferSent, + role: role || CredentialRole.Issuer, + threadId: threadId ?? 'add7e1a0-109e-4f37-9caa-cfd0fcdfe540', + connectionId: connectionId ?? '123', + tags, + protocolVersion: 'v2', + }) + + return credentialRecord +} +const inputDocAsJson: JsonCredential = { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + alumniOf: 'oops', + }, +} +const verificationMethod = `8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K#8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K` + +const signCredentialOptions: JsonLdCredentialDetailFormat = { + credential: inputDocAsJson, + options: { + proofPurpose: 'assertionMethod', + proofType: 'Ed25519Signature2018', + }, +} + +const requestAttachment = new Attachment({ + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(signCredentialOptions), + }), +}) +let jsonLdFormatService: CredentialFormatService +let w3cCredentialService: W3cCredentialService +let w3cJsonLdCredentialService: W3cJsonLdCredentialService +let didResolver: DidResolverService + +describe('JsonLd CredentialFormatService', () => { + let agentContext: AgentContext + beforeEach(async () => { + w3cCredentialService = new W3cCredentialServiceMock() + w3cJsonLdCredentialService = new W3cJsonLdCredentialServiceMock() + didResolver = new DidResolverServiceMock() + + const agentConfig = getAgentConfig('JsonLdCredentialFormatServiceTest') + agentContext = getAgentContext({ + registerInstances: [ + [DidResolverService, didResolver], + [W3cCredentialService, w3cCredentialService], + [W3cJsonLdCredentialService, w3cJsonLdCredentialService], + ], + agentConfig, + }) + + jsonLdFormatService = new JsonLdCredentialFormatService() + }) + + describe('Create JsonLd Credential Proposal / Offer', () => { + test(`Creates JsonLd Credential Proposal`, async () => { + // when + const { attachment, format } = await jsonLdFormatService.createProposal(agentContext, { + credentialRecord: mockCredentialRecord(), + credentialFormats: { + jsonld: signCredentialOptions, + }, + }) + + // then + expect(attachment).toMatchObject({ + id: expect.any(String), + description: undefined, + filename: undefined, + mimeType: 'application/json', + lastmodTime: undefined, + byteCount: undefined, + data: { + base64: + 'eyJjcmVkZW50aWFsIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlVuaXZlcnNpdHlEZWdyZWVDcmVkZW50aWFsIl0sImlzc3VlciI6ImRpZDprZXk6ejZNa2dnMzQyWWNwdWsyNjNSOWQ4QXE2TVVheFBuMUREZUh5R28zOEVlZlhtZ0RMIiwiaXNzdWFuY2VEYXRlIjoiMjAxNy0xMC0yMlQxMjoyMzo0OFoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifSwiYWx1bW5pT2YiOiJvb3BzIn19LCJvcHRpb25zIjp7InByb29mUHVycG9zZSI6ImFzc2VydGlvbk1ldGhvZCIsInByb29mVHlwZSI6IkVkMjU1MTlTaWduYXR1cmUyMDE4In19', + json: undefined, + links: undefined, + jws: undefined, + sha256: undefined, + }, + }) + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'aries/ld-proof-vc-detail@v1.0', + }) + }) + + test(`Creates JsonLd Credential Offer`, async () => { + // when + const { attachment, previewAttributes, format } = await jsonLdFormatService.createOffer(agentContext, { + credentialFormats: { + jsonld: signCredentialOptions, + }, + credentialRecord: mockCredentialRecord(), + }) + + // then + expect(attachment).toMatchObject({ + id: expect.any(String), + description: undefined, + filename: undefined, + mimeType: 'application/json', + lastmodTime: undefined, + byteCount: undefined, + data: { + base64: + 'eyJjcmVkZW50aWFsIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlVuaXZlcnNpdHlEZWdyZWVDcmVkZW50aWFsIl0sImlzc3VlciI6ImRpZDprZXk6ejZNa2dnMzQyWWNwdWsyNjNSOWQ4QXE2TVVheFBuMUREZUh5R28zOEVlZlhtZ0RMIiwiaXNzdWFuY2VEYXRlIjoiMjAxNy0xMC0yMlQxMjoyMzo0OFoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifSwiYWx1bW5pT2YiOiJvb3BzIn19LCJvcHRpb25zIjp7InByb29mUHVycG9zZSI6ImFzc2VydGlvbk1ldGhvZCIsInByb29mVHlwZSI6IkVkMjU1MTlTaWduYXR1cmUyMDE4In19', + json: undefined, + links: undefined, + jws: undefined, + sha256: undefined, + }, + }) + + expect(previewAttributes).toBeUndefined() + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'aries/ld-proof-vc-detail@v1.0', + }) + }) + }) + + describe('Accept Credential Offer', () => { + test('returns credential request message base on existing credential offer message', async () => { + // when + const { attachment, format } = await jsonLdFormatService.acceptOffer(agentContext, { + credentialFormats: { + jsonld: undefined, + }, + offerAttachment, + credentialRecord: mockCredentialRecord({ + state: CredentialState.OfferReceived, + threadId: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }), + }) + + // then + expect(attachment).toMatchObject({ + id: expect.any(String), + description: undefined, + filename: undefined, + mimeType: 'application/json', + lastmodTime: undefined, + byteCount: undefined, + data: { + base64: + 'eyJzY2hlbWFfaWQiOiJhYWEiLCJjcmVkX2RlZl9pZCI6IlRoN01wVGFSWlZSWW5QaWFiZHM4MVk6MzpDTDoxNzpUQUciLCJub25jZSI6Im5vbmNlIiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7fX0=', + json: undefined, + links: undefined, + jws: undefined, + sha256: undefined, + }, + }) + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'aries/ld-proof-vc-detail@v1.0', + }) + }) + }) + + describe('Accept Request', () => { + const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' + + test('Derive Verification Method', async () => { + mockFunction(didResolver.resolveDidDocument).mockReturnValue(Promise.resolve(didDocument)) + mockFunction(w3cJsonLdCredentialService.getVerificationMethodTypesByProofType).mockReturnValue([ + 'Ed25519VerificationKey2018', + ]) + + const service = jsonLdFormatService as JsonLdCredentialFormatService + const credentialRequest = requestAttachment.getDataAsJson() + + // calls private method in the format service + const verificationMethod = await service['deriveVerificationMethod']( + agentContext, + signCredentialOptions.credential, + credentialRequest + ) + expect(verificationMethod).toBe( + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL' + ) + }) + + test('Creates a credential', async () => { + // given + mockFunction(w3cJsonLdCredentialService.signCredential).mockReturnValue(Promise.resolve(vc)) + + const credentialRecord = mockCredentialRecord({ + state: CredentialState.RequestReceived, + threadId, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + + const { format, attachment } = await jsonLdFormatService.acceptRequest(agentContext, { + credentialRecord, + credentialFormats: { + jsonld: { + verificationMethod, + }, + }, + requestAttachment, + offerAttachment, + }) + //then + expect(w3cJsonLdCredentialService.signCredential).toHaveBeenCalledTimes(1) + + expect(attachment).toMatchObject({ + id: expect.any(String), + description: undefined, + filename: undefined, + mimeType: 'application/json', + lastmodTime: undefined, + byteCount: undefined, + data: { + base64: expect.any(String), + json: undefined, + links: undefined, + jws: undefined, + sha256: undefined, + }, + }) + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'aries/ld-proof-vc@v1.0', + }) + }) + }) + + describe('Process Credential', () => { + const credentialRecord = mockCredentialRecord({ + state: CredentialState.RequestSent, + }) + let w3c: W3cCredentialRecord + let signCredentialOptionsWithProperty: JsonLdCredentialDetailFormat + beforeEach(async () => { + signCredentialOptionsWithProperty = signCredentialOptions + signCredentialOptionsWithProperty.options = { + proofPurpose: 'assertionMethod', + proofType: 'Ed25519Signature2018', + } + + w3c = new W3cCredentialRecord({ + id: 'foo', + createdAt: new Date(), + credential: vc, + tags: { + expandedTypes: [ + 'https://www.w3.org/2018/credentials#VerifiableCredential', + 'https://example.org/examples#UniversityDegreeCredential', + ], + }, + }) + }) + test('finds credential record by thread ID and saves credential attachment into the wallet', async () => { + // given + mockFunction(w3cCredentialService.storeCredential).mockReturnValue(Promise.resolve(w3c)) + + // when + await jsonLdFormatService.processCredential(agentContext, { + offerAttachment, + attachment: credentialAttachment, + requestAttachment: requestAttachment, + credentialRecord, + }) + + // then + expect(w3cCredentialService.storeCredential).toHaveBeenCalledTimes(1) + expect(credentialRecord.credentials.length).toBe(1) + expect(credentialRecord.credentials[0].credentialRecordType).toBe('w3c') + expect(credentialRecord.credentials[0].credentialRecordId).toBe('foo') + }) + + test('throws error if credential subject not equal to request subject', async () => { + const vcJson = { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + credentialSubject: { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED.credentialSubject, + // missing alumni + }, + } + + const credentialAttachment = new Attachment({ + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(vcJson), + }), + }) + + // given + mockFunction(w3cCredentialService.storeCredential).mockReturnValue(Promise.resolve(w3c)) + + // when/then + await expect( + jsonLdFormatService.processCredential(agentContext, { + offerAttachment: offerAttachment, + attachment: credentialAttachment, + requestAttachment: requestAttachment, + credentialRecord, + }) + ).rejects.toThrow('Received credential does not match credential request') + }) + + test('throws error if credential domain not equal to request domain', async () => { + // this property is not supported yet by us, but could be in the credential we received + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + signCredentialOptionsWithProperty.options.domain = 'https://test.com' + const requestAttachmentWithDomain = new Attachment({ + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(signCredentialOptionsWithProperty), + }), + }) + // given + mockFunction(w3cCredentialService.storeCredential).mockReturnValue(Promise.resolve(w3c)) + + // when/then + await expect( + jsonLdFormatService.processCredential(agentContext, { + offerAttachment, + attachment: credentialAttachment, + requestAttachment: requestAttachmentWithDomain, + credentialRecord, + }) + ).rejects.toThrow('Received credential proof domain does not match domain from credential request') + }) + + test('throws error if credential challenge not equal to request challenge', async () => { + // this property is not supported yet by us, but could be in the credential we received + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + signCredentialOptionsWithProperty.options.challenge = '7bf32d0b-39d4-41f3-96b6-45de52988e4c' + + const requestAttachmentWithChallenge = new Attachment({ + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(signCredentialOptionsWithProperty), + }), + }) + + // given + mockFunction(w3cCredentialService.storeCredential).mockReturnValue(Promise.resolve(w3c)) + + // when/then + await expect( + jsonLdFormatService.processCredential(agentContext, { + offerAttachment, + attachment: credentialAttachment, + requestAttachment: requestAttachmentWithChallenge, + credentialRecord, + }) + ).rejects.toThrow('Received credential proof challenge does not match challenge from credential request') + }) + + test('throws error if credential proof type not equal to request proof type', async () => { + signCredentialOptionsWithProperty.options.proofType = 'Ed25519Signature2016' + const requestAttachmentWithProofType = new Attachment({ + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(signCredentialOptionsWithProperty), + }), + }) + + // given + mockFunction(w3cCredentialService.storeCredential).mockReturnValue(Promise.resolve(w3c)) + + // when/then + await expect( + jsonLdFormatService.processCredential(agentContext, { + offerAttachment, + attachment: credentialAttachment, + requestAttachment: requestAttachmentWithProofType, + credentialRecord, + }) + ).rejects.toThrow('Received credential proof type does not match proof type from credential request') + }) + + test('throws error if credential proof purpose not equal to request proof purpose', async () => { + signCredentialOptionsWithProperty.options.proofPurpose = 'authentication' + const requestAttachmentWithProofPurpose = new Attachment({ + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(signCredentialOptionsWithProperty), + }), + }) + + // given + mockFunction(w3cCredentialService.storeCredential).mockReturnValue(Promise.resolve(w3c)) + + // when/then + await expect( + jsonLdFormatService.processCredential(agentContext, { + offerAttachment, + attachment: credentialAttachment, + requestAttachment: requestAttachmentWithProofPurpose, + credentialRecord, + }) + ).rejects.toThrow('Received credential proof purpose does not match proof purpose from credential request') + }) + + test('are credentials equal', async () => { + const message1 = new Attachment({ + id: 'cdb0669b-7cd6-46bc-b1c7-7034f86083ac', + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(inputDocAsJson), + }), + }) + + const message2 = new Attachment({ + id: '9a8ff4fb-ac86-478f-b7f9-fbf3f9cc60e6', + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(inputDocAsJson), + }), + }) + + // indirectly test areCredentialsEqual as black box rather than expose that method in the API + let areCredentialsEqual = await jsonLdFormatService.shouldAutoRespondToProposal(agentContext, { + credentialRecord, + proposalAttachment: message1, + offerAttachment: message2, + }) + expect(areCredentialsEqual).toBe(true) + + const inputDoc2 = { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/citizenship/v1', + 'https://w3id.org/security/bbs/v1', + ], + } + message2.data = new AttachmentData({ + base64: JsonEncoder.toBase64(inputDoc2), + }) + + areCredentialsEqual = await jsonLdFormatService.shouldAutoRespondToProposal(agentContext, { + credentialRecord, + proposalAttachment: message1, + offerAttachment: message2, + }) + expect(areCredentialsEqual).toBe(false) + }) + }) +}) diff --git a/packages/core/src/modules/credentials/formats/jsonld/index.ts b/packages/core/src/modules/credentials/formats/jsonld/index.ts new file mode 100644 index 0000000000..f672c3e342 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/jsonld/index.ts @@ -0,0 +1,4 @@ +export * from './JsonLdCredentialFormatService' +export * from './JsonLdCredentialFormat' +export * from './JsonLdCredentialDetail' +export * from './JsonLdCredentialDetailOptions' diff --git a/packages/core/src/modules/credentials/index.ts b/packages/core/src/modules/credentials/index.ts new file mode 100644 index 0000000000..50577689ad --- /dev/null +++ b/packages/core/src/modules/credentials/index.ts @@ -0,0 +1,9 @@ +export * from './CredentialEvents' +export * from './CredentialsApi' +export * from './CredentialsApiOptions' +export * from './CredentialsModule' +export * from './CredentialsModuleConfig' +export * from './formats' +export * from './models' +export * from './protocol' +export * from './repository' diff --git a/packages/core/src/modules/credentials/models/CredentialAutoAcceptType.ts b/packages/core/src/modules/credentials/models/CredentialAutoAcceptType.ts new file mode 100644 index 0000000000..397e1ff70a --- /dev/null +++ b/packages/core/src/modules/credentials/models/CredentialAutoAcceptType.ts @@ -0,0 +1,13 @@ +/** + * Typing of the state for auto acceptance + */ +export enum AutoAcceptCredential { + /** Always auto accepts the credential no matter if it changed in subsequent steps */ + Always = 'always', + + /** Needs one acceptation and the rest will be automated if nothing changes */ + ContentApproved = 'contentApproved', + + /** Never auto accept a credential */ + Never = 'never', +} diff --git a/packages/core/src/modules/credentials/models/CredentialFormatSpec.ts b/packages/core/src/modules/credentials/models/CredentialFormatSpec.ts new file mode 100644 index 0000000000..621b0f54b4 --- /dev/null +++ b/packages/core/src/modules/credentials/models/CredentialFormatSpec.ts @@ -0,0 +1,25 @@ +import { Expose } from 'class-transformer' +import { IsString } from 'class-validator' + +import { uuid } from '../../../utils/uuid' + +export interface CredentialFormatSpecOptions { + attachmentId?: string + format: string +} + +export class CredentialFormatSpec { + public constructor(options: CredentialFormatSpecOptions) { + if (options) { + this.attachmentId = options.attachmentId ?? uuid() + this.format = options.format + } + } + + @Expose({ name: 'attach_id' }) + @IsString() + public attachmentId!: string + + @IsString() + public format!: string +} diff --git a/packages/core/src/modules/credentials/models/CredentialPreviewAttribute.ts b/packages/core/src/modules/credentials/models/CredentialPreviewAttribute.ts new file mode 100644 index 0000000000..0f341785c4 --- /dev/null +++ b/packages/core/src/modules/credentials/models/CredentialPreviewAttribute.ts @@ -0,0 +1,39 @@ +import { Expose } from 'class-transformer' +import { IsMimeType, IsOptional, IsString } from 'class-validator' + +import { JsonTransformer } from '../../../utils/JsonTransformer' + +export interface CredentialPreviewAttributeOptions { + name: string + mimeType?: string + value: string +} + +export class CredentialPreviewAttribute { + public constructor(options: CredentialPreviewAttributeOptions) { + if (options) { + this.name = options.name + this.mimeType = options.mimeType + this.value = options.value + } + } + + @IsString() + public name!: string + + @Expose({ name: 'mime-type' }) + @IsOptional() + @IsMimeType() + public mimeType?: string = 'text/plain' + + @IsString() + public value!: string + + public toJSON(): Record { + return JsonTransformer.toJSON(this) + } +} + +export interface CredentialPreviewOptions { + attributes: CredentialPreviewAttributeOptions[] +} diff --git a/packages/core/src/modules/credentials/models/CredentialProblemReportReason.ts b/packages/core/src/modules/credentials/models/CredentialProblemReportReason.ts new file mode 100644 index 0000000000..cf8bdb95bf --- /dev/null +++ b/packages/core/src/modules/credentials/models/CredentialProblemReportReason.ts @@ -0,0 +1,8 @@ +/** + * Credential error code in RFC 0036. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0036-issue-credential/README.md + */ +export enum CredentialProblemReportReason { + IssuanceAbandoned = 'issuance-abandoned', +} diff --git a/packages/core/src/modules/credentials/models/CredentialRole.ts b/packages/core/src/modules/credentials/models/CredentialRole.ts new file mode 100644 index 0000000000..6156118343 --- /dev/null +++ b/packages/core/src/modules/credentials/models/CredentialRole.ts @@ -0,0 +1,4 @@ +export enum CredentialRole { + Issuer = 'issuer', + Holder = 'holder', +} diff --git a/src/lib/modules/credentials/CredentialState.ts b/packages/core/src/modules/credentials/models/CredentialState.ts similarity index 80% rename from src/lib/modules/credentials/CredentialState.ts rename to packages/core/src/modules/credentials/models/CredentialState.ts index 5f01838827..225f755b2f 100644 --- a/src/lib/modules/credentials/CredentialState.ts +++ b/packages/core/src/modules/credentials/models/CredentialState.ts @@ -1,5 +1,5 @@ /** - * Issue Credential states as defined in RFC 0036 + * Issue Credential states as defined in RFC 0036 and RFC 0453 * * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0036-issue-credential/README.md#states */ @@ -8,9 +8,11 @@ export enum CredentialState { ProposalReceived = 'proposal-received', OfferSent = 'offer-sent', OfferReceived = 'offer-received', + Declined = 'declined', RequestSent = 'request-sent', RequestReceived = 'request-received', CredentialIssued = 'credential-issued', CredentialReceived = 'credential-received', Done = 'done', + Abandoned = 'abandoned', } diff --git a/packages/core/src/modules/credentials/models/RevocationNotification.ts b/packages/core/src/modules/credentials/models/RevocationNotification.ts new file mode 100644 index 0000000000..b26a52c7ad --- /dev/null +++ b/packages/core/src/modules/credentials/models/RevocationNotification.ts @@ -0,0 +1,9 @@ +export class RevocationNotification { + public revocationDate: Date + public comment?: string + + public constructor(comment?: string, revocationDate: Date = new Date()) { + this.revocationDate = revocationDate + this.comment = comment + } +} diff --git a/packages/core/src/modules/credentials/models/__tests__/CredentialState.test.ts b/packages/core/src/modules/credentials/models/__tests__/CredentialState.test.ts new file mode 100644 index 0000000000..e371d0f646 --- /dev/null +++ b/packages/core/src/modules/credentials/models/__tests__/CredentialState.test.ts @@ -0,0 +1,29 @@ +import { CredentialState } from '../CredentialState' + +describe('CredentialState', () => { + test('state matches Issue Credential 1.0 (RFC 0036) state value', () => { + expect(CredentialState.ProposalSent).toBe('proposal-sent') + expect(CredentialState.ProposalReceived).toBe('proposal-received') + expect(CredentialState.OfferSent).toBe('offer-sent') + expect(CredentialState.OfferReceived).toBe('offer-received') + expect(CredentialState.Declined).toBe('declined') + expect(CredentialState.RequestSent).toBe('request-sent') + expect(CredentialState.RequestReceived).toBe('request-received') + expect(CredentialState.CredentialIssued).toBe('credential-issued') + expect(CredentialState.CredentialReceived).toBe('credential-received') + expect(CredentialState.Done).toBe('done') + }) + + test('state matches Issue Credential 2.0 (RFC 0453) state value', () => { + expect(CredentialState.ProposalSent).toBe('proposal-sent') + expect(CredentialState.ProposalReceived).toBe('proposal-received') + expect(CredentialState.OfferSent).toBe('offer-sent') + expect(CredentialState.OfferReceived).toBe('offer-received') + expect(CredentialState.Declined).toBe('declined') + expect(CredentialState.RequestSent).toBe('request-sent') + expect(CredentialState.RequestReceived).toBe('request-received') + expect(CredentialState.CredentialIssued).toBe('credential-issued') + expect(CredentialState.CredentialReceived).toBe('credential-received') + expect(CredentialState.Done).toBe('done') + }) +}) diff --git a/packages/core/src/modules/credentials/models/index.ts b/packages/core/src/modules/credentials/models/index.ts new file mode 100644 index 0000000000..18fce8726f --- /dev/null +++ b/packages/core/src/modules/credentials/models/index.ts @@ -0,0 +1,7 @@ +export * from './RevocationNotification' +export * from './CredentialPreviewAttribute' +export * from './CredentialAutoAcceptType' +export * from './CredentialFormatSpec' +export * from './CredentialState' +export * from './CredentialProblemReportReason' +export * from './CredentialRole' diff --git a/packages/core/src/modules/credentials/protocol/BaseCredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/BaseCredentialProtocol.ts new file mode 100644 index 0000000000..488b372ea7 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/BaseCredentialProtocol.ts @@ -0,0 +1,348 @@ +import type { CredentialProtocol } from './CredentialProtocol' +import type { + CreateCredentialProposalOptions, + CredentialProtocolMsgReturnType, + DeleteCredentialOptions, + AcceptCredentialProposalOptions, + NegotiateCredentialProposalOptions, + CreateCredentialOfferOptions, + NegotiateCredentialOfferOptions, + CreateCredentialRequestOptions, + AcceptCredentialOfferOptions, + AcceptCredentialRequestOptions, + AcceptCredentialOptions, + GetCredentialFormatDataReturn, + CreateCredentialProblemReportOptions, +} from './CredentialProtocolOptions' +import type { AgentContext } from '../../../agent' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { FeatureRegistry } from '../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../plugins' +import type { Query, QueryOptions } from '../../../storage/StorageService' +import type { ProblemReportMessage } from '../../problem-reports' +import type { CredentialStateChangedEvent } from '../CredentialEvents' +import type { CredentialFormatService, ExtractCredentialFormats } from '../formats' +import type { CredentialRole } from '../models' +import type { CredentialExchangeRecord } from '../repository' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { DidCommMessageRepository } from '../../../storage/didcomm' +import { ConnectionService } from '../../connections' +import { CredentialEventTypes } from '../CredentialEvents' +import { CredentialState } from '../models/CredentialState' +import { CredentialRepository } from '../repository' + +/** + * Base implementation of the CredentialProtocol that can be used as a foundation for implementing + * the CredentialProtocol interface. + */ +export abstract class BaseCredentialProtocol + implements CredentialProtocol +{ + public abstract readonly version: string + + protected abstract getFormatServiceForRecordType(credentialRecordType: string): CFs[number] + + // methods for proposal + public abstract createProposal( + agentContext: AgentContext, + options: CreateCredentialProposalOptions + ): Promise> + public abstract processProposal( + messageContext: InboundMessageContext + ): Promise + public abstract acceptProposal( + agentContext: AgentContext, + options: AcceptCredentialProposalOptions + ): Promise> + public abstract negotiateProposal( + agentContext: AgentContext, + options: NegotiateCredentialProposalOptions + ): Promise> + + // methods for offer + public abstract createOffer( + agentContext: AgentContext, + options: CreateCredentialOfferOptions + ): Promise> + public abstract processOffer(messageContext: InboundMessageContext): Promise + public abstract acceptOffer( + agentContext: AgentContext, + options: AcceptCredentialOfferOptions + ): Promise> + public abstract negotiateOffer( + agentContext: AgentContext, + options: NegotiateCredentialOfferOptions + ): Promise> + + // methods for request + public abstract createRequest( + agentContext: AgentContext, + options: CreateCredentialRequestOptions + ): Promise> + public abstract processRequest(messageContext: InboundMessageContext): Promise + public abstract acceptRequest( + agentContext: AgentContext, + options: AcceptCredentialRequestOptions + ): Promise> + + // methods for issue + public abstract processCredential( + messageContext: InboundMessageContext + ): Promise + public abstract acceptCredential( + agentContext: AgentContext, + options: AcceptCredentialOptions + ): Promise> + + // methods for ack + public abstract processAck(messageContext: InboundMessageContext): Promise + + // methods for problem-report + public abstract createProblemReport( + agentContext: AgentContext, + options: CreateCredentialProblemReportOptions + ): Promise> + + public abstract findProposalMessage( + agentContext: AgentContext, + credentialExchangeId: string + ): Promise + public abstract findOfferMessage( + agentContext: AgentContext, + credentialExchangeId: string + ): Promise + public abstract findRequestMessage( + agentContext: AgentContext, + credentialExchangeId: string + ): Promise + public abstract findCredentialMessage( + agentContext: AgentContext, + credentialExchangeId: string + ): Promise + public abstract getFormatData( + agentContext: AgentContext, + credentialExchangeId: string + ): Promise>> + + public abstract register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void + + /** + * Process a received credential {@link ProblemReportMessage}. + * + * @param messageContext The message context containing a credential problem report message + * @returns credential record associated with the credential problem report message + */ + public async processProblemReport( + messageContext: InboundMessageContext + ): Promise { + const { message: credentialProblemReportMessage, agentContext, connection } = messageContext + + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing problem report with message id ${credentialProblemReportMessage.id}`) + + const credentialRecord = await this.getByProperties(agentContext, { + threadId: credentialProblemReportMessage.threadId, + }) + + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + expectedConnectionId: credentialRecord.connectionId, + }) + + // This makes sure that the sender of the incoming message is authorized to do so. + if (!credentialRecord?.connectionId) { + await connectionService.matchIncomingMessageToRequestMessageInOutOfBandExchange(messageContext, { + expectedConnectionId: credentialRecord?.connectionId, + }) + + credentialRecord.connectionId = connection?.id + } + + // Update record + credentialRecord.errorMessage = `${credentialProblemReportMessage.description.code}: ${credentialProblemReportMessage.description.en}` + await this.updateState(agentContext, credentialRecord, CredentialState.Abandoned) + return credentialRecord + } + + /** + * Update the record to a new state and emit an state changed event. Also updates the record + * in storage. + * + * @param credentialRecord The credential record to update the state for + * @param newState The state to update to + * + */ + public async updateState( + agentContext: AgentContext, + credentialRecord: CredentialExchangeRecord, + newState: CredentialState + ) { + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + agentContext.config.logger.debug( + `Updating credential record ${credentialRecord.id} to state ${newState} (previous=${credentialRecord.state})` + ) + + const previousState = credentialRecord.state + credentialRecord.state = newState + await credentialRepository.update(agentContext, credentialRecord) + + this.emitStateChangedEvent(agentContext, credentialRecord, previousState) + } + + protected emitStateChangedEvent( + agentContext: AgentContext, + credentialRecord: CredentialExchangeRecord, + previousState: CredentialState | null + ) { + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + + eventEmitter.emit(agentContext, { + type: CredentialEventTypes.CredentialStateChanged, + payload: { + credentialRecord: credentialRecord.clone(), + previousState: previousState, + }, + }) + } + + /** + * Retrieve a credential record by id + * + * @param credentialRecordId The credential record id + * @throws {RecordNotFoundError} If no record is found + * @return The credential record + * + */ + public getById(agentContext: AgentContext, credentialRecordId: string): Promise { + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + return credentialRepository.getById(agentContext, credentialRecordId) + } + + /** + * Retrieve all credential records + * + * @returns List containing all credential records + */ + public getAll(agentContext: AgentContext): Promise { + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + return credentialRepository.getAll(agentContext) + } + + public async findAllByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ): Promise { + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + return credentialRepository.findByQuery(agentContext, query, queryOptions) + } + + /** + * Find a credential record by id + * + * @param credentialRecordId the credential record id + * @returns The credential record or null if not found + */ + public findById(agentContext: AgentContext, proofRecordId: string): Promise { + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + return credentialRepository.findById(agentContext, proofRecordId) + } + + public async delete( + agentContext: AgentContext, + credentialRecord: CredentialExchangeRecord, + options?: DeleteCredentialOptions + ): Promise { + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + await credentialRepository.delete(agentContext, credentialRecord) + + const deleteAssociatedCredentials = options?.deleteAssociatedCredentials ?? true + const deleteAssociatedDidCommMessages = options?.deleteAssociatedDidCommMessages ?? true + + if (deleteAssociatedCredentials) { + for (const credential of credentialRecord.credentials) { + const formatService = this.getFormatServiceForRecordType(credential.credentialRecordType) + await formatService.deleteCredentialById(agentContext, credential.credentialRecordId) + } + } + + if (deleteAssociatedDidCommMessages) { + const didCommMessages = await didCommMessageRepository.findByQuery(agentContext, { + associatedRecordId: credentialRecord.id, + }) + for (const didCommMessage of didCommMessages) { + await didCommMessageRepository.delete(agentContext, didCommMessage) + } + } + } + + /** + * Retrieve a credential record by connection id and thread id + * + * @param properties Properties to query by + * + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * @returns The credential record + */ + public getByProperties( + agentContext: AgentContext, + properties: { + threadId: string + role?: CredentialRole + connectionId?: string + } + ): Promise { + const { role, connectionId, threadId } = properties + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + return credentialRepository.getSingleByQuery(agentContext, { + connectionId, + threadId, + role, + }) + } + + /** + * Find a credential record by connection id and thread id, returns null if not found + * + * @param threadId The thread id + * @param role The role of the record, i.e. holder or issuer + * @param connectionId The connection id + * + * @returns The credential record + */ + public findByProperties( + agentContext: AgentContext, + properties: { + threadId: string + role?: CredentialRole + connectionId?: string + } + ): Promise { + const { role, connectionId, threadId } = properties + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + return credentialRepository.findSingleByQuery(agentContext, { + connectionId, + threadId, + role, + }) + } + + public async update(agentContext: AgentContext, credentialRecord: CredentialExchangeRecord) { + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + return await credentialRepository.update(agentContext, credentialRecord) + } +} diff --git a/packages/core/src/modules/credentials/protocol/CredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/CredentialProtocol.ts new file mode 100644 index 0000000000..a5c8024681 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/CredentialProtocol.ts @@ -0,0 +1,136 @@ +import type { + CreateCredentialProposalOptions, + CredentialProtocolMsgReturnType, + DeleteCredentialOptions, + AcceptCredentialProposalOptions, + NegotiateCredentialProposalOptions, + CreateCredentialOfferOptions, + NegotiateCredentialOfferOptions, + CreateCredentialRequestOptions, + AcceptCredentialOfferOptions, + AcceptCredentialRequestOptions, + AcceptCredentialOptions, + GetCredentialFormatDataReturn, + CreateCredentialProblemReportOptions, +} from './CredentialProtocolOptions' +import type { AgentContext } from '../../../agent' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { FeatureRegistry } from '../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../plugins' +import type { Query, QueryOptions } from '../../../storage/StorageService' +import type { ProblemReportMessage } from '../../problem-reports' +import type { CredentialFormatService, ExtractCredentialFormats } from '../formats' +import type { CredentialRole } from '../models' +import type { CredentialState } from '../models/CredentialState' +import type { CredentialExchangeRecord } from '../repository' + +export interface CredentialProtocol { + readonly version: string + + // methods for proposal + createProposal( + agentContext: AgentContext, + options: CreateCredentialProposalOptions + ): Promise> + processProposal(messageContext: InboundMessageContext): Promise + acceptProposal( + agentContext: AgentContext, + options: AcceptCredentialProposalOptions + ): Promise> + negotiateProposal( + agentContext: AgentContext, + options: NegotiateCredentialProposalOptions + ): Promise> + + // methods for offer + createOffer( + agentContext: AgentContext, + options: CreateCredentialOfferOptions + ): Promise> + processOffer(messageContext: InboundMessageContext): Promise + acceptOffer( + agentContext: AgentContext, + options: AcceptCredentialOfferOptions + ): Promise> + negotiateOffer( + agentContext: AgentContext, + options: NegotiateCredentialOfferOptions + ): Promise> + + // methods for request + createRequest( + agentContext: AgentContext, + options: CreateCredentialRequestOptions + ): Promise> + processRequest(messageContext: InboundMessageContext): Promise + acceptRequest( + agentContext: AgentContext, + options: AcceptCredentialRequestOptions + ): Promise> + + // methods for issue + processCredential(messageContext: InboundMessageContext): Promise + acceptCredential( + agentContext: AgentContext, + options: AcceptCredentialOptions + ): Promise> + + // methods for ack + processAck(messageContext: InboundMessageContext): Promise + + // methods for problem-report + createProblemReport( + agentContext: AgentContext, + options: CreateCredentialProblemReportOptions + ): Promise> + processProblemReport(messageContext: InboundMessageContext): Promise + + findProposalMessage(agentContext: AgentContext, credentialExchangeId: string): Promise + findOfferMessage(agentContext: AgentContext, credentialExchangeId: string): Promise + findRequestMessage(agentContext: AgentContext, credentialExchangeId: string): Promise + findCredentialMessage(agentContext: AgentContext, credentialExchangeId: string): Promise + getFormatData( + agentContext: AgentContext, + credentialExchangeId: string + ): Promise>> + + // Repository methods + updateState( + agentContext: AgentContext, + credentialRecord: CredentialExchangeRecord, + newState: CredentialState + ): Promise + getById(agentContext: AgentContext, credentialExchangeId: string): Promise + getAll(agentContext: AgentContext): Promise + findAllByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ): Promise + findById(agentContext: AgentContext, credentialExchangeId: string): Promise + delete( + agentContext: AgentContext, + credentialRecord: CredentialExchangeRecord, + options?: DeleteCredentialOptions + ): Promise + getByProperties( + agentContext: AgentContext, + properties: { + threadId: string + connectionId?: string + role?: CredentialRole + } + ): Promise + findByProperties( + agentContext: AgentContext, + properties: { + threadId: string + connectionId?: string + role?: CredentialRole + } + ): Promise + update(agentContext: AgentContext, credentialRecord: CredentialExchangeRecord): Promise + + register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void +} diff --git a/packages/core/src/modules/credentials/protocol/CredentialProtocolOptions.ts b/packages/core/src/modules/credentials/protocol/CredentialProtocolOptions.ts new file mode 100644 index 0000000000..1d571511a8 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/CredentialProtocolOptions.ts @@ -0,0 +1,177 @@ +import type { CredentialProtocol } from './CredentialProtocol' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { ConnectionRecord } from '../../connections/repository/ConnectionRecord' +import type { + CredentialFormat, + CredentialFormatPayload, + CredentialFormatService, + ExtractCredentialFormats, +} from '../formats' +import type { CredentialPreviewAttributeOptions } from '../models' +import type { AutoAcceptCredential } from '../models/CredentialAutoAcceptType' +import type { CredentialExchangeRecord } from '../repository/CredentialExchangeRecord' + +/** + * Get the format data payload for a specific message from a list of CredentialFormat interfaces and a message + * + * For an indy offer, this resolves to the cred abstract format as defined here: + * https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0592-indy-attachments#cred-abstract-format + * + * @example + * ``` + * + * type OfferFormatData = CredentialFormatDataMessagePayload<[IndyCredentialFormat, JsonLdCredentialFormat], 'createOffer'> + * + * // equal to + * type OfferFormatData = { + * indy: { + * // ... payload for indy offer attachment as defined in RFC 0592 ... + * }, + * jsonld: { + * // ... payload for jsonld offer attachment as defined in RFC 0593 ... + * } + * } + * ``` + */ +export type CredentialFormatDataMessagePayload< + CFs extends CredentialFormat[] = CredentialFormat[], + M extends keyof CredentialFormat['formatData'] = keyof CredentialFormat['formatData'] +> = { + [Service in CFs[number] as Service['formatKey']]?: Service['formatData'][M] +} + +/** + * Infer the {@link CredentialFormat} types based on an array of {@link CredentialProtocol} types. + * + * It does this by extracting the `CredentialFormatServices` generic from the `CredentialProtocol`, and + * then extracting the `CredentialFormat` generic from each of the `CredentialFormatService` types. + * + * @example + * ``` + * // TheCredentialFormatServices is now equal to [IndyCredentialFormatService] + * type TheCredentialFormatServices = CredentialFormatsFromProtocols<[V1CredentialProtocol]> + * ``` + * + * Because the `V1CredentialProtocol` is defined as follows: + * ``` + * class V1CredentialProtocol implements CredentialProtocol<[IndyCredentialFormatService]> { + * } + * ``` + */ +export type CredentialFormatsFromProtocols = Type[number] extends CredentialProtocol< + infer CredentialFormatServices +> + ? CredentialFormatServices extends CredentialFormatService[] + ? ExtractCredentialFormats + : never + : never + +/** + * Get format data return value. Each key holds a mapping of credential format key to format data. + * + * @example + * ``` + * { + * proposal: { + * indy: { + * cred_def_id: string + * } + * } + * } + * ``` + */ +export type GetCredentialFormatDataReturn = { + proposalAttributes?: CredentialPreviewAttributeOptions[] + proposal?: CredentialFormatDataMessagePayload + offer?: CredentialFormatDataMessagePayload + offerAttributes?: CredentialPreviewAttributeOptions[] + request?: CredentialFormatDataMessagePayload + credential?: CredentialFormatDataMessagePayload +} + +interface BaseOptions { + comment?: string + autoAcceptCredential?: AutoAcceptCredential + + /** + * Will be ignored for v1 protocol as it is not supported + */ + goalCode?: string + + /** + * Will be ignored for v1 protocol as it is not supported + */ + goal?: string +} + +export interface CreateCredentialProposalOptions extends BaseOptions { + connectionRecord: ConnectionRecord + credentialFormats: CredentialFormatPayload, 'createProposal'> +} + +export interface AcceptCredentialProposalOptions extends BaseOptions { + credentialRecord: CredentialExchangeRecord + credentialFormats?: CredentialFormatPayload, 'acceptProposal'> +} + +export interface NegotiateCredentialProposalOptions { + credentialRecord: CredentialExchangeRecord + credentialFormats: CredentialFormatPayload, 'createOffer'> + autoAcceptCredential?: AutoAcceptCredential + comment?: string + + /** + * Will be ignored for v1 protocol as it is not supported + */ + goalCode?: string + + /** + * Will be ignored for v1 protocol as it is not supported + */ + goal?: string +} + +export interface CreateCredentialOfferOptions extends BaseOptions { + // Create offer can also be used for connection-less, so connection is optional + connectionRecord?: ConnectionRecord + credentialFormats: CredentialFormatPayload, 'createOffer'> +} + +export interface AcceptCredentialOfferOptions extends BaseOptions { + credentialRecord: CredentialExchangeRecord + credentialFormats?: CredentialFormatPayload, 'acceptOffer'> +} + +export interface NegotiateCredentialOfferOptions extends BaseOptions { + credentialRecord: CredentialExchangeRecord + credentialFormats: CredentialFormatPayload, 'createProposal'> +} + +export interface CreateCredentialRequestOptions extends BaseOptions { + connectionRecord: ConnectionRecord + credentialFormats: CredentialFormatPayload, 'createRequest'> +} + +export interface AcceptCredentialRequestOptions extends BaseOptions { + credentialRecord: CredentialExchangeRecord + credentialFormats?: CredentialFormatPayload, 'acceptRequest'> +} + +export interface AcceptCredentialOptions { + credentialRecord: CredentialExchangeRecord +} + +export interface CreateCredentialProblemReportOptions { + credentialRecord: CredentialExchangeRecord + description: string +} + +export interface CredentialProtocolMsgReturnType { + message: MessageType + credentialRecord: CredentialExchangeRecord +} + +export interface DeleteCredentialOptions { + deleteAssociatedCredentials?: boolean + deleteAssociatedDidCommMessages?: boolean +} diff --git a/packages/core/src/modules/credentials/protocol/index.ts b/packages/core/src/modules/credentials/protocol/index.ts new file mode 100644 index 0000000000..cb3d5c3b51 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/index.ts @@ -0,0 +1,11 @@ +export * from './v2' +export * from './revocation-notification' +import * as CredentialProtocolOptions from './CredentialProtocolOptions' + +export { CredentialProtocol } from './CredentialProtocol' +// NOTE: ideally we don't export the BaseCredentialProtocol, but as the V1CredentialProtocol is defined in the +// anoncreds package, we need to export it. We should at some point look at creating a core package which can be used for +// sharing internal types, and when you want to build you own modules, and an agent package, which is the one you use when +// consuming the framework +export { BaseCredentialProtocol } from './BaseCredentialProtocol' +export { CredentialProtocolOptions } diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/handlers/V1RevocationNotificationHandler.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/handlers/V1RevocationNotificationHandler.ts new file mode 100644 index 0000000000..36a41caf16 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/handlers/V1RevocationNotificationHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { RevocationNotificationService } from '../services' + +import { V1RevocationNotificationMessage } from '../messages/V1RevocationNotificationMessage' + +export class V1RevocationNotificationHandler implements MessageHandler { + private revocationService: RevocationNotificationService + public supportedMessages = [V1RevocationNotificationMessage] + + public constructor(revocationService: RevocationNotificationService) { + this.revocationService = revocationService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.revocationService.v1ProcessRevocationNotification(messageContext) + } +} diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/handlers/V2RevocationNotificationHandler.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/handlers/V2RevocationNotificationHandler.ts new file mode 100644 index 0000000000..2057a49d14 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/handlers/V2RevocationNotificationHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { RevocationNotificationService } from '../services' + +import { V2RevocationNotificationMessage } from '../messages/V2RevocationNotificationMessage' + +export class V2RevocationNotificationHandler implements MessageHandler { + private revocationService: RevocationNotificationService + public supportedMessages = [V2RevocationNotificationMessage] + + public constructor(revocationService: RevocationNotificationService) { + this.revocationService = revocationService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.revocationService.v2ProcessRevocationNotification(messageContext) + } +} diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/handlers/index.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/handlers/index.ts new file mode 100644 index 0000000000..5eb3e5f7c9 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/handlers/index.ts @@ -0,0 +1,2 @@ +export * from './V1RevocationNotificationHandler' +export * from './V2RevocationNotificationHandler' diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/index.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/index.ts new file mode 100644 index 0000000000..f1b26f8e08 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/index.ts @@ -0,0 +1 @@ +export * from './messages' diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/messages/V1RevocationNotificationMessage.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/messages/V1RevocationNotificationMessage.ts new file mode 100644 index 0000000000..c0af69539d --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/messages/V1RevocationNotificationMessage.ts @@ -0,0 +1,38 @@ +import type { AckDecorator } from '../../../../../decorators/ack/AckDecorator' + +import { Expose } from 'class-transformer' +import { IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface RevocationNotificationMessageV1Options { + issueThread: string + id?: string + comment?: string + pleaseAck?: AckDecorator +} + +export class V1RevocationNotificationMessage extends AgentMessage { + public constructor(options: RevocationNotificationMessageV1Options) { + super() + if (options) { + this.issueThread = options.issueThread + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.pleaseAck = options.pleaseAck + } + } + + @IsValidMessageType(V1RevocationNotificationMessage.type) + public readonly type = V1RevocationNotificationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/revocation_notification/1.0/revoke') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'thread_id' }) + @IsString() + public issueThread!: string +} diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/messages/V2RevocationNotificationMessage.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/messages/V2RevocationNotificationMessage.ts new file mode 100644 index 0000000000..a0d19ba5a3 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/messages/V2RevocationNotificationMessage.ts @@ -0,0 +1,44 @@ +import type { AckDecorator } from '../../../../../decorators/ack/AckDecorator' + +import { Expose } from 'class-transformer' +import { IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface RevocationNotificationMessageV2Options { + revocationFormat: string + credentialId: string + id?: string + comment?: string + pleaseAck?: AckDecorator +} + +export class V2RevocationNotificationMessage extends AgentMessage { + public constructor(options: RevocationNotificationMessageV2Options) { + super() + if (options) { + this.revocationFormat = options.revocationFormat + this.credentialId = options.credentialId + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.pleaseAck = options.pleaseAck + } + } + + @IsValidMessageType(V2RevocationNotificationMessage.type) + public readonly type = V2RevocationNotificationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/revocation_notification/2.0/revoke') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'revocation_format' }) + @IsString() + public revocationFormat!: string + + @Expose({ name: 'credential_id' }) + @IsString() + public credentialId!: string +} diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/messages/index.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/messages/index.ts new file mode 100644 index 0000000000..728f7ea6e9 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/messages/index.ts @@ -0,0 +1,2 @@ +export * from './V1RevocationNotificationMessage' +export * from './V2RevocationNotificationMessage' diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts new file mode 100644 index 0000000000..ff0a1dd4b0 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts @@ -0,0 +1,191 @@ +import type { V2CreateRevocationNotificationMessageOptions } from './RevocationNotificationServiceOptions' +import type { AgentContext } from '../../../../../agent' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { ConnectionRecord } from '../../../../connections' +import type { RevocationNotificationReceivedEvent } from '../../../CredentialEvents' +import type { V1RevocationNotificationMessage } from '../messages/V1RevocationNotificationMessage' + +import { EventEmitter } from '../../../../../agent/EventEmitter' +import { MessageHandlerRegistry } from '../../../../../agent/MessageHandlerRegistry' +import { InjectionSymbols } from '../../../../../constants' +import { CredoError } from '../../../../../error/CredoError' +import { Logger } from '../../../../../logger' +import { inject, injectable } from '../../../../../plugins' +import { CredentialEventTypes } from '../../../CredentialEvents' +import { RevocationNotification } from '../../../models/RevocationNotification' +import { CredentialRepository } from '../../../repository' +import { V1RevocationNotificationHandler, V2RevocationNotificationHandler } from '../handlers' +import { V2RevocationNotificationMessage } from '../messages/V2RevocationNotificationMessage' +import { + v1ThreadRegex, + v2AnonCredsRevocationFormat, + v2AnonCredsRevocationIdentifierRegex, + v2IndyRevocationFormat, + v2IndyRevocationIdentifierRegex, +} from '../util/revocationIdentifier' + +@injectable() +export class RevocationNotificationService { + private credentialRepository: CredentialRepository + private eventEmitter: EventEmitter + private logger: Logger + + public constructor( + credentialRepository: CredentialRepository, + eventEmitter: EventEmitter, + messageHandlerRegistry: MessageHandlerRegistry, + @inject(InjectionSymbols.Logger) logger: Logger + ) { + this.credentialRepository = credentialRepository + this.eventEmitter = eventEmitter + this.logger = logger + + this.registerMessageHandlers(messageHandlerRegistry) + } + + private async processRevocationNotification( + agentContext: AgentContext, + anonCredsRevocationRegistryId: string, + anonCredsCredentialRevocationId: string, + connection: ConnectionRecord, + comment?: string + ) { + // TODO: can we extract support for this revocation notification handler to the anoncreds module? + // Search for the revocation registry in both qualified and unqualified forms + const query = { + $or: [ + { + anonCredsRevocationRegistryId, + anonCredsCredentialRevocationId, + connectionId: connection.id, + }, + { + anonCredsUnqualifiedRevocationRegistryId: anonCredsRevocationRegistryId, + anonCredsCredentialRevocationId, + connectionId: connection.id, + }, + ], + } + + this.logger.trace(`Getting record by query for revocation notification:`, query) + const credentialRecord = await this.credentialRepository.getSingleByQuery(agentContext, query) + + credentialRecord.revocationNotification = new RevocationNotification(comment) + await this.credentialRepository.update(agentContext, credentialRecord) + + this.logger.trace('Emitting RevocationNotificationReceivedEvent') + this.eventEmitter.emit(agentContext, { + type: CredentialEventTypes.RevocationNotificationReceived, + payload: { + // Clone record to prevent mutations after emitting event. + credentialRecord: credentialRecord.clone(), + }, + }) + } + + /** + * Process a received {@link V1RevocationNotificationMessage}. This will create a + * {@link RevocationNotification} and store it in the corresponding {@link CredentialRecord} + * + * @param messageContext message context of RevocationNotificationMessageV1 + */ + public async v1ProcessRevocationNotification( + messageContext: InboundMessageContext + ): Promise { + this.logger.info('Processing revocation notification v1', { message: messageContext.message }) + + // ThreadID = indy:::: + const threadId = messageContext.message.issueThread + + try { + const threadIdGroups = threadId.match(v1ThreadRegex) + if (!threadIdGroups) { + throw new CredoError( + `Incorrect revocation notification threadId format: \n${threadId}\ndoes not match\n"indy::::"` + ) + } + + const [, , anonCredsRevocationRegistryId, anonCredsCredentialRevocationId] = threadIdGroups + const comment = messageContext.message.comment + const connection = messageContext.assertReadyConnection() + + await this.processRevocationNotification( + messageContext.agentContext, + anonCredsRevocationRegistryId, + anonCredsCredentialRevocationId, + connection, + comment + ) + } catch (error) { + this.logger.warn('Failed to process revocation notification message', { error, threadId }) + } + } + + /** + * Create a V2 Revocation Notification message + */ + + public async v2CreateRevocationNotification( + options: V2CreateRevocationNotificationMessageOptions + ): Promise<{ message: V2RevocationNotificationMessage }> { + const { credentialId, revocationFormat, comment, requestAck } = options + const message = new V2RevocationNotificationMessage({ + credentialId, + revocationFormat, + comment, + }) + if (requestAck) { + message.setPleaseAck() + } + + return { message } + } + + /** + * Process a received {@link V2RevocationNotificationMessage}. This will create a + * {@link RevocationNotification} and store it in the corresponding {@link CredentialRecord} + * + * @param messageContext message context of RevocationNotificationMessageV2 + */ + public async v2ProcessRevocationNotification( + messageContext: InboundMessageContext + ): Promise { + this.logger.info('Processing revocation notification v2', { message: messageContext.message }) + + const credentialId = messageContext.message.credentialId + + if (![v2IndyRevocationFormat, v2AnonCredsRevocationFormat].includes(messageContext.message.revocationFormat)) { + throw new CredoError( + `Unknown revocation format: ${messageContext.message.revocationFormat}. Supported formats are indy-anoncreds and anoncreds` + ) + } + + try { + const credentialIdGroups = + credentialId.match(v2IndyRevocationIdentifierRegex) ?? credentialId.match(v2AnonCredsRevocationIdentifierRegex) + if (!credentialIdGroups) { + throw new CredoError( + `Incorrect revocation notification credentialId format: \n${credentialId}\ndoes not match\n"::"` + ) + } + + const [, anonCredsRevocationRegistryId, anonCredsCredentialRevocationId] = credentialIdGroups + const comment = messageContext.message.comment + const connection = messageContext.assertReadyConnection() + await this.processRevocationNotification( + messageContext.agentContext, + anonCredsRevocationRegistryId, + anonCredsCredentialRevocationId, + connection, + comment + ) + } catch (error) { + this.logger.warn('Failed to process revocation notification message', { error, credentialId }) + } + } + + private registerMessageHandlers(messageHandlerRegistry: MessageHandlerRegistry) { + messageHandlerRegistry.registerMessageHandler(new V1RevocationNotificationHandler(this)) + messageHandlerRegistry.registerMessageHandler(new V2RevocationNotificationHandler(this)) + } +} diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationServiceOptions.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationServiceOptions.ts new file mode 100644 index 0000000000..406013b18b --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationServiceOptions.ts @@ -0,0 +1,6 @@ +export interface V2CreateRevocationNotificationMessageOptions { + credentialId: string + revocationFormat: string + comment?: string + requestAck?: boolean +} diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts new file mode 100644 index 0000000000..d8c545a0fc --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts @@ -0,0 +1,275 @@ +import type { AgentContext } from '../../../../../../agent' +import type { RevocationNotificationReceivedEvent } from '../../../../CredentialEvents' +import type { AnonCredsCredentialMetadata } from '@credo-ts/anoncreds' + +import { Subject } from 'rxjs' + +import { CredentialExchangeRecord, CredentialRole, CredentialState, InboundMessageContext } from '../../../../../..' +import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../../../tests/helpers' +import { EventEmitter } from '../../../../../../agent/EventEmitter' +import { MessageHandlerRegistry } from '../../../../../../agent/MessageHandlerRegistry' +import { DidExchangeState } from '../../../../../connections' +import { CredentialEventTypes } from '../../../../CredentialEvents' +import { CredentialRepository } from '../../../../repository/CredentialRepository' +import { V1RevocationNotificationMessage, V2RevocationNotificationMessage } from '../../messages' +import { RevocationNotificationService } from '../RevocationNotificationService' + +jest.mock('../../../../repository/CredentialRepository') +const CredentialRepositoryMock = CredentialRepository as jest.Mock +const credentialRepository = new CredentialRepositoryMock() + +jest.mock('../../../../../../agent/MessageHandlerRegistry') +const MessageHandlerRegistryMock = MessageHandlerRegistry as jest.Mock +const messageHandlerRegistry = new MessageHandlerRegistryMock() + +const connection = getMockConnection({ + state: DidExchangeState.Completed, +}) + +describe('RevocationNotificationService', () => { + let revocationNotificationService: RevocationNotificationService + let agentContext: AgentContext + let eventEmitter: EventEmitter + + beforeEach(() => { + const agentConfig = getAgentConfig('RevocationNotificationService') + + agentContext = getAgentContext() + + eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) + revocationNotificationService = new RevocationNotificationService( + credentialRepository, + eventEmitter, + messageHandlerRegistry, + agentConfig.logger + ) + }) + + afterEach(() => { + jest.resetAllMocks() + }) + + describe('v1ProcessRevocationNotification', () => { + test('emits revocation notification event if credential record exists for indy thread', async () => { + const eventListenerMock = jest.fn() + + eventEmitter.on( + CredentialEventTypes.RevocationNotificationReceived, + eventListenerMock + ) + + const date = new Date('2020-01-01T00:00:00.000Z') + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => date) + + const credentialRecord = new CredentialExchangeRecord({ + threadId: 'thread-id', + protocolVersion: 'v1', + state: CredentialState.Done, + role: CredentialRole.Holder, + }) + + const metadata = { + revocationRegistryId: + 'AsB27X6KRrJFsqZ3unNAH6:4:AsB27X6KRrJFsqZ3unNAH6:3:cl:48187:default:CL_ACCUM:3b24a9b0-a979-41e0-9964-2292f2b1b7e9', + credentialRevocationId: '1', + } satisfies AnonCredsCredentialMetadata + + // Set required tags + credentialRecord.setTag('anonCredsUnqualifiedRevocationRegistryId', metadata.revocationRegistryId) + credentialRecord.setTag('anonCredsCredentialRevocationId', metadata.credentialRevocationId) + + mockFunction(credentialRepository.getSingleByQuery).mockResolvedValueOnce(credentialRecord) + + const revocationNotificationThreadId = `indy::${metadata.revocationRegistryId}::${metadata.credentialRevocationId}` + const revocationNotificationMessage = new V1RevocationNotificationMessage({ + issueThread: revocationNotificationThreadId, + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage, { + connection, + agentContext, + }) + + await revocationNotificationService.v1ProcessRevocationNotification(messageContext) + + const clonedCredentialRecord = eventListenerMock.mock.calls[0][0].payload.credentialRecord + expect(clonedCredentialRecord.toJSON()).toEqual(credentialRecord.toJSON()) + + expect(credentialRecord.revocationNotification).toMatchObject({ + revocationDate: date, + comment: 'Credential has been revoked', + }) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'RevocationNotificationReceived', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + credentialRecord: expect.any(CredentialExchangeRecord), + }, + }) + + dateSpy.mockRestore() + }) + + test('does not emit revocation notification event if no credential record exists for indy thread', async () => { + const eventListenerMock = jest.fn() + + eventEmitter.on( + CredentialEventTypes.RevocationNotificationReceived, + eventListenerMock + ) + + const revocationRegistryId = + 'ABC12D3EFgHIjKL4mnOPQ5:4:AsB27X6KRrJFsqZ3unNAH6:3:cl:48187:default:CL_ACCUM:3b24a9b0-a979-41e0-9964-2292f2b1b7e9' + const credentialRevocationId = '2' + const revocationNotificationThreadId = `indy::${revocationRegistryId}::${credentialRevocationId}` + + mockFunction(credentialRepository.getSingleByQuery).mockRejectedValueOnce(new Error('Could not find record')) + + const revocationNotificationMessage = new V1RevocationNotificationMessage({ + issueThread: revocationNotificationThreadId, + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage, { connection, agentContext }) + + await revocationNotificationService.v1ProcessRevocationNotification(messageContext) + + expect(eventListenerMock).not.toHaveBeenCalled() + }) + + test('does not emit revocation notification event if invalid threadId is passed', async () => { + const eventListenerMock = jest.fn() + + eventEmitter.on( + CredentialEventTypes.RevocationNotificationReceived, + eventListenerMock + ) + + const revocationNotificationThreadId = 'notIndy::invalidRevRegId::invalidCredRevId' + const revocationNotificationMessage = new V1RevocationNotificationMessage({ + issueThread: revocationNotificationThreadId, + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage, { agentContext }) + + await revocationNotificationService.v1ProcessRevocationNotification(messageContext) + + expect(eventListenerMock).not.toHaveBeenCalled() + }) + }) + + describe('v2ProcessRevocationNotification', () => { + test('emits revocation notification event if credential record exists for indy thread', async () => { + const eventListenerMock = jest.fn() + + eventEmitter.on( + CredentialEventTypes.RevocationNotificationReceived, + eventListenerMock + ) + + const date = new Date('2020-01-01T00:00:00.000Z') + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => date) + + const credentialRecord = new CredentialExchangeRecord({ + threadId: 'thread-id', + protocolVersion: 'v2', + state: CredentialState.Done, + role: CredentialRole.Holder, + }) + + const metadata = { + revocationRegistryId: + 'AsB27X6KRrJFsqZ3unNAH6:4:AsB27X6KRrJFsqZ3unNAH6:3:cl:48187:default:CL_ACCUM:3b24a9b0-a979-41e0-9964-2292f2b1b7e9', + credentialRevocationId: '1', + } + + mockFunction(credentialRepository.getSingleByQuery).mockResolvedValueOnce(credentialRecord) + const revocationNotificationCredentialId = `${metadata.revocationRegistryId}::${metadata.credentialRevocationId}` + + const revocationNotificationMessage = new V2RevocationNotificationMessage({ + credentialId: revocationNotificationCredentialId, + revocationFormat: 'indy-anoncreds', + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage, { agentContext, connection }) + + await revocationNotificationService.v2ProcessRevocationNotification(messageContext) + + const clonedCredentialRecord = eventListenerMock.mock.calls[0][0].payload.credentialRecord + expect(clonedCredentialRecord.toJSON()).toEqual(credentialRecord.toJSON()) + + expect(credentialRecord.revocationNotification).toMatchObject({ + revocationDate: date, + comment: 'Credential has been revoked', + }) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'RevocationNotificationReceived', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + credentialRecord: expect.any(CredentialExchangeRecord), + }, + }) + + dateSpy.mockRestore() + }) + + test('does not emit revocation notification event if no credential record exists for indy thread', async () => { + const eventListenerMock = jest.fn() + + eventEmitter.on( + CredentialEventTypes.RevocationNotificationReceived, + eventListenerMock + ) + + const revocationRegistryId = + 'ABC12D3EFgHIjKL4mnOPQ5:4:AsB27X6KRrJFsqZ3unNAH6:3:cl:48187:default:CL_ACCUM:3b24a9b0-a979-41e0-9964-2292f2b1b7e9' + const credentialRevocationId = '2' + const credentialId = `${revocationRegistryId}::${credentialRevocationId}` + + mockFunction(credentialRepository.getSingleByQuery).mockRejectedValueOnce(new Error('Could not find record')) + + const revocationNotificationMessage = new V2RevocationNotificationMessage({ + credentialId, + revocationFormat: 'indy-anoncreds', + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage, { connection, agentContext }) + + await revocationNotificationService.v2ProcessRevocationNotification(messageContext) + + expect(eventListenerMock).not.toHaveBeenCalled() + }) + + test('does not emit revocation notification event if invalid threadId is passed', async () => { + const eventListenerMock = jest.fn() + + eventEmitter.on( + CredentialEventTypes.RevocationNotificationReceived, + eventListenerMock + ) + + const invalidCredentialId = 'notIndy::invalidRevRegId::invalidCredRevId' + const revocationNotificationMessage = new V2RevocationNotificationMessage({ + credentialId: invalidCredentialId, + revocationFormat: 'indy-anoncreds', + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage, { agentContext }) + + await revocationNotificationService.v2ProcessRevocationNotification(messageContext) + + expect(eventListenerMock).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/index.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/index.ts new file mode 100644 index 0000000000..e5696a92e1 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/index.ts @@ -0,0 +1,2 @@ +export * from './RevocationNotificationService' +export * from './RevocationNotificationServiceOptions' diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/util/__tests__/revocationIdentifier.test.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/util/__tests__/revocationIdentifier.test.ts new file mode 100644 index 0000000000..f1e732d3e0 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/util/__tests__/revocationIdentifier.test.ts @@ -0,0 +1,44 @@ +import { v1ThreadRegex, v2IndyRevocationIdentifierRegex } from '../revocationIdentifier' + +describe('revocationIdentifier', () => { + describe('v1ThreadRegex', () => { + test('should match', () => { + const revocationRegistryId = + 'AABC12D3EFgHIjKL4mnOPQ:4:AsB27X6KRrJFsqZ3unNAH6:3:cl:48187:N4s7y-5hema_tag ;:CL_ACCUM:3b24a9b0-a979-41e0-9964-2292f2b1b7e9' + const credentialRevocationId = '2' + + const revocationThreadId = `indy::${revocationRegistryId}::${credentialRevocationId}` + + const [mRevocationThreadId, mIndy, mRevocationRegistryId, mCredentialRevocationId] = revocationThreadId.match( + v1ThreadRegex + ) as string[] + + expect([mRevocationThreadId, mIndy, mRevocationRegistryId, mCredentialRevocationId]).toStrictEqual([ + revocationThreadId, + 'indy', + revocationRegistryId, + credentialRevocationId, + ]) + }) + }) + + describe('v2IndyRevocationIdentifierRegex', () => { + test('should match', () => { + const revocationRegistryId = + 'AABC12D3EFgHIjKL4mnOPQ:4:AsB27X6KRrJFsqZ3unNAH6:3:cl:48187:N4s7y-5hema_tag ;:CL_ACCUM:3b24a9b0-a979-41e0-9964-2292f2b1b7e9' + const credentialRevocationId = '2' + + const revocationCredentialId = `${revocationRegistryId}::${credentialRevocationId}` + + const [mRevocationCredentialId, mRevocationRegistryId, mCredentialRevocationId] = revocationCredentialId.match( + v2IndyRevocationIdentifierRegex + ) as string[] + + expect([mRevocationCredentialId, mRevocationRegistryId, mCredentialRevocationId]).toStrictEqual([ + revocationCredentialId, + revocationRegistryId, + credentialRevocationId, + ]) + }) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.ts new file mode 100644 index 0000000000..c1bc1d35f2 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.ts @@ -0,0 +1,16 @@ +// indyRevocationIdentifier = :: + +// ThreadID = indy:::: +export const v1ThreadRegex = + /(indy)::((?:[\dA-z]{21,22}):4:(?:[\dA-z]{21,22}):3:[Cc][Ll]:(?:(?:[1-9][0-9]*)|(?:[\dA-z]{21,22}:2:.+:[0-9.]+)):.+?:CL_ACCUM:(?:[\dA-z-]+))::(\d+)$/ + +// CredentialID = :: +export const v2IndyRevocationIdentifierRegex = + /((?:[\dA-z]{21,22}):4:(?:[\dA-z]{21,22}):3:[Cc][Ll]:(?:(?:[1-9][0-9]*)|(?:[\dA-z]{21,22}:2:.+:[0-9.]+)):.+?:CL_ACCUM:(?:[\dA-z-]+))::(\d+)$/ + +export const v2IndyRevocationFormat = 'indy-anoncreds' + +// CredentialID = :: +export const v2AnonCredsRevocationIdentifierRegex = /([a-zA-Z0-9+\-.]+:.+)::(\d+)$/ + +export const v2AnonCredsRevocationFormat = 'anoncreds' diff --git a/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts b/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts new file mode 100644 index 0000000000..d97c14bc72 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts @@ -0,0 +1,631 @@ +import type { AgentContext } from '../../../../agent' +import type { Attachment } from '../../../../decorators/attachment/Attachment' +import type { CredentialFormatPayload, CredentialFormatService, ExtractCredentialFormats } from '../../formats' +import type { CredentialFormatSpec } from '../../models' +import type { CredentialExchangeRecord } from '../../repository/CredentialExchangeRecord' + +import { CredoError } from '../../../../error/CredoError' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage/didcomm' + +import { + V2IssueCredentialMessage, + V2OfferCredentialMessage, + V2ProposeCredentialMessage, + V2RequestCredentialMessage, + V2CredentialPreview, +} from './messages' + +export class CredentialFormatCoordinator { + /** + * Create a {@link V2ProposeCredentialMessage}. + * + * @param options + * @returns The created {@link V2ProposeCredentialMessage} + * + */ + public async createProposal( + agentContext: AgentContext, + { + credentialFormats, + formatServices, + credentialRecord, + comment, + goalCode, + goal, + }: { + formatServices: CredentialFormatService[] + credentialFormats: CredentialFormatPayload, 'createProposal'> + credentialRecord: CredentialExchangeRecord + comment?: string + goalCode?: string + goal?: string + } + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: CredentialFormatSpec[] = [] + const proposalAttachments: Attachment[] = [] + let credentialPreview: V2CredentialPreview | undefined + + for (const formatService of formatServices) { + const { format, attachment, previewAttributes } = await formatService.createProposal(agentContext, { + credentialFormats, + credentialRecord, + }) + + if (previewAttributes) { + credentialPreview = new V2CredentialPreview({ + attributes: previewAttributes, + }) + } + + proposalAttachments.push(attachment) + formats.push(format) + } + + credentialRecord.credentialAttributes = credentialPreview?.attributes + + const message = new V2ProposeCredentialMessage({ + id: credentialRecord.threadId, + formats, + proposalAttachments, + comment, + credentialPreview, + goalCode, + goal, + }) + + message.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + return message + } + + public async processProposal( + agentContext: AgentContext, + { + credentialRecord, + message, + formatServices, + }: { + credentialRecord: CredentialExchangeRecord + message: V2ProposeCredentialMessage + formatServices: CredentialFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.formats, message.proposalAttachments) + + await formatService.processProposal(agentContext, { + attachment, + credentialRecord, + }) + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } + + public async acceptProposal( + agentContext: AgentContext, + { + credentialRecord, + credentialFormats, + formatServices, + comment, + goalCode, + goal, + }: { + credentialRecord: CredentialExchangeRecord + credentialFormats?: CredentialFormatPayload, 'acceptProposal'> + formatServices: CredentialFormatService[] + comment?: string + goalCode?: string + goal?: string + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: CredentialFormatSpec[] = [] + const offerAttachments: Attachment[] = [] + let credentialPreview: V2CredentialPreview | undefined + + const proposalMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2ProposeCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + // NOTE: We set the credential attributes from the proposal on the record as we've 'accepted' them + // and can now use them to create the offer in the format services. It may be overwritten later on + // if the user provided other attributes in the credentialFormats array. + credentialRecord.credentialAttributes = proposalMessage.credentialPreview?.attributes + + for (const formatService of formatServices) { + const proposalAttachment = this.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + + const { attachment, format, previewAttributes } = await formatService.acceptProposal(agentContext, { + credentialRecord, + credentialFormats, + proposalAttachment, + }) + + if (previewAttributes) { + credentialPreview = new V2CredentialPreview({ + attributes: previewAttributes, + }) + } + + offerAttachments.push(attachment) + formats.push(format) + } + + credentialRecord.credentialAttributes = credentialPreview?.attributes + + if (!credentialPreview) { + // If no preview attributes were provided, use a blank preview. Not all formats use this object + // but it is required by the protocol + credentialPreview = new V2CredentialPreview({ + attributes: [], + }) + } + + const message = new V2OfferCredentialMessage({ + formats, + credentialPreview, + offerAttachments, + comment, + goalCode, + goal, + }) + + message.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: credentialRecord.id, + role: DidCommMessageRole.Sender, + }) + + return message + } + + /** + * Create a {@link V2OfferCredentialMessage}. + * + * @param options + * @returns The created {@link V2OfferCredentialMessage} + * + */ + public async createOffer( + agentContext: AgentContext, + { + credentialFormats, + formatServices, + credentialRecord, + comment, + goalCode, + goal, + }: { + formatServices: CredentialFormatService[] + credentialFormats: CredentialFormatPayload, 'createOffer'> + credentialRecord: CredentialExchangeRecord + comment?: string + goalCode?: string + goal?: string + } + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: CredentialFormatSpec[] = [] + const offerAttachments: Attachment[] = [] + let credentialPreview: V2CredentialPreview | undefined + + for (const formatService of formatServices) { + const { format, attachment, previewAttributes } = await formatService.createOffer(agentContext, { + credentialFormats, + credentialRecord, + }) + + if (previewAttributes) { + credentialPreview = new V2CredentialPreview({ + attributes: previewAttributes, + }) + } + + offerAttachments.push(attachment) + formats.push(format) + } + + credentialRecord.credentialAttributes = credentialPreview?.attributes + + if (!credentialPreview) { + // If no preview attributes were provided, use a blank preview. Not all formats use this object + // but it is required by the protocol + credentialPreview = new V2CredentialPreview({ + attributes: [], + }) + } + + const message = new V2OfferCredentialMessage({ + formats, + comment, + goalCode, + goal, + offerAttachments, + credentialPreview, + }) + + message.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + return message + } + + public async processOffer( + agentContext: AgentContext, + { + credentialRecord, + message, + formatServices, + }: { + credentialRecord: CredentialExchangeRecord + message: V2OfferCredentialMessage + formatServices: CredentialFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.formats, message.offerAttachments) + + await formatService.processOffer(agentContext, { + attachment, + credentialRecord, + }) + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } + + public async acceptOffer( + agentContext: AgentContext, + { + credentialRecord, + credentialFormats, + formatServices, + comment, + goalCode, + goal, + }: { + credentialRecord: CredentialExchangeRecord + credentialFormats?: CredentialFormatPayload, 'acceptOffer'> + formatServices: CredentialFormatService[] + comment?: string + goalCode?: string + goal?: string + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const offerMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: CredentialFormatSpec[] = [] + const requestAttachments: Attachment[] = [] + const requestAppendAttachments: Attachment[] = [] + + for (const formatService of formatServices) { + const offerAttachment = this.getAttachmentForService( + formatService, + offerMessage.formats, + offerMessage.offerAttachments + ) + + const { attachment, format, appendAttachments } = await formatService.acceptOffer(agentContext, { + offerAttachment, + credentialRecord, + credentialFormats, + }) + + requestAttachments.push(attachment) + formats.push(format) + if (appendAttachments) requestAppendAttachments.push(...appendAttachments) + } + + credentialRecord.credentialAttributes = offerMessage.credentialPreview?.attributes + + const message = new V2RequestCredentialMessage({ + formats, + attachments: requestAppendAttachments, + requestAttachments: requestAttachments, + comment, + goalCode, + goal, + }) + + message.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: credentialRecord.id, + role: DidCommMessageRole.Sender, + }) + + return message + } + + /** + * Create a {@link V2RequestCredentialMessage}. + * + * @param options + * @returns The created {@link V2RequestCredentialMessage} + * + */ + public async createRequest( + agentContext: AgentContext, + { + credentialFormats, + formatServices, + credentialRecord, + comment, + goalCode, + goal, + }: { + formatServices: CredentialFormatService[] + credentialFormats: CredentialFormatPayload, 'createRequest'> + credentialRecord: CredentialExchangeRecord + comment?: string + goalCode?: string + goal?: string + } + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: CredentialFormatSpec[] = [] + const requestAttachments: Attachment[] = [] + + for (const formatService of formatServices) { + const { format, attachment } = await formatService.createRequest(agentContext, { + credentialFormats, + credentialRecord, + }) + + requestAttachments.push(attachment) + formats.push(format) + } + + const message = new V2RequestCredentialMessage({ + formats, + comment, + goalCode, + goal, + requestAttachments: requestAttachments, + }) + + message.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + return message + } + + public async processRequest( + agentContext: AgentContext, + { + credentialRecord, + message, + formatServices, + }: { + credentialRecord: CredentialExchangeRecord + message: V2RequestCredentialMessage + formatServices: CredentialFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.formats, message.requestAttachments) + + await formatService.processRequest(agentContext, { + attachment, + credentialRecord, + }) + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } + + public async acceptRequest( + agentContext: AgentContext, + { + credentialRecord, + credentialFormats, + formatServices, + comment, + goalCode, + goal, + }: { + credentialRecord: CredentialExchangeRecord + credentialFormats?: CredentialFormatPayload, 'acceptRequest'> + formatServices: CredentialFormatService[] + comment?: string + goalCode?: string + goal?: string + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2RequestCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + const offerMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + role: DidCommMessageRole.Sender, + }) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: CredentialFormatSpec[] = [] + const credentialAttachments: Attachment[] = [] + + for (const formatService of formatServices) { + const requestAttachment = this.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const offerAttachment = offerMessage + ? this.getAttachmentForService(formatService, offerMessage.formats, offerMessage.offerAttachments) + : undefined + + const { attachment, format } = await formatService.acceptRequest(agentContext, { + requestAttachment, + offerAttachment, + credentialRecord, + credentialFormats, + requestAppendAttachments: requestMessage.appendedAttachments, + }) + + credentialAttachments.push(attachment) + formats.push(format) + } + + const message = new V2IssueCredentialMessage({ + formats, + credentialAttachments: credentialAttachments, + comment, + goalCode, + goal, + }) + + message.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + message.setPleaseAck() + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: credentialRecord.id, + role: DidCommMessageRole.Sender, + }) + + return message + } + + public async processCredential( + agentContext: AgentContext, + { + credentialRecord, + message, + requestMessage, + formatServices, + }: { + credentialRecord: CredentialExchangeRecord + message: V2IssueCredentialMessage + requestMessage: V2RequestCredentialMessage + formatServices: CredentialFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const offerMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + for (const formatService of formatServices) { + const offerAttachment = this.getAttachmentForService( + formatService, + offerMessage.formats, + offerMessage.offerAttachments + ) + + const attachment = this.getAttachmentForService(formatService, message.formats, message.credentialAttachments) + const requestAttachment = this.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + await formatService.processCredential(agentContext, { + attachment, + offerAttachment, + requestAttachment, + credentialRecord, + requestAppendAttachments: requestMessage.appendedAttachments, + }) + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } + + public getAttachmentForService( + credentialFormatService: CredentialFormatService, + formats: CredentialFormatSpec[], + attachments: Attachment[] + ) { + const attachmentId = this.getAttachmentIdForService(credentialFormatService, formats) + const attachment = attachments.find((attachment) => attachment.id === attachmentId) + + if (!attachment) { + throw new CredoError(`Attachment with id ${attachmentId} not found in attachments.`) + } + + return attachment + } + + private getAttachmentIdForService(credentialFormatService: CredentialFormatService, formats: CredentialFormatSpec[]) { + const format = formats.find((format) => credentialFormatService.supportsFormat(format.format)) + + if (!format) throw new CredoError(`No attachment found for service ${credentialFormatService.formatKey}`) + + return format.attachmentId + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts new file mode 100644 index 0000000000..82b19132cb --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts @@ -0,0 +1,1370 @@ +import type { AgentContext } from '../../../../agent' +import type { AgentMessage } from '../../../../agent/AgentMessage' +import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import type { MessageHandlerInboundMessage } from '../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../../plugins' +import type { ProblemReportMessage } from '../../../problem-reports' +import type { + CredentialFormat, + CredentialFormatPayload, + CredentialFormatService, + ExtractCredentialFormats, +} from '../../formats' +import type { CredentialFormatSpec } from '../../models/CredentialFormatSpec' +import type { CredentialProtocol } from '../CredentialProtocol' +import type { + AcceptCredentialOptions, + AcceptCredentialOfferOptions, + AcceptCredentialProposalOptions, + AcceptCredentialRequestOptions, + CreateCredentialOfferOptions, + CreateCredentialProposalOptions, + CreateCredentialRequestOptions, + CredentialProtocolMsgReturnType, + CredentialFormatDataMessagePayload, + CreateCredentialProblemReportOptions, + GetCredentialFormatDataReturn, + NegotiateCredentialOfferOptions, + NegotiateCredentialProposalOptions, +} from '../CredentialProtocolOptions' + +import { Protocol } from '../../../../agent/models/features/Protocol' +import { CredoError } from '../../../../error' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage/didcomm' +import { uuid } from '../../../../utils/uuid' +import { AckStatus } from '../../../common' +import { ConnectionService } from '../../../connections' +import { CredentialsModuleConfig } from '../../CredentialsModuleConfig' +import { AutoAcceptCredential, CredentialProblemReportReason, CredentialRole, CredentialState } from '../../models' +import { CredentialExchangeRecord, CredentialRepository } from '../../repository' +import { composeAutoAccept } from '../../util/composeAutoAccept' +import { arePreviewAttributesEqual } from '../../util/previewAttributes' +import { BaseCredentialProtocol } from '../BaseCredentialProtocol' + +import { CredentialFormatCoordinator } from './CredentialFormatCoordinator' +import { + V2OfferCredentialHandler, + V2CredentialAckHandler, + V2IssueCredentialHandler, + V2ProposeCredentialHandler, + V2RequestCredentialHandler, +} from './handlers' +import { V2CredentialProblemReportHandler } from './handlers/V2CredentialProblemReportHandler' +import { + V2CredentialAckMessage, + V2CredentialProblemReportMessage, + V2IssueCredentialMessage, + V2OfferCredentialMessage, + V2ProposeCredentialMessage, + V2RequestCredentialMessage, +} from './messages' + +export interface V2CredentialProtocolConfig { + credentialFormats: CredentialFormatServices +} + +export class V2CredentialProtocol + extends BaseCredentialProtocol + implements CredentialProtocol +{ + private credentialFormatCoordinator = new CredentialFormatCoordinator() + private credentialFormats: CFs + + public constructor({ credentialFormats }: V2CredentialProtocolConfig) { + super() + + this.credentialFormats = credentialFormats + } + + /** + * The version of the issue credential protocol this service supports + */ + public readonly version = 'v2' as const + + /** + * Registers the protocol implementation (handlers, feature registry) on the agent. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Register message handlers for the Issue Credential V2 Protocol + dependencyManager.registerMessageHandlers([ + new V2ProposeCredentialHandler(this), + new V2OfferCredentialHandler(this), + new V2RequestCredentialHandler(this), + new V2IssueCredentialHandler(this), + new V2CredentialAckHandler(this), + new V2CredentialProblemReportHandler(this), + ]) + + // Register Issue Credential V2 in feature registry, with supported roles + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/issue-credential/2.0', + roles: ['holder', 'issuer'], + }) + ) + } + + /** + * Create a {@link V2ProposeCredentialMessage} not bound to an existing credential exchange. + * + * @param proposal The ProposeCredentialOptions object containing the important fields for the credential message + * @returns Object containing proposal message and associated credential record + * + */ + public async createProposal( + agentContext: AgentContext, + { + connectionRecord, + credentialFormats, + comment, + goal, + goalCode, + autoAcceptCredential, + }: CreateCredentialProposalOptions + ): Promise> { + agentContext.config.logger.debug('Get the Format Service and Create Proposal Message') + + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + const formatServices = this.getFormatServices(credentialFormats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to create proposal. No supported formats`) + } + + const credentialRecord = new CredentialExchangeRecord({ + connectionId: connectionRecord.id, + threadId: uuid(), + state: CredentialState.ProposalSent, + role: CredentialRole.Holder, + autoAcceptCredential, + protocolVersion: 'v2', + }) + + const proposalMessage = await this.credentialFormatCoordinator.createProposal(agentContext, { + credentialFormats, + credentialRecord, + formatServices, + comment, + goal, + goalCode, + }) + + agentContext.config.logger.debug('Save record and emit state change event') + await credentialRepository.save(agentContext, credentialRecord) + this.emitStateChangedEvent(agentContext, credentialRecord, null) + + return { credentialRecord, message: proposalMessage } + } + + /** + * Method called by {@link V2ProposeCredentialHandler} on reception of a propose credential message + * We do the necessary processing here to accept the proposal and do the state change, emit event etc. + * @param messageContext the inbound propose credential message + * @returns credential record appropriate for this incoming message (once accepted) + */ + public async processProposal( + messageContext: InboundMessageContext + ): Promise { + const { message: proposalMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing credential proposal with id ${proposalMessage.id}`) + + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + let credentialRecord = await this.findByProperties(messageContext.agentContext, { + threadId: proposalMessage.threadId, + role: CredentialRole.Issuer, + connectionId: connection?.id, + }) + + const formatServices = this.getFormatServicesFromMessage(proposalMessage.formats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to process proposal. No supported formats`) + } + + // credential record already exists + if (credentialRecord) { + const proposalCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2ProposeCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + const offerCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + role: DidCommMessageRole.Sender, + }) + + // Assert + credentialRecord.assertProtocolVersion('v2') + credentialRecord.assertState(CredentialState.OfferSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: proposalCredentialMessage ?? undefined, + lastSentMessage: offerCredentialMessage ?? undefined, + expectedConnectionId: credentialRecord.connectionId, + }) + + await this.credentialFormatCoordinator.processProposal(messageContext.agentContext, { + credentialRecord, + formatServices, + message: proposalMessage, + }) + + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.ProposalReceived) + + return credentialRecord + } else { + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + + // No credential record exists with thread id + credentialRecord = new CredentialExchangeRecord({ + connectionId: connection?.id, + threadId: proposalMessage.threadId, + parentThreadId: proposalMessage.thread?.parentThreadId, + state: CredentialState.ProposalReceived, + role: CredentialRole.Issuer, + protocolVersion: 'v2', + }) + + await this.credentialFormatCoordinator.processProposal(messageContext.agentContext, { + credentialRecord, + formatServices, + message: proposalMessage, + }) + + // Save record and emit event + await credentialRepository.save(messageContext.agentContext, credentialRecord) + this.emitStateChangedEvent(messageContext.agentContext, credentialRecord, null) + + return credentialRecord + } + } + + public async acceptProposal( + agentContext: AgentContext, + { + credentialRecord, + credentialFormats, + autoAcceptCredential, + comment, + goal, + goalCode, + }: AcceptCredentialProposalOptions + ): Promise> { + // Assert + credentialRecord.assertProtocolVersion('v2') + credentialRecord.assertState(CredentialState.ProposalReceived) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Use empty credentialFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(credentialFormats ?? {}) + + // if no format services could be extracted from the credentialFormats + // take all available format services from the proposal message + if (formatServices.length === 0) { + const proposalMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2ProposeCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + formatServices = this.getFormatServicesFromMessage(proposalMessage.formats) + } + + // If the format services list is still empty, throw an error as we don't support any + // of the formats + if (formatServices.length === 0) { + throw new CredoError(`Unable to accept proposal. No supported formats provided as input or in proposal message`) + } + + const offerMessage = await this.credentialFormatCoordinator.acceptProposal(agentContext, { + credentialRecord, + formatServices, + comment, + goal, + goalCode, + credentialFormats, + }) + + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.OfferSent) + + return { credentialRecord, message: offerMessage } + } + + /** + * Negotiate a credential proposal as issuer (by sending a credential offer message) to the connection + * associated with the credential record. + * + * @param options configuration for the offer see {@link NegotiateCredentialProposalOptions} + * @returns Credential exchange record associated with the credential offer + * + */ + public async negotiateProposal( + agentContext: AgentContext, + { + credentialRecord, + credentialFormats, + autoAcceptCredential, + comment, + goal, + goalCode, + }: NegotiateCredentialProposalOptions + ): Promise> { + // Assert + credentialRecord.assertProtocolVersion('v2') + credentialRecord.assertState(CredentialState.ProposalReceived) + + if (!credentialRecord.connectionId) { + throw new CredoError( + `No connectionId found for credential record '${credentialRecord.id}'. Connection-less issuance does not support negotiation.` + ) + } + + const formatServices = this.getFormatServices(credentialFormats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to create offer. No supported formats`) + } + + const offerMessage = await this.credentialFormatCoordinator.createOffer(agentContext, { + formatServices, + credentialFormats, + credentialRecord, + comment, + goal, + goalCode, + }) + + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.OfferSent) + + return { credentialRecord, message: offerMessage } + } + + /** + * Create a {@link V2OfferCredentialMessage} as beginning of protocol process. If no connectionId is provided, the + * exchange will be created without a connection for usage in oob and connection-less issuance. + * + * @param formatService {@link CredentialFormatService} the format service object containing format-specific logic + * @param options attributes of the original offer + * @returns Object containing offer message and associated credential record + * + */ + public async createOffer( + agentContext: AgentContext, + { + credentialFormats, + autoAcceptCredential, + comment, + goal, + goalCode, + connectionRecord, + }: CreateCredentialOfferOptions + ): Promise> { + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + const formatServices = this.getFormatServices(credentialFormats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to create offer. No supported formats`) + } + + const credentialRecord = new CredentialExchangeRecord({ + connectionId: connectionRecord?.id, + threadId: uuid(), + state: CredentialState.OfferSent, + role: CredentialRole.Issuer, + autoAcceptCredential, + protocolVersion: 'v2', + }) + + const offerMessage = await this.credentialFormatCoordinator.createOffer(agentContext, { + formatServices, + credentialFormats, + credentialRecord, + comment, + goal, + goalCode, + }) + + agentContext.config.logger.debug( + `Saving record and emitting state changed for credential exchange record ${credentialRecord.id}` + ) + await credentialRepository.save(agentContext, credentialRecord) + this.emitStateChangedEvent(agentContext, credentialRecord, null) + + return { credentialRecord, message: offerMessage } + } + + /** + * Method called by {@link V2OfferCredentialHandler} on reception of a offer credential message + * We do the necessary processing here to accept the offer and do the state change, emit event etc. + * @param messageContext the inbound offer credential message + * @returns credential record appropriate for this incoming message (once accepted) + */ + public async processOffer( + messageContext: MessageHandlerInboundMessage + ): Promise { + const { message: offerMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing credential offer with id ${offerMessage.id}`) + + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + let credentialRecord = await this.findByProperties(messageContext.agentContext, { + threadId: offerMessage.threadId, + role: CredentialRole.Holder, + connectionId: connection?.id, + }) + + const formatServices = this.getFormatServicesFromMessage(offerMessage.formats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to process offer. No supported formats`) + } + + // credential record already exists + if (credentialRecord) { + const proposeCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2ProposeCredentialMessage, + role: DidCommMessageRole.Sender, + }) + const offerCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + credentialRecord.assertProtocolVersion('v2') + credentialRecord.assertState(CredentialState.ProposalSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: offerCredentialMessage ?? undefined, + lastSentMessage: proposeCredentialMessage ?? undefined, + expectedConnectionId: credentialRecord.connectionId, + }) + + await this.credentialFormatCoordinator.processOffer(messageContext.agentContext, { + credentialRecord, + formatServices, + message: offerMessage, + }) + + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.OfferReceived) + return credentialRecord + } else { + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + + // No credential record exists with thread id + agentContext.config.logger.debug('No credential record found for offer, creating a new one') + credentialRecord = new CredentialExchangeRecord({ + connectionId: connection?.id, + threadId: offerMessage.threadId, + parentThreadId: offerMessage.thread?.parentThreadId, + state: CredentialState.OfferReceived, + role: CredentialRole.Holder, + protocolVersion: 'v2', + }) + + await this.credentialFormatCoordinator.processOffer(messageContext.agentContext, { + credentialRecord, + formatServices, + message: offerMessage, + }) + + // Save in repository + agentContext.config.logger.debug('Saving credential record and emit offer-received event') + await credentialRepository.save(messageContext.agentContext, credentialRecord) + + this.emitStateChangedEvent(messageContext.agentContext, credentialRecord, null) + return credentialRecord + } + } + + public async acceptOffer( + agentContext: AgentContext, + { + credentialRecord, + autoAcceptCredential, + comment, + goal, + goalCode, + credentialFormats, + }: AcceptCredentialOfferOptions + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Assert + credentialRecord.assertProtocolVersion('v2') + credentialRecord.assertState(CredentialState.OfferReceived) + + // Use empty credentialFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(credentialFormats ?? {}) + + // if no format services could be extracted from the credentialFormats + // take all available format services from the offer message + if (formatServices.length === 0) { + const offerMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + formatServices = this.getFormatServicesFromMessage(offerMessage.formats) + } + + // If the format services list is still empty, throw an error as we don't support any + // of the formats + if (formatServices.length === 0) { + throw new CredoError(`Unable to accept offer. No supported formats provided as input or in offer message`) + } + + const message = await this.credentialFormatCoordinator.acceptOffer(agentContext, { + credentialRecord, + formatServices, + comment, + goal, + goalCode, + credentialFormats, + }) + + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.RequestSent) + + return { credentialRecord, message } + } + + /** + * Create a {@link ProposePresentationMessage} as response to a received credential offer. + * To create a proposal not bound to an existing credential exchange, use {@link createProposal}. + * + * @param options configuration to use for the proposal + * @returns Object containing proposal message and associated credential record + * + */ + public async negotiateOffer( + agentContext: AgentContext, + { + credentialRecord, + credentialFormats, + autoAcceptCredential, + comment, + goal, + goalCode, + }: NegotiateCredentialOfferOptions + ): Promise> { + // Assert + credentialRecord.assertProtocolVersion('v2') + credentialRecord.assertState(CredentialState.OfferReceived) + + if (!credentialRecord.connectionId) { + throw new CredoError( + `No connectionId found for credential record '${credentialRecord.id}'. Connection-less issuance does not support negotiation.` + ) + } + + const formatServices = this.getFormatServices(credentialFormats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to create proposal. No supported formats`) + } + + const proposalMessage = await this.credentialFormatCoordinator.createProposal(agentContext, { + formatServices, + credentialFormats, + credentialRecord, + comment, + goal, + goalCode, + }) + + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.ProposalSent) + + return { credentialRecord, message: proposalMessage } + } + + /** + * Create a {@link V2RequestCredentialMessage} as beginning of protocol process. + * @returns Object containing offer message and associated credential record + * + */ + public async createRequest( + agentContext: AgentContext, + { + credentialFormats, + autoAcceptCredential, + comment, + goal, + goalCode, + connectionRecord, + }: CreateCredentialRequestOptions + ): Promise> { + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + const formatServices = this.getFormatServices(credentialFormats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to create request. No supported formats`) + } + + const credentialRecord = new CredentialExchangeRecord({ + connectionId: connectionRecord.id, + threadId: uuid(), + state: CredentialState.RequestSent, + role: CredentialRole.Holder, + autoAcceptCredential, + protocolVersion: 'v2', + }) + + const requestMessage = await this.credentialFormatCoordinator.createRequest(agentContext, { + formatServices, + credentialFormats, + credentialRecord, + comment, + goal, + goalCode, + }) + + agentContext.config.logger.debug( + `Saving record and emitting state changed for credential exchange record ${credentialRecord.id}` + ) + await credentialRepository.save(agentContext, credentialRecord) + this.emitStateChangedEvent(agentContext, credentialRecord, null) + + return { credentialRecord, message: requestMessage } + } + + /** + * Process a received {@link RequestCredentialMessage}. This will not accept the credential request + * or send a credential. It will only update the existing credential record with + * the information from the credential request message. Use {@link createCredential} + * after calling this method to create a credential. + *z + * @param messageContext The message context containing a v2 credential request message + * @returns credential record associated with the credential request message + * + */ + public async processRequest( + messageContext: InboundMessageContext + ): Promise { + const { message: requestMessage, connection, agentContext } = messageContext + + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing credential request with id ${requestMessage.id}`) + + let credentialRecord = await this.findByProperties(messageContext.agentContext, { + threadId: requestMessage.threadId, + role: CredentialRole.Issuer, + }) + + const formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to process request. No supported formats`) + } + + // credential record already exists + if (credentialRecord) { + const proposalMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2ProposeCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + const offerMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + role: DidCommMessageRole.Sender, + }) + + // Assert + credentialRecord.assertProtocolVersion('v2') + credentialRecord.assertState(CredentialState.OfferSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: proposalMessage ?? undefined, + lastSentMessage: offerMessage ?? undefined, + expectedConnectionId: credentialRecord.connectionId, + }) + + // This makes sure that the sender of the incoming message is authorized to do so. + if (!credentialRecord.connectionId) { + await connectionService.matchIncomingMessageToRequestMessageInOutOfBandExchange(messageContext, { + expectedConnectionId: credentialRecord.connectionId, + }) + + credentialRecord.connectionId = connection?.id + } + + await this.credentialFormatCoordinator.processRequest(messageContext.agentContext, { + credentialRecord, + formatServices, + message: requestMessage, + }) + + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.RequestReceived) + return credentialRecord + } else { + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + + // No credential record exists with thread id + agentContext.config.logger.debug('No credential record found for request, creating a new one') + credentialRecord = new CredentialExchangeRecord({ + connectionId: connection?.id, + threadId: requestMessage.threadId, + parentThreadId: requestMessage.thread?.parentThreadId, + state: CredentialState.RequestReceived, + role: CredentialRole.Issuer, + protocolVersion: 'v2', + }) + + await this.credentialFormatCoordinator.processRequest(messageContext.agentContext, { + credentialRecord, + formatServices, + message: requestMessage, + }) + + // Save in repository + agentContext.config.logger.debug('Saving credential record and emit request-received event') + await credentialRepository.save(messageContext.agentContext, credentialRecord) + + this.emitStateChangedEvent(messageContext.agentContext, credentialRecord, null) + return credentialRecord + } + } + + public async acceptRequest( + agentContext: AgentContext, + { + credentialRecord, + autoAcceptCredential, + comment, + goal, + goalCode, + credentialFormats, + }: AcceptCredentialRequestOptions + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Assert + credentialRecord.assertProtocolVersion('v2') + credentialRecord.assertState(CredentialState.RequestReceived) + + // Use empty credentialFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(credentialFormats ?? {}) + + // if no format services could be extracted from the credentialFormats + // take all available format services from the request message + if (formatServices.length === 0) { + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2RequestCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + } + + // If the format services list is still empty, throw an error as we don't support any + // of the formats + if (formatServices.length === 0) { + throw new CredoError(`Unable to accept request. No supported formats provided as input or in request message`) + } + const message = await this.credentialFormatCoordinator.acceptRequest(agentContext, { + credentialRecord, + formatServices, + comment, + goal, + goalCode, + credentialFormats, + }) + + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.CredentialIssued) + + return { credentialRecord, message } + } + + /** + * Process a received {@link V2IssueCredentialMessage}. This will not accept the credential + * or send a credential acknowledgement. It will only update the existing credential record with + * the information from the issue credential message. Use {@link createAck} + * after calling this method to create a credential acknowledgement. + * + * @param messageContext The message context containing an issue credential message + * + * @returns credential record associated with the issue credential message + * + */ + public async processCredential( + messageContext: InboundMessageContext + ): Promise { + const { message: credentialMessage, connection, agentContext } = messageContext + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing credential with id ${credentialMessage.id}`) + + const credentialRecord = await this.getByProperties(messageContext.agentContext, { + threadId: credentialMessage.threadId, + role: CredentialRole.Holder, + connectionId: connection?.id, + }) + + const requestMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2RequestCredentialMessage, + role: DidCommMessageRole.Sender, + }) + const offerMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + // Assert + credentialRecord.assertProtocolVersion('v2') + credentialRecord.assertState(CredentialState.RequestSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: offerMessage ?? undefined, + lastSentMessage: requestMessage, + expectedConnectionId: credentialRecord.connectionId, + }) + + const formatServices = this.getFormatServicesFromMessage(credentialMessage.formats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to process credential. No supported formats`) + } + + await this.credentialFormatCoordinator.processCredential(messageContext.agentContext, { + credentialRecord, + formatServices, + requestMessage: requestMessage, + message: credentialMessage, + }) + + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.CredentialReceived) + + return credentialRecord + } + + /** + * Create a {@link V2CredentialAckMessage} as response to a received credential. + * + * @param credentialRecord The credential record for which to create the credential acknowledgement + * @returns Object containing credential acknowledgement message and associated credential record + * + */ + public async acceptCredential( + agentContext: AgentContext, + { credentialRecord }: AcceptCredentialOptions + ): Promise> { + credentialRecord.assertProtocolVersion('v2') + credentialRecord.assertState(CredentialState.CredentialReceived) + + // Create message + const ackMessage = new V2CredentialAckMessage({ + status: AckStatus.OK, + threadId: credentialRecord.threadId, + }) + + ackMessage.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + + await this.updateState(agentContext, credentialRecord, CredentialState.Done) + + return { message: ackMessage, credentialRecord } + } + + /** + * Process a received {@link CredentialAckMessage}. + * + * @param messageContext The message context containing a credential acknowledgement message + * @returns credential record associated with the credential acknowledgement message + * + */ + public async processAck( + messageContext: InboundMessageContext + ): Promise { + const { message: ackMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing credential ack with id ${ackMessage.id}`) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + const credentialRecord = await this.getByProperties(messageContext.agentContext, { + threadId: ackMessage.threadId, + role: CredentialRole.Issuer, + connectionId: connection?.id, + }) + credentialRecord.connectionId = connection?.id + + const requestMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2RequestCredentialMessage, + role: DidCommMessageRole.Receiver, + }) + + const credentialMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2IssueCredentialMessage, + role: DidCommMessageRole.Sender, + }) + + // Assert + credentialRecord.assertProtocolVersion('v2') + credentialRecord.assertState(CredentialState.CredentialIssued) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: requestMessage, + lastSentMessage: credentialMessage, + expectedConnectionId: credentialRecord.connectionId, + }) + + // Update record + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.Done) + + return credentialRecord + } + + /** + * Create a {@link V2CredentialProblemReportMessage} to be sent. + * + * @param message message to send + * @returns a {@link V2CredentialProblemReportMessage} + * + */ + public async createProblemReport( + _agentContext: AgentContext, + { credentialRecord, description }: CreateCredentialProblemReportOptions + ): Promise> { + const message = new V2CredentialProblemReportMessage({ + description: { + en: description, + code: CredentialProblemReportReason.IssuanceAbandoned, + }, + }) + + message.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) + + return { credentialRecord, message } + } + + // AUTO ACCEPT METHODS + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + options: { + credentialRecord: CredentialExchangeRecord + proposalMessage: V2ProposeCredentialMessage + } + ): Promise { + const { credentialRecord, proposalMessage } = options + const credentialsModuleConfig = agentContext.dependencyManager.resolve(CredentialsModuleConfig) + + const autoAccept = composeAutoAccept( + credentialRecord.autoAcceptCredential, + credentialsModuleConfig.autoAcceptCredentials + ) + + // Handle always / never cases + if (autoAccept === AutoAcceptCredential.Always) return true + if (autoAccept === AutoAcceptCredential.Never) return false + + const offerMessage = await this.findOfferMessage(agentContext, credentialRecord.id) + if (!offerMessage) return false + + // NOTE: we take the formats from the offerMessage so we always check all services that we last sent + // Otherwise we'll only check the formats from the proposal, which could be different from the formats + // we use. + const formatServices = this.getFormatServicesFromMessage(offerMessage.formats) + + for (const formatService of formatServices) { + const offerAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + offerMessage.formats, + offerMessage.offerAttachments + ) + + const proposalAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToProposal(agentContext, { + credentialRecord, + offerAttachment, + proposalAttachment, + }) + // If any of the formats return false, we should not auto accept + if (!shouldAutoRespondToFormat) return false + } + + // not all formats use the proposal and preview, we only check if they're present on + // either or both of the messages + if (proposalMessage.credentialPreview || offerMessage.credentialPreview) { + // if one of the message doesn't have a preview, we should not auto accept + if (!proposalMessage.credentialPreview || !offerMessage.credentialPreview) return false + + // Check if preview values match + return arePreviewAttributesEqual( + proposalMessage.credentialPreview.attributes, + offerMessage.credentialPreview.attributes + ) + } + + return true + } + + public async shouldAutoRespondToOffer( + agentContext: AgentContext, + options: { + credentialRecord: CredentialExchangeRecord + offerMessage: V2OfferCredentialMessage + } + ): Promise { + const { credentialRecord, offerMessage } = options + const credentialsModuleConfig = agentContext.dependencyManager.resolve(CredentialsModuleConfig) + + const autoAccept = composeAutoAccept( + credentialRecord.autoAcceptCredential, + credentialsModuleConfig.autoAcceptCredentials + ) + // Handle always / never cases + if (autoAccept === AutoAcceptCredential.Always) return true + if (autoAccept === AutoAcceptCredential.Never) return false + + const proposalMessage = await this.findProposalMessage(agentContext, credentialRecord.id) + if (!proposalMessage) return false + + // NOTE: we take the formats from the proposalMessage so we always check all services that we last sent + // Otherwise we'll only check the formats from the offer, which could be different from the formats + // we use. + const formatServices = this.getFormatServicesFromMessage(proposalMessage.formats) + + for (const formatService of formatServices) { + const offerAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + offerMessage.formats, + offerMessage.offerAttachments + ) + + const proposalAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToOffer(agentContext, { + credentialRecord, + offerAttachment, + proposalAttachment, + }) + + // If any of the formats return false, we should not auto accept + + if (!shouldAutoRespondToFormat) return false + } + + // if one of the message doesn't have a preview, we should not auto accept + if (proposalMessage.credentialPreview || offerMessage.credentialPreview) { + // Check if preview values match + return arePreviewAttributesEqual( + proposalMessage.credentialPreview?.attributes ?? [], + offerMessage.credentialPreview?.attributes ?? [] + ) + } + return true + } + + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + options: { + credentialRecord: CredentialExchangeRecord + requestMessage: V2RequestCredentialMessage + } + ): Promise { + const { credentialRecord, requestMessage } = options + const credentialsModuleConfig = agentContext.dependencyManager.resolve(CredentialsModuleConfig) + + const autoAccept = composeAutoAccept( + credentialRecord.autoAcceptCredential, + credentialsModuleConfig.autoAcceptCredentials + ) + + // Handle always / never cases + if (autoAccept === AutoAcceptCredential.Always) return true + if (autoAccept === AutoAcceptCredential.Never) return false + + const proposalMessage = await this.findProposalMessage(agentContext, credentialRecord.id) + + const offerMessage = await this.findOfferMessage(agentContext, credentialRecord.id) + if (!offerMessage) return false + + // NOTE: we take the formats from the offerMessage so we always check all services that we last sent + // Otherwise we'll only check the formats from the request, which could be different from the formats + // we use. + const formatServices = this.getFormatServicesFromMessage(offerMessage.formats) + + for (const formatService of formatServices) { + const offerAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + offerMessage.formats, + offerMessage.offerAttachments + ) + + const proposalAttachment = proposalMessage + ? this.credentialFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + : undefined + + const requestAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToRequest(agentContext, { + credentialRecord, + offerAttachment, + requestAttachment, + proposalAttachment, + }) + + // If any of the formats return false, we should not auto accept + if (!shouldAutoRespondToFormat) return false + } + + return true + } + + public async shouldAutoRespondToCredential( + agentContext: AgentContext, + options: { + credentialRecord: CredentialExchangeRecord + credentialMessage: V2IssueCredentialMessage + } + ): Promise { + const { credentialRecord, credentialMessage } = options + const credentialsModuleConfig = agentContext.dependencyManager.resolve(CredentialsModuleConfig) + + const autoAccept = composeAutoAccept( + credentialRecord.autoAcceptCredential, + credentialsModuleConfig.autoAcceptCredentials + ) + + // Handle always / never cases + if (autoAccept === AutoAcceptCredential.Always) return true + if (autoAccept === AutoAcceptCredential.Never) return false + + const proposalMessage = await this.findProposalMessage(agentContext, credentialRecord.id) + const offerMessage = await this.findOfferMessage(agentContext, credentialRecord.id) + + const requestMessage = await this.findRequestMessage(agentContext, credentialRecord.id) + if (!requestMessage) return false + + // NOTE: we take the formats from the requestMessage so we always check all services that we last sent + // Otherwise we'll only check the formats from the credential, which could be different from the formats + // we use. + const formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + + for (const formatService of formatServices) { + const offerAttachment = offerMessage + ? this.credentialFormatCoordinator.getAttachmentForService( + formatService, + offerMessage.formats, + offerMessage.offerAttachments + ) + : undefined + + const proposalAttachment = proposalMessage + ? this.credentialFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + : undefined + + const requestAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const credentialAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + credentialMessage.formats, + credentialMessage.credentialAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToCredential(agentContext, { + credentialRecord, + offerAttachment, + credentialAttachment, + requestAttachment, + proposalAttachment, + }) + + // If any of the formats return false, we should not auto accept + if (!shouldAutoRespondToFormat) return false + } + return true + } + + public async findProposalMessage(agentContext: AgentContext, credentialExchangeId: string) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialExchangeId, + messageClass: V2ProposeCredentialMessage, + }) + } + + public async findOfferMessage(agentContext: AgentContext, credentialExchangeId: string) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialExchangeId, + messageClass: V2OfferCredentialMessage, + }) + } + + public async findRequestMessage(agentContext: AgentContext, credentialExchangeId: string) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialExchangeId, + messageClass: V2RequestCredentialMessage, + }) + } + + public async findCredentialMessage(agentContext: AgentContext, credentialExchangeId: string) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialExchangeId, + messageClass: V2IssueCredentialMessage, + }) + } + + public async getFormatData( + agentContext: AgentContext, + credentialExchangeId: string + ): Promise>> { + // TODO: we could looking at fetching all record using a single query and then filtering based on the type of the message. + const [proposalMessage, offerMessage, requestMessage, credentialMessage] = await Promise.all([ + this.findProposalMessage(agentContext, credentialExchangeId), + this.findOfferMessage(agentContext, credentialExchangeId), + this.findRequestMessage(agentContext, credentialExchangeId), + this.findCredentialMessage(agentContext, credentialExchangeId), + ]) + + // Create object with the keys and the message formats/attachments. We can then loop over this in a generic + // way so we don't have to add the same operation code four times + const messages = { + proposal: [proposalMessage?.formats, proposalMessage?.proposalAttachments], + offer: [offerMessage?.formats, offerMessage?.offerAttachments], + request: [requestMessage?.formats, requestMessage?.requestAttachments], + credential: [credentialMessage?.formats, credentialMessage?.credentialAttachments], + } as const + + const formatData: GetCredentialFormatDataReturn = { + proposalAttributes: proposalMessage?.credentialPreview?.attributes, + offerAttributes: offerMessage?.credentialPreview?.attributes, + } + + // We loop through all of the message keys as defined above + for (const [messageKey, [formats, attachments]] of Object.entries(messages)) { + // Message can be undefined, so we continue if it is not defined + if (!formats || !attachments) continue + + // Find all format services associated with the message + const formatServices = this.getFormatServicesFromMessage(formats) + const messageFormatData: CredentialFormatDataMessagePayload = {} + + // Loop through all of the format services, for each we will extract the attachment data and assign this to the object + // using the unique format key (e.g. indy) + for (const formatService of formatServices) { + const attachment = this.credentialFormatCoordinator.getAttachmentForService(formatService, formats, attachments) + + messageFormatData[formatService.formatKey] = attachment.getDataAsJson() + } + + formatData[messageKey as Exclude] = + messageFormatData + } + + return formatData + } + + /** + * Get all the format service objects for a given credential format from an incoming message + * @param messageFormats the format objects containing the format name (eg indy) + * @return the credential format service objects in an array - derived from format object keys + */ + private getFormatServicesFromMessage(messageFormats: CredentialFormatSpec[]): CredentialFormatService[] { + const formatServices = new Set() + + for (const msg of messageFormats) { + const service = this.getFormatServiceForFormat(msg.format) + if (service) formatServices.add(service) + } + + return Array.from(formatServices) + } + + /** + * Get all the format service objects for a given credential format + * @param credentialFormats the format object containing various optional parameters + * @return the credential format service objects in an array - derived from format object keys + */ + private getFormatServices( + credentialFormats: CredentialFormatPayload, M> + ): CredentialFormatService[] { + const formats = new Set() + + for (const formatKey of Object.keys(credentialFormats)) { + const formatService = this.getFormatServiceForFormatKey(formatKey) + + if (formatService) formats.add(formatService) + } + + return Array.from(formats) + } + + private getFormatServiceForFormatKey(formatKey: string): CredentialFormatService | null { + const formatService = this.credentialFormats.find((credentialFormat) => credentialFormat.formatKey === formatKey) + + return formatService ?? null + } + + private getFormatServiceForFormat(format: string): CredentialFormatService | null { + const formatService = this.credentialFormats.find((credentialFormat) => credentialFormat.supportsFormat(format)) + + return formatService ?? null + } + + protected getFormatServiceForRecordType(credentialRecordType: string) { + const formatService = this.credentialFormats.find( + (credentialFormat) => credentialFormat.credentialRecordType === credentialRecordType + ) + + if (!formatService) { + throw new CredoError( + `No format service found for credential record type ${credentialRecordType} in v2 credential protocol` + ) + } + + return formatService + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolCred.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolCred.test.ts new file mode 100644 index 0000000000..0ead36528f --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolCred.test.ts @@ -0,0 +1,828 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { AgentContext } from '../../../../../agent' +import type { GetAgentMessageOptions } from '../../../../../storage' +import type { PlaintextMessage } from '../../../../../types' +import type { CredentialStateChangedEvent } from '../../../CredentialEvents' +import type { + CredentialFormat, + CredentialFormatAcceptRequestOptions, + CredentialFormatCreateOfferOptions, + CredentialFormatService, +} from '../../../formats' +import type { CredentialPreviewAttribute } from '../../../models/CredentialPreviewAttribute' +import type { CustomCredentialTags } from '../../../repository/CredentialExchangeRecord' + +import { Subject } from 'rxjs' + +import { CredoError, CredentialFormatSpec, CredentialRole } from '../../../../..' +import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../../tests/helpers' +import { EventEmitter } from '../../../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' +import { DidCommMessageRecord, DidCommMessageRole, DidCommMessageRepository } from '../../../../../storage' +import { JsonTransformer } from '../../../../../utils' +import { JsonEncoder } from '../../../../../utils/JsonEncoder' +import { AckStatus } from '../../../../common/messages/AckMessage' +import { DidExchangeState } from '../../../../connections' +import { ConnectionService } from '../../../../connections/services/ConnectionService' +import { CredentialEventTypes } from '../../../CredentialEvents' +import { credReq } from '../../../__tests__/fixtures' +import { CredentialProblemReportReason } from '../../../models/CredentialProblemReportReason' +import { CredentialState } from '../../../models/CredentialState' +import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import { CredentialRepository } from '../../../repository/CredentialRepository' +import { V2CredentialProtocol } from '../V2CredentialProtocol' +import { V2CredentialPreview, V2ProposeCredentialMessage } from '../messages' +import { V2CredentialAckMessage } from '../messages/V2CredentialAckMessage' +import { V2CredentialProblemReportMessage } from '../messages/V2CredentialProblemReportMessage' +import { V2IssueCredentialMessage } from '../messages/V2IssueCredentialMessage' +import { V2OfferCredentialMessage } from '../messages/V2OfferCredentialMessage' +import { V2RequestCredentialMessage } from '../messages/V2RequestCredentialMessage' + +// Mock classes +jest.mock('../../../repository/CredentialRepository') +jest.mock('../../../../../storage/didcomm/DidCommMessageRepository') +jest.mock('../../../../routing/services/RoutingService') +jest.mock('../../../../connections/services/ConnectionService') +jest.mock('../../../../../agent/Dispatcher') + +// Mock typed object +const CredentialRepositoryMock = CredentialRepository as jest.Mock +const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock +const ConnectionServiceMock = ConnectionService as jest.Mock + +const credentialRepository = new CredentialRepositoryMock() +const didCommMessageRepository = new DidCommMessageRepositoryMock() +const connectionService = new ConnectionServiceMock() + +const agentConfig = getAgentConfig('V2CredentialProtocolCredTest') +const eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) + +const agentContext = getAgentContext({ + registerInstances: [ + [CredentialRepository, credentialRepository], + [DidCommMessageRepository, didCommMessageRepository], + [ConnectionService, connectionService], + [EventEmitter, eventEmitter], + ], + agentConfig, +}) + +const connection = getMockConnection({ + id: '123', + state: DidExchangeState.Completed, +}) + +const offerAttachment = new Attachment({ + id: 'offer-attachment-id', + mimeType: 'application/json', + data: new AttachmentData({ + base64: + 'eyJzY2hlbWFfaWQiOiJhYWEiLCJjcmVkX2RlZl9pZCI6IlRoN01wVGFSWlZSWW5QaWFiZHM4MVk6MzpDTDoxNzpUQUciLCJub25jZSI6Im5vbmNlIiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7fX0', + }), +}) + +const requestAttachment = new Attachment({ + id: 'request-attachment-id', + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(credReq), + }), +}) + +const credentialAttachment = new Attachment({ + id: 'credential-attachment-id', + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64({ + values: {}, + }), + }), +}) + +const requestFormat = new CredentialFormatSpec({ + attachmentId: 'request-attachment-id', + format: 'hlindy/cred-filter@v2.0', +}) + +const proposalAttachment = new Attachment({ + id: 'proposal-attachment-id', + data: new AttachmentData({ + json: { + any: 'value', + }, + }), +}) + +const offerFormat = new CredentialFormatSpec({ + attachmentId: 'offer-attachment-id', + format: 'hlindy/cred-abstract@v2.0', +}) + +const proposalFormat = new CredentialFormatSpec({ + attachmentId: 'proposal-attachment-id', + format: 'hlindy/cred-abstract@v2.0', +}) + +const credentialFormat = new CredentialFormatSpec({ + attachmentId: 'credential-attachment-id', + format: 'hlindy/cred@v2.0', +}) + +const credentialProposalMessage = new V2ProposeCredentialMessage({ + formats: [proposalFormat], + proposalAttachments: [proposalAttachment], +}) +const credentialRequestMessage = new V2RequestCredentialMessage({ + formats: [requestFormat], + requestAttachments: [requestAttachment], +}) +credentialRequestMessage.setThread({ threadId: 'somethreadid' }) + +const credentialOfferMessage = new V2OfferCredentialMessage({ + formats: [offerFormat], + comment: 'some comment', + credentialPreview: new V2CredentialPreview({ + attributes: [], + }), + offerAttachments: [offerAttachment], +}) +const credentialIssueMessage = new V2IssueCredentialMessage({ + credentialAttachments: [credentialAttachment], + formats: [credentialFormat], +}) +credentialIssueMessage.setThread({ threadId: 'somethreadid' }) + +const didCommMessageRecord = new DidCommMessageRecord({ + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + message: {} as PlaintextMessage, + role: DidCommMessageRole.Receiver, +}) + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getAgentMessageMock = async (_agentContext: AgentContext, options: GetAgentMessageOptions) => { + if (options.messageClass === V2ProposeCredentialMessage) { + return credentialProposalMessage + } + if (options.messageClass === V2OfferCredentialMessage) { + return credentialOfferMessage + } + if (options.messageClass === V2RequestCredentialMessage) { + return credentialRequestMessage + } + if (options.messageClass === V2IssueCredentialMessage) { + return credentialIssueMessage + } + + throw new CredoError('Could not find message') +} + +// A record is deserialized to JSON when it's stored into the storage. We want to simulate this behaviour for `offer` +// object to test our service would behave correctly. We use type assertion for `offer` attribute to `any`. +const mockCredentialRecord = ({ + state, + role, + threadId, + connectionId, + tags, + id, + credentialAttributes, +}: { + state?: CredentialState + role?: CredentialRole + tags?: CustomCredentialTags + threadId?: string + connectionId?: string + id?: string + credentialAttributes?: CredentialPreviewAttribute[] +} = {}) => { + const credentialRecord = new CredentialExchangeRecord({ + id, + credentialAttributes: credentialAttributes, + state: state || CredentialState.OfferSent, + role: role || CredentialRole.Issuer, + threadId: threadId || 'thread-id', + connectionId: connectionId ?? '123', + credentials: [ + { + credentialRecordType: 'test', + credentialRecordId: '123456', + }, + ], + tags, + protocolVersion: 'v2', + }) + + return credentialRecord +} + +interface TestCredentialFormat extends CredentialFormat { + formatKey: 'test' + credentialRecordType: 'test' +} + +type TestCredentialFormatService = CredentialFormatService + +export const testCredentialFormatService = { + credentialRecordType: 'test', + formatKey: 'test', + supportsFormat: (_format: string) => true, + createOffer: async ( + _agentContext: AgentContext, + _options: CredentialFormatCreateOfferOptions + ) => ({ + attachment: offerAttachment, + format: offerFormat, + }), + acceptRequest: async ( + _agentContext: AgentContext, + _options: CredentialFormatAcceptRequestOptions + ) => ({ attachment: credentialAttachment, format: credentialFormat }), + deleteCredentialById: jest.fn(), + processCredential: jest.fn(), + acceptOffer: () => ({ attachment: requestAttachment, format: requestFormat }), + processRequest: jest.fn(), +} as unknown as TestCredentialFormatService + +describe('credentialProtocol', () => { + let credentialProtocol: V2CredentialProtocol + + beforeEach(async () => { + // mock function implementations + mockFunction(connectionService.getById).mockResolvedValue(connection) + mockFunction(didCommMessageRepository.findAgentMessage).mockImplementation(getAgentMessageMock) + mockFunction(didCommMessageRepository.getAgentMessage).mockImplementation(getAgentMessageMock) + mockFunction(didCommMessageRepository.findByQuery).mockResolvedValue([ + didCommMessageRecord, + didCommMessageRecord, + didCommMessageRecord, + ]) + + credentialProtocol = new V2CredentialProtocol({ + credentialFormats: [testCredentialFormatService], + }) + }) + + afterEach(() => { + jest.resetAllMocks() + }) + + describe('acceptOffer', () => { + test(`updates state to ${CredentialState.RequestSent}`, async () => { + const credentialRecord = mockCredentialRecord({ + state: CredentialState.OfferReceived, + threadId: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + + // when + await credentialProtocol.acceptOffer(agentContext, { + credentialRecord, + credentialFormats: {}, + }) + + // then + expect(credentialRepository.update).toHaveBeenNthCalledWith( + 1, + agentContext, + expect.objectContaining({ + state: CredentialState.RequestSent, + }) + ) + }) + + test('returns credential request message base on existing credential offer message', async () => { + // given + const comment = 'credential request comment' + + const credentialRecord = mockCredentialRecord({ + state: CredentialState.OfferReceived, + threadId: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + + // when + const { message: credentialRequest } = await credentialProtocol.acceptOffer(agentContext, { + credentialRecord, + comment, + }) + + // then + expect(credentialRequest.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/request-credential', + '~thread': { + thid: credentialRecord.threadId, + }, + formats: [JsonTransformer.toJSON(requestFormat)], + comment, + 'requests~attach': [JsonTransformer.toJSON(requestAttachment)], + }) + }) + + const validState = CredentialState.OfferReceived + const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) + test(`throws an error when state transition is invalid`, async () => { + await Promise.all( + invalidCredentialStates.map(async (state) => { + await expect( + credentialProtocol.acceptOffer(agentContext, { credentialRecord: mockCredentialRecord({ state }) }) + ).rejects.toThrow(`Credential record is in invalid state ${state}. Valid states are: ${validState}.`) + }) + ) + }) + }) + + describe('processRequest', () => { + test(`updates state to ${CredentialState.RequestReceived}, set request and returns credential record`, async () => { + const credentialRecord = mockCredentialRecord({ state: CredentialState.OfferSent }) + const messageContext = new InboundMessageContext(credentialRequestMessage, { + connection, + agentContext, + }) + + // given + mockFunction(credentialRepository.findSingleByQuery).mockResolvedValue(credentialRecord) + + // when + const returnedCredentialRecord = await credentialProtocol.processRequest(messageContext) + + // then + expect(credentialRepository.findSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { + threadId: 'somethreadid', + role: CredentialRole.Issuer, + }) + expect(credentialRepository.update).toHaveBeenCalledTimes(1) + expect(returnedCredentialRecord.state).toEqual(CredentialState.RequestReceived) + }) + + test(`emits stateChange event from ${CredentialState.OfferSent} to ${CredentialState.RequestReceived}`, async () => { + const credentialRecord = mockCredentialRecord({ state: CredentialState.OfferSent }) + const messageContext = new InboundMessageContext(credentialRequestMessage, { + connection, + agentContext, + }) + + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + mockFunction(credentialRepository.findSingleByQuery).mockResolvedValue(credentialRecord) + + const returnedCredentialRecord = await credentialProtocol.processRequest(messageContext) + + // then + expect(credentialRepository.findSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { + threadId: 'somethreadid', + role: CredentialRole.Issuer, + }) + expect(eventListenerMock).toHaveBeenCalled() + expect(returnedCredentialRecord.state).toEqual(CredentialState.RequestReceived) + }) + + const validState = CredentialState.OfferSent + const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) + test(`throws an error when state transition is invalid`, async () => { + const messageContext = new InboundMessageContext(credentialRequestMessage, { + connection, + agentContext, + }) + + await Promise.all( + invalidCredentialStates.map(async (state) => { + mockFunction(credentialRepository.findSingleByQuery).mockReturnValue( + Promise.resolve(mockCredentialRecord({ state })) + ) + await expect(credentialProtocol.processRequest(messageContext)).rejects.toThrow( + `Credential record is in invalid state ${state}. Valid states are: ${validState}.` + ) + }) + ) + }) + }) + + describe('acceptRequest', () => { + test(`updates state to ${CredentialState.CredentialIssued}`, async () => { + const credentialRecord = mockCredentialRecord({ + state: CredentialState.RequestReceived, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + + await credentialProtocol.acceptRequest(agentContext, { + credentialRecord, + comment: 'credential response comment', + }) + + // then + expect(credentialRepository.update).toHaveBeenNthCalledWith( + 1, + agentContext, + expect.objectContaining({ + state: CredentialState.CredentialIssued, + }) + ) + }) + + test(`emits stateChange event from ${CredentialState.RequestReceived} to ${CredentialState.CredentialIssued}`, async () => { + const credentialRecord = mockCredentialRecord({ + state: CredentialState.RequestReceived, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + + const eventListenerMock = jest.fn() + + // given + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + // when + await credentialProtocol.acceptRequest(agentContext, { + credentialRecord, + comment: 'credential response comment', + }) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: CredentialState.RequestReceived, + credentialRecord: expect.objectContaining({ + state: CredentialState.CredentialIssued, + }), + }, + }) + }) + + test('returns credential response message base on credential request message', async () => { + const credentialRecord = mockCredentialRecord({ + state: CredentialState.RequestReceived, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + + // given + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + const comment = 'credential response comment' + + // when + const { message: credentialResponse } = await credentialProtocol.acceptRequest(agentContext, { + comment: 'credential response comment', + credentialRecord, + }) + + // then + expect(credentialResponse.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/issue-credential', + '~thread': { + thid: credentialRecord.threadId, + }, + comment, + formats: [JsonTransformer.toJSON(credentialFormat)], + 'credentials~attach': [JsonTransformer.toJSON(credentialAttachment)], + '~please_ack': expect.any(Object), + }) + }) + }) + + describe('processCredential', () => { + test('finds credential record by thread ID and saves credential attachment into the wallet', async () => { + const credentialRecord = mockCredentialRecord({ + state: CredentialState.RequestSent, + }) + + const messageContext = new InboundMessageContext(credentialIssueMessage, { + connection, + agentContext, + }) + + // given + mockFunction(credentialRepository.getSingleByQuery).mockResolvedValue(credentialRecord) + + await credentialProtocol.processCredential(messageContext) + }) + }) + + describe('acceptCredential', () => { + test(`updates state to ${CredentialState.Done}`, async () => { + const credentialRecord = mockCredentialRecord({ + state: CredentialState.CredentialReceived, + threadId: 'somethreadid', + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + + // when + await credentialProtocol.acceptCredential(agentContext, { credentialRecord }) + + // then + expect(credentialRepository.update).toHaveBeenNthCalledWith( + 1, + agentContext, + expect.objectContaining({ + state: CredentialState.Done, + }) + ) + }) + + test(`emits stateChange event from ${CredentialState.CredentialReceived} to ${CredentialState.Done}`, async () => { + const credentialRecord = mockCredentialRecord({ + state: CredentialState.CredentialReceived, + threadId: 'somethreadid', + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + // when + await credentialProtocol.acceptCredential(agentContext, { credentialRecord }) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: CredentialState.CredentialReceived, + credentialRecord: expect.objectContaining({ + state: CredentialState.Done, + }), + }, + }) + }) + + test('returns ack message base on credential issue message', async () => { + const credentialRecord = mockCredentialRecord({ + state: CredentialState.CredentialReceived, + threadId: 'somethreadid', + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + + // given + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + + // when + const { message: ackMessage } = await credentialProtocol.acceptCredential(agentContext, { credentialRecord }) + + // then + expect(ackMessage.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/ack', + '~thread': { + thid: 'somethreadid', + }, + }) + }) + + const validState = CredentialState.CredentialReceived + const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) + test(`throws an error when state transition is invalid`, async () => { + await Promise.all( + invalidCredentialStates.map(async (state) => { + await expect( + credentialProtocol.acceptCredential(agentContext, { + credentialRecord: mockCredentialRecord({ + state, + threadId: 'somethreadid', + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }), + }) + ).rejects.toThrow(`Credential record is in invalid state ${state}. Valid states are: ${validState}.`) + }) + ) + }) + }) + + describe('processAck', () => { + const credentialRequest = new V2CredentialAckMessage({ + status: AckStatus.OK, + threadId: 'somethreadid', + }) + const messageContext = new InboundMessageContext(credentialRequest, { agentContext, connection }) + + test(`updates state to ${CredentialState.Done} and returns credential record`, async () => { + const credentialRecord = mockCredentialRecord({ + state: CredentialState.CredentialIssued, + }) + + // given + mockFunction(credentialRepository.getSingleByQuery).mockResolvedValue(credentialRecord) + + // when + const returnedCredentialRecord = await credentialProtocol.processAck(messageContext) + + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { + threadId: 'somethreadid', + connectionId: '123', + role: CredentialRole.Issuer, + }) + + expect(returnedCredentialRecord.state).toBe(CredentialState.Done) + }) + }) + + describe('createProblemReport', () => { + test('returns problem report message base once get error', async () => { + // given + const credentialRecord = mockCredentialRecord({ + state: CredentialState.OfferReceived, + threadId: 'somethreadid', + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + const description = 'Indy error' + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + + // when + const { message } = await credentialProtocol.createProblemReport(agentContext, { + description, + credentialRecord, + }) + + message.setThread({ threadId: 'somethreadid' }) + // then + expect(message.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/problem-report', + '~thread': { + thid: 'somethreadid', + }, + description: { + code: CredentialProblemReportReason.IssuanceAbandoned, + en: description, + }, + }) + }) + }) + + describe('processProblemReport', () => { + const message = new V2CredentialProblemReportMessage({ + description: { + en: 'Indy error', + code: CredentialProblemReportReason.IssuanceAbandoned, + }, + }) + message.setThread({ threadId: 'somethreadid' }) + const messageContext = new InboundMessageContext(message, { + connection, + agentContext, + }) + + test(`updates problem report error message and returns credential record`, async () => { + const credentialRecord = mockCredentialRecord({ + state: CredentialState.OfferReceived, + }) + + // given + mockFunction(credentialRepository.getSingleByQuery).mockResolvedValue(credentialRecord) + + // when + const returnedCredentialRecord = await credentialProtocol.processProblemReport(messageContext) + + // then + + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { + threadId: 'somethreadid', + }) + expect(credentialRepository.update).toHaveBeenCalled() + expect(returnedCredentialRecord.errorMessage).toBe('issuance-abandoned: Indy error') + }) + }) + + describe('repository methods', () => { + it('getById should return value from credentialRepository.getById', async () => { + const expected = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(expected)) + const result = await credentialProtocol.getById(agentContext, expected.id) + expect(credentialRepository.getById).toHaveBeenCalledWith(agentContext, expected.id) + + expect(result).toBe(expected) + }) + + it('getById should return value from credentialRepository.getSingleByQuery', async () => { + const expected = mockCredentialRecord() + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(expected)) + + const result = await credentialProtocol.getByProperties(agentContext, { + threadId: 'threadId', + role: CredentialRole.Issuer, + connectionId: 'connectionId', + }) + + expect(credentialRepository.getSingleByQuery).toHaveBeenCalledWith(agentContext, { + threadId: 'threadId', + role: CredentialRole.Issuer, + connectionId: 'connectionId', + }) + + expect(result).toBe(expected) + }) + + it('findById should return value from credentialRepository.findById', async () => { + const expected = mockCredentialRecord() + mockFunction(credentialRepository.findById).mockReturnValue(Promise.resolve(expected)) + const result = await credentialProtocol.findById(agentContext, expected.id) + expect(credentialRepository.findById).toHaveBeenCalledWith(agentContext, expected.id) + + expect(result).toBe(expected) + }) + + it('getAll should return value from credentialRepository.getAll', async () => { + const expected = [mockCredentialRecord(), mockCredentialRecord()] + + mockFunction(credentialRepository.getAll).mockReturnValue(Promise.resolve(expected)) + const result = await credentialProtocol.getAll(agentContext) + expect(credentialRepository.getAll).toHaveBeenCalledWith(agentContext) + + expect(result).toEqual(expect.arrayContaining(expected)) + }) + + it('findAllByQuery should return value from credentialRepository.findByQuery', async () => { + const expected = [mockCredentialRecord(), mockCredentialRecord()] + + mockFunction(credentialRepository.findByQuery).mockReturnValue(Promise.resolve(expected)) + const result = await credentialProtocol.findAllByQuery(agentContext, { state: CredentialState.OfferSent }, {}) + expect(credentialRepository.findByQuery).toHaveBeenCalledWith( + agentContext, + { state: CredentialState.OfferSent }, + {} + ) + + expect(result).toEqual(expect.arrayContaining(expected)) + }) + }) + + describe('deleteCredential', () => { + it('should call delete from repository', async () => { + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credentialRecord)) + + const repositoryDeleteSpy = jest.spyOn(credentialRepository, 'delete') + await credentialProtocol.delete(agentContext, credentialRecord) + expect(repositoryDeleteSpy).toHaveBeenNthCalledWith(1, agentContext, credentialRecord) + }) + + it('should call deleteCredentialById in testCredentialFormatService if deleteAssociatedCredential is true', async () => { + const deleteCredentialMock = mockFunction(testCredentialFormatService.deleteCredentialById) + + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + + await credentialProtocol.delete(agentContext, credentialRecord, { + deleteAssociatedCredentials: true, + deleteAssociatedDidCommMessages: false, + }) + + expect(deleteCredentialMock).toHaveBeenNthCalledWith( + 1, + agentContext, + credentialRecord.credentials[0].credentialRecordId + ) + }) + + it('should not call deleteCredentialById in testCredentialFormatService if deleteAssociatedCredential is false', async () => { + const deleteCredentialMock = mockFunction(testCredentialFormatService.deleteCredentialById) + + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + + await credentialProtocol.delete(agentContext, credentialRecord, { + deleteAssociatedCredentials: false, + deleteAssociatedDidCommMessages: false, + }) + + expect(deleteCredentialMock).not.toHaveBeenCalled() + }) + + it('deleteAssociatedCredentials should default to true', async () => { + const deleteCredentialMock = mockFunction(testCredentialFormatService.deleteCredentialById) + + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + + await credentialProtocol.delete(agentContext, credentialRecord) + + expect(deleteCredentialMock).toHaveBeenNthCalledWith( + 1, + agentContext, + credentialRecord.credentials[0].credentialRecordId + ) + }) + it('deleteAssociatedDidCommMessages should default to true', async () => { + const deleteCredentialMock = mockFunction(testCredentialFormatService.deleteCredentialById) + + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) + + await credentialProtocol.delete(agentContext, credentialRecord) + + expect(deleteCredentialMock).toHaveBeenNthCalledWith( + 1, + agentContext, + credentialRecord.credentials[0].credentialRecordId + ) + expect(didCommMessageRepository.delete).toHaveBeenCalledTimes(3) + }) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolOffer.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolOffer.test.ts new file mode 100644 index 0000000000..84d0a05779 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolOffer.test.ts @@ -0,0 +1,267 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { AgentContext } from '../../../../../agent' +import type { CredentialStateChangedEvent } from '../../../CredentialEvents' +import type { CredentialFormat, CredentialFormatCreateOfferOptions, CredentialFormatService } from '../../../formats' +import type { CreateCredentialOfferOptions } from '../../CredentialProtocolOptions' + +import { Subject } from 'rxjs' + +import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../../tests/helpers' +import { Dispatcher } from '../../../../../agent/Dispatcher' +import { EventEmitter } from '../../../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' +import { DidCommMessageRepository } from '../../../../../storage' +import { JsonTransformer } from '../../../../../utils' +import { DidExchangeState } from '../../../../connections' +import { ConnectionService } from '../../../../connections/services/ConnectionService' +import { RoutingService } from '../../../../routing/services/RoutingService' +import { CredentialEventTypes } from '../../../CredentialEvents' +import { CredentialFormatSpec } from '../../../models' +import { CredentialState } from '../../../models/CredentialState' +import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import { CredentialRepository } from '../../../repository/CredentialRepository' +import { V2CredentialProtocol } from '../V2CredentialProtocol' +import { V2CredentialPreview } from '../messages' +import { V2OfferCredentialMessage } from '../messages/V2OfferCredentialMessage' + +const offerFormat = new CredentialFormatSpec({ + attachmentId: 'offer-attachment-id', + format: 'hlindy/cred-abstract@v2.0', +}) + +const offerAttachment = new Attachment({ + id: 'offer-attachment-id', + mimeType: 'application/json', + data: new AttachmentData({ + base64: + 'eyJzY2hlbWFfaWQiOiJhYWEiLCJjcmVkX2RlZl9pZCI6IlRoN01wVGFSWlZSWW5QaWFiZHM4MVk6MzpDTDoxNzpUQUciLCJub25jZSI6Im5vbmNlIiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7fX0', + }), +}) + +interface TestCredentialFormat extends CredentialFormat { + formatKey: 'test' + credentialRecordType: 'test' +} + +type TestCredentialFormatService = CredentialFormatService + +export const testCredentialFormatService = { + credentialRecordType: 'test', + formatKey: 'test', + supportsFormat: (_format: string) => true, + createOffer: async ( + _agentContext: AgentContext, + _options: CredentialFormatCreateOfferOptions + ) => ({ + attachment: offerAttachment, + format: offerFormat, + previewAttributes: [ + { + mimeType: 'text/plain', + name: 'name', + value: 'John', + }, + { + mimeType: 'text/plain', + name: 'age', + value: '99', + }, + ], + }), + acceptRequest: jest.fn(), + deleteCredentialById: jest.fn(), + processCredential: jest.fn(), + acceptOffer: jest.fn(), + processRequest: jest.fn(), + processOffer: jest.fn(), +} as unknown as TestCredentialFormatService + +// Mock classes +jest.mock('../../../repository/CredentialRepository') +jest.mock('../../../../../storage/didcomm/DidCommMessageRepository') +jest.mock('../../../../routing/services/RoutingService') +jest.mock('../../../../connections/services/ConnectionService') +jest.mock('../../../../../agent/Dispatcher') + +// Mock typed object +const CredentialRepositoryMock = CredentialRepository as jest.Mock +const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock +const RoutingServiceMock = RoutingService as jest.Mock +const ConnectionServiceMock = ConnectionService as jest.Mock +const DispatcherMock = Dispatcher as jest.Mock + +const credentialRepository = new CredentialRepositoryMock() +const didCommMessageRepository = new DidCommMessageRepositoryMock() +const routingService = new RoutingServiceMock() +const dispatcher = new DispatcherMock() +const connectionService = new ConnectionServiceMock() + +const agentConfig = getAgentConfig('V2CredentialProtocolOfferTest') +const eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) + +const agentContext = getAgentContext({ + registerInstances: [ + [CredentialRepository, credentialRepository], + [DidCommMessageRepository, didCommMessageRepository], + [RoutingService, routingService], + [Dispatcher, dispatcher], + [ConnectionService, connectionService], + [EventEmitter, eventEmitter], + ], + agentConfig, +}) + +const connectionRecord = getMockConnection({ + id: '123', + state: DidExchangeState.Completed, +}) + +describe('V2CredentialProtocolOffer', () => { + let credentialProtocol: V2CredentialProtocol + + beforeEach(async () => { + // mock function implementations + mockFunction(connectionService.getById).mockResolvedValue(connectionRecord) + + credentialProtocol = new V2CredentialProtocol({ + credentialFormats: [testCredentialFormatService], + }) + }) + + afterEach(() => { + jest.resetAllMocks() + }) + + describe('createOffer', () => { + const offerOptions: CreateCredentialOfferOptions<[TestCredentialFormatService]> = { + comment: 'some comment', + connectionRecord, + credentialFormats: { + test: {}, + }, + } + + test(`creates credential record in ${CredentialState.OfferSent} state with offer, thread ID`, async () => { + // when + await credentialProtocol.createOffer(agentContext, offerOptions) + + // then + expect(credentialRepository.save).toHaveBeenNthCalledWith( + 1, + agentContext, + expect.objectContaining({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.OfferSent, + connectionId: connectionRecord.id, + }) + ) + }) + + test(`emits stateChange event with a new credential in ${CredentialState.OfferSent} state`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + await credentialProtocol.createOffer(agentContext, offerOptions) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: null, + credentialRecord: expect.objectContaining({ + state: CredentialState.OfferSent, + }), + }, + }) + }) + + test('returns credential offer message', async () => { + const { message: credentialOffer } = await credentialProtocol.createOffer(agentContext, offerOptions) + + expect(credentialOffer.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + comment: 'some comment', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/2.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + ], + }, + formats: [JsonTransformer.toJSON(offerFormat)], + 'offers~attach': [JsonTransformer.toJSON(offerAttachment)], + }) + }) + }) + + describe('processOffer', () => { + const credentialOfferMessage = new V2OfferCredentialMessage({ + formats: [offerFormat], + comment: 'some comment', + credentialPreview: new V2CredentialPreview({ + attributes: [], + }), + offerAttachments: [offerAttachment], + }) + + const messageContext = new InboundMessageContext(credentialOfferMessage, { + agentContext, + connection: connectionRecord, + }) + + test(`creates and return credential record in ${CredentialState.OfferReceived} state with offer, thread ID`, async () => { + // when + await credentialProtocol.processOffer(messageContext) + + // then + expect(credentialRepository.save).toHaveBeenNthCalledWith( + 1, + agentContext, + expect.objectContaining({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: credentialOfferMessage.id, + connectionId: connectionRecord.id, + state: CredentialState.OfferReceived, + }) + ) + }) + + test(`emits stateChange event with ${CredentialState.OfferReceived}`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + // when + await credentialProtocol.processOffer(messageContext) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: null, + credentialRecord: expect.objectContaining({ + state: CredentialState.OfferReceived, + }), + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.test.ts new file mode 100644 index 0000000000..8d8d4a0f86 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.test.ts @@ -0,0 +1,266 @@ +import type { SubjectMessage } from '../../../../../../../../tests/transport/SubjectInboundTransport' +import type { AnonCredsTestsAgent } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import type { CredentialStateChangedEvent } from '../../../CredentialEvents' +import type { AcceptCredentialOfferOptions, AcceptCredentialRequestOptions } from '../../../CredentialsApiOptions' + +import { ReplaySubject, Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../../../../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../../../../../../tests/transport/SubjectOutboundTransport' +import { getAnonCredsIndyModules } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import { + anoncredsDefinitionFourAttributesNoRevocation, + storePreCreatedAnonCredsDefinition, +} from '../../../../../../../anoncreds/tests/preCreatedAnonCredsDefinition' +import { waitForCredentialRecordSubject, getInMemoryAgentOptions } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { Agent } from '../../../../../agent/Agent' +import { CredentialEventTypes } from '../../../CredentialEvents' +import { AutoAcceptCredential } from '../../../models/CredentialAutoAcceptType' +import { CredentialState } from '../../../models/CredentialState' +import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import { V2CredentialPreview } from '../messages' + +const faberAgentOptions = getInMemoryAgentOptions( + 'Faber connection-less Credentials V2', + { + endpoints: ['rxjs:faber'], + }, + getAnonCredsIndyModules() +) + +const aliceAgentOptions = getInMemoryAgentOptions( + 'Alice connection-less Credentials V2', + { + endpoints: ['rxjs:alice'], + }, + getAnonCredsIndyModules() +) + +const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'true', + profile_picture: 'looking_good', +}) + +describe('V2 Connectionless Credentials', () => { + let faberAgent: AnonCredsTestsAgent + let aliceAgent: AnonCredsTestsAgent + let faberReplay: ReplaySubject + let aliceReplay: ReplaySubject + + beforeEach(async () => { + const faberMessages = new Subject() + const aliceMessages = new Subject() + + const subjectMap = { + 'rxjs:faber': faberMessages, + 'rxjs:alice': aliceMessages, + } + faberAgent = new Agent(faberAgentOptions) + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + + aliceAgent = new Agent(aliceAgentOptions) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + // Make sure the pre-created credential definition is in the wallet + await storePreCreatedAnonCredsDefinition(faberAgent, anoncredsDefinitionFourAttributesNoRevocation) + + faberReplay = new ReplaySubject() + aliceReplay = new ReplaySubject() + + faberAgent.events + .observable(CredentialEventTypes.CredentialStateChanged) + .subscribe(faberReplay) + aliceAgent.events + .observable(CredentialEventTypes.CredentialStateChanged) + .subscribe(aliceReplay) + }) + + afterEach(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Faber starts with connection-less credential offer to Alice', async () => { + testLogger.test('Faber sends credential offer to Alice') + + // eslint-disable-next-line prefer-const + let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOffer({ + comment: 'V2 Out of Band offer', + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: anoncredsDefinitionFourAttributesNoRevocation.credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + }) + + const { message: offerMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberCredentialRecord.id, + message, + domain: 'https://a-domain.com', + }) + + await aliceAgent.receiveMessage(offerMessage.toJSON()) + + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + testLogger.test('Alice sends credential request to Faber') + const acceptOfferOptions: AcceptCredentialOfferOptions = { + credentialRecordId: aliceCredentialRecord.id, + } + const credentialRecord = await aliceAgent.credentials.acceptOffer(acceptOfferOptions) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + const options: AcceptCredentialRequestOptions = { + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Credential', + } + faberCredentialRecord = await faberAgent.credentials.acceptRequest(options) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Alice sends credential ack to Faber') + aliceCredentialRecord = await aliceAgent.credentials.acceptCredential({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + credentialDefinitionId: anoncredsDefinitionFourAttributesNoRevocation.credentialDefinitionId, + }, + }, + }, + credentials: [ + { + credentialRecordType: 'w3c', + credentialRecordId: expect.any(String), + }, + ], + state: CredentialState.Done, + threadId: expect.any(String), + }) + + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + credentialDefinitionId: anoncredsDefinitionFourAttributesNoRevocation.credentialDefinitionId, + }, + }, + }, + state: CredentialState.Done, + threadId: expect.any(String), + }) + }) + + test('Faber starts with connection-less credential offer to Alice with auto-accept enabled', async () => { + // eslint-disable-next-line prefer-const + let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOffer({ + comment: 'V2 Out of Band offer', + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: anoncredsDefinitionFourAttributesNoRevocation.credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + }) + + const { message: offerMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberCredentialRecord.id, + message, + domain: 'https://a-domain.com', + }) + + // Receive Message + await aliceAgent.receiveMessage(offerMessage.toJSON()) + + // Wait for it to be processed + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + }) + + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + credentialDefinitionId: anoncredsDefinitionFourAttributesNoRevocation.credentialDefinitionId, + }, + }, + }, + credentials: [ + { + credentialRecordType: 'w3c', + credentialRecordId: expect.any(String), + }, + ], + state: CredentialState.Done, + threadId: expect.any(String), + }) + + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + threadId: expect.any(String), + }) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.test.ts new file mode 100644 index 0000000000..4d6058c5ef --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.test.ts @@ -0,0 +1,457 @@ +import type { AnonCredsTestsAgent } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import type { EventReplaySubject } from '../../../../../../tests' + +import { setupAnonCredsTests } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import { anoncredsDefinitionFourAttributesNoRevocation } from '../../../../../../../anoncreds/tests/preCreatedAnonCredsDefinition' +import { + waitForCredentialRecord, + waitForCredentialRecordSubject, + waitForAgentMessageProcessedEventSubject, +} from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { CredentialRole } from '../../../models' +import { AutoAcceptCredential } from '../../../models/CredentialAutoAcceptType' +import { CredentialState } from '../../../models/CredentialState' +import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import { V2ProposeCredentialMessage } from '../messages' +import { V2CredentialPreview } from '../messages/V2CredentialPreview' + +describe('V2 Credentials Auto Accept', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let credentialDefinitionId: string + let schemaId: string + let faberConnectionId: string + let aliceConnectionId: string + + const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', + }) + const newCredentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'another x-ray value', + profile_picture: 'another profile picture', + }) + + describe("Auto accept on 'always'", () => { + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + schemaId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'faber agent: always v2', + holderName: 'alice agent: always v2', + autoAcceptCredentials: AutoAcceptCredential.Always, + preCreatedDefinition: anoncredsDefinitionFourAttributesNoRevocation, + })) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test("Alice starts with V2 credential proposal to Faber, both with autoAcceptCredential on 'always'", async () => { + testLogger.test('Alice sends credential proposal to Faber') + let aliceCredentialRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + comment: 'v2 propose credential test', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + state: CredentialState.Done, + threadId: aliceCredentialRecord.threadId, + }) + + testLogger.test('Faber waits for credential ack from Alice') + await waitForCredentialRecordSubject(faberReplay, { + state: CredentialState.Done, + threadId: aliceCredentialRecord.threadId, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + schemaId, + credentialDefinitionId: credentialDefinitionId, + }, + }, + }, + state: CredentialState.Done, + }) + }) + + test("Faber starts with V2 credential offer to Alice, both with autoAcceptCredential on 'always'", async () => { + testLogger.test('Faber sends credential offer to Alice') + let faberCredentialRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + }) + + testLogger.test('Alice waits for credential from Faber') + const aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + state: CredentialState.CredentialReceived, + threadId: faberCredentialRecord.threadId, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credentialRequest': expect.any(Object), + '_anoncreds/credential': { + schemaId, + credentialDefinitionId: credentialDefinitionId, + }, + }, + }, + credentials: [ + { + credentialRecordType: 'w3c', + credentialRecordId: expect.any(String), + }, + ], + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + state: CredentialState.Done, + threadId: faberCredentialRecord.threadId, + }) + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + }) + }) + }) + + describe("Auto accept on 'contentApproved'", () => { + // FIXME: we don't need to set up the agent and create all schemas/credential definitions again, just change the auto accept credential setting + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + schemaId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber Agent: Always V2', + holderName: 'Alice Agent: Always V2', + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + preCreatedDefinition: anoncredsDefinitionFourAttributesNoRevocation, + })) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test("Alice starts with V2 credential proposal to Faber, both with autoAcceptCredential on 'contentApproved'", async () => { + testLogger.test('Alice sends credential proposal to Faber') + + let aliceCredentialRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + }) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + state: CredentialState.ProposalReceived, + threadId: aliceCredentialRecord.threadId, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Offer', + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + state: CredentialState.Done, + threadId: faberCredentialRecord.threadId, + }) + + faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credentialRequest': expect.any(Object), + '_anoncreds/credential': { + schemaId, + credentialDefinitionId: credentialDefinitionId, + }, + }, + }, + credentials: [ + { + credentialRecordType: 'w3c', + credentialRecordId: expect.any(String), + }, + ], + state: CredentialState.Done, + }) + + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + schemaId, + credentialDefinitionId: credentialDefinitionId, + }, + }, + }, + state: CredentialState.Done, + }) + }) + + test("Faber starts with V2 credential offer to Alice, both with autoAcceptCredential on 'contentApproved'", async () => { + testLogger.test('Faber sends credential offer to Alice') + let faberCredentialRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + state: CredentialState.OfferReceived, + threadId: faberCredentialRecord.threadId, + }) + + // below values are not in json object + expect(aliceCredentialRecord.id).not.toBeNull() + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: aliceCredentialRecord.threadId, + state: aliceCredentialRecord.state, + connectionId: aliceConnectionId, + role: CredentialRole.Holder, + credentialIds: [], + }) + testLogger.test('Alice received credential offer from Faber') + + testLogger.test('alice sends credential request to faber') + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + state: CredentialState.Done, + threadId: faberCredentialRecord.threadId, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credentialRequest': expect.any(Object), + '_anoncreds/credential': { + schemaId, + credentialDefinitionId: credentialDefinitionId, + }, + }, + }, + credentials: [ + { + credentialRecordType: 'w3c', + credentialRecordId: expect.any(String), + }, + ], + state: CredentialState.Done, + }) + + testLogger.test('Faber waits for credential ack from Alice') + + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + }) + }) + + test("Alice starts with V2 credential proposal to Faber, both have autoAcceptCredential on 'contentApproved' and attributes did change", async () => { + testLogger.test('Alice sends credential proposal to Faber') + let aliceCredentialRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + comment: 'v2 propose credential test', + }) + expect(aliceCredentialRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + state: CredentialState.ProposalReceived, + threadId: aliceCredentialRecord.threadId, + }) + + testLogger.test('Faber negotiated proposal, sending credential offer to Alice') + faberCredentialRecord = await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + state: CredentialState.OfferReceived, + threadId: faberCredentialRecord.threadId, + }) + + // below values are not in json object + expect(aliceCredentialRecord.id).not.toBeNull() + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: aliceCredentialRecord.threadId, + state: aliceCredentialRecord.state, + connectionId: aliceConnectionId, + role: CredentialRole.Holder, + credentialIds: [], + }) + }) + + test("Faber starts with V2 credential offer to Alice, both have autoAcceptCredential on 'contentApproved' and attributes did change", async () => { + testLogger.test('Faber sends credential offer to Alice') + const faberCredentialRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + }) + + testLogger.test('Alice waits for credential offer from Faber') + const aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + state: CredentialState.OfferReceived, + threadId: faberCredentialRecord.threadId, + }) + + // below values are not in json object + expect(aliceCredentialRecord.id).not.toBeNull() + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: aliceCredentialRecord.threadId, + state: aliceCredentialRecord.state, + connectionId: aliceConnectionId, + role: CredentialRole.Holder, + credentialIds: [], + }) + + testLogger.test('Alice sends credential request to Faber') + await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + anoncreds: { + attributes: newCredentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + comment: 'v2 propose credential test', + }) + + await waitForCredentialRecordSubject(faberReplay, { + state: CredentialState.ProposalReceived, + threadId: aliceCredentialRecord.threadId, + }) + + // ProposalReceived is emitted before the whole message is finished processing + // So to not get errors when shutting down the agent, we wait for the message to be processed + await waitForAgentMessageProcessedEventSubject(faberReplay, { + threadId: aliceCredentialRecord.threadId, + messageType: V2ProposeCredentialMessage.type.messageTypeUri, + }) + }) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials.e2e.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials.e2e.test.ts new file mode 100644 index 0000000000..5eb7bc2d18 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials.e2e.test.ts @@ -0,0 +1,672 @@ +import type { AnonCredsHolderService } from '../../../../../../../anoncreds/src' +import type { LegacyIndyProposeCredentialFormat } from '../../../../../../../anoncreds/src/formats/LegacyIndyCredentialFormat' +import type { AnonCredsTestsAgent } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import type { EventReplaySubject } from '../../../../../../tests' + +import { AnonCredsHolderServiceSymbol } from '../../../../../../../anoncreds/src' +import { + issueLegacyAnonCredsCredential, + setupAnonCredsTests, +} from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import { waitForCredentialRecord, waitForCredentialRecordSubject } from '../../../../../../tests' +import testLogger from '../../../../../../tests/logger' +import { DidCommMessageRepository } from '../../../../../storage' +import { JsonTransformer } from '../../../../../utils' +import { CredentialRole } from '../../../models' +import { CredentialState } from '../../../models/CredentialState' +import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import { + V2CredentialPreview, + V2IssueCredentialMessage, + V2OfferCredentialMessage, + V2ProposeCredentialMessage, + V2RequestCredentialMessage, +} from '../messages' + +const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', +}) + +describe('v2 credentials', () => { + let faberAgent: AnonCredsTestsAgent + let aliceAgent: AnonCredsTestsAgent + let credentialDefinitionId: string + let faberConnectionId: string + let aliceConnectionId: string + + let faberReplay: EventReplaySubject + let aliceReplay: EventReplaySubject + + let indyCredentialProposal: LegacyIndyProposeCredentialFormat + + const newCredentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'another x-ray value', + profile_picture: 'another profile picture', + }) + + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber Agent Credentials v2', + holderName: 'Alice Agent Credentials v2', + attributeNames: ['name', 'age', 'x-ray', 'profile_picture'], + })) + + indyCredentialProposal = { + credentialDefinitionId: credentialDefinitionId, + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + } + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with V2 credential proposal to Faber', async () => { + testLogger.test('Alice sends (v2) credential proposal to Faber') + + const credentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', + }, + }, + comment: 'v2 propose credential test', + }) + + expect(credentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.ProposalSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Proposal', + credentialFormats: { + indy: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) + const offerMessage = await didCommMessageRepository.findAgentMessage(faberAgent.context, { + associatedRecordId: faberCredentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + + expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + comment: 'V2 Indy Proposal', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/2.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + { + name: 'x-ray', + 'mime-type': 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + 'mime-type': 'text/plain', + value: 'profile picture', + }, + ], + }, + 'offers~attach': expect.any(Array), + }) + + expect(aliceCredentialRecord).toMatchObject({ + id: expect.any(String), + connectionId: expect.any(String), + type: CredentialExchangeRecord.type, + }) + + // below values are not in json object + expect(aliceCredentialRecord.getTags()).toEqual({ + role: CredentialRole.Holder, + parentThreadId: undefined, + threadId: faberCredentialRecord.threadId, + connectionId: aliceCredentialRecord.connectionId, + state: aliceCredentialRecord.state, + credentialIds: [], + }) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.RequestSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + await aliceAgent.credentials.acceptCredential({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Faber waits for state done') + await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + }) + + test('Faber issues credential which is then deleted from Alice`s wallet', async () => { + const { holderCredentialExchangeRecord } = await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnectionId, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + offer: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }) + + // test that delete credential removes from both repository and wallet + // latter is tested by spying on holder service to + // see if deleteCredential is called + const holderService = aliceAgent.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const deleteCredentialSpy = jest.spyOn(holderService, 'deleteCredential') + await aliceAgent.credentials.deleteById(holderCredentialExchangeRecord.id, { + deleteAssociatedCredentials: true, + deleteAssociatedDidCommMessages: true, + }) + expect(deleteCredentialSpy).toHaveBeenNthCalledWith( + 1, + aliceAgent.context, + holderCredentialExchangeRecord.credentials[0].credentialRecordId + ) + + return expect(aliceAgent.credentials.getById(holderCredentialExchangeRecord.id)).rejects.toThrowError( + `CredentialRecord: record with id ${holderCredentialExchangeRecord.id} not found.` + ) + }) + + test('Alice starts with proposal, faber sends a counter offer, alice sends second proposal, faber sends second offer', async () => { + // proposeCredential -> negotiateProposal -> negotiateOffer -> negotiateProposal -> acceptOffer -> acceptRequest -> DONE (credential issued) + + let faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Alice sends credential proposal to Faber') + let aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + indy: { + ...indyCredentialProposal, + attributes: credentialPreview.attributes, + }, + }, + comment: 'v2 propose credential test', + }) + expect(aliceCredentialExchangeRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await faberCredentialRecordPromise + + let aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + faberCredentialRecord = await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + indy: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await aliceCredentialRecordPromise + + // Check if the state of the credential records did not change + faberCredentialRecord = await faberAgent.credentials.getById(faberCredentialRecord.id) + faberCredentialRecord.assertState(CredentialState.OfferSent) + + aliceCredentialRecord = await aliceAgent.credentials.getById(aliceCredentialRecord.id) + aliceCredentialRecord.assertState(CredentialState.OfferReceived) + + faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + // second proposal + aliceCredentialExchangeRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + indy: { + ...indyCredentialProposal, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + expect(aliceCredentialExchangeRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + faberCredentialRecord = await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + indy: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + + aliceCredentialRecord = await aliceCredentialRecordPromise + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialExchangeRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + state: CredentialState.RequestSent, + protocolVersion: 'v2', + threadId: aliceCredentialExchangeRecord.threadId, + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.RequestReceived, + }) + testLogger.test('Faber sends credential to Alice') + + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + // testLogger.test('Alice sends credential ack to Faber') + await aliceAgent.credentials.acceptCredential({ credentialRecordId: aliceCredentialRecord.id }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: expect.any(String), + connectionId: expect.any(String), + state: CredentialState.CredentialReceived, + }) + }) + + test('Faber starts with offer, alice sends counter proposal, faber sends second offer, alice sends second proposal', async () => { + let aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + let faberCredentialRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await aliceCredentialRecordPromise + + let faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + aliceCredentialRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + indy: { + ...indyCredentialProposal, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + expect(aliceCredentialRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + faberCredentialRecord = await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + indy: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + + aliceCredentialRecord = await aliceCredentialRecordPromise + + faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + aliceCredentialRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + indy: { + ...indyCredentialProposal, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + expect(aliceCredentialRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Proposal', + credentialFormats: { + indy: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialRecord = await aliceCredentialRecordPromise + + faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + state: CredentialState.RequestSent, + protocolVersion: 'v2', + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await aliceCredentialRecordPromise + + const proposalMessage = await aliceAgent.credentials.findProposalMessage(aliceCredentialRecord.id) + const offerMessage = await aliceAgent.credentials.findOfferMessage(aliceCredentialRecord.id) + const requestMessage = await aliceAgent.credentials.findRequestMessage(aliceCredentialRecord.id) + const credentialMessage = await aliceAgent.credentials.findCredentialMessage(aliceCredentialRecord.id) + + expect(proposalMessage).toBeInstanceOf(V2ProposeCredentialMessage) + expect(offerMessage).toBeInstanceOf(V2OfferCredentialMessage) + expect(requestMessage).toBeInstanceOf(V2RequestCredentialMessage) + expect(credentialMessage).toBeInstanceOf(V2IssueCredentialMessage) + + const formatData = await aliceAgent.credentials.getFormatData(aliceCredentialRecord.id) + expect(formatData).toMatchObject({ + proposalAttributes: [ + { + name: 'name', + mimeType: 'text/plain', + value: 'John', + }, + { + name: 'age', + mimeType: 'text/plain', + value: '99', + }, + { + name: 'x-ray', + mimeType: 'text/plain', + value: 'another x-ray value', + }, + { + name: 'profile_picture', + mimeType: 'text/plain', + value: 'another profile picture', + }, + ], + proposal: { + indy: { + schema_issuer_did: expect.any(String), + schema_id: expect.any(String), + schema_name: expect.any(String), + schema_version: expect.any(String), + cred_def_id: expect.any(String), + issuer_did: expect.any(String), + }, + }, + offer: { + indy: { + schema_id: expect.any(String), + cred_def_id: expect.any(String), + key_correctness_proof: expect.any(Object), + nonce: expect.any(String), + }, + }, + offerAttributes: [ + { + name: 'name', + mimeType: 'text/plain', + value: 'John', + }, + { + name: 'age', + mimeType: 'text/plain', + value: '99', + }, + { + name: 'x-ray', + mimeType: 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + mimeType: 'text/plain', + value: 'profile picture', + }, + ], + request: { + indy: { + prover_did: expect.any(String), + cred_def_id: expect.any(String), + blinded_ms: expect.any(Object), + blinded_ms_correctness_proof: expect.any(Object), + nonce: expect.any(String), + }, + }, + credential: { + indy: { + schema_id: expect.any(String), + cred_def_id: expect.any(String), + rev_reg_id: null, + values: { + age: { raw: '99', encoded: '99' }, + profile_picture: { + raw: 'profile picture', + encoded: '28661874965215723474150257281172102867522547934697168414362313592277831163345', + }, + name: { + raw: 'John', + encoded: '76355713903561865866741292988746191972523015098789458240077478826513114743258', + }, + 'x-ray': { + raw: 'some x-ray', + encoded: '43715611391396952879378357808399363551139229809726238083934532929974486114650', + }, + }, + signature: expect.any(Object), + signature_correctness_proof: expect.any(Object), + rev_reg: null, + witness: null, + }, + }, + }) + }) + + test('Faber starts with V2 offer, alice declines the offer', async () => { + testLogger.test('Faber sends credential offer to Alice') + const faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + expect(aliceCredentialRecord).toMatchObject({ + id: expect.any(String), + type: CredentialExchangeRecord.type, + }) + + testLogger.test('Alice declines offer') + aliceCredentialRecord = await aliceAgent.credentials.declineOffer(aliceCredentialRecord.id) + + expect(aliceCredentialRecord.state).toBe(CredentialState.Declined) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.connectionless-credentials.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.connectionless-credentials.test.ts new file mode 100644 index 0000000000..7472cca215 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.connectionless-credentials.test.ts @@ -0,0 +1,159 @@ +import type { EventReplaySubject, JsonLdTestsAgent } from '../../../../../../tests' +import type { V2OfferCredentialMessage } from '../messages/V2OfferCredentialMessage' + +import { setupJsonLdTests, waitForCredentialRecordSubject } from '../../../../../../tests' +import testLogger from '../../../../../../tests/logger' +import { KeyType } from '../../../../../crypto' +import { TypedArrayEncoder } from '../../../../../utils' +import { CREDENTIALS_CONTEXT_V1_URL } from '../../../../vc/constants' +import { CredentialState } from '../../../models' +import { CredentialExchangeRecord } from '../../../repository' + +const signCredentialOptions = { + credential: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + }, + options: { + proofType: 'Ed25519Signature2018', + proofPurpose: 'assertionMethod', + }, +} + +describe('credentials', () => { + let faberAgent: JsonLdTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: JsonLdTestsAgent + let aliceReplay: EventReplaySubject + + beforeEach(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + } = await setupJsonLdTests({ + issuerName: 'Faber LD connection-less Credentials V2', + holderName: 'Alice LD connection-less Credentials V2', + createConnections: false, + })) + + await faberAgent.context.wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + keyType: KeyType.Ed25519, + }) + }) + + afterEach(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Faber starts with V2 W3C connection-less credential offer to Alice', async () => { + testLogger.test('Faber sends credential offer to Alice') + + // eslint-disable-next-line prefer-const + let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOffer({ + comment: 'V2 Out of Band offer (W3C)', + credentialFormats: { + jsonld: signCredentialOptions, + }, + protocolVersion: 'v2', + }) + + const offerMessage = message as V2OfferCredentialMessage + const attachment = offerMessage?.offerAttachments[0] + + expect(attachment?.getDataAsJson()).toMatchObject({ + credential: { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + name: 'Bachelor of Science and Arts', + type: 'BachelorDegree', + }, + }, + }, + options: { + proofType: 'Ed25519Signature2018', + proofPurpose: 'assertionMethod', + }, + }) + + const { message: connectionlessOfferMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberCredentialRecord.id, + message, + domain: 'https://a-domain.com', + }) + await aliceAgent.receiveMessage(connectionlessOfferMessage.toJSON()) + + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + testLogger.test('Alice sends credential request to Faber') + + const credentialRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + faberCredentialRecord = await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Alice sends credential ack to Faber') + aliceCredentialRecord = await aliceAgent.credentials.acceptCredential({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + threadId: expect.any(String), + }) + + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + threadId: expect.any(String), + }) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials-auto-accept.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials-auto-accept.test.ts new file mode 100644 index 0000000000..cacd194c61 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials-auto-accept.test.ts @@ -0,0 +1,406 @@ +import type { JsonLdTestsAgent } from '../../../../../../tests' + +import { setupJsonLdTests } from '../../../../../../tests' +import { waitForCredentialRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { KeyType } from '../../../../../crypto' +import { CredoError } from '../../../../../error/CredoError' +import { TypedArrayEncoder } from '../../../../../utils' +import { CREDENTIALS_CONTEXT_V1_URL } from '../../../../vc/constants' +import { AutoAcceptCredential, CredentialRole, CredentialState } from '../../../models' +import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' + +const signCredentialOptions = { + credential: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + }, + options: { + proofType: 'Ed25519Signature2018', + proofPurpose: 'assertionMethod', + }, +} + +describe('V2 Credentials - JSON-LD - Auto Accept Always', () => { + let faberAgent: JsonLdTestsAgent + let aliceAgent: JsonLdTestsAgent + let faberConnectionId: string + let aliceConnectionId: string + + describe("Auto accept on 'always'", () => { + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupJsonLdTests({ + issuerName: 'faber agent: always v2 jsonld', + holderName: 'alice agent: always v2 jsonld', + autoAcceptCredentials: AutoAcceptCredential.Always, + })) + + await faberAgent.context.wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + keyType: KeyType.Ed25519, + }) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test("Alice starts with V2 credential proposal to Faber, both with autoAcceptCredential on 'always'", async () => { + testLogger.test('Alice sends credential proposal to Faber') + + const aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + jsonld: signCredentialOptions, + }, + comment: 'v2 propose credential test', + }) + + testLogger.test('Alice waits for credential from Faber') + + let aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber waits for credential ack from Alice') + aliceCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: {}, + state: CredentialState.Done, + }) + }) + test("Faber starts with V2 credential offer to Alice, both with autoAcceptCredential on 'always'", async () => { + testLogger.test('Faber sends V2 credential offer to Alice as start of protocol process') + + const faberCredentialExchangeRecord: CredentialExchangeRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + jsonld: signCredentialOptions, + }, + protocolVersion: 'v2', + }) + testLogger.test('Alice waits for credential from Faber') + let aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + testLogger.test('Faber waits for credential ack from Alice') + const faberCredentialRecord: CredentialExchangeRecord = await waitForCredentialRecord(faberAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: {}, + state: CredentialState.CredentialReceived, + }) + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + }) + }) + }) + + describe("Auto accept on 'contentApproved'", () => { + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupJsonLdTests({ + issuerName: 'faber agent: ContentApproved v2 jsonld', + holderName: 'alice agent: ContentApproved v2 jsonld', + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + })) + + await faberAgent.context.wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + keyType: KeyType.Ed25519, + }) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test("Alice starts with V2 credential proposal to Faber, both with autoAcceptCredential on 'contentApproved'", async () => { + testLogger.test('Alice sends credential proposal to Faber') + const aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + jsonld: signCredentialOptions, + }, + comment: 'v2 propose credential test', + }) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + const faberCredentialExchangeRecord = await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 JsonLd Offer', + }) + + testLogger.test('Alice waits for credential from Faber') + const aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber waits for credential ack from Alice') + + faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: {}, + state: CredentialState.CredentialReceived, + }) + + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: {}, + state: CredentialState.Done, + }) + }) + test("Faber starts with V2 credential offer to Alice, both with autoAcceptCredential on 'contentApproved'", async () => { + testLogger.test('Faber sends credential offer to Alice') + + let faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + jsonld: signCredentialOptions, + }, + protocolVersion: 'v2', + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + // below values are not in json object + expect(aliceCredentialRecord.id).not.toBeNull() + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: aliceCredentialRecord.threadId, + state: aliceCredentialRecord.state, + connectionId: aliceConnectionId, + role: CredentialRole.Holder, + credentialIds: [], + }) + expect(aliceCredentialRecord.type).toBe(CredentialExchangeRecord.type) + if (!aliceCredentialRecord.connectionId) { + throw new CredoError('missing alice connection id') + } + + // we do not need to specify connection id in this object + // it is either connectionless or included in the offer message + testLogger.test('Alice sends credential request to faber') + faberCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber waits for credential ack from Alice') + + const faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: {}, + state: CredentialState.CredentialReceived, + }) + + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + }) + }) + test("Faber starts with V2 credential offer to Alice, both have autoAcceptCredential on 'contentApproved' and attributes did change", async () => { + testLogger.test('Faber sends credential offer to Alice') + + const faberCredentialExchangeRecord: CredentialExchangeRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + jsonld: signCredentialOptions, + }, + protocolVersion: 'v2', + }) + testLogger.test('Alice waits for credential from Faber') + let aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + // below values are not in json object + expect(aliceCredentialRecord.id).not.toBeNull() + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: aliceCredentialRecord.threadId, + state: aliceCredentialRecord.state, + connectionId: aliceConnectionId, + role: CredentialRole.Holder, + credentialIds: [], + }) + expect(aliceCredentialRecord.type).toBe(CredentialExchangeRecord.type) + + testLogger.test('Alice sends credential request to Faber') + + const aliceExchangeCredentialRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + // Send a different object + jsonld: { + ...signCredentialOptions, + credential: { + ...signCredentialOptions.credential, + credentialSubject: { + ...signCredentialOptions.credential.credentialSubject, + name: 'Different Property', + }, + }, + }, + }, + comment: 'v2 propose credential test', + }) + + testLogger.test('Faber waits for credential proposal from Alice') + const faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceExchangeCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + // Check if the state of faber credential record did not change + const faberRecord = await faberAgent.credentials.getById(faberCredentialRecord.id) + faberRecord.assertState(CredentialState.ProposalReceived) + + aliceCredentialRecord = await aliceAgent.credentials.getById(aliceCredentialRecord.id) + aliceCredentialRecord.assertState(CredentialState.ProposalSent) + }) + + test("Alice starts with V2 credential proposal to Faber, both have autoAcceptCredential on 'contentApproved' and attributes did change", async () => { + testLogger.test('Alice sends credential proposal to Faber') + const aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + jsonld: signCredentialOptions, + }, + comment: 'v2 propose credential test', + }) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + // Send a different object + jsonld: { + ...signCredentialOptions, + credential: { + ...signCredentialOptions.credential, + credentialSubject: { + ...signCredentialOptions.credential.credentialSubject, + name: 'Different Property', + }, + }, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + + const record = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + // below values are not in json object + expect(record.id).not.toBeNull() + expect(record.getTags()).toEqual({ + threadId: record.threadId, + state: record.state, + connectionId: aliceConnectionId, + role: CredentialRole.Holder, + credentialIds: [], + }) + expect(record.type).toBe(CredentialExchangeRecord.type) + + // Check if the state of the credential records did not change + faberCredentialRecord = await faberAgent.credentials.getById(faberCredentialRecord.id) + faberCredentialRecord.assertState(CredentialState.OfferSent) + + const aliceRecord = await aliceAgent.credentials.getById(record.id) + aliceRecord.assertState(CredentialState.OfferReceived) + }) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.e2e.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.e2e.test.ts new file mode 100644 index 0000000000..bde3f9def5 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.e2e.test.ts @@ -0,0 +1,593 @@ +import type { EventReplaySubject } from '../../../../../../tests' + +import { + LegacyIndyCredentialFormatService, + LegacyIndyProofFormatService, + V1CredentialProtocol, + V1ProofProtocol, +} from '../../../../../../../anoncreds/src' +import { + getAnonCredsIndyModules, + prepareForAnonCredsIssuance, +} from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import { + getInMemoryAgentOptions, + setupEventReplaySubjects, + setupSubjectTransports, + waitForCredentialRecordSubject, + testLogger, + makeConnection, +} from '../../../../../../tests' +import { Agent } from '../../../../../agent/Agent' +import { KeyType } from '../../../../../crypto' +import { TypedArrayEncoder } from '../../../../../utils' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { CacheModule, InMemoryLruCache } from '../../../../cache' +import { ProofEventTypes, ProofsModule, V2ProofProtocol } from '../../../../proofs' +import { W3cCredentialsModule } from '../../../../vc' +import { customDocumentLoader } from '../../../../vc/data-integrity/__tests__/documentLoader' +import { CredentialEventTypes } from '../../../CredentialEvents' +import { CredentialsModule } from '../../../CredentialsModule' +import { JsonLdCredentialFormatService } from '../../../formats' +import { CredentialState } from '../../../models' +import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import { V2CredentialProtocol } from '../V2CredentialProtocol' +import { V2CredentialPreview } from '../messages' + +const signCredentialOptions = { + credential: { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/citizenship/v1', + 'https://w3id.org/security/bbs/v1', + ], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2019-12-03T12:19:52Z', + expirationDate: '2029-12-03T12:19:52Z', + identifier: '83627465', + name: 'Permanent Resident Card', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['PermanentResident', 'Person'], + givenName: 'JOHN', + familyName: 'SMITH', + gender: 'Male', + image: 'data:image/png;base64,iVBORw0KGgokJggg==', + residentSince: '2015-01-01', + description: 'Government of Example Permanent Resident Card.', + lprCategory: 'C09', + lprNumber: '999-999-999', + commuterClassification: 'C1', + birthCountry: 'Bahamas', + birthDate: '1958-07-17', + }, + }, + options: { + proofType: 'Ed25519Signature2018', + proofPurpose: 'assertionMethod', + }, +} + +const indyCredentialFormat = new LegacyIndyCredentialFormatService() +const jsonLdCredentialFormat = new JsonLdCredentialFormatService() +const indyProofFormat = new LegacyIndyProofFormatService() + +const getIndyJsonLdModules = () => + ({ + ...getAnonCredsIndyModules(), + credentials: new CredentialsModule({ + credentialProtocols: [ + new V1CredentialProtocol({ indyCredentialFormat }), + new V2CredentialProtocol({ + credentialFormats: [indyCredentialFormat, jsonLdCredentialFormat], + }), + ], + }), + proofs: new ProofsModule({ + proofProtocols: [ + new V1ProofProtocol({ indyProofFormat }), + new V2ProofProtocol({ + proofFormats: [indyProofFormat], + }), + ], + }), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 100 }), + }), + w3cCredentials: new W3cCredentialsModule({ + documentLoader: customDocumentLoader, + }), + } as const) + +// TODO: extract these very specific tests to the jsonld format +describe('V2 Credentials - JSON-LD - Ed25519', () => { + let faberAgent: Agent> + let faberReplay: EventReplaySubject + let aliceAgent: Agent> + let aliceReplay: EventReplaySubject + let aliceConnectionId: string + let credentialDefinitionId: string + + beforeAll(async () => { + faberAgent = new Agent( + getInMemoryAgentOptions( + 'Faber Agent Indy/JsonLD', + { + endpoints: ['rxjs:faber'], + }, + getIndyJsonLdModules() + ) + ) + aliceAgent = new Agent( + getInMemoryAgentOptions( + 'Alice Agent Indy/JsonLD', + { + endpoints: ['rxjs:alice'], + }, + getIndyJsonLdModules() + ) + ) + + setupSubjectTransports([faberAgent, aliceAgent]) + ;[faberReplay, aliceReplay] = setupEventReplaySubjects( + [faberAgent, aliceAgent], + [CredentialEventTypes.CredentialStateChanged, ProofEventTypes.ProofStateChanged] + ) + await faberAgent.initialize() + await aliceAgent.initialize() + ;[, { id: aliceConnectionId }] = await makeConnection(faberAgent, aliceAgent) + + const { credentialDefinition } = await prepareForAnonCredsIssuance(faberAgent, { + attributeNames: ['name', 'age', 'profile_picture', 'x-ray'], + }) + credentialDefinitionId = credentialDefinition.credentialDefinitionId + + await faberAgent.context.wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + keyType: KeyType.Ed25519, + }) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with V2 (ld format, Ed25519 signature) credential proposal to Faber', async () => { + testLogger.test('Alice sends (v2 jsonld) credential proposal to Faber') + + const credentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + jsonld: signCredentialOptions, + }, + comment: 'v2 propose credential test for W3C Credentials', + }) + + expect(credentialExchangeRecord.connectionId).toEqual(aliceConnectionId) + expect(credentialExchangeRecord.protocolVersion).toEqual('v2') + expect(credentialExchangeRecord.state).toEqual(CredentialState.ProposalSent) + expect(credentialExchangeRecord.threadId).not.toBeNull() + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 W3C Offer', + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const offerMessage = await aliceAgent.credentials.findOfferMessage(aliceCredentialRecord.id) + expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({ + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + '@id': expect.any(String), + comment: 'V2 W3C Offer', + formats: [ + { + attach_id: expect.any(String), + format: 'aries/ld-proof-vc-detail@v1.0', + }, + ], + 'offers~attach': [ + { + '@id': expect.any(String), + 'mime-type': 'application/json', + data: expect.any(Object), + lastmod_time: undefined, + byte_count: undefined, + }, + ], + '~thread': { + thid: expect.any(String), + pthid: undefined, + sender_order: undefined, + received_orders: undefined, + }, + '~service': undefined, + '~attach': undefined, + '~please_ack': undefined, + '~timing': undefined, + '~transport': undefined, + '~l10n': undefined, + credential_preview: expect.any(Object), + replacement_id: undefined, + }) + expect(aliceCredentialRecord.id).not.toBeNull() + expect(aliceCredentialRecord.type).toBe(CredentialExchangeRecord.type) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + jsonld: {}, + }, + }) + + expect(offerCredentialExchangeRecord.connectionId).toEqual(aliceConnectionId) + expect(offerCredentialExchangeRecord.protocolVersion).toEqual('v2') + expect(offerCredentialExchangeRecord.state).toEqual(CredentialState.RequestSent) + expect(offerCredentialExchangeRecord.threadId).not.toBeNull() + + testLogger.test('Faber waits for credential request from Alice') + await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Alice sends credential ack to Faber') + await aliceAgent.credentials.acceptCredential({ credentialRecordId: aliceCredentialRecord.id }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: expect.any(String), + connectionId: expect.any(String), + state: CredentialState.CredentialReceived, + }) + + const credentialMessage = await faberAgent.credentials.findCredentialMessage(faberCredentialRecord.id) + expect(JsonTransformer.toJSON(credentialMessage)).toMatchObject({ + '@type': 'https://didcomm.org/issue-credential/2.0/issue-credential', + '@id': expect.any(String), + comment: 'V2 Indy Credential', + formats: [ + { + attach_id: expect.any(String), + format: 'aries/ld-proof-vc@v1.0', + }, + ], + 'credentials~attach': [ + { + '@id': expect.any(String), + 'mime-type': 'application/json', + data: expect.any(Object), + lastmod_time: undefined, + byte_count: undefined, + }, + ], + '~thread': { + thid: expect.any(String), + pthid: undefined, + sender_order: undefined, + received_orders: undefined, + }, + '~please_ack': { on: ['RECEIPT'] }, + '~service': undefined, + '~attach': undefined, + '~timing': undefined, + '~transport': undefined, + '~l10n': undefined, + }) + }) + + test('Multiple Formats: Alice starts with V2 (both ld and indy formats) credential proposal to Faber', async () => { + testLogger.test('Alice sends (v2 jsonld) credential proposal to Faber') + // set the propose options - using both indy and ld credential formats here + const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', + }) + + testLogger.test('Alice sends (v2, Indy) credential proposal to Faber') + + const credentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', + }, + jsonld: signCredentialOptions, + }, + comment: 'v2 propose credential test', + }) + + expect(credentialExchangeRecord.connectionId).toEqual(aliceConnectionId) + expect(credentialExchangeRecord.protocolVersion).toEqual('v2') + expect(credentialExchangeRecord.state).toEqual(CredentialState.ProposalSent) + expect(credentialExchangeRecord.threadId).not.toBeNull() + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 W3C & INDY Proposals', + credentialFormats: { + indy: { + credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + jsonld: {}, // this is to ensure both services are formatted + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const offerMessage = await faberAgent.credentials.findOfferMessage(faberCredentialRecord.id) + const credentialOfferJson = offerMessage?.offerAttachments[1].getDataAsJson() + expect(credentialOfferJson).toMatchObject({ + credential: { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/citizenship/v1', + 'https://w3id.org/security/bbs/v1', + ], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2019-12-03T12:19:52Z', + expirationDate: '2029-12-03T12:19:52Z', + identifier: '83627465', + name: 'Permanent Resident Card', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: expect.any(Array), + givenName: 'JOHN', + familyName: 'SMITH', + gender: 'Male', + image: 'data:image/png;base64,iVBORw0KGgokJggg==', + description: 'Government of Example Permanent Resident Card.', + residentSince: '2015-01-01', + lprCategory: 'C09', + lprNumber: '999-999-999', + commuterClassification: 'C1', + birthCountry: 'Bahamas', + birthDate: '1958-07-17', + }, + }, + options: { + proofType: 'Ed25519Signature2018', + proofPurpose: 'assertionMethod', + }, + }) + expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({ + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + '@id': expect.any(String), + comment: 'V2 W3C & INDY Proposals', + formats: [ + { + attach_id: expect.any(String), + format: 'hlindy/cred-abstract@v2.0', + }, + { + attach_id: expect.any(String), + format: 'aries/ld-proof-vc-detail@v1.0', + }, + ], + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/2.0/credential-preview', + attributes: expect.any(Array), + }, + 'offers~attach': [ + { + '@id': expect.any(String), + 'mime-type': 'application/json', + data: expect.any(Object), + lastmod_time: undefined, + byte_count: undefined, + }, + { + '@id': expect.any(String), + 'mime-type': 'application/json', + data: expect.any(Object), + lastmod_time: undefined, + byte_count: undefined, + }, + ], + '~thread': { + thid: expect.any(String), + pthid: undefined, + sender_order: undefined, + received_orders: undefined, + }, + '~service': undefined, + '~attach': undefined, + '~please_ack': undefined, + '~timing': undefined, + '~transport': undefined, + '~l10n': undefined, + replacement_id: undefined, + }) + expect(aliceCredentialRecord.id).not.toBeNull() + expect(aliceCredentialRecord.type).toBe(CredentialExchangeRecord.type) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + expect(offerCredentialExchangeRecord.connectionId).toEqual(aliceConnectionId) + expect(offerCredentialExchangeRecord.protocolVersion).toEqual('v2') + expect(offerCredentialExchangeRecord.state).toEqual(CredentialState.RequestSent) + expect(offerCredentialExchangeRecord.threadId).not.toBeNull() + + testLogger.test('Faber waits for credential request from Alice') + await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Alice sends credential ack to Faber') + await aliceAgent.credentials.acceptCredential({ credentialRecordId: aliceCredentialRecord.id }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: expect.any(String), + connectionId: expect.any(String), + state: CredentialState.CredentialReceived, + }) + + const credentialMessage = await faberAgent.credentials.findCredentialMessage(faberCredentialRecord.id) + const w3cCredential = credentialMessage?.credentialAttachments[1].getDataAsJson() + expect(w3cCredential).toMatchObject({ + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/citizenship/v1', + 'https://w3id.org/security/bbs/v1', + ], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2019-12-03T12:19:52Z', + expirationDate: '2029-12-03T12:19:52Z', + identifier: '83627465', + name: 'Permanent Resident Card', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['PermanentResident', 'Person'], + givenName: 'JOHN', + familyName: 'SMITH', + gender: 'Male', + image: 'data:image/png;base64,iVBORw0KGgokJggg==', + residentSince: '2015-01-01', + description: 'Government of Example Permanent Resident Card.', + lprCategory: 'C09', + lprNumber: '999-999-999', + commuterClassification: 'C1', + birthCountry: 'Bahamas', + birthDate: '1958-07-17', + }, + proof: { + type: 'Ed25519Signature2018', + created: expect.any(String), + verificationMethod: + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + proofPurpose: 'assertionMethod', + }, + }) + + expect(JsonTransformer.toJSON(credentialMessage)).toMatchObject({ + '@type': 'https://didcomm.org/issue-credential/2.0/issue-credential', + '@id': expect.any(String), + comment: 'V2 Indy Credential', + formats: [ + { + attach_id: expect.any(String), + format: 'hlindy/cred@v2.0', + }, + { + attach_id: expect.any(String), + format: 'aries/ld-proof-vc@v1.0', + }, + ], + 'credentials~attach': [ + { + '@id': expect.any(String), + 'mime-type': 'application/json', + data: expect.any(Object), + lastmod_time: undefined, + byte_count: undefined, + }, + { + '@id': expect.any(String), + 'mime-type': 'application/json', + data: expect.any(Object), + lastmod_time: undefined, + byte_count: undefined, + }, + ], + '~thread': { + thid: expect.any(String), + pthid: undefined, + sender_order: undefined, + received_orders: undefined, + }, + '~please_ack': { on: ['RECEIPT'] }, + '~service': undefined, + '~attach': undefined, + '~timing': undefined, + '~transport': undefined, + '~l10n': undefined, + }) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/v2/errors/V2CredentialProblemReportError.ts b/packages/core/src/modules/credentials/protocol/v2/errors/V2CredentialProblemReportError.ts new file mode 100644 index 0000000000..0db9672621 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/errors/V2CredentialProblemReportError.ts @@ -0,0 +1,23 @@ +import type { ProblemReportErrorOptions } from '../../../../problem-reports' +import type { CredentialProblemReportReason } from '../../../models/CredentialProblemReportReason' + +import { ProblemReportError } from '../../../../problem-reports/errors/ProblemReportError' +import { V2CredentialProblemReportMessage } from '../messages/V2CredentialProblemReportMessage' + +export interface V2CredentialProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: CredentialProblemReportReason +} + +export class V2CredentialProblemReportError extends ProblemReportError { + public problemReport: V2CredentialProblemReportMessage + + public constructor(message: string, { problemCode }: V2CredentialProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new V2CredentialProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/errors/index.ts b/packages/core/src/modules/credentials/protocol/v2/errors/index.ts new file mode 100644 index 0000000000..846017e442 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/errors/index.ts @@ -0,0 +1 @@ +export { V2CredentialProblemReportError, V2CredentialProblemReportErrorOptions } from './V2CredentialProblemReportError' diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2CredentialAckHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2CredentialAckHandler.ts new file mode 100644 index 0000000000..8fdf0b2a40 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2CredentialAckHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V2CredentialProtocol } from '../V2CredentialProtocol' + +import { V2CredentialAckMessage } from '../messages/V2CredentialAckMessage' + +export class V2CredentialAckHandler implements MessageHandler { + private credentialProtocol: V2CredentialProtocol + public supportedMessages = [V2CredentialAckMessage] + + public constructor(credentialProtocol: V2CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.credentialProtocol.processAck(messageContext) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2CredentialProblemReportHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2CredentialProblemReportHandler.ts new file mode 100644 index 0000000000..f3b7b60bf0 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2CredentialProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V2CredentialProtocol } from '../V2CredentialProtocol' + +import { V2CredentialProblemReportMessage } from '../messages/V2CredentialProblemReportMessage' + +export class V2CredentialProblemReportHandler implements MessageHandler { + private credentialProtocol: V2CredentialProtocol + public supportedMessages = [V2CredentialProblemReportMessage] + + public constructor(credentialProtocol: V2CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.credentialProtocol.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2IssueCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2IssueCredentialHandler.ts new file mode 100644 index 0000000000..f4217183bb --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2IssueCredentialHandler.ts @@ -0,0 +1,56 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import type { V2CredentialProtocol } from '../V2CredentialProtocol' + +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' +import { CredoError } from '../../../../../error' +import { V2IssueCredentialMessage } from '../messages/V2IssueCredentialMessage' + +export class V2IssueCredentialHandler implements MessageHandler { + private credentialProtocol: V2CredentialProtocol + public supportedMessages = [V2IssueCredentialMessage] + + public constructor(credentialProtocol: V2CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: InboundMessageContext) { + const credentialRecord = await this.credentialProtocol.processCredential(messageContext) + + const shouldAutoRespond = await this.credentialProtocol.shouldAutoRespondToCredential(messageContext.agentContext, { + credentialRecord, + credentialMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return await this.acceptCredential(credentialRecord, messageContext) + } + } + + private async acceptCredential( + credentialRecord: CredentialExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending acknowledgement with autoAccept`) + const { message } = await this.credentialProtocol.acceptCredential(messageContext.agentContext, { + credentialRecord, + }) + + const requestMessage = await this.credentialProtocol.findRequestMessage( + messageContext.agentContext, + credentialRecord.id + ) + if (!requestMessage) { + throw new CredoError(`No request message found for credential record with id '${credentialRecord.id}'`) + } + + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: requestMessage, + }) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts new file mode 100644 index 0000000000..ff2d08716a --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts @@ -0,0 +1,45 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import type { V2CredentialProtocol } from '../V2CredentialProtocol' + +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' +import { V2OfferCredentialMessage } from '../messages/V2OfferCredentialMessage' + +export class V2OfferCredentialHandler implements MessageHandler { + private credentialProtocol: V2CredentialProtocol + + public supportedMessages = [V2OfferCredentialMessage] + + public constructor(credentialProtocol: V2CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: InboundMessageContext) { + const credentialRecord = await this.credentialProtocol.processOffer(messageContext) + + const shouldAutoRespond = await this.credentialProtocol.shouldAutoRespondToOffer(messageContext.agentContext, { + credentialRecord, + offerMessage: messageContext.message, + }) + if (shouldAutoRespond) { + return await this.acceptOffer(credentialRecord, messageContext) + } + } + + private async acceptOffer( + credentialRecord: CredentialExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) + + const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { credentialRecord }) + + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + }) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2ProposeCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2ProposeCredentialHandler.ts new file mode 100644 index 0000000000..c28a77c608 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2ProposeCredentialHandler.ts @@ -0,0 +1,50 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import type { V2CredentialProtocol } from '../V2CredentialProtocol' + +import { OutboundMessageContext } from '../../../../../agent/models' +import { V2ProposeCredentialMessage } from '../messages/V2ProposeCredentialMessage' + +export class V2ProposeCredentialHandler implements MessageHandler { + private credentialProtocol: V2CredentialProtocol + + public supportedMessages = [V2ProposeCredentialMessage] + + public constructor(credentialProtocol: V2CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: InboundMessageContext) { + const credentialRecord = await this.credentialProtocol.processProposal(messageContext) + + const shouldAutoRespond = await this.credentialProtocol.shouldAutoRespondToProposal(messageContext.agentContext, { + credentialRecord, + proposalMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return await this.acceptProposal(credentialRecord, messageContext) + } + } + + private async acceptProposal( + credentialRecord: CredentialExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending offer with autoAccept`) + + if (!messageContext.connection) { + messageContext.agentContext.config.logger.error('No connection on the messageContext, aborting auto accept') + return + } + + const { message } = await this.credentialProtocol.acceptProposal(messageContext.agentContext, { credentialRecord }) + + return new OutboundMessageContext(message, { + agentContext: messageContext.agentContext, + connection: messageContext.connection, + associatedRecord: credentialRecord, + }) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2RequestCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2RequestCredentialHandler.ts new file mode 100644 index 0000000000..757598f4cf --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2RequestCredentialHandler.ts @@ -0,0 +1,58 @@ +import type { MessageHandler } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { CredentialExchangeRecord } from '../../../repository' +import type { V2CredentialProtocol } from '../V2CredentialProtocol' + +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' +import { CredoError } from '../../../../../error' +import { V2RequestCredentialMessage } from '../messages/V2RequestCredentialMessage' + +export class V2RequestCredentialHandler implements MessageHandler { + private credentialProtocol: V2CredentialProtocol + + public supportedMessages = [V2RequestCredentialMessage] + + public constructor(credentialProtocol: V2CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: InboundMessageContext) { + const credentialRecord = await this.credentialProtocol.processRequest(messageContext) + + const shouldAutoRespond = await this.credentialProtocol.shouldAutoRespondToRequest(messageContext.agentContext, { + credentialRecord, + requestMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return await this.acceptRequest(credentialRecord, messageContext) + } + } + + private async acceptRequest( + credentialRecord: CredentialExchangeRecord, + messageContext: InboundMessageContext + ) { + messageContext.agentContext.config.logger.info(`Automatically sending credential with autoAccept`) + + const offerMessage = await this.credentialProtocol.findOfferMessage( + messageContext.agentContext, + credentialRecord.id + ) + if (!offerMessage) { + throw new CredoError(`Could not find offer message for credential record with id ${credentialRecord.id}`) + } + + const { message } = await this.credentialProtocol.acceptRequest(messageContext.agentContext, { + credentialRecord, + }) + + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: offerMessage, + }) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/index.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/index.ts new file mode 100644 index 0000000000..882cd5e54f --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/index.ts @@ -0,0 +1,6 @@ +export * from './V2CredentialAckHandler' +export * from './V2IssueCredentialHandler' +export * from './V2OfferCredentialHandler' +export * from './V2ProposeCredentialHandler' +export * from './V2RequestCredentialHandler' +export * from './V2CredentialProblemReportHandler' diff --git a/packages/core/src/modules/credentials/protocol/v2/index.ts b/packages/core/src/modules/credentials/protocol/v2/index.ts new file mode 100644 index 0000000000..f50d673645 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/index.ts @@ -0,0 +1,3 @@ +export * from './V2CredentialProtocol' +export * from './messages' +export * from './errors' diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialAckMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialAckMessage.ts new file mode 100644 index 0000000000..d8549225f9 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialAckMessage.ts @@ -0,0 +1,23 @@ +import type { AckMessageOptions } from '../../../../common' + +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { AckMessage } from '../../../../common' + +export type V2CredentialAckMessageOptions = AckMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0015-acks/README.md#explicit-acks + */ +export class V2CredentialAckMessage extends AckMessage { + /** + * Create new CredentialAckMessage instance. + * @param options + */ + public constructor(options: V2CredentialAckMessageOptions) { + super(options) + } + + @IsValidMessageType(V2CredentialAckMessage.type) + public readonly type = V2CredentialAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/2.0/ack') +} diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialPreview.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialPreview.ts new file mode 100644 index 0000000000..ea78448593 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialPreview.ts @@ -0,0 +1,64 @@ +import type { CredentialPreviewOptions } from '../../../models/CredentialPreviewAttribute' + +import { Expose, Transform, Type } from 'class-transformer' +import { IsInstance, ValidateNested } from 'class-validator' + +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { IsValidMessageType, parseMessageType, replaceLegacyDidSovPrefix } from '../../../../../utils/messageType' +import { CredentialPreviewAttribute } from '../../../models/CredentialPreviewAttribute' + +/** + * Credential preview inner message class. + * + * This is not a message but an inner object for other messages in this protocol. It is used construct a preview of the data for the credential. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0453-issue-credential-v2#preview-credential + */ +export class V2CredentialPreview { + public constructor(options: CredentialPreviewOptions) { + if (options) { + this.attributes = options.attributes.map((a) => new CredentialPreviewAttribute(a)) + } + } + + @Expose({ name: '@type' }) + @IsValidMessageType(V2CredentialPreview.type) + @Transform(({ value }) => replaceLegacyDidSovPrefix(value), { + toClassOnly: true, + }) + public readonly type = V2CredentialPreview.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/2.0/credential-preview') + + @Type(() => CredentialPreviewAttribute) + @ValidateNested({ each: true }) + @IsInstance(CredentialPreviewAttribute, { each: true }) + public attributes!: CredentialPreviewAttribute[] + + public toJSON(): Record { + return JsonTransformer.toJSON(this) + } + + /** + * Create a credential preview from a record with name and value entries. + * + * @example + * const preview = CredentialPreview.fromRecord({ + * name: "Bob", + * age: "20" + * }) + */ + public static fromRecord(record: Record) { + const attributes = Object.entries(record).map( + ([name, value]) => + new CredentialPreviewAttribute({ + name, + mimeType: 'text/plain', + value, + }) + ) + + return new V2CredentialPreview({ + attributes, + }) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialProblemReportMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialProblemReportMessage.ts new file mode 100644 index 0000000000..44ef462474 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialProblemReportMessage.ts @@ -0,0 +1,23 @@ +import type { ProblemReportMessageOptions } from '../../../../problem-reports/messages/ProblemReportMessage' + +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { ProblemReportMessage } from '../../../../problem-reports/messages/ProblemReportMessage' + +export type V2CredentialProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class V2CredentialProblemReportMessage extends ProblemReportMessage { + /** + * Create new CredentialProblemReportMessage instance. + * @param options + */ + public constructor(options: V2CredentialProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(V2CredentialProblemReportMessage.type) + public readonly type = V2CredentialProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/2.0/problem-report') +} diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2IssueCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2IssueCredentialMessage.ts new file mode 100644 index 0000000000..737879c6c0 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2IssueCredentialMessage.ts @@ -0,0 +1,66 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { CredentialFormatSpec } from '../../../models' + +export interface V2IssueCredentialMessageOptions { + id?: string + comment?: string + goalCode?: string + goal?: string + formats: CredentialFormatSpec[] + credentialAttachments: Attachment[] +} + +export class V2IssueCredentialMessage extends AgentMessage { + public constructor(options: V2IssueCredentialMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.goalCode = options.goalCode + this.goal = options.goal + this.formats = options.formats + this.credentialAttachments = options.credentialAttachments + } + } + @Type(() => CredentialFormatSpec) + @ValidateNested() + @IsArray() + @IsInstance(CredentialFormatSpec, { each: true }) + public formats!: CredentialFormatSpec[] + + @IsValidMessageType(V2IssueCredentialMessage.type) + public readonly type = V2IssueCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/2.0/issue-credential') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'credentials~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public credentialAttachments!: Attachment[] + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + + @IsString() + @IsOptional() + public goal?: string + + public getCredentialAttachmentById(id: string): Attachment | undefined { + return this.credentialAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2OfferCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2OfferCredentialMessage.ts new file mode 100644 index 0000000000..1558259e76 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2OfferCredentialMessage.ts @@ -0,0 +1,83 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { CredentialFormatSpec } from '../../../models' + +import { V2CredentialPreview } from './V2CredentialPreview' + +export interface V2OfferCredentialMessageOptions { + id?: string + formats: CredentialFormatSpec[] + offerAttachments: Attachment[] + credentialPreview: V2CredentialPreview + replacementId?: string + comment?: string + goalCode?: string + goal?: string +} + +export class V2OfferCredentialMessage extends AgentMessage { + public constructor(options: V2OfferCredentialMessageOptions) { + super() + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.goalCode = options.goalCode + this.goal = options.goal + this.formats = options.formats + this.credentialPreview = options.credentialPreview + this.offerAttachments = options.offerAttachments + } + } + + @Type(() => CredentialFormatSpec) + @ValidateNested() + @IsArray() + @IsInstance(CredentialFormatSpec, { each: true }) + public formats!: CredentialFormatSpec[] + + @IsValidMessageType(V2OfferCredentialMessage.type) + public readonly type = V2OfferCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/2.0/offer-credential') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + + @IsString() + @IsOptional() + public goal?: string + + @Expose({ name: 'credential_preview' }) + @Type(() => V2CredentialPreview) + @IsOptional() + @ValidateNested() + @IsInstance(V2CredentialPreview) + public credentialPreview?: V2CredentialPreview + + @Expose({ name: 'offers~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public offerAttachments!: Attachment[] + + @Expose({ name: 'replacement_id' }) + @IsString() + @IsOptional() + public replacementId?: string + + public getOfferAttachmentById(id: string): Attachment | undefined { + return this.offerAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2ProposeCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2ProposeCredentialMessage.ts new file mode 100644 index 0000000000..121e15f5b2 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2ProposeCredentialMessage.ts @@ -0,0 +1,83 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { CredentialFormatSpec } from '../../../models' + +import { V2CredentialPreview } from './V2CredentialPreview' + +export interface V2ProposeCredentialMessageOptions { + id?: string + formats: CredentialFormatSpec[] + proposalAttachments: Attachment[] + comment?: string + goalCode?: string + goal?: string + credentialPreview?: V2CredentialPreview + attachments?: Attachment[] +} + +export class V2ProposeCredentialMessage extends AgentMessage { + public constructor(options: V2ProposeCredentialMessageOptions) { + super() + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.goalCode = options.goalCode + this.goal = options.goal + this.credentialPreview = options.credentialPreview + this.formats = options.formats + this.proposalAttachments = options.proposalAttachments + this.appendedAttachments = options.attachments + } + } + + @Type(() => CredentialFormatSpec) + @ValidateNested({ each: true }) + @IsArray() + @IsInstance(CredentialFormatSpec, { each: true }) + public formats!: CredentialFormatSpec[] + + @IsValidMessageType(V2ProposeCredentialMessage.type) + public readonly type = V2ProposeCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/2.0/propose-credential') + + @Expose({ name: 'credential_preview' }) + @Type(() => V2CredentialPreview) + @ValidateNested() + @IsOptional() + @IsInstance(V2CredentialPreview) + public credentialPreview?: V2CredentialPreview + + @Expose({ name: 'filters~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public proposalAttachments!: Attachment[] + + /** + * Human readable information about this Credential Proposal, + * so the proposal can be evaluated by human judgment. + */ + @IsOptional() + @IsString() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + + @IsString() + @IsOptional() + public goal?: string + + public getProposalAttachmentById(id: string): Attachment | undefined { + return this.proposalAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2RequestCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2RequestCredentialMessage.ts new file mode 100644 index 0000000000..58ebce4651 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2RequestCredentialMessage.ts @@ -0,0 +1,72 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { CredentialFormatSpec } from '../../../models' + +export interface V2RequestCredentialMessageOptions { + id?: string + formats: CredentialFormatSpec[] + goalCode?: string + goal?: string + requestAttachments: Attachment[] + comment?: string + attachments?: Attachment[] +} + +export class V2RequestCredentialMessage extends AgentMessage { + public constructor(options: V2RequestCredentialMessageOptions) { + super() + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.goalCode = options.goalCode + this.goal = options.goal + this.formats = options.formats + this.requestAttachments = options.requestAttachments + this.appendedAttachments = options.attachments + } + } + + @Type(() => CredentialFormatSpec) + @ValidateNested() + @IsArray() + @IsInstance(CredentialFormatSpec, { each: true }) + public formats!: CredentialFormatSpec[] + + @IsValidMessageType(V2RequestCredentialMessage.type) + public readonly type = V2RequestCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/2.0/request-credential') + + @Expose({ name: 'requests~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public requestAttachments!: Attachment[] + + /** + * Human readable information about this Credential Request, + * so the proposal can be evaluated by human judgment. + */ + @IsOptional() + @IsString() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + + @IsString() + @IsOptional() + public goal?: string + + public getRequestAttachmentById(id: string): Attachment | undefined { + return this.requestAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/index.ts b/packages/core/src/modules/credentials/protocol/v2/messages/index.ts new file mode 100644 index 0000000000..66b5b0f11e --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/index.ts @@ -0,0 +1,7 @@ +export * from './V2CredentialAckMessage' +export * from './V2CredentialProblemReportMessage' +export * from './V2IssueCredentialMessage' +export * from './V2OfferCredentialMessage' +export * from './V2ProposeCredentialMessage' +export * from './V2RequestCredentialMessage' +export * from './V2CredentialPreview' diff --git a/packages/core/src/modules/credentials/repository/CredentialExchangeRecord.ts b/packages/core/src/modules/credentials/repository/CredentialExchangeRecord.ts new file mode 100644 index 0000000000..4bf8aff890 --- /dev/null +++ b/packages/core/src/modules/credentials/repository/CredentialExchangeRecord.ts @@ -0,0 +1,138 @@ +import type { TagsBase } from '../../../storage/BaseRecord' +import type { CredentialRole } from '../models' +import type { AutoAcceptCredential } from '../models/CredentialAutoAcceptType' +import type { CredentialState } from '../models/CredentialState' +import type { RevocationNotification } from '../models/RevocationNotification' + +import { Type } from 'class-transformer' + +import { Attachment } from '../../../decorators/attachment/Attachment' +import { CredoError } from '../../../error' +import { BaseRecord } from '../../../storage/BaseRecord' +import { uuid } from '../../../utils/uuid' +import { CredentialPreviewAttribute } from '../models/CredentialPreviewAttribute' + +export interface CredentialExchangeRecordProps { + id?: string + createdAt?: Date + state: CredentialState + role: CredentialRole + connectionId?: string + threadId: string + parentThreadId?: string + protocolVersion: string + + tags?: CustomCredentialTags + credentialAttributes?: CredentialPreviewAttribute[] + autoAcceptCredential?: AutoAcceptCredential + linkedAttachments?: Attachment[] + revocationNotification?: RevocationNotification + errorMessage?: string + credentials?: CredentialRecordBinding[] +} + +export type CustomCredentialTags = TagsBase +export type DefaultCredentialTags = { + threadId: string + parentThreadId?: string + connectionId?: string + state: CredentialState + role: CredentialRole + credentialIds: string[] +} + +export interface CredentialRecordBinding { + credentialRecordType: string + credentialRecordId: string +} + +export class CredentialExchangeRecord extends BaseRecord { + public connectionId?: string + public threadId!: string + public parentThreadId?: string + public state!: CredentialState + public role!: CredentialRole + public autoAcceptCredential?: AutoAcceptCredential + public revocationNotification?: RevocationNotification + public errorMessage?: string + public protocolVersion!: string + public credentials: CredentialRecordBinding[] = [] + + @Type(() => CredentialPreviewAttribute) + public credentialAttributes?: CredentialPreviewAttribute[] + + @Type(() => Attachment) + public linkedAttachments?: Attachment[] + + // Type is CredentialRecord on purpose (without Exchange) as this is how the record was initially called. + public static readonly type = 'CredentialRecord' + public readonly type = CredentialExchangeRecord.type + + public constructor(props: CredentialExchangeRecordProps) { + super() + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.state = props.state + this.role = props.role + this.connectionId = props.connectionId + this.threadId = props.threadId + this.parentThreadId = props.parentThreadId + this.protocolVersion = props.protocolVersion + this._tags = props.tags ?? {} + + this.credentialAttributes = props.credentialAttributes + this.autoAcceptCredential = props.autoAcceptCredential + this.linkedAttachments = props.linkedAttachments + this.revocationNotification = props.revocationNotification + this.errorMessage = props.errorMessage + this.credentials = props.credentials || [] + } + } + + public getTags() { + const ids = this.credentials.map((c) => c.credentialRecordId) + + return { + ...this._tags, + threadId: this.threadId, + parentThreadId: this.parentThreadId, + connectionId: this.connectionId, + state: this.state, + role: this.role, + credentialIds: ids, + } + } + + public assertProtocolVersion(version: string) { + if (this.protocolVersion != version) { + throw new CredoError( + `Credential record has invalid protocol version ${this.protocolVersion}. Expected version ${version}` + ) + } + } + + public assertState(expectedStates: CredentialState | CredentialState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new CredoError( + `Credential record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.` + ) + } + } + + public assertConnection(currentConnectionId: string) { + if (!this.connectionId) { + throw new CredoError( + `Credential record is not associated with any connection. This is often the case with connection-less credential exchange` + ) + } else if (this.connectionId !== currentConnectionId) { + throw new CredoError( + `Credential record is associated with connection '${this.connectionId}'. Current connection is '${currentConnectionId}'` + ) + } + } +} diff --git a/packages/core/src/modules/credentials/repository/CredentialRepository.ts b/packages/core/src/modules/credentials/repository/CredentialRepository.ts new file mode 100644 index 0000000000..f81d9f81a7 --- /dev/null +++ b/packages/core/src/modules/credentials/repository/CredentialRepository.ts @@ -0,0 +1,17 @@ +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { CredentialExchangeRecord } from './CredentialExchangeRecord' + +@injectable() +export class CredentialRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(CredentialExchangeRecord, storageService, eventEmitter) + } +} diff --git a/packages/core/src/modules/credentials/repository/index.ts b/packages/core/src/modules/credentials/repository/index.ts new file mode 100644 index 0000000000..980f320cfd --- /dev/null +++ b/packages/core/src/modules/credentials/repository/index.ts @@ -0,0 +1,2 @@ +export * from './CredentialExchangeRecord' +export * from './CredentialRepository' diff --git a/packages/core/src/modules/credentials/util/__tests__/previewAttributes.test.ts b/packages/core/src/modules/credentials/util/__tests__/previewAttributes.test.ts new file mode 100644 index 0000000000..aaa03eb2e9 --- /dev/null +++ b/packages/core/src/modules/credentials/util/__tests__/previewAttributes.test.ts @@ -0,0 +1,146 @@ +import { CredentialPreviewAttribute } from '../../models' +import { arePreviewAttributesEqual } from '../previewAttributes' + +describe('previewAttributes', () => { + describe('arePreviewAttributesEqual', () => { + test('returns true if the attributes are equal', () => { + const firstAttributes = [ + new CredentialPreviewAttribute({ + name: 'firstName', + value: 'firstValue', + mimeType: 'text/grass', + }), + new CredentialPreviewAttribute({ + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }), + ] + + const secondAttribute = [ + new CredentialPreviewAttribute({ + name: 'firstName', + value: 'firstValue', + mimeType: 'text/grass', + }), + new CredentialPreviewAttribute({ + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }), + ] + + expect(arePreviewAttributesEqual(firstAttributes, secondAttribute)).toBe(true) + }) + + test('returns false if the attribute name and value are equal but the mime type is different', () => { + const firstAttributes = [ + new CredentialPreviewAttribute({ + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }), + ] + + const secondAttribute = [ + new CredentialPreviewAttribute({ + name: 'secondName', + value: 'secondValue', + mimeType: 'text/notGrass', + }), + ] + + expect(arePreviewAttributesEqual(firstAttributes, secondAttribute)).toBe(false) + }) + + test('returns false if the attribute name and mime type are equal but the value is different', () => { + const firstAttributes = [ + new CredentialPreviewAttribute({ + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }), + ] + + const secondAttribute = [ + new CredentialPreviewAttribute({ + name: 'secondName', + value: 'thirdValue', + mimeType: 'text/grass', + }), + ] + + expect(arePreviewAttributesEqual(firstAttributes, secondAttribute)).toBe(false) + }) + + test('returns false if the value and mime type are equal but the name is different', () => { + const firstAttributes = [ + new CredentialPreviewAttribute({ + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }), + ] + + const secondAttribute = [ + new CredentialPreviewAttribute({ + name: 'thirdName', + value: 'secondValue', + mimeType: 'text/grass', + }), + ] + + expect(arePreviewAttributesEqual(firstAttributes, secondAttribute)).toBe(false) + }) + + test('returns false if the length of the attributes does not match', () => { + const firstAttributes = [ + new CredentialPreviewAttribute({ + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }), + ] + + const secondAttribute = [ + new CredentialPreviewAttribute({ + name: 'thirdName', + value: 'secondValue', + mimeType: 'text/grass', + }), + new CredentialPreviewAttribute({ + name: 'fourthName', + value: 'secondValue', + mimeType: 'text/grass', + }), + ] + + expect(arePreviewAttributesEqual(firstAttributes, secondAttribute)).toBe(false) + }) + + test('returns false if duplicate key names exist', () => { + const firstAttributes = [ + new CredentialPreviewAttribute({ + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }), + new CredentialPreviewAttribute({ + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }), + ] + + const secondAttribute = [ + new CredentialPreviewAttribute({ + name: 'secondName', + value: 'secondValue', + mimeType: 'text/grass', + }), + ] + + expect(arePreviewAttributesEqual(firstAttributes, secondAttribute)).toBe(false) + }) + }) +}) diff --git a/packages/core/src/modules/credentials/util/composeAutoAccept.ts b/packages/core/src/modules/credentials/util/composeAutoAccept.ts new file mode 100644 index 0000000000..ace6fdf80c --- /dev/null +++ b/packages/core/src/modules/credentials/util/composeAutoAccept.ts @@ -0,0 +1,14 @@ +import { AutoAcceptCredential } from '../models/CredentialAutoAcceptType' + +/** + * Returns the credential auto accept config based on priority: + * - The record config takes first priority + * - Otherwise the agent config + * - Otherwise {@link AutoAcceptCredential.Never} is returned + */ +export function composeAutoAccept( + recordConfig: AutoAcceptCredential | undefined, + agentConfig: AutoAcceptCredential | undefined +) { + return recordConfig ?? agentConfig ?? AutoAcceptCredential.Never +} diff --git a/packages/core/src/modules/credentials/util/previewAttributes.ts b/packages/core/src/modules/credentials/util/previewAttributes.ts new file mode 100644 index 0000000000..8ee13cf84e --- /dev/null +++ b/packages/core/src/modules/credentials/util/previewAttributes.ts @@ -0,0 +1,27 @@ +import type { CredentialPreviewAttribute } from '../models' + +export function arePreviewAttributesEqual( + firstAttributes: CredentialPreviewAttribute[], + secondAttributes: CredentialPreviewAttribute[] +) { + if (firstAttributes.length !== secondAttributes.length) return false + + const secondAttributeMap = secondAttributes.reduce>( + (attributeMap, attribute) => ({ ...attributeMap, [attribute.name]: attribute }), + {} + ) + + // check if no duplicate keys exist + if (new Set(firstAttributes.map((attribute) => attribute.name)).size !== firstAttributes.length) return false + if (new Set(secondAttributes.map((attribute) => attribute.name)).size !== secondAttributes.length) return false + + for (const firstAttribute of firstAttributes) { + const secondAttribute = secondAttributeMap[firstAttribute.name] + + if (!secondAttribute) return false + if (firstAttribute.value !== secondAttribute.value) return false + if (firstAttribute.mimeType !== secondAttribute.mimeType) return false + } + + return true +} diff --git a/packages/core/src/modules/didcomm/index.ts b/packages/core/src/modules/didcomm/index.ts new file mode 100644 index 0000000000..ff4d44346c --- /dev/null +++ b/packages/core/src/modules/didcomm/index.ts @@ -0,0 +1,2 @@ +export * from './types' +export * from './services' diff --git a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts new file mode 100644 index 0000000000..caae5ce46c --- /dev/null +++ b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts @@ -0,0 +1,85 @@ +import type { AgentContext } from '../../../agent' +import type { Key } from '../../../crypto' +import type { ResolvedDidCommService } from '../types' + +import { KeyType } from '../../../crypto' +import { injectable } from '../../../plugins' +import { DidResolverService } from '../../dids' +import { DidCommV1Service, getKeyFromVerificationMethod, IndyAgentService, parseDid } from '../../dids/domain' +import { verkeyToInstanceOfKey } from '../../dids/helpers' +import { findMatchingEd25519Key } from '../util/matchingEd25519Key' + +@injectable() +export class DidCommDocumentService { + private didResolverService: DidResolverService + + public constructor(didResolverService: DidResolverService) { + this.didResolverService = didResolverService + } + + public async resolveServicesFromDid(agentContext: AgentContext, did: string): Promise { + const didDocument = await this.didResolverService.resolveDidDocument(agentContext, did) + + const resolvedServices: ResolvedDidCommService[] = [] + + // If did specifies a particular service, filter by its id + const didCommServices = parseDid(did).fragment + ? didDocument.didCommServices.filter((service) => service.id === did) + : didDocument.didCommServices + + // FIXME: we currently retrieve did documents for all didcomm services in the did document, and we don't have caching + // yet so this will re-trigger ledger resolves for each one. Should we only resolve the first service, then the second service, etc...? + for (const didCommService of didCommServices) { + if (didCommService.type === IndyAgentService.type) { + // IndyAgentService (DidComm v0) has keys encoded as raw publicKeyBase58 (verkeys) + resolvedServices.push({ + id: didCommService.id, + recipientKeys: didCommService.recipientKeys.map(verkeyToInstanceOfKey), + routingKeys: didCommService.routingKeys?.map(verkeyToInstanceOfKey) || [], + serviceEndpoint: didCommService.serviceEndpoint, + }) + } else if (didCommService.type === DidCommV1Service.type) { + // Resolve dids to DIDDocs to retrieve routingKeys + const routingKeys: Key[] = [] + for (const routingKey of didCommService.routingKeys ?? []) { + const routingDidDocument = await this.didResolverService.resolveDidDocument(agentContext, routingKey) + routingKeys.push( + getKeyFromVerificationMethod( + routingDidDocument.dereferenceKey(routingKey, ['authentication', 'keyAgreement']) + ) + ) + } + + // DidCommV1Service has keys encoded as key references + + // Dereference recipientKeys + const recipientKeys = didCommService.recipientKeys.map((recipientKeyReference) => { + // FIXME: we allow authentication keys as historically ed25519 keys have been used in did documents + // for didcomm. In the future we should update this to only be allowed for IndyAgent and DidCommV1 services + // as didcomm v2 doesn't have this issue anymore + const key = getKeyFromVerificationMethod( + didDocument.dereferenceKey(recipientKeyReference, ['authentication', 'keyAgreement']) + ) + + // try to find a matching Ed25519 key (https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html#did-document-notes) + // FIXME: Now that indy-sdk is deprecated, we should look into the possiblty of using the X25519 key directly + // removing the need to also include the Ed25519 key in the did document. + if (key.keyType === KeyType.X25519) { + const matchingEd25519Key = findMatchingEd25519Key(key, didDocument) + if (matchingEd25519Key) return matchingEd25519Key + } + return key + }) + + resolvedServices.push({ + id: didCommService.id, + recipientKeys, + routingKeys, + serviceEndpoint: didCommService.serviceEndpoint, + }) + } + } + + return resolvedServices + } +} diff --git a/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts b/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts new file mode 100644 index 0000000000..422db7f2a5 --- /dev/null +++ b/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts @@ -0,0 +1,205 @@ +import type { AgentContext } from '../../../../agent' +import type { VerificationMethod } from '../../../dids' + +import { getAgentContext, mockFunction } from '../../../../../tests/helpers' +import { Key, KeyType } from '../../../../crypto' +import { DidCommV1Service, DidDocument, IndyAgentService } from '../../../dids' +import { verkeyToInstanceOfKey } from '../../../dids/helpers' +import { DidResolverService } from '../../../dids/services/DidResolverService' +import { DidCommDocumentService } from '../DidCommDocumentService' + +jest.mock('../../../dids/services/DidResolverService') +const DidResolverServiceMock = DidResolverService as jest.Mock + +describe('DidCommDocumentService', () => { + let didCommDocumentService: DidCommDocumentService + let didResolverService: DidResolverService + let agentContext: AgentContext + + beforeEach(async () => { + didResolverService = new DidResolverServiceMock() + didCommDocumentService = new DidCommDocumentService(didResolverService) + agentContext = getAgentContext() + }) + + describe('resolveServicesFromDid', () => { + test('throw error when resolveDidDocument fails', async () => { + const error = new Error('test') + mockFunction(didResolverService.resolveDidDocument).mockRejectedValue(error) + + await expect(didCommDocumentService.resolveServicesFromDid(agentContext, 'did')).rejects.toThrowError(error) + }) + + test('resolves IndyAgentService', async () => { + mockFunction(didResolverService.resolveDidDocument).mockResolvedValue( + new DidDocument({ + context: ['https://w3id.org/did/v1'], + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + service: [ + new IndyAgentService({ + id: 'test-id', + serviceEndpoint: 'https://test.com', + recipientKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], + routingKeys: ['DADEajsDSaksLng9h'], + priority: 5, + }), + ], + }) + ) + + const resolved = await didCommDocumentService.resolveServicesFromDid( + agentContext, + 'did:sov:Q4zqM7aXqm7gDQkUVLng9h' + ) + expect(didResolverService.resolveDidDocument).toHaveBeenCalledWith(agentContext, 'did:sov:Q4zqM7aXqm7gDQkUVLng9h') + + expect(resolved).toHaveLength(1) + expect(resolved[0]).toMatchObject({ + id: 'test-id', + serviceEndpoint: 'https://test.com', + recipientKeys: [verkeyToInstanceOfKey('Q4zqM7aXqm7gDQkUVLng9h')], + routingKeys: [verkeyToInstanceOfKey('DADEajsDSaksLng9h')], + }) + }) + + test('resolves DidCommV1Service', async () => { + const publicKeyBase58Ed25519 = 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8' + const publicKeyBase58X25519 = 'S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud' + + const Ed25519VerificationMethod: VerificationMethod = { + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#key-1', + publicKeyBase58: publicKeyBase58Ed25519, + } + const X25519VerificationMethod: VerificationMethod = { + type: 'X25519KeyAgreementKey2019', + controller: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#key-agreement-1', + publicKeyBase58: publicKeyBase58X25519, + } + + mockFunction(didResolverService.resolveDidDocument).mockResolvedValue( + new DidDocument({ + context: [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + verificationMethod: [Ed25519VerificationMethod, X25519VerificationMethod], + authentication: [Ed25519VerificationMethod.id], + keyAgreement: [X25519VerificationMethod.id], + service: [ + new DidCommV1Service({ + id: 'test-id', + serviceEndpoint: 'https://test.com', + recipientKeys: [X25519VerificationMethod.id], + routingKeys: [Ed25519VerificationMethod.id], + priority: 5, + }), + ], + }) + ) + + const resolved = await didCommDocumentService.resolveServicesFromDid( + agentContext, + 'did:sov:Q4zqM7aXqm7gDQkUVLng9h' + ) + expect(didResolverService.resolveDidDocument).toHaveBeenCalledWith(agentContext, 'did:sov:Q4zqM7aXqm7gDQkUVLng9h') + + const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519) + expect(resolved).toHaveLength(1) + expect(resolved[0]).toMatchObject({ + id: 'test-id', + serviceEndpoint: 'https://test.com', + recipientKeys: [ed25519Key], + routingKeys: [ed25519Key], + }) + }) + + test('resolves specific DidCommV1Service', async () => { + const publicKeyBase58Ed25519 = 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8' + const publicKeyBase58X25519 = 'S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud' + + const Ed25519VerificationMethod: VerificationMethod = { + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#key-1', + publicKeyBase58: publicKeyBase58Ed25519, + } + const X25519VerificationMethod: VerificationMethod = { + type: 'X25519KeyAgreementKey2019', + controller: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#key-agreement-1', + publicKeyBase58: publicKeyBase58X25519, + } + + mockFunction(didResolverService.resolveDidDocument).mockResolvedValue( + new DidDocument({ + context: [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + verificationMethod: [Ed25519VerificationMethod, X25519VerificationMethod], + authentication: [Ed25519VerificationMethod.id], + keyAgreement: [X25519VerificationMethod.id], + service: [ + new DidCommV1Service({ + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id', + serviceEndpoint: 'https://test.com', + recipientKeys: [X25519VerificationMethod.id], + routingKeys: [Ed25519VerificationMethod.id], + priority: 5, + }), + new DidCommV1Service({ + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id-2', + serviceEndpoint: 'wss://test.com', + recipientKeys: [X25519VerificationMethod.id], + routingKeys: [Ed25519VerificationMethod.id], + priority: 6, + }), + ], + }) + ) + + let resolved = await didCommDocumentService.resolveServicesFromDid( + agentContext, + 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id' + ) + expect(didResolverService.resolveDidDocument).toHaveBeenCalledWith( + agentContext, + 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id' + ) + + let ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519) + expect(resolved).toHaveLength(1) + expect(resolved[0]).toMatchObject({ + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id', + serviceEndpoint: 'https://test.com', + recipientKeys: [ed25519Key], + routingKeys: [ed25519Key], + }) + + resolved = await didCommDocumentService.resolveServicesFromDid( + agentContext, + 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id-2' + ) + expect(didResolverService.resolveDidDocument).toHaveBeenCalledWith( + agentContext, + 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id-2' + ) + + ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519) + expect(resolved).toHaveLength(1) + expect(resolved[0]).toMatchObject({ + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id-2', + serviceEndpoint: 'wss://test.com', + recipientKeys: [ed25519Key], + routingKeys: [ed25519Key], + }) + }) + }) +}) diff --git a/packages/core/src/modules/didcomm/services/index.ts b/packages/core/src/modules/didcomm/services/index.ts new file mode 100644 index 0000000000..ae2cb50e2f --- /dev/null +++ b/packages/core/src/modules/didcomm/services/index.ts @@ -0,0 +1 @@ +export * from './DidCommDocumentService' diff --git a/packages/core/src/modules/didcomm/types.ts b/packages/core/src/modules/didcomm/types.ts new file mode 100644 index 0000000000..e8f9e9a9a8 --- /dev/null +++ b/packages/core/src/modules/didcomm/types.ts @@ -0,0 +1,8 @@ +import type { Key } from '../../crypto' + +export interface ResolvedDidCommService { + id: string + serviceEndpoint: string + recipientKeys: Key[] + routingKeys: Key[] +} diff --git a/packages/core/src/modules/didcomm/util/__tests__/matchingEd25519Key.test.ts b/packages/core/src/modules/didcomm/util/__tests__/matchingEd25519Key.test.ts new file mode 100644 index 0000000000..3987d045e3 --- /dev/null +++ b/packages/core/src/modules/didcomm/util/__tests__/matchingEd25519Key.test.ts @@ -0,0 +1,84 @@ +import type { VerificationMethod } from '../../../dids' + +import { Key, KeyType } from '../../../../crypto' +import { DidDocument } from '../../../dids' +import { findMatchingEd25519Key } from '../matchingEd25519Key' + +describe('findMatchingEd25519Key', () => { + const publicKeyBase58Ed25519 = 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8' + const Ed25519VerificationMethod: VerificationMethod = { + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:WJz9mHyW9BZksioQnRsrAo', + id: 'did:sov:WJz9mHyW9BZksioQnRsrAo#key-1', + publicKeyBase58: publicKeyBase58Ed25519, + } + + const publicKeyBase58X25519 = 'S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud' + const X25519VerificationMethod: VerificationMethod = { + type: 'X25519KeyAgreementKey2019', + controller: 'did:sov:WJz9mHyW9BZksioQnRsrAo', + id: 'did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1', + publicKeyBase58: publicKeyBase58X25519, + } + + describe('referenced verification method', () => { + const didDocument = new DidDocument({ + context: [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:sov:WJz9mHyW9BZksioQnRsrAo', + verificationMethod: [Ed25519VerificationMethod, X25519VerificationMethod], + authentication: [Ed25519VerificationMethod.id], + assertionMethod: [Ed25519VerificationMethod.id], + keyAgreement: [X25519VerificationMethod.id], + }) + + test('returns matching Ed25519 key if corresponding X25519 key supplied', () => { + const x25519Key = Key.fromPublicKeyBase58(publicKeyBase58X25519, KeyType.X25519) + const ed25519Key = findMatchingEd25519Key(x25519Key, didDocument) + expect(ed25519Key?.publicKeyBase58).toBe(Ed25519VerificationMethod.publicKeyBase58) + }) + + test('returns undefined if non-corresponding X25519 key supplied', () => { + const differentX25519Key = Key.fromPublicKeyBase58('Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', KeyType.X25519) + expect(findMatchingEd25519Key(differentX25519Key, didDocument)).toBeUndefined() + }) + + test('returns undefined if ed25519 key supplied', () => { + const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519) + expect(findMatchingEd25519Key(ed25519Key, didDocument)).toBeUndefined() + }) + }) + + describe('non-referenced authentication', () => { + const didDocument = new DidDocument({ + context: [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:sov:WJz9mHyW9BZksioQnRsrAo', + authentication: [Ed25519VerificationMethod], + assertionMethod: [Ed25519VerificationMethod], + keyAgreement: [X25519VerificationMethod], + }) + + test('returns matching Ed25519 key if corresponding X25519 key supplied', () => { + const x25519Key = Key.fromPublicKeyBase58(publicKeyBase58X25519, KeyType.X25519) + const ed25519Key = findMatchingEd25519Key(x25519Key, didDocument) + expect(ed25519Key?.publicKeyBase58).toBe(Ed25519VerificationMethod.publicKeyBase58) + }) + + test('returns undefined if non-corresponding X25519 key supplied', () => { + const differentX25519Key = Key.fromPublicKeyBase58('Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', KeyType.X25519) + expect(findMatchingEd25519Key(differentX25519Key, didDocument)).toBeUndefined() + }) + + test('returns undefined if ed25519 key supplied', () => { + const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519) + expect(findMatchingEd25519Key(ed25519Key, didDocument)).toBeUndefined() + }) + }) +}) diff --git a/packages/core/src/modules/didcomm/util/matchingEd25519Key.ts b/packages/core/src/modules/didcomm/util/matchingEd25519Key.ts new file mode 100644 index 0000000000..d261e33d71 --- /dev/null +++ b/packages/core/src/modules/didcomm/util/matchingEd25519Key.ts @@ -0,0 +1,32 @@ +import type { DidDocument, VerificationMethod } from '../../dids' + +import { Key, KeyType } from '../../../crypto' +import { getKeyFromVerificationMethod } from '../../dids' +import { convertPublicKeyToX25519 } from '../../dids/domain/key-type/ed25519' + +/** + * Tries to find a matching Ed25519 key to the supplied X25519 key + * @param x25519Key X25519 key + * @param didDocument Did document containing all the keys + * @returns a matching Ed25519 key or `undefined` (if no matching key found) + */ +export function findMatchingEd25519Key(x25519Key: Key, didDocument: DidDocument): Key | undefined { + if (x25519Key.keyType !== KeyType.X25519) return undefined + + const verificationMethods = didDocument.verificationMethod ?? [] + const keyAgreements = didDocument.keyAgreement ?? [] + const authentications = didDocument.authentication ?? [] + const allKeyReferences: VerificationMethod[] = [ + ...verificationMethods, + ...authentications.filter((keyAgreement): keyAgreement is VerificationMethod => typeof keyAgreement !== 'string'), + ...keyAgreements.filter((keyAgreement): keyAgreement is VerificationMethod => typeof keyAgreement !== 'string'), + ] + + return allKeyReferences + .map((keyReference) => getKeyFromVerificationMethod(didDocument.dereferenceKey(keyReference.id))) + .filter((key) => key?.keyType === KeyType.Ed25519) + .find((keyEd25519) => { + const keyX25519 = Key.fromPublicKey(convertPublicKeyToX25519(keyEd25519.publicKey), KeyType.X25519) + return keyX25519.publicKeyBase58 === x25519Key.publicKeyBase58 + }) +} diff --git a/packages/core/src/modules/dids/DidsApi.ts b/packages/core/src/modules/dids/DidsApi.ts new file mode 100644 index 0000000000..21074ac8e0 --- /dev/null +++ b/packages/core/src/modules/dids/DidsApi.ts @@ -0,0 +1,184 @@ +import type { ImportDidOptions } from './DidsApiOptions' +import type { + DidCreateOptions, + DidCreateResult, + DidDeactivateOptions, + DidDeactivateResult, + DidResolutionOptions, + DidUpdateOptions, + DidUpdateResult, +} from './types' + +import { AgentContext } from '../../agent' +import { CredoError } from '../../error' +import { injectable } from '../../plugins' +import { WalletKeyExistsError } from '../../wallet/error' + +import { DidsModuleConfig } from './DidsModuleConfig' +import { getAlternativeDidsForPeerDid, isValidPeerDid } from './methods' +import { DidRepository } from './repository' +import { DidRegistrarService, DidResolverService } from './services' + +@injectable() +export class DidsApi { + public config: DidsModuleConfig + + private didResolverService: DidResolverService + private didRegistrarService: DidRegistrarService + private didRepository: DidRepository + private agentContext: AgentContext + + public constructor( + didResolverService: DidResolverService, + didRegistrarService: DidRegistrarService, + didRepository: DidRepository, + agentContext: AgentContext, + config: DidsModuleConfig + ) { + this.didResolverService = didResolverService + this.didRegistrarService = didRegistrarService + this.didRepository = didRepository + this.agentContext = agentContext + this.config = config + } + + /** + * Resolve a did to a did document. + * + * Follows the interface as defined in https://w3c-ccg.github.io/did-resolution/ + */ + public resolve(didUrl: string, options?: DidResolutionOptions) { + return this.didResolverService.resolve(this.agentContext, didUrl, options) + } + + /** + * Create, register and store a did and did document. + * + * Follows the interface as defined in https://identity.foundation/did-registration + */ + public create( + options: CreateOptions + ): Promise { + return this.didRegistrarService.create(this.agentContext, options) + } + + /** + * Update an existing did document. + * + * Follows the interface as defined in https://identity.foundation/did-registration + */ + public update( + options: UpdateOptions + ): Promise { + return this.didRegistrarService.update(this.agentContext, options) + } + + /** + * Deactivate an existing did. + * + * Follows the interface as defined in https://identity.foundation/did-registration + */ + public deactivate( + options: DeactivateOptions + ): Promise { + return this.didRegistrarService.deactivate(this.agentContext, options) + } + + /** + * Resolve a did to a did document. This won't return the associated metadata as defined + * in the did resolution specification, and will throw an error if the did document could not + * be resolved. + */ + public resolveDidDocument(didUrl: string) { + return this.didResolverService.resolveDidDocument(this.agentContext, didUrl) + } + + /** + * Get a list of all dids created by the agent. This will return a list of {@link DidRecord} objects. + * Each document will have an id property with the value of the did. Optionally, it will contain a did document, + * but this is only for documents that can't be resolved from the did itself or remotely. + * + * You can call `${@link DidsModule.resolve} to resolve the did document based on the did itself. + */ + public getCreatedDids({ method, did }: { method?: string; did?: string } = {}) { + return this.didRepository.getCreatedDids(this.agentContext, { method, did }) + } + + /** + * Import an existing did that was created outside of the DidsApi. This will create a `DidRecord` for the did + * and will allow the did to be used in other parts of the agent. If you need to create a new did document, + * you can use the {@link DidsApi.create} method to create and register the did. + * + * If no `didDocument` is provided, the did document will be resolved using the did resolver. You can optionally provide a list + * of private key buffer with the respective private key bytes. These keys will be stored in the wallet, and allows you to use the + * did for other operations. Providing keys that already exist in the wallet is allowed, and those keys will be skipped from being + * added to the wallet. + * + * By default, this method will throw an error if the did already exists in the wallet. You can override this behavior by setting + * the `overwrite` option to `true`. This will update the did document in the record, and allows you to update the did over time. + */ + public async import({ did, didDocument, privateKeys = [], overwrite }: ImportDidOptions) { + if (didDocument && didDocument.id !== did) { + throw new CredoError(`Did document id ${didDocument.id} does not match did ${did}`) + } + + const existingDidRecord = await this.didRepository.findCreatedDid(this.agentContext, did) + if (existingDidRecord && !overwrite) { + throw new CredoError( + `A created did ${did} already exists. If you want to override the existing did, set the 'overwrite' option to update the did.` + ) + } + + if (!didDocument) { + didDocument = await this.resolveDidDocument(did) + } + + // Loop over all private keys and store them in the wallet. We don't check whether the keys are actually associated + // with the did document, this is up to the user. + for (const key of privateKeys) { + try { + // We can't check whether the key already exists in the wallet, but we can try to create it and catch the error + // if the key already exists. + await this.agentContext.wallet.createKey({ + keyType: key.keyType, + privateKey: key.privateKey, + }) + } catch (error) { + if (error instanceof WalletKeyExistsError) { + // If the error is a WalletKeyExistsError, we can ignore it. This means the key + // already exists in the wallet. We don't want to throw an error in this case. + } else { + throw error + } + } + } + + // Update existing did record + if (existingDidRecord) { + existingDidRecord.didDocument = didDocument + existingDidRecord.setTags({ + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(did) : undefined, + }) + + await this.didRepository.update(this.agentContext, existingDidRecord) + return + } + + // Create new did record + await this.didRepository.storeCreatedDid(this.agentContext, { + did, + didDocument, + tags: { + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(did) : undefined, + }, + }) + } + + public get supportedResolverMethods() { + return this.didResolverService.supportedMethods + } + + public get supportedRegistrarMethods() { + return this.didRegistrarService.supportedMethods + } +} diff --git a/packages/core/src/modules/dids/DidsApiOptions.ts b/packages/core/src/modules/dids/DidsApiOptions.ts new file mode 100644 index 0000000000..8561296a66 --- /dev/null +++ b/packages/core/src/modules/dids/DidsApiOptions.ts @@ -0,0 +1,33 @@ +import type { DidDocument } from './domain' +import type { KeyType } from '../../crypto' +import type { Buffer } from '../../utils' + +interface PrivateKey { + keyType: KeyType + privateKey: Buffer +} + +export interface ImportDidOptions { + /** + * The did to import. + */ + did: string + + /** + * Optional did document to import. If not provided, the did document will be resolved using the did resolver. + */ + didDocument?: DidDocument + + /** + * List of private keys associated with the did document that should be stored in the wallet. + */ + privateKeys?: PrivateKey[] + + /** + * Whether to overwrite an existing did record if it exists. If set to false, + * an error will be thrown if the did record already exists. + * + * @default false + */ + overwrite?: boolean +} diff --git a/packages/core/src/modules/dids/DidsModule.ts b/packages/core/src/modules/dids/DidsModule.ts new file mode 100644 index 0000000000..72a6ae96f1 --- /dev/null +++ b/packages/core/src/modules/dids/DidsModule.ts @@ -0,0 +1,30 @@ +import type { DidsModuleConfigOptions } from './DidsModuleConfig' +import type { DependencyManager, Module } from '../../plugins' + +import { DidsApi } from './DidsApi' +import { DidsModuleConfig } from './DidsModuleConfig' +import { DidRepository } from './repository' +import { DidResolverService, DidRegistrarService } from './services' + +export class DidsModule implements Module { + public readonly config: DidsModuleConfig + + public constructor(config?: DidsModuleConfigOptions) { + this.config = new DidsModuleConfig(config) + } + + public readonly api = DidsApi + + /** + * Registers the dependencies of the dids module module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Config + dependencyManager.registerInstance(DidsModuleConfig, this.config) + + // Services + dependencyManager.registerSingleton(DidResolverService) + dependencyManager.registerSingleton(DidRegistrarService) + dependencyManager.registerSingleton(DidRepository) + } +} diff --git a/packages/core/src/modules/dids/DidsModuleConfig.ts b/packages/core/src/modules/dids/DidsModuleConfig.ts new file mode 100644 index 0000000000..868c4d23da --- /dev/null +++ b/packages/core/src/modules/dids/DidsModuleConfig.ts @@ -0,0 +1,110 @@ +import type { DidRegistrar, DidResolver } from './domain' + +import { + KeyDidRegistrar, + PeerDidRegistrar, + KeyDidResolver, + PeerDidResolver, + WebDidResolver, + JwkDidRegistrar, + JwkDidResolver, +} from './methods' + +/** + * DidsModuleConfigOptions defines the interface for the options of the DidsModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface DidsModuleConfigOptions { + /** + * List of did registrars that should be used by the dids module. The registrar must + * be an instance of the {@link DidRegistrar} interface. + * + * If no registrars are provided, the default registrars will be used. `PeerDidRegistrar` and `KeyDidRegistrar` + * will ALWAYS be registered, as they are needed for connections, mediation and out of band modules to function. + * Other did methods can be disabled. + * + * @default [KeyDidRegistrar, PeerDidRegistrar, JwkDidRegistrar] + */ + registrars?: DidRegistrar[] + + /** + * List of did resolvers that should be used by the dids module. The resolver must + * be an instance of the {@link DidResolver} interface. + * + * If no resolvers are provided, the default resolvers will be used. `PeerDidResolver` and `KeyDidResolver` + * will ALWAYS be registered, as they are needed for connections, mediation and out of band modules to function. + * Other did methods can be disabled. + * + * @default [WebDidResolver, KeyDidResolver, PeerDidResolver, JwkDidResolver] + */ + resolvers?: DidResolver[] +} + +export class DidsModuleConfig { + private options: DidsModuleConfigOptions + private _registrars: DidRegistrar[] | undefined + private _resolvers: DidResolver[] | undefined + + public constructor(options?: DidsModuleConfigOptions) { + this.options = options ?? {} + } + + /** See {@link DidsModuleConfigOptions.registrars} */ + public get registrars(): DidRegistrar[] { + // This prevents creating new instances every time this property is accessed + if (this._registrars) return this._registrars + + let registrars = this.options.registrars ?? [new KeyDidRegistrar(), new PeerDidRegistrar(), new JwkDidRegistrar()] + + // Add peer did registrar if it is not included yet + if (!registrars.find((registrar) => registrar instanceof PeerDidRegistrar)) { + // Do not modify original options array + registrars = [...registrars, new PeerDidRegistrar()] + } + + // Add key did registrar if it is not included yet + if (!registrars.find((registrar) => registrar instanceof KeyDidRegistrar)) { + // Do not modify original options array + registrars = [...registrars, new KeyDidRegistrar()] + } + + this._registrars = registrars + return registrars + } + + public addRegistrar(registrar: DidRegistrar) { + this.registrars.push(registrar) + } + + /** See {@link DidsModuleConfigOptions.resolvers} */ + public get resolvers() { + // This prevents creating new instances every time this property is accessed + if (this._resolvers) return this._resolvers + + let resolvers = this.options.resolvers ?? [ + new WebDidResolver(), + new KeyDidResolver(), + new PeerDidResolver(), + new JwkDidResolver(), + ] + + // Add peer did resolver if it is not included yet + if (!resolvers.find((resolver) => resolver instanceof PeerDidResolver)) { + // Do not modify original options array + resolvers = [...resolvers, new PeerDidResolver()] + } + + // Add key did resolver if it is not included yet + if (!resolvers.find((resolver) => resolver instanceof KeyDidResolver)) { + // Do not modify original options array + resolvers = [...resolvers, new KeyDidResolver()] + } + + this._resolvers = resolvers + return resolvers + } + + public addResolver(resolver: DidResolver) { + this.resolvers.push(resolver) + } +} diff --git a/packages/core/src/modules/dids/__tests__/DidsApi.test.ts b/packages/core/src/modules/dids/__tests__/DidsApi.test.ts new file mode 100644 index 0000000000..274a5bc031 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/DidsApi.test.ts @@ -0,0 +1,268 @@ +import { getInMemoryAgentOptions } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { isLongFormDidPeer4, isShortFormDidPeer4 } from '../methods/peer/peerDidNumAlgo4' + +import { + DidDocument, + DidDocumentService, + KeyType, + PeerDidNumAlgo, + TypedArrayEncoder, + createPeerDidDocumentFromServices, +} from '@credo-ts/core' + +const agentOptions = getInMemoryAgentOptions('DidsApi') + +const agent = new Agent(agentOptions) + +describe('DidsApi', () => { + beforeAll(async () => { + await agent.initialize() + }) + + afterAll(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + test('import an existing did without providing a did document', async () => { + const createKeySpy = jest.spyOn(agent.context.wallet, 'createKey') + + // Private key is for public key associated with did:key did + const privateKey = TypedArrayEncoder.fromString('a-sample-seed-of-32-bytes-in-tot') + const did = 'did:key:z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty' + + expect(await agent.dids.getCreatedDids({ did })).toHaveLength(0) + + await agent.dids.import({ + did, + privateKeys: [ + { + privateKey, + keyType: KeyType.Ed25519, + }, + ], + }) + + expect(createKeySpy).toHaveBeenCalledWith({ + privateKey, + keyType: KeyType.Ed25519, + }) + + const createdDids = await agent.dids.getCreatedDids({ + did, + }) + expect(createdDids).toHaveLength(1) + + expect(createdDids[0].getTags()).toEqual({ + did, + legacyUnqualifiedDid: undefined, + method: 'key', + methodSpecificIdentifier: 'z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty', + role: 'created', + alternativeDids: undefined, + recipientKeyFingerprints: [], + }) + + expect(createdDids[0].toJSON()).toMatchObject({ + did, + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:key:z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty', + + verificationMethod: [ + { + id: 'did:key:z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty#z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty', + publicKeyBase58: '5nKwL9aJ9kpnEE1pSsqvLMqDnE1ubeBr4TjzC56roC7b', + }, + ], + + authentication: [ + 'did:key:z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty#z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty', + ], + assertionMethod: [ + 'did:key:z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty#z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty', + ], + keyAgreement: [ + { + id: 'did:key:z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty#z6LSd6ed6s6HGsVsDL9vyx3s1Vi2jQYsX9TqjqVFam2oz776', + type: 'X25519KeyAgreementKey2019', + controller: 'did:key:z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty', + publicKeyBase58: '2RUTaZHRBQn87wnATJXuguVYtG1kpYHgrrma6JPHGjLL', + }, + ], + capabilityInvocation: [ + 'did:key:z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty#z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty', + ], + capabilityDelegation: [ + 'did:key:z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty#z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty', + ], + }, + }) + }) + + test('import an existing did with providing a did document', async () => { + const createKeySpy = jest.spyOn(agent.context.wallet, 'createKey') + + // Private key is for public key associated with did:key did + const privateKey = TypedArrayEncoder.fromString('a-new-sample-seed-of-32-bytes-in') + const did = 'did:peer:0z6Mkhu3G8viiebsWmCiSgWiQoCZrTeuX76oLDow81YNYvJQM' + + expect(await agent.dids.getCreatedDids({ did })).toHaveLength(0) + + await agent.dids.import({ + did, + didDocument: new DidDocument({ + id: did, + }), + privateKeys: [ + { + privateKey, + keyType: KeyType.Ed25519, + }, + ], + }) + + expect(createKeySpy).toHaveBeenCalledWith({ + privateKey, + keyType: KeyType.Ed25519, + }) + + const createdDids = await agent.dids.getCreatedDids({ + did, + }) + expect(createdDids).toHaveLength(1) + + expect(createdDids[0].getTags()).toEqual({ + did, + legacyUnqualifiedDid: undefined, + method: 'peer', + methodSpecificIdentifier: '0z6Mkhu3G8viiebsWmCiSgWiQoCZrTeuX76oLDow81YNYvJQM', + role: 'created', + alternativeDids: undefined, + recipientKeyFingerprints: [], + }) + + expect(createdDids[0].toJSON()).toMatchObject({ + did, + didDocument: { + id: did, + }, + }) + }) + + test('can only overwrite if overwrite option is set', async () => { + const did = 'did:example:123' + const didDocument = new DidDocument({ id: did }) + const didDocument2 = new DidDocument({ + id: did, + service: [new DidDocumentService({ id: 'did:example:123#service', type: 'test', serviceEndpoint: 'test' })], + }) + + expect(await agent.dids.getCreatedDids({ did })).toHaveLength(0) + + // First import, should work + await agent.dids.import({ + did, + didDocument, + }) + + expect(await agent.dids.getCreatedDids({ did })).toHaveLength(1) + expect( + agent.dids.import({ + did, + didDocument: didDocument2, + }) + ).rejects.toThrowError( + "A created did did:example:123 already exists. If you want to override the existing did, set the 'overwrite' option to update the did." + ) + + // Should not have stored the updated record + const createdDids = await agent.dids.getCreatedDids({ did }) + expect(createdDids[0].didDocument?.service).toBeUndefined() + + // Should work, overwrite is set + await agent.dids.import({ + did, + didDocument: didDocument2, + overwrite: true, + }) + + // Should not have stored the updated record + const createdDidsOverwrite = await agent.dids.getCreatedDids({ did }) + expect(createdDidsOverwrite[0].didDocument?.service).toHaveLength(1) + }) + + test('providing privateKeys that already exist is allowd', async () => { + const privateKey = TypedArrayEncoder.fromString('another-samples-seed-of-32-bytes') + + const did = 'did:example:456' + const didDocument = new DidDocument({ id: did }) + + await agent.dids.import({ + did, + didDocument, + privateKeys: [ + { + keyType: KeyType.Ed25519, + privateKey, + }, + ], + }) + + // Provide the same key again, should work + await agent.dids.import({ + did, + didDocument, + overwrite: true, + privateKeys: [ + { + keyType: KeyType.Ed25519, + privateKey, + }, + ], + }) + }) + + test('create and resolve did:peer:4 in short and long form', async () => { + const routing = await agent.mediationRecipient.getRouting({}) + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [routing.recipientKey], + routingKeys: routing.routingKeys, + serviceEndpoint: routing.endpoints[0], + }, + ]) + + const result = await agent.dids.create({ + method: 'peer', + didDocument, + options: { + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + }, + }) + + const longFormDid = result.didState.did + const shortFormDid = result.didState.didDocument?.alsoKnownAs + ? result.didState.didDocument?.alsoKnownAs[0] + : undefined + + if (!longFormDid) fail('Long form did not defined') + if (!shortFormDid) fail('Short form did not defined') + + expect(isLongFormDidPeer4(longFormDid)).toBeTruthy() + expect(isShortFormDidPeer4(shortFormDid)).toBeTruthy() + + const didDocumentFromLongFormDid = await agent.dids.resolveDidDocument(longFormDid) + const didDocumentFromShortFormDid = await agent.dids.resolveDidDocument(shortFormDid) + + expect(didDocumentFromLongFormDid).toEqual(didDocumentFromShortFormDid) + }) +}) diff --git a/packages/core/src/modules/dids/__tests__/DidsModule.test.ts b/packages/core/src/modules/dids/__tests__/DidsModule.test.ts new file mode 100644 index 0000000000..e09efd52fd --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/DidsModule.test.ts @@ -0,0 +1,25 @@ +import { DependencyManager } from '../../../plugins/DependencyManager' +import { DidsModule } from '../DidsModule' +import { DidsModuleConfig } from '../DidsModuleConfig' +import { DidRepository } from '../repository' +import { DidRegistrarService, DidResolverService } from '../services' + +jest.mock('../../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +describe('DidsModule', () => { + test('registers dependencies on the dependency manager', () => { + const didsModule = new DidsModule() + didsModule.register(dependencyManager) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(DidsModuleConfig, didsModule.config) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidResolverService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidRegistrarService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidRepository) + }) +}) diff --git a/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts b/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts new file mode 100644 index 0000000000..cf1d4a2e59 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts @@ -0,0 +1,72 @@ +import type { DidRegistrar, DidResolver } from '../domain' + +import { DidsModuleConfig } from '../DidsModuleConfig' +import { + KeyDidRegistrar, + PeerDidRegistrar, + KeyDidResolver, + PeerDidResolver, + WebDidResolver, + JwkDidRegistrar, + JwkDidResolver, +} from '../methods' + +describe('DidsModuleConfig', () => { + test('sets default values', () => { + const config = new DidsModuleConfig() + + expect(config.registrars).toEqual([ + expect.any(KeyDidRegistrar), + expect.any(PeerDidRegistrar), + expect.any(JwkDidRegistrar), + ]) + expect(config.resolvers).toEqual([ + expect.any(WebDidResolver), + expect.any(KeyDidResolver), + expect.any(PeerDidResolver), + expect.any(JwkDidResolver), + ]) + }) + + test('sets values', () => { + const registrars = [new PeerDidRegistrar(), new KeyDidRegistrar(), {} as DidRegistrar] + const resolvers = [new PeerDidResolver(), new KeyDidResolver(), {} as DidResolver] + const config = new DidsModuleConfig({ + registrars, + resolvers, + }) + + expect(config.registrars).toEqual(registrars) + expect(config.resolvers).toEqual(resolvers) + }) + + test('adds peer and key did resolvers and registrars if not provided in config', () => { + const registrar = {} as DidRegistrar + const resolver = {} as DidResolver + const config = new DidsModuleConfig({ + registrars: [registrar], + resolvers: [resolver], + }) + + expect(config.registrars).toEqual([registrar, expect.any(PeerDidRegistrar), expect.any(KeyDidRegistrar)]) + expect(config.resolvers).toEqual([resolver, expect.any(PeerDidResolver), expect.any(KeyDidResolver)]) + }) + + test('add resolver and registrar after creation', () => { + const registrar = {} as DidRegistrar + const resolver = {} as DidResolver + const config = new DidsModuleConfig({ + resolvers: [], + registrars: [], + }) + + expect(config.registrars).not.toContain(registrar) + expect(config.resolvers).not.toContain(resolver) + + config.addRegistrar(registrar) + config.addResolver(resolver) + + expect(config.registrars).toContain(registrar) + expect(config.resolvers).toContain(resolver) + }) +}) diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123.json new file mode 100644 index 0000000000..92b7eceb91 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123.json @@ -0,0 +1,100 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:example:123", + "alsoKnownAs": ["did:example:456"], + "controller": ["did:example:456"], + "verificationMethod": [ + { + "id": "did:example:123#key-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC X..." + }, + { + "id": "did:example:123#key-2", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "-----BEGIN PUBLIC 9..." + }, + { + "id": "did:example:123#key-3", + "type": "Secp256k1VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyHex": "-----BEGIN PUBLIC A..." + } + ], + "service": [ + { + "id": "did:example:123#service-1", + "type": "Mediator", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" + }, + { + "id": "did:example:123#service-2", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "priority": 5 + }, + { + "id": "did:example:123#service-3", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["DADEajsDSaksLng9h"], + "routingKeys": ["DADEajsDSaksLng9h"], + "priority": 10 + } + ], + "authentication": [ + "did:example:123#key-1", + { + "id": "did:example:123#authentication-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "assertionMethod": [ + "did:example:123#key-1", + { + "id": "did:example:123#assertionMethod-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityDelegation": [ + "did:example:123#key-1", + { + "id": "did:example:123#capabilityDelegation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityInvocation": [ + "did:example:123#key-1", + { + "id": "did:example:123#capabilityInvocation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "keyAgreement": [ + "did:example:123#key-1", + { + "id": "did:example:123#keyAgreement-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + }, + { + "id": "did:example:123#keyAgreement-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123DidcommV2Service.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123DidcommV2Service.json new file mode 100644 index 0000000000..d07b599648 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123DidcommV2Service.json @@ -0,0 +1,30 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:example:123", + "service": [ + { + "id": "did:example:123#service-1", + "type": "DIDCommMessaging", + "serviceEndpoint": { + "uri": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"] + } + }, + { + "id": "did:example:123#service-2", + "type": "DIDComm", + "serviceEndpoint": "https://agent.com/did-comm", + "routingKeys": ["DADEajsDSaksLng9h"] + }, + { + "id": "did:example:123#service-3", + "type": "DIDCommMessaging", + "serviceEndpoint": [ + { + "uri": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"] + } + ] + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didExample456Invalid.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample456Invalid.json new file mode 100644 index 0000000000..83b25c0e48 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample456Invalid.json @@ -0,0 +1,87 @@ +{ + "@context": "https://w3id.org/did/v1", + "id": "did:example:456", + "alsoKnownAs": "did:example:123", + "controller": "did:example:123", + "verificationMethod": [ + "did:example:456#key-1", + { + "id": "did:example:456#key-2", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "-----BEGIN PUBLIC 9..." + }, + { + "id": "did:example:456#key-3", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyHex": "-----BEGIN PUBLIC A..." + } + ], + "service": [ + { + "id": "did:example:123#service-1", + "type": "Mediator", + "serviceEndpoint": "uri:uri" + }, + { + "id": "did:example:123#service-2", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "priority": 5 + }, + { + "id": "did:example:123#service-3", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["DADEajsDSaksLng9h"], + "routingKeys": ["DADEajsDSaksLng9h"], + "priority": 10 + } + ], + "authentication": [ + "did:example:123#key-1", + { + "id": "did:example:123#authentication-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "assertionMethod": [ + "did:example:123#key-1", + { + "id": "did:example:123#assertionMethod-1", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityDelegation": [ + "did:example:123#key-1", + { + "id": "did:example:123#capabilityDelegation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityInvocation": [ + "did:example:123#key-1", + { + "id": "did:example:123#capabilityInvocation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "keyAgreement": [ + "did:example:123#key-1", + { + "id": "did:example:123#keyAgreement-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1.json new file mode 100644 index 0000000000..64ea24fb7e --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1.json @@ -0,0 +1,24 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/bbs/v1"], + "id": "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA", + "verificationMethod": [ + { + "id": "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA", + "type": "Bls12381G1Key2020", + "controller": "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA", + "publicKeyBase58": "6FywSzB5BPd7xehCo1G4nYHAoZPMMP3gd4PLnvgA6SsTsogtz8K7RDznqLpFPLZXAE" + } + ], + "authentication": [ + "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA" + ], + "assertionMethod": [ + "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA" + ], + "capabilityDelegation": [ + "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA" + ], + "capabilityInvocation": [ + "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA" + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1g2.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1g2.json new file mode 100644 index 0000000000..898bf59d77 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1g2.json @@ -0,0 +1,34 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/bbs/v1"], + "id": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s", + "verificationMethod": [ + { + "id": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", + "type": "Bls12381G1Key2020", + "controller": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s", + "publicKeyBase58": "7BVES4h78wzabPAfMhchXyH5d8EX78S5TtzePH2YkftWcE6by9yj3NTAv9nsyCeYch" + }, + { + "id": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM", + "type": "Bls12381G2Key2020", + "controller": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s", + "publicKeyBase58": "26d2BdqELsXg7ZHCWKL2D5Y2S7mYrpkdhJemSEEvokd4qy4TULJeeU44hYPGKo4x4DbBp5ARzkv1D6xuB3bmhpdpKAXuXtode67wzh9PCtW8kTqQhH19VSiFZkLNkhe9rtf3" + } + ], + "authentication": [ + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM" + ], + "assertionMethod": [ + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM" + ], + "capabilityDelegation": [ + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM" + ], + "capabilityInvocation": [ + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM" + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g2.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g2.json new file mode 100644 index 0000000000..29724406d1 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g2.json @@ -0,0 +1,24 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/bbs/v1"], + "id": "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT", + "verificationMethod": [ + { + "id": "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT", + "type": "Bls12381G2Key2020", + "controller": "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT", + "publicKeyBase58": "mxE4sHTpbPcmxNviRVR9r7D2taXcNyVJmf9TBUFS1gRt3j3Ej9Seo59GQeCzYwbQgDrfWCwEJvmBwjLvheAky5N2NqFVzk4kuq3S8g4Fmekai4P622vHqWjFrsioYYDqhf9" + } + ], + "authentication": [ + "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT" + ], + "assertionMethod": [ + "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT" + ], + "capabilityDelegation": [ + "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT" + ], + "capabilityInvocation": [ + "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT" + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyEd25519.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyEd25519.json new file mode 100644 index 0000000000..8cfad8b6d1 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyEd25519.json @@ -0,0 +1,36 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "id": "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th", + "verificationMethod": [ + { + "id": "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th#z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th", + "type": "Ed25519VerificationKey2018", + "controller": "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th", + "publicKeyBase58": "8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K" + } + ], + "assertionMethod": [ + "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th#z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th" + ], + "authentication": [ + "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th#z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th" + ], + "capabilityInvocation": [ + "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th#z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th" + ], + "capabilityDelegation": [ + "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th#z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th" + ], + "keyAgreement": [ + { + "id": "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th#z6LShpNhGwSupbB7zjuivH156vhLJBDDzmQtA4BY9S94pe1K", + "type": "X25519KeyAgreementKey2019", + "controller": "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th", + "publicKeyBase58": "79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ" + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyK256.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyK256.json new file mode 100644 index 0000000000..aae86c5876 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyK256.json @@ -0,0 +1,29 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/suites/jws-2020/v1"], + "id": "did:key:zQ3shjRPgHQQbTtXyofk1ygghRJ75RZpXmWBMY1BKnhyz7zKp", + "verificationMethod": [ + { + "id": "did:key:zQ3shjRPgHQQbTtXyofk1ygghRJ75RZpXmWBMY1BKnhyz7zKp#zQ3shjRPgHQQbTtXyofk1ygghRJ75RZpXmWBMY1BKnhyz7zKp", + "type": "JsonWebKey2020", + "controller": "did:key:zQ3shjRPgHQQbTtXyofk1ygghRJ75RZpXmWBMY1BKnhyz7zKp", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "RwiZITTa2Dcmq-V1j-5tgPUshOLO31FbsnhVS-7lskc", + "y": "3o1-UCc3ABh757P58gDISSc4hOj9qyfSGl3SGGA7xdc" + } + } + ], + "authentication": [ + "did:key:zQ3shjRPgHQQbTtXyofk1ygghRJ75RZpXmWBMY1BKnhyz7zKp#zQ3shjRPgHQQbTtXyofk1ygghRJ75RZpXmWBMY1BKnhyz7zKp" + ], + "assertionMethod": [ + "did:key:zQ3shjRPgHQQbTtXyofk1ygghRJ75RZpXmWBMY1BKnhyz7zKp#zQ3shjRPgHQQbTtXyofk1ygghRJ75RZpXmWBMY1BKnhyz7zKp" + ], + "capabilityInvocation": [ + "did:key:zQ3shjRPgHQQbTtXyofk1ygghRJ75RZpXmWBMY1BKnhyz7zKp#zQ3shjRPgHQQbTtXyofk1ygghRJ75RZpXmWBMY1BKnhyz7zKp" + ], + "capabilityDelegation": [ + "did:key:zQ3shjRPgHQQbTtXyofk1ygghRJ75RZpXmWBMY1BKnhyz7zKp#zQ3shjRPgHQQbTtXyofk1ygghRJ75RZpXmWBMY1BKnhyz7zKp" + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP256.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP256.json new file mode 100644 index 0000000000..5465e191de --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP256.json @@ -0,0 +1,32 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/suites/jws-2020/v1"], + "id": "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv", + "verificationMethod": [ + { + "id": "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv", + "type": "JsonWebKey2020", + "controller": "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-256", + "x": "igrFmi0whuihKnj9R3Om1SoMph72wUGeFaBbzG2vzns", + "y": "efsX5b10x8yjyrj4ny3pGfLcY7Xby1KzgqOdqnsrJIM" + } + } + ], + "assertionMethod": [ + "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv" + ], + "authentication": [ + "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv" + ], + "capabilityInvocation": [ + "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv" + ], + "capabilityDelegation": [ + "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv" + ], + "keyAgreement": [ + "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv" + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP384.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP384.json new file mode 100644 index 0000000000..b5249b1afc --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP384.json @@ -0,0 +1,32 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/suites/jws-2020/v1"], + "id": "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9", + "verificationMethod": [ + { + "id": "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9", + "type": "JsonWebKey2020", + "controller": "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-384", + "x": "lInTxl8fjLKp_UCrxI0WDklahi-7-_6JbtiHjiRvMvhedhKVdHBfi2HCY8t_QJyc", + "y": "y6N1IC-2mXxHreETBW7K3mBcw0qGr3CWHCs-yl09yCQRLcyfGv7XhqAngHOu51Zv" + } + } + ], + "assertionMethod": [ + "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9" + ], + "authentication": [ + "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9" + ], + "capabilityInvocation": [ + "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9" + ], + "capabilityDelegation": [ + "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9" + ], + "keyAgreement": [ + "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9" + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP521.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP521.json new file mode 100644 index 0000000000..bafea05578 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP521.json @@ -0,0 +1,32 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/suites/jws-2020/v1"], + "id": "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7", + "verificationMethod": [ + { + "id": "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7", + "type": "JsonWebKey2020", + "controller": "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-521", + "x": "ASUHPMyichQ0QbHZ9ofNx_l4y7luncn5feKLo3OpJ2nSbZoC7mffolj5uy7s6KSKXFmnNWxGJ42IOrjZ47qqwqyS", + "y": "AW9ziIC4ZQQVSNmLlp59yYKrjRY0_VqO-GOIYQ9tYpPraBKUloEId6cI_vynCzlZWZtWpgOM3HPhYEgawQ703RjC" + } + } + ], + "assertionMethod": [ + "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7" + ], + "authentication": [ + "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7" + ], + "capabilityInvocation": [ + "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7" + ], + "capabilityDelegation": [ + "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7" + ], + "keyAgreement": [ + "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7" + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyX25519.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyX25519.json new file mode 100644 index 0000000000..ad660d24f3 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyX25519.json @@ -0,0 +1,12 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/suites/x25519-2019/v1"], + "id": "did:key:z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE", + "keyAgreement": [ + { + "id": "did:key:z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE#z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE", + "type": "X25519KeyAgreementKey2019", + "controller": "did:key:z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE", + "publicKeyBase58": "6fUMuABnqSDsaGKojbUF3P7ZkEL3wi2njsDdUWZGNgCU" + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didPeer1zQmY.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didPeer1zQmY.json new file mode 100644 index 0000000000..4a33648df6 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didPeer1zQmY.json @@ -0,0 +1,33 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmchWGXSsHohSMrgts5oxG76zAfG49RkMZbhrYqPJeVXc1", + "service": [ + { + "id": "#service-0", + "serviceEndpoint": "https://example.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#d0d32199-851f-48e3-b178-6122bd4216a4"], + "routingKeys": [ + "did:key:z6Mkh66d8nyf6EGUaeN2oWFAxv4qxppwUwnmy9crnZoseN7h#z6LSdgnNCDyjAvZHRHfA9rUfrcEk2vndbPsBo85BuZpc1hFC" + ], + "accept": ["didcomm/aip2;env=rfc19"] + } + ], + "authentication": [ + { + "id": "#d0d32199-851f-48e3-b178-6122bd4216a4", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "CQZzRfoJMRzoESU2VtWrgx3rTsk9yjrjqXL2UdxWjX2q" + } + ], + "keyAgreement": [ + { + "id": "#08673492-3c44-47fe-baa4-a1780c585d75", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "7SbWSgJgjSvSTc7ZAKHJiaZbTBwNM9TdFUAU1UyZfJn8" + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/dids-registrar.test.ts b/packages/core/src/modules/dids/__tests__/dids-registrar.test.ts new file mode 100644 index 0000000000..457cb2d73d --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/dids-registrar.test.ts @@ -0,0 +1,152 @@ +import type { KeyDidCreateOptions } from '../methods/key/KeyDidRegistrar' +import type { PeerDidNumAlgo0CreateOptions } from '../methods/peer/PeerDidRegistrar' + +import { getInMemoryAgentOptions } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { KeyType } from '../../../crypto' +import { PeerDidNumAlgo } from '../methods/peer/didPeer' + +import { JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' + +const agentOptions = getInMemoryAgentOptions('Faber Dids Registrar') + +describe('dids', () => { + let agent: Agent + + beforeAll(async () => { + agent = new Agent(agentOptions) + await agent.initialize() + }) + + afterAll(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + it('should create a did:key did', async () => { + const did = await agent.dids.create({ + method: 'key', + options: { + keyType: KeyType.Ed25519, + }, + secret: { + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + }, + }) + + // Same seed should resolve to same did:key + expect(JsonTransformer.toJSON(did)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + publicKeyBase58: 'ApA26cozGW5Maa62TNTwtgcxrb7bYjAmf9aQ5cYruCDE', + }, + ], + service: undefined, + authentication: [ + 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + ], + assertionMethod: [ + 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + ], + keyAgreement: [ + { + id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6LSjDbRQQKm9HM4qPBErYyX93BCSzSk1XkwP5EgDrL6eNhh', + type: 'X25519KeyAgreementKey2019', + controller: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + publicKeyBase58: '8YRFt6Wu3pdKjzoUKuTZpSxibqudJvanW6WzjPgZvzvw', + }, + ], + capabilityInvocation: [ + 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + ], + capabilityDelegation: [ + 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + ], + id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + }, + }) + }) + + it('should create a did:peer did', async () => { + const privateKey = TypedArrayEncoder.fromString('e008ef10b7c163114b3857542b3736eb') + + const did = await agent.dids.create({ + method: 'peer', + options: { + keyType: KeyType.Ed25519, + numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, + }, + secret: { + privateKey, + }, + }) + + // Same seed should resolve to same did:peer + expect(JsonTransformer.toJSON(did)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + id: 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh#z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + type: 'Ed25519VerificationKey2018', + controller: 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + publicKeyBase58: 'GLsyPBT2AgMne8XUvmZKkqLUuFkSjLp3ibkcjc6gjhyK', + }, + ], + service: undefined, + authentication: [ + 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh#z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + ], + assertionMethod: [ + 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh#z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + ], + keyAgreement: [ + { + id: 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh#z6LSdqscQpQy12kNU1kYf7odtabo2Nhr3x3coUjsUZgwxwCj', + type: 'X25519KeyAgreementKey2019', + controller: 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + publicKeyBase58: '3AhStWc6ua2dNdNn8UHgZzPKBEAjMLsTvW2Bz73RFZRy', + }, + ], + capabilityInvocation: [ + 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh#z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + ], + capabilityDelegation: [ + 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh#z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + ], + id: 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + }, + secret: { privateKey }, + }, + }) + }) +}) diff --git a/packages/core/src/modules/dids/__tests__/dids-resolver.test.ts b/packages/core/src/modules/dids/__tests__/dids-resolver.test.ts new file mode 100644 index 0000000000..feba6ee688 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/dids-resolver.test.ts @@ -0,0 +1,116 @@ +import { getInMemoryAgentOptions } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { JsonTransformer } from '../../../utils' + +const agent = new Agent(getInMemoryAgentOptions('Faber Dids')) + +describe('dids', () => { + beforeAll(async () => { + await agent.initialize() + }) + + afterAll(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + it('should resolve a did:key did', async () => { + const did = await agent.dids.resolve('did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + + expect(JsonTransformer.toJSON(did)).toMatchObject({ + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + id: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + publicKeyBase58: '6fioC1zcDPyPEL19pXRS2E4iJ46zH7xP6uSgAaPdwDrx', + }, + ], + authentication: [ + 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + assertionMethod: [ + 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + capabilityInvocation: [ + 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + capabilityDelegation: [ + 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + keyAgreement: [ + { + id: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6LSrdqo4M24WRDJj1h2hXxgtDTyzjjKCiyapYVgrhwZAySn', + type: 'X25519KeyAgreementKey2019', + controller: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + publicKeyBase58: 'FxfdY3DCQxVZddKGAtSjZdFW9bCCW7oRwZn1NFJ2Tbg2', + }, + ], + service: undefined, + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should resolve a did:peer did', async () => { + const did = await agent.dids.resolve('did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + + expect(JsonTransformer.toJSON(did)).toMatchObject({ + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + id: 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + type: 'Ed25519VerificationKey2018', + controller: 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + publicKeyBase58: '6fioC1zcDPyPEL19pXRS2E4iJ46zH7xP6uSgAaPdwDrx', + }, + ], + authentication: [ + 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + assertionMethod: [ + 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + capabilityInvocation: [ + 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + capabilityDelegation: [ + 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + keyAgreement: [ + { + id: 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6LSrdqo4M24WRDJj1h2hXxgtDTyzjjKCiyapYVgrhwZAySn', + type: 'X25519KeyAgreementKey2019', + controller: 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + publicKeyBase58: 'FxfdY3DCQxVZddKGAtSjZdFW9bCCW7oRwZn1NFJ2Tbg2', + }, + ], + service: undefined, + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) +}) diff --git a/packages/core/src/modules/dids/__tests__/keyDidDocument.test.ts b/packages/core/src/modules/dids/__tests__/keyDidDocument.test.ts new file mode 100644 index 0000000000..387d2f5ec3 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/keyDidDocument.test.ts @@ -0,0 +1,52 @@ +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { getDidDocumentForKey } from '../domain/keyDidDocument' +import { DidKey } from '../methods/key' + +import didKeyBls12381g1Fixture from './__fixtures__/didKeyBls12381g1.json' +import didKeyBls12381g1g2Fixture from './__fixtures__/didKeyBls12381g1g2.json' +import didKeyBls12381g2Fixture from './__fixtures__/didKeyBls12381g2.json' +import didKeyEd25519Fixture from './__fixtures__/didKeyEd25519.json' +import didKeyX25519Fixture from './__fixtures__/didKeyX25519.json' + +const TEST_X25519_DID = 'did:key:z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE' +const TEST_ED25519_DID = `did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th` +const TEST_BLS12381G1_DID = `did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA` +const TEST_BLS12381G2_DID = `did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT` +const TEST_BLS12381G1G2_DID = `did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s` + +describe('getDidDocumentForKey', () => { + it('should return a valid did:key did document for and x25519 key', () => { + const didKey = DidKey.fromDid(TEST_X25519_DID) + const didDocument = getDidDocumentForKey(TEST_X25519_DID, didKey.key) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyX25519Fixture) + }) + + it('should return a valid did:key did document for and ed25519 key', () => { + const didKey = DidKey.fromDid(TEST_ED25519_DID) + const didDocument = getDidDocumentForKey(TEST_ED25519_DID, didKey.key) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyEd25519Fixture) + }) + + it('should return a valid did:key did document for and bls12381g1 key', () => { + const didKey = DidKey.fromDid(TEST_BLS12381G1_DID) + const didDocument = getDidDocumentForKey(TEST_BLS12381G1_DID, didKey.key) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyBls12381g1Fixture) + }) + + it('should return a valid did:key did document for and bls12381g2 key', () => { + const didKey = DidKey.fromDid(TEST_BLS12381G2_DID) + const didDocument = getDidDocumentForKey(TEST_BLS12381G2_DID, didKey.key) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyBls12381g2Fixture) + }) + + it('should return a valid did:key did document for and bls12381g1g2 key', () => { + const didKey = DidKey.fromDid(TEST_BLS12381G1G2_DID) + const didDocument = getDidDocumentForKey(TEST_BLS12381G1G2_DID, didKey.key) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyBls12381g1g2Fixture) + }) +}) diff --git a/packages/core/src/modules/dids/__tests__/peer-did.test.ts b/packages/core/src/modules/dids/__tests__/peer-did.test.ts new file mode 100644 index 0000000000..532c42638d --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/peer-did.test.ts @@ -0,0 +1,194 @@ +import type { AgentContext } from '../../../agent' +import type { Wallet } from '../../../wallet' + +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' +import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' +import { getAgentConfig, getAgentContext } from '../../../../tests/helpers' +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { Key, KeyType } from '../../../crypto' +import { JsonTransformer, TypedArrayEncoder } from '../../../utils' +import { DidsModuleConfig } from '../DidsModuleConfig' +import { + DidCommV1Service, + DidDocument, + DidDocumentBuilder, + convertPublicKeyToX25519, + getEd25519VerificationKey2018, + getX25519KeyAgreementKey2019, +} from '../domain' +import { DidDocumentRole } from '../domain/DidDocumentRole' +import { PeerDidResolver } from '../methods' +import { DidKey } from '../methods/key' +import { getNumAlgoFromPeerDid, PeerDidNumAlgo } from '../methods/peer/didPeer' +import { didDocumentJsonToNumAlgo1Did } from '../methods/peer/peerDidNumAlgo1' +import { DidRecord, DidRepository } from '../repository' +import { DidResolverService } from '../services' + +import didPeer1zQmY from './__fixtures__/didPeer1zQmY.json' + +describe('peer dids', () => { + const config = getAgentConfig('Peer DIDs Lifecycle') + + let didRepository: DidRepository + let didResolverService: DidResolverService + let wallet: Wallet + let agentContext: AgentContext + let eventEmitter: EventEmitter + + beforeEach(async () => { + wallet = new InMemoryWallet() + const storageService = new InMemoryStorageService() + eventEmitter = new EventEmitter(config.agentDependencies, new Subject()) + didRepository = new DidRepository(storageService, eventEmitter) + + agentContext = getAgentContext({ + wallet, + registerInstances: [ + [DidRepository, didRepository], + [InjectionSymbols.StorageService, storageService], + ], + }) + await wallet.createAndOpen(config.walletConfig) + + didResolverService = new DidResolverService( + config.logger, + new DidsModuleConfig({ resolvers: [new PeerDidResolver()] }), + {} as unknown as DidRepository + ) + }) + + afterEach(async () => { + await wallet.delete() + }) + + test('create a peer did method 1 document from ed25519 keys with a service', async () => { + // The following scenario show how we could create a key and create a did document from it for DID Exchange + + const ed25519Key = await wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('astringoftotalin32characterslong'), + keyType: KeyType.Ed25519, + }) + const mediatorEd25519Key = await wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('anotherstringof32characterslong1'), + keyType: KeyType.Ed25519, + }) + + const x25519Key = Key.fromPublicKey(convertPublicKeyToX25519(ed25519Key.publicKey), KeyType.X25519) + + const ed25519VerificationMethod = getEd25519VerificationKey2018({ + // The id can either be the first 8 characters of the key data (for ed25519 it's publicKeyBase58) + // uuid is easier as it is consistent between different key types. Normally you would dynamically + // generate the uuid, but static for testing purposes + id: `#d0d32199-851f-48e3-b178-6122bd4216a4`, + key: ed25519Key, + // For peer dids generated with method 1, the controller MUST be #id as we don't know the did yet + controller: '#id', + }) + const x25519VerificationMethod = getX25519KeyAgreementKey2019({ + // The id can either be the first 8 characters of the key data (for ed25519 it's publicKeyBase58) + // uuid is easier as it is consistent between different key types. Normally you would dynamically + // generate the uuid, but static for testing purposes + id: `#08673492-3c44-47fe-baa4-a1780c585d75`, + key: x25519Key, + // For peer dids generated with method 1, the controller MUST be #id as we don't know the did yet + controller: '#id', + }) + + const mediatorEd25519DidKey = new DidKey(mediatorEd25519Key) + const mediatorX25519Key = Key.fromPublicKey(convertPublicKeyToX25519(mediatorEd25519Key.publicKey), KeyType.X25519) + + // Use ed25519 did:key, which also includes the x25519 key used for didcomm + const mediatorRoutingKey = `${mediatorEd25519DidKey.did}#${mediatorX25519Key.fingerprint}` + + const service = new DidCommV1Service({ + id: '#service-0', + // Fixme: can we use relative reference (#id) instead of absolute reference here (did:example:123#id)? + // We don't know the did yet + recipientKeys: [ed25519VerificationMethod.id], + serviceEndpoint: 'https://example.com', + accept: ['didcomm/aip2;env=rfc19'], + // It is important that we encode the routing keys as key references. + // So instead of using plain verkeys, we should encode them as did:key dids + routingKeys: [mediatorRoutingKey], + }) + + const didDocument = + // placeholder did, as it is generated from the did document + new DidDocumentBuilder('') + // ed25519 authentication method for signatures + .addAuthentication(ed25519VerificationMethod) + // x25519 for key agreement + .addKeyAgreement(x25519VerificationMethod) + .addService(service) + .build() + + const didDocumentJson = didDocument.toJSON() + const did = didDocumentJsonToNumAlgo1Did(didDocumentJson) + + expect(did).toBe(didPeer1zQmY.id) + + // Set did after generating it + didDocument.id = did + + expect(didDocument.toJSON()).toMatchObject(didPeer1zQmY) + + // Save the record to storage + const didDocumentRecord = new DidRecord({ + did: didPeer1zQmY.id, + role: DidDocumentRole.Created, + // It is important to take the did document from the PeerDid class + // as it will have the id property + didDocument: didDocument, + }) + + await didRepository.save(agentContext, didDocumentRecord) + }) + + test('receive a did and did document', async () => { + // This flow assumes peer dids. When implementing for did exchange other did methods could be used + + // We receive the did and did document from the did exchange message (request or response) + // It is important to not parse the did document to a DidDocument class yet as we need the raw json + // to consistently verify the hash of the did document + const did = didPeer1zQmY.id + const numAlgo = getNumAlgoFromPeerDid(did) + + // Note that the did document could be undefined (if inlined did:peer or public did) + const didDocument = JsonTransformer.fromJSON(didPeer1zQmY, DidDocument) + + // make sure the dids are valid by matching them against our encoded variants + expect(didDocumentJsonToNumAlgo1Did(didPeer1zQmY)).toBe(did) + + // If a did document was provided, we match it against the did document of the peer did + // This validates whether we get the same did document + if (didDocument) { + expect(didDocument.toJSON()).toMatchObject(didPeer1zQmY) + } + + const didDocumentRecord = new DidRecord({ + did: did, + role: DidDocumentRole.Received, + // If the method is a genesis doc (did:peer:1) we should store the document + // Otherwise we only need to store the did itself (as the did can be generated) + didDocument: numAlgo === PeerDidNumAlgo.GenesisDoc ? didDocument : undefined, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + }) + + await didRepository.save(agentContext, didDocumentRecord) + + // Then we save the did (not the did document) in the connection record + // connectionRecord.theirDid = didPeer.did + + // Then when we want to send a message we can resolve the did document + const { didDocument: resolvedDidDocument } = await didResolverService.resolve(agentContext, did) + expect(resolvedDidDocument).toBeInstanceOf(DidDocument) + expect(resolvedDidDocument?.toJSON()).toMatchObject(didPeer1zQmY) + }) +}) diff --git a/packages/core/src/modules/dids/domain/DidDocument.ts b/packages/core/src/modules/dids/domain/DidDocument.ts new file mode 100644 index 0000000000..7a9c97d801 --- /dev/null +++ b/packages/core/src/modules/dids/domain/DidDocument.ts @@ -0,0 +1,245 @@ +import type { DidDocumentService } from './service' + +import { Expose, Type } from 'class-transformer' +import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { Key } from '../../../crypto/Key' +import { KeyType } from '../../../crypto/KeyType' +import { CredoError } from '../../../error' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { IsStringOrStringArray } from '../../../utils/transformers' + +import { getKeyFromVerificationMethod } from './key-type' +import { IndyAgentService, ServiceTransformer, DidCommV1Service } from './service' +import { VerificationMethodTransformer, VerificationMethod, IsStringOrVerificationMethod } from './verificationMethod' + +export type DidPurpose = + | 'authentication' + | 'keyAgreement' + | 'assertionMethod' + | 'capabilityInvocation' + | 'capabilityDelegation' + +type DidVerificationMethods = DidPurpose | 'verificationMethod' + +interface DidDocumentOptions { + context?: string | string[] + id: string + alsoKnownAs?: string[] + controller?: string | string[] + verificationMethod?: VerificationMethod[] + service?: DidDocumentService[] + authentication?: Array + assertionMethod?: Array + keyAgreement?: Array + capabilityInvocation?: Array + capabilityDelegation?: Array +} + +export class DidDocument { + @Expose({ name: '@context' }) + @IsStringOrStringArray() + public context: string | string[] = ['https://w3id.org/did/v1'] + + @IsString() + public id!: string + + @IsArray() + @IsString({ each: true }) + @IsOptional() + public alsoKnownAs?: string[] + + @IsStringOrStringArray() + @IsOptional() + public controller?: string | string[] + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => VerificationMethod) + @IsOptional() + public verificationMethod?: VerificationMethod[] + + @IsArray() + @ServiceTransformer() + @IsOptional() + public service?: DidDocumentService[] + + @IsArray() + @VerificationMethodTransformer() + @IsStringOrVerificationMethod({ each: true }) + @IsOptional() + public authentication?: Array + + @IsArray() + @VerificationMethodTransformer() + @IsStringOrVerificationMethod({ each: true }) + @IsOptional() + public assertionMethod?: Array + + @IsArray() + @VerificationMethodTransformer() + @IsStringOrVerificationMethod({ each: true }) + @IsOptional() + public keyAgreement?: Array + + @IsArray() + @VerificationMethodTransformer() + @IsStringOrVerificationMethod({ each: true }) + @IsOptional() + public capabilityInvocation?: Array + + @IsArray() + @VerificationMethodTransformer() + @IsStringOrVerificationMethod({ each: true }) + @IsOptional() + public capabilityDelegation?: Array + + public constructor(options: DidDocumentOptions) { + if (options) { + this.context = options.context ?? this.context + this.id = options.id + this.alsoKnownAs = options.alsoKnownAs + this.controller = options.controller + this.verificationMethod = options.verificationMethod + this.service = options.service + this.authentication = options.authentication + this.assertionMethod = options.assertionMethod + this.keyAgreement = options.keyAgreement + this.capabilityInvocation = options.capabilityInvocation + this.capabilityDelegation = options.capabilityDelegation + } + } + + public dereferenceVerificationMethod(keyId: string) { + // TODO: once we use JSON-LD we should use that to resolve references in did documents. + // for now we check whether the key id ends with the keyId. + // so if looking for #123 and key.id is did:key:123#123, it is valid. But #123 as key.id is also valid + const verificationMethod = this.verificationMethod?.find((key) => key.id.endsWith(keyId) || key.controller === keyId) + + if (!verificationMethod) { + throw new CredoError(`Unable to locate verification method with id '${keyId}'`) + } + + return verificationMethod + } + + public dereferenceKey(keyId: string, allowedPurposes?: DidVerificationMethods[]) { + const allPurposes: DidVerificationMethods[] = [ + 'authentication', + 'keyAgreement', + 'assertionMethod', + 'capabilityInvocation', + 'capabilityDelegation', + 'verificationMethod', + ] + + const purposes = allowedPurposes ?? allPurposes + + for (const purpose of purposes) { + for (const key of this[purpose] ?? []) { + if (typeof key === 'string' && key.endsWith(keyId)) { + return this.dereferenceVerificationMethod(key) + } else if (typeof key !== 'string' && key.id.endsWith(keyId)) { + return key + } + } + } + + throw new CredoError(`Unable to locate verification method with id '${keyId}' in purposes ${purposes}`) + } + + /** + * Returns all of the service endpoints matching the given type. + * + * @param type The type of service(s) to query. + */ + public getServicesByType(type: string): S[] { + return (this.service?.filter((service) => service.type === type) ?? []) as S[] + } + + /** + * Returns all of the service endpoints matching the given class + * + * @param classType The class to query services. + */ + public getServicesByClassType( + classType: new (...args: never[]) => S + ): S[] { + return (this.service?.filter((service) => service instanceof classType) ?? []) as S[] + } + + /** + * Get all DIDComm services ordered by priority descending. This means the highest + * priority will be the first entry. + */ + public get didCommServices(): Array { + const didCommServiceTypes = [IndyAgentService.type, DidCommV1Service.type] + const services = (this.service?.filter((service) => didCommServiceTypes.includes(service.type)) ?? []) as Array< + IndyAgentService | DidCommV1Service + > + + // Sort services based on indicated priority + return services.sort((a, b) => a.priority - b.priority) + } + + // TODO: it would probably be easier if we add a utility to each service so we don't have to handle logic for all service types here + public get recipientKeys(): Key[] { + let recipientKeys: Key[] = [] + + for (const service of this.didCommServices) { + if (service.type === IndyAgentService.type) { + recipientKeys = [ + ...recipientKeys, + ...service.recipientKeys.map((publicKeyBase58) => Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519)), + ] + } else if (service.type === DidCommV1Service.type) { + recipientKeys = [ + ...recipientKeys, + ...service.recipientKeys.map((recipientKey) => + getKeyFromVerificationMethod(this.dereferenceKey(recipientKey, ['authentication', 'keyAgreement'])) + ), + ] + } + } + + return recipientKeys + } + + public toJSON() { + return JsonTransformer.toJSON(this) + } +} + +/** + * Extracting the verification method for signature type + * @param type Signature type + * @param didDocument DidDocument + * @returns verification method + */ +export async function findVerificationMethodByKeyType( + keyType: string, + didDocument: DidDocument +): Promise { + const didVerificationMethods: DidVerificationMethods[] = [ + 'verificationMethod', + 'authentication', + 'keyAgreement', + 'assertionMethod', + 'capabilityInvocation', + 'capabilityDelegation', + ] + for await (const purpose of didVerificationMethods) { + const key: VerificationMethod[] | (string | VerificationMethod)[] | undefined = didDocument[purpose] + if (key instanceof Array) { + for await (const method of key) { + if (typeof method !== 'string') { + if (method.type === keyType) { + return method + } + } + } + } + } + + return null +} diff --git a/packages/core/src/modules/dids/domain/DidDocumentBuilder.ts b/packages/core/src/modules/dids/domain/DidDocumentBuilder.ts new file mode 100644 index 0000000000..f06e03b8ae --- /dev/null +++ b/packages/core/src/modules/dids/domain/DidDocumentBuilder.ts @@ -0,0 +1,124 @@ +import type { DidDocumentService } from './service' + +import { asArray } from '../../../utils' + +import { DidDocument } from './DidDocument' +import { VerificationMethod } from './verificationMethod' + +export class DidDocumentBuilder { + private didDocument: DidDocument + + public constructor(id: string) { + this.didDocument = new DidDocument({ + id, + }) + } + + public addContext(context: string) { + const currentContexts = asArray(this.didDocument.context) + if (currentContexts.includes(context)) return this + + this.didDocument.context = [...currentContexts, context] + return this + } + + public addService(service: DidDocumentService) { + if (!this.didDocument.service) { + this.didDocument.service = [] + } + + this.didDocument.service.push(service) + + return this + } + + public addVerificationMethod(verificationMethod: VerificationMethod) { + if (!this.didDocument.verificationMethod) { + this.didDocument.verificationMethod = [] + } + + this.didDocument.verificationMethod.push( + verificationMethod instanceof VerificationMethod ? verificationMethod : new VerificationMethod(verificationMethod) + ) + + return this + } + + public addAuthentication(authentication: string | VerificationMethod) { + if (!this.didDocument.authentication) { + this.didDocument.authentication = [] + } + + const verificationMethod = + authentication instanceof VerificationMethod || typeof authentication === 'string' + ? authentication + : new VerificationMethod(authentication) + + this.didDocument.authentication.push(verificationMethod) + + return this + } + + public addAssertionMethod(assertionMethod: string | VerificationMethod) { + if (!this.didDocument.assertionMethod) { + this.didDocument.assertionMethod = [] + } + + const verificationMethod = + assertionMethod instanceof VerificationMethod || typeof assertionMethod === 'string' + ? assertionMethod + : new VerificationMethod(assertionMethod) + + this.didDocument.assertionMethod.push(verificationMethod) + + return this + } + + public addCapabilityDelegation(capabilityDelegation: string | VerificationMethod) { + if (!this.didDocument.capabilityDelegation) { + this.didDocument.capabilityDelegation = [] + } + + const verificationMethod = + capabilityDelegation instanceof VerificationMethod || typeof capabilityDelegation === 'string' + ? capabilityDelegation + : new VerificationMethod(capabilityDelegation) + + this.didDocument.capabilityDelegation.push(verificationMethod) + + return this + } + public addCapabilityInvocation(capabilityInvocation: string | VerificationMethod) { + if (!this.didDocument.capabilityInvocation) { + this.didDocument.capabilityInvocation = [] + } + + const verificationMethod = + capabilityInvocation instanceof VerificationMethod || typeof capabilityInvocation === 'string' + ? capabilityInvocation + : new VerificationMethod(capabilityInvocation) + + this.didDocument.capabilityInvocation.push(verificationMethod) + + return this + } + + public addKeyAgreement(keyAgreement: string | VerificationMethod) { + if (!this.didDocument.keyAgreement) { + this.didDocument.keyAgreement = [] + } + + const verificationMethod = + keyAgreement instanceof VerificationMethod || typeof keyAgreement === 'string' + ? keyAgreement + : new VerificationMethod(keyAgreement) + + this.didDocument.keyAgreement.push(verificationMethod) + + return this + } + + public build(): DidDocument { + return this.didDocument + } +} diff --git a/packages/core/src/modules/dids/domain/DidDocumentRole.ts b/packages/core/src/modules/dids/domain/DidDocumentRole.ts new file mode 100644 index 0000000000..66ba66e488 --- /dev/null +++ b/packages/core/src/modules/dids/domain/DidDocumentRole.ts @@ -0,0 +1,4 @@ +export enum DidDocumentRole { + Created = 'created', + Received = 'received', +} diff --git a/packages/core/src/modules/dids/domain/DidRegistrar.ts b/packages/core/src/modules/dids/domain/DidRegistrar.ts new file mode 100644 index 0000000000..fabca7bc5d --- /dev/null +++ b/packages/core/src/modules/dids/domain/DidRegistrar.ts @@ -0,0 +1,17 @@ +import type { AgentContext } from '../../../agent' +import type { + DidCreateOptions, + DidDeactivateOptions, + DidUpdateOptions, + DidCreateResult, + DidUpdateResult, + DidDeactivateResult, +} from '../types' + +export interface DidRegistrar { + readonly supportedMethods: string[] + + create(agentContext: AgentContext, options: DidCreateOptions): Promise + update(agentContext: AgentContext, options: DidUpdateOptions): Promise + deactivate(agentContext: AgentContext, options: DidDeactivateOptions): Promise +} diff --git a/packages/core/src/modules/dids/domain/DidResolver.ts b/packages/core/src/modules/dids/domain/DidResolver.ts new file mode 100644 index 0000000000..7582dced1b --- /dev/null +++ b/packages/core/src/modules/dids/domain/DidResolver.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '../../../agent' +import type { ParsedDid, DidResolutionResult, DidResolutionOptions } from '../types' + +export interface DidResolver { + readonly supportedMethods: string[] + readonly allowsCaching: boolean + + /** + * Whether the resolver allows using a local created did document from + * a did record to resolve the did document. + * + * @default false + * @todo make required in 0.6.0 + */ + readonly allowsLocalDidRecord?: boolean + + resolve( + agentContext: AgentContext, + did: string, + parsed: ParsedDid, + didResolutionOptions: DidResolutionOptions + ): Promise +} diff --git a/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts b/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts new file mode 100644 index 0000000000..f8b607a619 --- /dev/null +++ b/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts @@ -0,0 +1,222 @@ +import { ClassValidationError } from '../../../../error/ClassValidationError' +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import didExample123Fixture from '../../__tests__/__fixtures__/didExample123.json' +import didExample456Invalid from '../../__tests__/__fixtures__/didExample456Invalid.json' +import { DidDocument, findVerificationMethodByKeyType } from '../DidDocument' +import { DidDocumentService, IndyAgentService, DidCommV1Service } from '../service' +import { VerificationMethod } from '../verificationMethod' + +const didDocumentInstance = new DidDocument({ + id: 'did:example:123', + alsoKnownAs: ['did:example:456'], + controller: ['did:example:456'], + verificationMethod: [ + new VerificationMethod({ + id: 'did:example:123#key-1', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC X...', + }), + new VerificationMethod({ + id: 'did:example:123#key-2', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyBase58: '-----BEGIN PUBLIC 9...', + }), + new VerificationMethod({ + id: 'did:example:123#key-3', + type: 'Secp256k1VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyHex: '-----BEGIN PUBLIC A...', + }), + ], + service: [ + new DidDocumentService({ + id: 'did:example:123#service-1', + type: 'Mediator', + serviceEndpoint: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + }), + new IndyAgentService({ + id: 'did:example:123#service-2', + serviceEndpoint: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + recipientKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], + routingKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], + priority: 5, + }), + new DidCommV1Service({ + id: 'did:example:123#service-3', + serviceEndpoint: 'https://agent.com/did-comm', + recipientKeys: ['DADEajsDSaksLng9h'], + routingKeys: ['DADEajsDSaksLng9h'], + priority: 10, + }), + ], + authentication: [ + 'did:example:123#key-1', + new VerificationMethod({ + id: 'did:example:123#authentication-1', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC A...', + }), + ], + assertionMethod: [ + 'did:example:123#key-1', + new VerificationMethod({ + id: 'did:example:123#assertionMethod-1', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC A...', + }), + ], + capabilityDelegation: [ + 'did:example:123#key-1', + new VerificationMethod({ + id: 'did:example:123#capabilityDelegation-1', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC A...', + }), + ], + capabilityInvocation: [ + 'did:example:123#key-1', + new VerificationMethod({ + id: 'did:example:123#capabilityInvocation-1', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC A...', + }), + ], + keyAgreement: [ + 'did:example:123#key-1', + new VerificationMethod({ + id: 'did:example:123#keyAgreement-1', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC A...', + }), + new VerificationMethod({ + id: 'did:example:123#keyAgreement-1', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC A...', + }), + ], +}) + +describe('Did | DidDocument', () => { + it('should correctly transforms Json to DidDocument class', () => { + const didDocument = JsonTransformer.fromJSON(didExample123Fixture, DidDocument) + + // Check other properties + expect(didDocument.id).toBe(didExample123Fixture.id) + expect(didDocument.alsoKnownAs).toEqual(didExample123Fixture.alsoKnownAs) + expect(didDocument.context).toEqual(didExample123Fixture['@context']) + expect(didDocument.controller).toEqual(didExample123Fixture.controller) + + // Check verification method + const verificationMethods = didDocument.verificationMethod ?? [] + expect(verificationMethods[0]).toBeInstanceOf(VerificationMethod) + expect(verificationMethods[1]).toBeInstanceOf(VerificationMethod) + expect(verificationMethods[2]).toBeInstanceOf(VerificationMethod) + + // Check Service + const services = didDocument.service ?? [] + expect(services[0]).toBeInstanceOf(DidDocumentService) + expect(services[1]).toBeInstanceOf(IndyAgentService) + expect(services[2]).toBeInstanceOf(DidCommV1Service) + + // Check Authentication + const authentication = didDocument.authentication ?? [] + expect(typeof authentication[0]).toBe('string') + expect(authentication[1]).toBeInstanceOf(VerificationMethod) + + // Check assertionMethod + const assertionMethod = didDocument.assertionMethod ?? [] + expect(typeof assertionMethod[0]).toBe('string') + expect(assertionMethod[1]).toBeInstanceOf(VerificationMethod) + + // Check capabilityDelegation + const capabilityDelegation = didDocument.capabilityDelegation ?? [] + expect(typeof capabilityDelegation[0]).toBe('string') + expect(capabilityDelegation[1]).toBeInstanceOf(VerificationMethod) + + // Check capabilityInvocation + const capabilityInvocation = didDocument.capabilityInvocation ?? [] + expect(typeof capabilityInvocation[0]).toBe('string') + expect(capabilityInvocation[1]).toBeInstanceOf(VerificationMethod) + + // Check keyAgreement + const keyAgreement = didDocument.keyAgreement ?? [] + expect(typeof keyAgreement[0]).toBe('string') + expect(keyAgreement[1]).toBeInstanceOf(VerificationMethod) + }) + + it('validation should throw an error if the did document is invalid', () => { + try { + JsonTransformer.fromJSON(didExample456Invalid, DidDocument) + } catch (error) { + expect(error).toBeInstanceOf(ClassValidationError) + expect(error.message).toMatch(/property type has failed the following constraints: type must be a string/) + expect(error.validationErrors).toMatchObject([ + { + children: [], + constraints: { + isString: 'type must be a string', + }, + property: 'type', + target: { + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + id: 'did:example:123#assertionMethod-1', + publicKeyPem: '-----BEGIN PUBLIC A...', + }, + value: undefined, + }, + ]) + } + }) + + it('should correctly transforms DidDoc class to Json', () => { + const didDocumentJson = JsonTransformer.toJSON(didDocumentInstance) + + expect(didDocumentJson).toMatchObject(didExample123Fixture) + }) + + describe('getServicesByType', () => { + it('returns all services with specified type', async () => { + expect(didDocumentInstance.getServicesByType('IndyAgent')).toEqual( + didDocumentInstance.service?.filter((service) => service.type === 'IndyAgent') + ) + }) + }) + + describe('getServicesByClassType', () => { + it('returns all services with specified class', async () => { + expect(didDocumentInstance.getServicesByClassType(IndyAgentService)).toEqual( + didDocumentInstance.service?.filter((service) => service instanceof IndyAgentService) + ) + }) + }) + + describe('didCommServices', () => { + it('returns all IndyAgentService and DidCommService instances', async () => { + const services = didDocumentInstance.service ?? [] + + expect(didDocumentInstance.didCommServices).toEqual(expect.arrayContaining([services[1], services[2]])) + }) + + it('returns all IndyAgentService and DidCommService instances sorted by priority', async () => { + const services = didDocumentInstance.service ?? [] + + expect(didDocumentInstance.didCommServices).toEqual([services[1], services[2]]) + }) + }) + + describe('findVerificationMethodByKeyType', () => { + it('return first verification method that match key type', async () => { + expect(await findVerificationMethodByKeyType('Ed25519VerificationKey2018', didDocumentInstance)).toBeInstanceOf( + VerificationMethod + ) + }) + }) +}) diff --git a/packages/core/src/modules/dids/domain/index.ts b/packages/core/src/modules/dids/domain/index.ts new file mode 100644 index 0000000000..27b0ca3633 --- /dev/null +++ b/packages/core/src/modules/dids/domain/index.ts @@ -0,0 +1,9 @@ +export * from './service' +export * from './verificationMethod' +export * from './DidDocument' +export * from './DidDocumentBuilder' +export * from './DidDocumentRole' +export * from './DidRegistrar' +export * from './DidResolver' +export * from './key-type' +export { parseDid } from './parse' diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1.test.ts new file mode 100644 index 0000000000..fe2b3f0c03 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1.test.ts @@ -0,0 +1,75 @@ +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' +import keyBls12381g1Fixture from '../../../__tests__/__fixtures__/didKeyBls12381g1.json' +import { VerificationMethod } from '../../verificationMethod' +import { keyDidBls12381g1 } from '../bls12381g1' + +const TEST_BLS12381G1_BASE58_KEY = '6FywSzB5BPd7xehCo1G4nYHAoZPMMP3gd4PLnvgA6SsTsogtz8K7RDznqLpFPLZXAE' +const TEST_BLS12381G1_FINGERPRINT = 'z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA' +const TEST_BLS12381G1_DID = `did:key:${TEST_BLS12381G1_FINGERPRINT}` +const TEST_BLS12381G1_PREFIX_BYTES = Buffer.concat([ + new Uint8Array([234, 1]), + TypedArrayEncoder.fromBase58(TEST_BLS12381G1_BASE58_KEY), +]) + +describe('bls12381g1', () => { + it('creates a Key instance from public key bytes and bls12381g1 key type', async () => { + const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_BLS12381G1_BASE58_KEY) + + const key = Key.fromPublicKey(publicKeyBytes, KeyType.Bls12381g1) + + expect(key.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) + }) + + it('creates a Key instance from a base58 encoded public key and bls12381g1 key type', async () => { + const key = Key.fromPublicKeyBase58(TEST_BLS12381G1_BASE58_KEY, KeyType.Bls12381g1) + + expect(key.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) + }) + + it('creates a Key instance from a fingerprint', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G1_FINGERPRINT) + + expect(key.publicKeyBase58).toBe(TEST_BLS12381G1_BASE58_KEY) + }) + + it('should correctly calculate the getter properties', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G1_FINGERPRINT) + + expect(key.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) + expect(key.publicKeyBase58).toBe(TEST_BLS12381G1_BASE58_KEY) + expect(key.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_BLS12381G1_BASE58_KEY)) + expect(key.keyType).toBe(KeyType.Bls12381g1) + expect(key.prefixedPublicKey.equals(TEST_BLS12381G1_PREFIX_BYTES)).toBe(true) + }) + + it('should return a valid verification method', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G1_FINGERPRINT) + const verificationMethods = keyDidBls12381g1.getVerificationMethods(TEST_BLS12381G1_DID, key) + + expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([keyBls12381g1Fixture.verificationMethod[0]]) + }) + + it('supports Bls12381G1Key2020 verification method type', () => { + expect(keyDidBls12381g1.supportedVerificationMethodTypes).toMatchObject(['Bls12381G1Key2020']) + }) + + it('returns key for Bls12381G1Key2020 verification method', () => { + const verificationMethod = JsonTransformer.fromJSON(keyBls12381g1Fixture.verificationMethod[0], VerificationMethod) + + const key = keyDidBls12381g1.getKeyFromVerificationMethod(verificationMethod) + + expect(key.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) + }) + + it('throws an error if an invalid verification method is passed', () => { + const verificationMethod = JsonTransformer.fromJSON(keyBls12381g1Fixture.verificationMethod[0], VerificationMethod) + + verificationMethod.type = 'SomeRandomType' + + expect(() => keyDidBls12381g1.getKeyFromVerificationMethod(verificationMethod)).toThrowError( + "Verification method with type 'SomeRandomType' not supported for key type 'bls12381g1'" + ) + }) +}) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1g2.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1g2.test.ts new file mode 100644 index 0000000000..442422f2cb --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1g2.test.ts @@ -0,0 +1,104 @@ +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' +import keyBls12381g1g2Fixture from '../../../__tests__/__fixtures__/didKeyBls12381g1g2.json' +import { VerificationMethod } from '../../verificationMethod' +import { keyDidBls12381g1g2 } from '../bls12381g1g2' + +const TEST_BLS12381G1G2_BASE58_KEY = + 'AQ4MiG1JKHmM5N4CgkF9uQ484PHN7gXB3ctF4ayL8hT6FdD6rcfFS3ZnMNntYsyJBckfNPf3HL8VU8jzgyT3qX88Yg3TeF2NkG2aZnJDNnXH1jkJStWMxjLw22LdphqAj1rSorsDhHjE8Rtz61bD6FP9aPokQUDVpZ4zXqsXVcxJ7YEc66TTLTTPwQPS7uNM4u2Fs' +const TEST_BLS12381G1G2_FINGERPRINT = + 'z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s' +const TEST_BLS12381G1G2_DID = `did:key:${TEST_BLS12381G1G2_FINGERPRINT}` + +const TEST_BLS12381G1_BASE58_KEY = '7BVES4h78wzabPAfMhchXyH5d8EX78S5TtzePH2YkftWcE6by9yj3NTAv9nsyCeYch' +const TEST_BLS12381G1_FINGERPRINT = 'z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd' + +const TEST_BLS12381G2_BASE58_KEY = + '26d2BdqELsXg7ZHCWKL2D5Y2S7mYrpkdhJemSEEvokd4qy4TULJeeU44hYPGKo4x4DbBp5ARzkv1D6xuB3bmhpdpKAXuXtode67wzh9PCtW8kTqQhH19VSiFZkLNkhe9rtf3' +const TEST_BLS12381G2_FINGERPRINT = + 'zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM' + +const TEST_BLS12381G1G2_PREFIX_BYTES = Buffer.concat([ + new Uint8Array([238, 1]), + TypedArrayEncoder.fromBase58(TEST_BLS12381G1G2_BASE58_KEY), +]) + +describe('bls12381g1g2', () => { + it('creates a Key instance from public key bytes and bls12381g1g2 key type', async () => { + const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_BLS12381G1G2_BASE58_KEY) + + const key = Key.fromPublicKey(publicKeyBytes, KeyType.Bls12381g1g2) + + expect(key.fingerprint).toBe(TEST_BLS12381G1G2_FINGERPRINT) + }) + + it('creates a Key instance from a base58 encoded public key and bls12381g1g2 key type', async () => { + const key = Key.fromPublicKeyBase58(TEST_BLS12381G1G2_BASE58_KEY, KeyType.Bls12381g1g2) + + expect(key.fingerprint).toBe(TEST_BLS12381G1G2_FINGERPRINT) + }) + + it('creates a Key instance from a fingerprint', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) + + expect(key.publicKeyBase58).toBe(TEST_BLS12381G1G2_BASE58_KEY) + }) + + it('should correctly calculate the getter properties', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) + + expect(key.fingerprint).toBe(TEST_BLS12381G1G2_FINGERPRINT) + expect(key.publicKeyBase58).toBe(TEST_BLS12381G1G2_BASE58_KEY) + expect(key.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_BLS12381G1G2_BASE58_KEY)) + expect(key.keyType).toBe(KeyType.Bls12381g1g2) + expect(key.prefixedPublicKey.equals(TEST_BLS12381G1G2_PREFIX_BYTES)).toBe(true) + }) + + it('should return a valid verification method', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) + const verificationMethods = keyDidBls12381g1g2.getVerificationMethods(TEST_BLS12381G1G2_DID, key) + + expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject(keyBls12381g1g2Fixture.verificationMethod) + }) + + it('supports no verification method type', () => { + // Verification methods can be handled by g1 or g2 key types. No reason to do it in here + expect(keyDidBls12381g1g2.supportedVerificationMethodTypes).toMatchObject([]) + }) + + it('throws an error for getKeyFromVerificationMethod as it is not supported for bls12381g1g2 key types', () => { + const verificationMethod = JsonTransformer.fromJSON( + keyBls12381g1g2Fixture.verificationMethod[0], + VerificationMethod + ) + + expect(() => keyDidBls12381g1g2.getKeyFromVerificationMethod(verificationMethod)).toThrowError( + 'Not supported for bls12381g1g2 key' + ) + }) + + it('should correctly go from g1g2 to g1', async () => { + const g1g2Key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) + + const g1PublicKey = g1g2Key.publicKey.slice(0, 48) + const g1DidKey = Key.fromPublicKey(g1PublicKey, KeyType.Bls12381g1) + + expect(g1DidKey.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) + expect(g1DidKey.publicKeyBase58).toBe(TEST_BLS12381G1_BASE58_KEY) + expect(g1DidKey.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_BLS12381G1_BASE58_KEY)) + expect(g1DidKey.keyType).toBe(KeyType.Bls12381g1) + }) + + it('should correctly go from g1g2 to g2', async () => { + const g1g2Key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) + + const g2PublicKey = g1g2Key.publicKey.slice(48) + const g2DidKey = Key.fromPublicKey(g2PublicKey, KeyType.Bls12381g2) + + expect(g2DidKey.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) + expect(g2DidKey.publicKeyBase58).toBe(TEST_BLS12381G2_BASE58_KEY) + expect(g2DidKey.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_BLS12381G2_BASE58_KEY)) + expect(g2DidKey.keyType).toBe(KeyType.Bls12381g2) + }) +}) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g2.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g2.test.ts new file mode 100644 index 0000000000..14ab8d9fbd --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g2.test.ts @@ -0,0 +1,77 @@ +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' +import keyBls12381g2Fixture from '../../../__tests__/__fixtures__/didKeyBls12381g2.json' +import { VerificationMethod } from '../../verificationMethod' +import { keyDidBls12381g2 } from '../bls12381g2' + +const TEST_BLS12381G2_BASE58_KEY = + 'mxE4sHTpbPcmxNviRVR9r7D2taXcNyVJmf9TBUFS1gRt3j3Ej9Seo59GQeCzYwbQgDrfWCwEJvmBwjLvheAky5N2NqFVzk4kuq3S8g4Fmekai4P622vHqWjFrsioYYDqhf9' +const TEST_BLS12381G2_FINGERPRINT = + 'zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT' +const TEST_BLS12381G2_DID = `did:key:${TEST_BLS12381G2_FINGERPRINT}` +const TEST_BLS12381G2_PREFIX_BYTES = Buffer.concat([ + new Uint8Array([235, 1]), + TypedArrayEncoder.fromBase58(TEST_BLS12381G2_BASE58_KEY), +]) + +describe('bls12381g2', () => { + it('creates a Key instance from public key bytes and bls12381g2 key type', async () => { + const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_BLS12381G2_BASE58_KEY) + + const key = Key.fromPublicKey(publicKeyBytes, KeyType.Bls12381g2) + + expect(key.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) + }) + + it('creates a Key instance from a base58 encoded public key and bls12381g2 key type', async () => { + const key = Key.fromPublicKeyBase58(TEST_BLS12381G2_BASE58_KEY, KeyType.Bls12381g2) + + expect(key.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) + }) + + it('creates a Key instance from a fingerprint', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G2_FINGERPRINT) + + expect(key.publicKeyBase58).toBe(TEST_BLS12381G2_BASE58_KEY) + }) + + it('should correctly calculate the getter properties', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G2_FINGERPRINT) + + expect(key.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) + expect(key.publicKeyBase58).toBe(TEST_BLS12381G2_BASE58_KEY) + expect(key.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_BLS12381G2_BASE58_KEY)) + expect(key.keyType).toBe(KeyType.Bls12381g2) + expect(key.prefixedPublicKey.equals(TEST_BLS12381G2_PREFIX_BYTES)).toBe(true) + }) + + it('should return a valid verification method', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G2_FINGERPRINT) + const verificationMethods = keyDidBls12381g2.getVerificationMethods(TEST_BLS12381G2_DID, key) + + expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([keyBls12381g2Fixture.verificationMethod[0]]) + }) + + it('supports Bls12381G2Key2020 verification method type', () => { + expect(keyDidBls12381g2.supportedVerificationMethodTypes).toMatchObject(['Bls12381G2Key2020']) + }) + + it('returns key for Bls12381G2Key2020 verification method', () => { + const verificationMethod = JsonTransformer.fromJSON(keyBls12381g2Fixture.verificationMethod[0], VerificationMethod) + + const key = keyDidBls12381g2.getKeyFromVerificationMethod(verificationMethod) + + expect(key.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) + }) + + it('throws an error if an invalid verification method is passed', () => { + const verificationMethod = JsonTransformer.fromJSON(keyBls12381g2Fixture.verificationMethod[0], VerificationMethod) + + verificationMethod.type = 'SomeRandomType' + + expect(() => keyDidBls12381g2.getKeyFromVerificationMethod(verificationMethod)).toThrowError( + "Verification method with type 'SomeRandomType' not supported for key type 'bls12381g2'" + ) + }) +}) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts new file mode 100644 index 0000000000..f57600a3c0 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts @@ -0,0 +1,96 @@ +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' +import didKeyEd25519Fixture from '../../../__tests__/__fixtures__//didKeyEd25519.json' +import { VerificationMethod } from '../../../domain/verificationMethod' +import { keyDidEd25519 } from '../ed25519' + +const TEST_ED25519_BASE58_KEY = '8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K' +const TEST_ED25519_FINGERPRINT = 'z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th' +const TEST_ED25519_DID = `did:key:${TEST_ED25519_FINGERPRINT}` +const TEST_ED25519_PREFIX_BYTES = Buffer.concat([ + new Uint8Array([237, 1]), + TypedArrayEncoder.fromBase58(TEST_ED25519_BASE58_KEY), +]) + +describe('ed25519', () => { + it('creates a Key instance from public key bytes and ed25519 key type', async () => { + const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_ED25519_BASE58_KEY) + + const didKey = Key.fromPublicKey(publicKeyBytes, KeyType.Ed25519) + + expect(didKey.fingerprint).toBe(TEST_ED25519_FINGERPRINT) + }) + + it('creates a Key instance from a base58 encoded public key and ed25519 key type', async () => { + const didKey = Key.fromPublicKeyBase58(TEST_ED25519_BASE58_KEY, KeyType.Ed25519) + + expect(didKey.fingerprint).toBe(TEST_ED25519_FINGERPRINT) + }) + + it('creates a Key instance from a fingerprint', async () => { + const didKey = Key.fromFingerprint(TEST_ED25519_FINGERPRINT) + + expect(didKey.fingerprint).toBe(TEST_ED25519_FINGERPRINT) + }) + + it('should correctly calculate the getter properties', async () => { + const didKey = Key.fromFingerprint(TEST_ED25519_FINGERPRINT) + + expect(didKey.fingerprint).toBe(TEST_ED25519_FINGERPRINT) + expect(didKey.publicKeyBase58).toBe(TEST_ED25519_BASE58_KEY) + expect(didKey.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_ED25519_BASE58_KEY)) + expect(didKey.keyType).toBe(KeyType.Ed25519) + expect(didKey.prefixedPublicKey.equals(TEST_ED25519_PREFIX_BYTES)).toBe(true) + }) + + it('should return a valid verification method', async () => { + const key = Key.fromFingerprint(TEST_ED25519_FINGERPRINT) + const verificationMethods = keyDidEd25519.getVerificationMethods(TEST_ED25519_DID, key) + + expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([didKeyEd25519Fixture.verificationMethod[0]]) + }) + + it('supports Ed25519VerificationKey2018 verification method type', () => { + expect(keyDidEd25519.supportedVerificationMethodTypes).toMatchObject([ + 'Ed25519VerificationKey2018', + 'Ed25519VerificationKey2020', + 'JsonWebKey2020', + 'Multikey', + ]) + }) + + it('returns key for Ed25519VerificationKey2018 verification method', () => { + const verificationMethod = JsonTransformer.fromJSON(didKeyEd25519Fixture.verificationMethod[0], VerificationMethod) + + const key = keyDidEd25519.getKeyFromVerificationMethod(verificationMethod) + + expect(key.fingerprint).toBe(TEST_ED25519_FINGERPRINT) + }) + + it('returns key for Ed25519VerificationKey2020 verification method', () => { + const verificationMethod = JsonTransformer.fromJSON( + { + id: 'did:example:123', + type: 'Ed25519VerificationKey2020', + controller: 'did:example:123', + publicKeyMultibase: 'z6MkkBWg1AnNxxWiq77gJDeHsLhGN6JV9Y3d6WiTifUs1sZi', + }, + VerificationMethod + ) + + const key = keyDidEd25519.getKeyFromVerificationMethod(verificationMethod) + + expect(key.publicKeyBase58).toBe('6jFdQvXwdR2FicGycegT2F9GYX2djeoGQVoXtPWr6enL') + }) + + it('throws an error if an invalid verification method is passed', () => { + const verificationMethod = JsonTransformer.fromJSON(didKeyEd25519Fixture.verificationMethod[0], VerificationMethod) + + verificationMethod.type = 'SomeRandomType' + + expect(() => keyDidEd25519.getKeyFromVerificationMethod(verificationMethod)).toThrowError( + "Verification method with type 'SomeRandomType' not supported for key type 'ed25519'" + ) + }) +}) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/jwk.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/jwk.test.ts new file mode 100644 index 0000000000..aa186aef56 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/jwk.test.ts @@ -0,0 +1,42 @@ +import { Key } from '../../../../../crypto/Key' +import { JsonTransformer } from '../../../../../utils' +import didKeyP256Fixture from '../../../__tests__/__fixtures__/didKeyP256.json' +import { VerificationMethod } from '../../verificationMethod' +import { VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020 } from '../../verificationMethod/JsonWebKey2020' +import { keyDidJsonWebKey } from '../keyDidJsonWebKey' + +const TEST_P256_FINGERPRINT = 'zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv' +const TEST_P256_DID = `did:key:${TEST_P256_FINGERPRINT}` + +describe('keyDidJsonWebKey', () => { + it('should return a valid verification method', async () => { + const key = Key.fromFingerprint(TEST_P256_FINGERPRINT) + const verificationMethods = keyDidJsonWebKey.getVerificationMethods(TEST_P256_DID, key) + + expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([didKeyP256Fixture.verificationMethod[0]]) + }) + + it('supports no verification method type', () => { + expect(keyDidJsonWebKey.supportedVerificationMethodTypes).toMatchObject([ + VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, + ]) + }) + + it('returns key for JsonWebKey2020 verification method', () => { + const verificationMethod = JsonTransformer.fromJSON(didKeyP256Fixture.verificationMethod[0], VerificationMethod) + + const key = keyDidJsonWebKey.getKeyFromVerificationMethod(verificationMethod) + + expect(key.fingerprint).toBe(TEST_P256_FINGERPRINT) + }) + + it('throws an error if an invalid verification method is passed', () => { + const verificationMethod = JsonTransformer.fromJSON(didKeyP256Fixture.verificationMethod[0], VerificationMethod) + + verificationMethod.type = 'SomeRandomType' + + expect(() => keyDidJsonWebKey.getKeyFromVerificationMethod(verificationMethod)).toThrowError( + 'Invalid verification method passed' + ) + }) +}) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/x25519.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/x25519.test.ts new file mode 100644 index 0000000000..5fb6490e43 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/x25519.test.ts @@ -0,0 +1,79 @@ +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' +import didKeyX25519Fixture from '../../../__tests__/__fixtures__/didKeyX25519.json' +import { VerificationMethod } from '../../verificationMethod' +import { keyDidX25519 } from '../x25519' + +const TEST_X25519_BASE58_KEY = '6fUMuABnqSDsaGKojbUF3P7ZkEL3wi2njsDdUWZGNgCU' +const TEST_X25519_FINGERPRINT = 'z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE' +const TEST_X25519_DID = `did:key:${TEST_X25519_FINGERPRINT}` +const TEST_X25519_PREFIX_BYTES = Buffer.concat([ + new Uint8Array([236, 1]), + TypedArrayEncoder.fromBase58(TEST_X25519_BASE58_KEY), +]) + +describe('x25519', () => { + it('creates a Key instance from public key bytes and x25519 key type', async () => { + const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_X25519_BASE58_KEY) + + const didKey = Key.fromPublicKey(publicKeyBytes, KeyType.X25519) + + expect(didKey.fingerprint).toBe(TEST_X25519_FINGERPRINT) + }) + + it('creates a Key instance from a base58 encoded public key and x25519 key type', async () => { + const didKey = Key.fromPublicKeyBase58(TEST_X25519_BASE58_KEY, KeyType.X25519) + + expect(didKey.fingerprint).toBe(TEST_X25519_FINGERPRINT) + }) + + it('creates a Key instance from a fingerprint', async () => { + const didKey = Key.fromFingerprint(TEST_X25519_FINGERPRINT) + + expect(didKey.fingerprint).toBe(TEST_X25519_FINGERPRINT) + }) + + it('should correctly calculate the getter properties', async () => { + const didKey = Key.fromFingerprint(TEST_X25519_FINGERPRINT) + + expect(didKey.fingerprint).toBe(TEST_X25519_FINGERPRINT) + expect(didKey.publicKeyBase58).toBe(TEST_X25519_BASE58_KEY) + expect(didKey.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_X25519_BASE58_KEY)) + expect(didKey.keyType).toBe(KeyType.X25519) + expect(didKey.prefixedPublicKey.equals(TEST_X25519_PREFIX_BYTES)).toBe(true) + }) + + it('should return a valid verification method', async () => { + const key = Key.fromFingerprint(TEST_X25519_FINGERPRINT) + const verificationMethods = keyDidX25519.getVerificationMethods(TEST_X25519_DID, key) + + expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([didKeyX25519Fixture.keyAgreement[0]]) + }) + + it('supports X25519KeyAgreementKey2019 verification method type', () => { + expect(keyDidX25519.supportedVerificationMethodTypes).toMatchObject([ + 'X25519KeyAgreementKey2019', + 'JsonWebKey2020', + 'Multikey', + ]) + }) + + it('returns key for X25519KeyAgreementKey2019 verification method', () => { + const verificationMethod = JsonTransformer.fromJSON(didKeyX25519Fixture.keyAgreement[0], VerificationMethod) + + const key = keyDidX25519.getKeyFromVerificationMethod(verificationMethod) + + expect(key.fingerprint).toBe(TEST_X25519_FINGERPRINT) + }) + + it('throws an error if an invalid verification method is passed', () => { + const verificationMethod = JsonTransformer.fromJSON(didKeyX25519Fixture.keyAgreement[0], VerificationMethod) + + verificationMethod.type = 'SomeRandomType' + + expect(() => keyDidX25519.getKeyFromVerificationMethod(verificationMethod)).toThrowError( + `Verification method with type 'SomeRandomType' not supported for key type 'x25519'` + ) + }) +}) diff --git a/packages/core/src/modules/dids/domain/key-type/bls12381g1.ts b/packages/core/src/modules/dids/domain/key-type/bls12381g1.ts new file mode 100644 index 0000000000..edcbd2d97c --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/bls12381g1.ts @@ -0,0 +1,28 @@ +import type { KeyDidMapping } from './keyDidMapping' +import type { VerificationMethod } from '../verificationMethod' + +import { KeyType } from '../../../../crypto/KeyType' +import { CredoError } from '../../../../error' +import { + getKeyFromBls12381G1Key2020, + isBls12381G1Key2020, + getBls12381G1Key2020, + VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020, +} from '../verificationMethod' + +export const keyDidBls12381g1: KeyDidMapping = { + supportedVerificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020], + + getVerificationMethods: (did, key) => [ + getBls12381G1Key2020({ id: `${did}#${key.fingerprint}`, key, controller: did }), + ], + getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { + if (isBls12381G1Key2020(verificationMethod)) { + return getKeyFromBls12381G1Key2020(verificationMethod) + } + + throw new CredoError( + `Verification method with type '${verificationMethod.type}' not supported for key type '${KeyType.Bls12381g1}'` + ) + }, +} diff --git a/packages/core/src/modules/dids/domain/key-type/bls12381g1g2.ts b/packages/core/src/modules/dids/domain/key-type/bls12381g1g2.ts new file mode 100644 index 0000000000..e5d402de4c --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/bls12381g1g2.ts @@ -0,0 +1,36 @@ +import type { KeyDidMapping } from './keyDidMapping' + +import { Key } from '../../../../crypto/Key' +import { KeyType } from '../../../../crypto/KeyType' +import { CredoError } from '../../../../error' +import { getBls12381G1Key2020, getBls12381G2Key2020 } from '../verificationMethod' + +export function getBls12381g1g2VerificationMethod(did: string, key: Key) { + const g1PublicKey = key.publicKey.slice(0, 48) + const g2PublicKey = key.publicKey.slice(48) + + const bls12381g1Key = Key.fromPublicKey(g1PublicKey, KeyType.Bls12381g1) + const bls12381g2Key = Key.fromPublicKey(g2PublicKey, KeyType.Bls12381g2) + + const bls12381g1VerificationMethod = getBls12381G1Key2020({ + id: `${did}#${bls12381g1Key.fingerprint}`, + key: bls12381g1Key, + controller: did, + }) + const bls12381g2VerificationMethod = getBls12381G2Key2020({ + id: `${did}#${bls12381g2Key.fingerprint}`, + key: bls12381g2Key, + controller: did, + }) + + return [bls12381g1VerificationMethod, bls12381g2VerificationMethod] +} + +export const keyDidBls12381g1g2: KeyDidMapping = { + supportedVerificationMethodTypes: [], + // For a G1G2 key, we return two verification methods + getVerificationMethods: getBls12381g1g2VerificationMethod, + getKeyFromVerificationMethod: () => { + throw new CredoError('Not supported for bls12381g1g2 key') + }, +} diff --git a/packages/core/src/modules/dids/domain/key-type/bls12381g2.ts b/packages/core/src/modules/dids/domain/key-type/bls12381g2.ts new file mode 100644 index 0000000000..395bb083fa --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/bls12381g2.ts @@ -0,0 +1,29 @@ +import type { KeyDidMapping } from './keyDidMapping' +import type { VerificationMethod } from '../verificationMethod' + +import { KeyType } from '../../../../crypto/KeyType' +import { CredoError } from '../../../../error' +import { + getBls12381G2Key2020, + getKeyFromBls12381G2Key2020, + isBls12381G2Key2020, + VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020, +} from '../verificationMethod' + +export const keyDidBls12381g2: KeyDidMapping = { + supportedVerificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], + + getVerificationMethods: (did, key) => [ + getBls12381G2Key2020({ id: `${did}#${key.fingerprint}`, key, controller: did }), + ], + + getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { + if (isBls12381G2Key2020(verificationMethod)) { + return getKeyFromBls12381G2Key2020(verificationMethod) + } + + throw new CredoError( + `Verification method with type '${verificationMethod.type}' not supported for key type '${KeyType.Bls12381g2}'` + ) + }, +} diff --git a/packages/core/src/modules/dids/domain/key-type/ed25519.ts b/packages/core/src/modules/dids/domain/key-type/ed25519.ts new file mode 100644 index 0000000000..35754ca407 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/ed25519.ts @@ -0,0 +1,55 @@ +import type { KeyDidMapping } from './keyDidMapping' +import type { VerificationMethod } from '../verificationMethod' + +import { KeyType } from '../../../../crypto/KeyType' +import { CredoError } from '../../../../error' +import { + getKeyFromEd25519VerificationKey2018, + isEd25519VerificationKey2018, + getKeyFromEd25519VerificationKey2020, + getKeyFromJsonWebKey2020, + isEd25519VerificationKey2020, + isJsonWebKey2020, + getEd25519VerificationKey2018, + getKeyFromMultikey, + isMultikey, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, + VERIFICATION_METHOD_TYPE_MULTIKEY, +} from '../verificationMethod' + +export { convertPublicKeyToX25519 } from '@stablelib/ed25519' + +export const keyDidEd25519: KeyDidMapping = { + supportedVerificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, + VERIFICATION_METHOD_TYPE_MULTIKEY, + ], + getVerificationMethods: (did, key) => [ + getEd25519VerificationKey2018({ id: `${did}#${key.fingerprint}`, key, controller: did }), + ], + getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { + if (isEd25519VerificationKey2018(verificationMethod)) { + return getKeyFromEd25519VerificationKey2018(verificationMethod) + } + + if (isEd25519VerificationKey2020(verificationMethod)) { + return getKeyFromEd25519VerificationKey2020(verificationMethod) + } + + if (isJsonWebKey2020(verificationMethod)) { + return getKeyFromJsonWebKey2020(verificationMethod) + } + + if (isMultikey(verificationMethod)) { + return getKeyFromMultikey(verificationMethod) + } + + throw new CredoError( + `Verification method with type '${verificationMethod.type}' not supported for key type '${KeyType.Ed25519}'` + ) + }, +} diff --git a/packages/core/src/modules/dids/domain/key-type/index.ts b/packages/core/src/modules/dids/domain/key-type/index.ts new file mode 100644 index 0000000000..be3f4beda4 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/index.ts @@ -0,0 +1,11 @@ +export { + getKeyDidMappingByKeyType, + getKeyFromVerificationMethod, + getSupportedVerificationMethodTypesFromKeyType, +} from './keyDidMapping' + +export * from './bls12381g2' +export * from './bls12381g1' +export * from './bls12381g1g2' +export * from './ed25519' +export * from './x25519' diff --git a/packages/core/src/modules/dids/domain/key-type/keyDidJsonWebKey.ts b/packages/core/src/modules/dids/domain/key-type/keyDidJsonWebKey.ts new file mode 100644 index 0000000000..3bf5d2f43e --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/keyDidJsonWebKey.ts @@ -0,0 +1,20 @@ +import type { KeyDidMapping } from './keyDidMapping' +import type { VerificationMethod } from '../verificationMethod' + +import { getJwkFromJson } from '../../../../crypto/jose/jwk' +import { CredoError } from '../../../../error' +import { getJsonWebKey2020 } from '../verificationMethod' +import { VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, isJsonWebKey2020 } from '../verificationMethod/JsonWebKey2020' + +export const keyDidJsonWebKey: KeyDidMapping = { + supportedVerificationMethodTypes: [VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020], + getVerificationMethods: (did, key) => [getJsonWebKey2020({ did, key })], + + getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { + if (!isJsonWebKey2020(verificationMethod) || !verificationMethod.publicKeyJwk) { + throw new CredoError('Invalid verification method passed') + } + + return getJwkFromJson(verificationMethod.publicKeyJwk).key + }, +} diff --git a/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts b/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts new file mode 100644 index 0000000000..1307861455 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts @@ -0,0 +1,112 @@ +import type { Key } from '../../../../crypto/Key' +import type { VerificationMethod } from '../verificationMethod' + +import { KeyType } from '../../../../crypto/KeyType' +import { getJwkFromJson } from '../../../../crypto/jose/jwk' +import { CredoError } from '../../../../error' +import { VERIFICATION_METHOD_TYPE_MULTIKEY, isMultikey, getKeyFromMultikey } from '../verificationMethod' +import { isJsonWebKey2020, VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020 } from '../verificationMethod/JsonWebKey2020' + +import { keyDidBls12381g1 } from './bls12381g1' +import { keyDidBls12381g1g2 } from './bls12381g1g2' +import { keyDidBls12381g2 } from './bls12381g2' +import { keyDidEd25519 } from './ed25519' +import { keyDidJsonWebKey } from './keyDidJsonWebKey' +import { keyDidSecp256k1 } from './secp256k1' +import { keyDidX25519 } from './x25519' + +export interface KeyDidMapping { + getVerificationMethods: (did: string, key: Key) => VerificationMethod[] + getKeyFromVerificationMethod(verificationMethod: VerificationMethod): Key + supportedVerificationMethodTypes: string[] +} + +// TODO: Maybe we should make this dynamically? +const keyDidMapping: Record = { + [KeyType.Ed25519]: keyDidEd25519, + [KeyType.X25519]: keyDidX25519, + [KeyType.Bls12381g1]: keyDidBls12381g1, + [KeyType.Bls12381g2]: keyDidBls12381g2, + [KeyType.Bls12381g1g2]: keyDidBls12381g1g2, + [KeyType.P256]: keyDidJsonWebKey, + [KeyType.P384]: keyDidJsonWebKey, + [KeyType.P521]: keyDidJsonWebKey, + [KeyType.K256]: keyDidSecp256k1, +} + +/** + * Dynamically creates a mapping from verification method key type to the key Did interface + * for all key types. + * + * { + * "Ed25519VerificationKey2018": KeyDidMapping + * } + */ +const verificationMethodKeyDidMapping = Object.values(KeyType).reduce>( + (mapping, keyType) => { + const supported = keyDidMapping[keyType].supportedVerificationMethodTypes.reduce>( + (accumulator, vMethodKeyType) => ({ + ...accumulator, + [vMethodKeyType]: keyDidMapping[keyType], + }), + {} + ) + + return { + ...mapping, + ...supported, + } + }, + {} +) + +export function getKeyDidMappingByKeyType(keyType: KeyType) { + const keyDid = keyDidMapping[keyType] + + if (!keyDid) { + throw new CredoError(`Unsupported key did from key type '${keyType}'`) + } + + return keyDid +} + +export function getKeyFromVerificationMethod(verificationMethod: VerificationMethod): Key { + // This is a special verification method, as it supports basically all key types. + if (isJsonWebKey2020(verificationMethod)) { + // TODO: move this validation to another place + if (!verificationMethod.publicKeyJwk) { + throw new CredoError( + `Missing publicKeyJwk on verification method with type ${VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020}` + ) + } + + return getJwkFromJson(verificationMethod.publicKeyJwk).key + } + + if (isMultikey(verificationMethod)) { + if (!verificationMethod.publicKeyMultibase) { + throw new CredoError( + `Missing publicKeyMultibase on verification method with type ${VERIFICATION_METHOD_TYPE_MULTIKEY}` + ) + } + + return getKeyFromMultikey(verificationMethod) + } + + const keyDid = verificationMethodKeyDidMapping[verificationMethod.type] + if (!keyDid) { + throw new CredoError(`Unsupported key did from verification method type '${verificationMethod.type}'`) + } + + return keyDid.getKeyFromVerificationMethod(verificationMethod) +} + +export function getSupportedVerificationMethodTypesFromKeyType(keyType: KeyType) { + const keyDid = keyDidMapping[keyType] + + if (!keyDid) { + throw new CredoError(`Unsupported key did from key type '${keyType}'`) + } + + return keyDid.supportedVerificationMethodTypes +} diff --git a/packages/core/src/modules/dids/domain/key-type/secp256k1.ts b/packages/core/src/modules/dids/domain/key-type/secp256k1.ts new file mode 100644 index 0000000000..2c1694afdb --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/secp256k1.ts @@ -0,0 +1,35 @@ +import type { KeyDidMapping } from './keyDidMapping' +import type { VerificationMethod } from '../verificationMethod' + +import { KeyType } from '../../../../crypto/KeyType' +import { CredoError } from '../../../../error' +import { + VERIFICATION_METHOD_TYPE_ECDSA_SECP256K1_VERIFICATION_KEY_2019, + VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, + getJsonWebKey2020, + getKeyFromEcdsaSecp256k1VerificationKey2019, + getKeyFromJsonWebKey2020, + isEcdsaSecp256k1VerificationKey2019, + isJsonWebKey2020, +} from '../verificationMethod' + +export const keyDidSecp256k1: KeyDidMapping = { + supportedVerificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ECDSA_SECP256K1_VERIFICATION_KEY_2019, + VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, + ], + getVerificationMethods: (did, key) => [getJsonWebKey2020({ did, key })], + getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { + if (isEcdsaSecp256k1VerificationKey2019(verificationMethod)) { + return getKeyFromEcdsaSecp256k1VerificationKey2019(verificationMethod) + } + + if (isJsonWebKey2020(verificationMethod)) { + return getKeyFromJsonWebKey2020(verificationMethod) + } + + throw new CredoError( + `Verification method with type '${verificationMethod.type}' not supported for key type '${KeyType.K256}'` + ) + }, +} diff --git a/packages/core/src/modules/dids/domain/key-type/x25519.ts b/packages/core/src/modules/dids/domain/key-type/x25519.ts new file mode 100644 index 0000000000..69e56b0e9c --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/x25519.ts @@ -0,0 +1,45 @@ +import type { KeyDidMapping } from './keyDidMapping' +import type { VerificationMethod } from '../verificationMethod' + +import { KeyType } from '../../../../crypto/KeyType' +import { CredoError } from '../../../../error' +import { + getKeyFromX25519KeyAgreementKey2019, + VERIFICATION_METHOD_TYPE_X25519_KEY_AGREEMENT_KEY_2019, + getX25519KeyAgreementKey2019, + isX25519KeyAgreementKey2019, + getKeyFromJsonWebKey2020, + isJsonWebKey2020, + VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, + VERIFICATION_METHOD_TYPE_MULTIKEY, + isMultikey, + getKeyFromMultikey, +} from '../verificationMethod' + +export const keyDidX25519: KeyDidMapping = { + supportedVerificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_X25519_KEY_AGREEMENT_KEY_2019, + VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, + VERIFICATION_METHOD_TYPE_MULTIKEY, + ], + getVerificationMethods: (did, key) => [ + getX25519KeyAgreementKey2019({ id: `${did}#${key.fingerprint}`, key, controller: did }), + ], + getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { + if (isJsonWebKey2020(verificationMethod)) { + return getKeyFromJsonWebKey2020(verificationMethod) + } + + if (isX25519KeyAgreementKey2019(verificationMethod)) { + return getKeyFromX25519KeyAgreementKey2019(verificationMethod) + } + + if (isMultikey(verificationMethod)) { + return getKeyFromMultikey(verificationMethod) + } + + throw new CredoError( + `Verification method with type '${verificationMethod.type}' not supported for key type '${KeyType.X25519}'` + ) + }, +} diff --git a/packages/core/src/modules/dids/domain/keyDidDocument.ts b/packages/core/src/modules/dids/domain/keyDidDocument.ts new file mode 100644 index 0000000000..1078b7548e --- /dev/null +++ b/packages/core/src/modules/dids/domain/keyDidDocument.ts @@ -0,0 +1,154 @@ +import type { DidDocument } from './DidDocument' +import type { VerificationMethod } from './verificationMethod/VerificationMethod' + +import { Key } from '../../../crypto/Key' +import { KeyType } from '../../../crypto/KeyType' +import { CredoError } from '../../../error' +import { SECURITY_CONTEXT_BBS_URL, SECURITY_JWS_CONTEXT_URL, SECURITY_X25519_CONTEXT_URL } from '../../vc/constants' +import { ED25519_SUITE_CONTEXT_URL_2018 } from '../../vc/data-integrity/signature-suites/ed25519/constants' + +import { DidDocumentBuilder } from './DidDocumentBuilder' +import { getBls12381g1g2VerificationMethod } from './key-type' +import { convertPublicKeyToX25519 } from './key-type/ed25519' +import { + getBls12381G1Key2020, + getBls12381G2Key2020, + getEd25519VerificationKey2018, + getJsonWebKey2020, + getX25519KeyAgreementKey2019, +} from './verificationMethod' + +const didDocumentKeyTypeMapping: Record DidDocument> = { + [KeyType.Ed25519]: getEd25519DidDoc, + [KeyType.X25519]: getX25519DidDoc, + [KeyType.Bls12381g1]: getBls12381g1DidDoc, + [KeyType.Bls12381g2]: getBls12381g2DidDoc, + [KeyType.Bls12381g1g2]: getBls12381g1g2DidDoc, + [KeyType.P256]: getJsonWebKey2020DidDocument, + [KeyType.P384]: getJsonWebKey2020DidDocument, + [KeyType.P521]: getJsonWebKey2020DidDocument, + [KeyType.K256]: getJsonWebKey2020DidDocument, +} + +export function getDidDocumentForKey(did: string, key: Key) { + const getDidDocument = didDocumentKeyTypeMapping[key.keyType] + + return getDidDocument(did, key) +} + +function getBls12381g1DidDoc(did: string, key: Key) { + const verificationMethod = getBls12381G1Key2020({ id: `${did}#${key.fingerprint}`, key, controller: did }) + + return getSignatureKeyBase({ + did, + key, + verificationMethod, + }) + .addContext(SECURITY_CONTEXT_BBS_URL) + .build() +} + +function getBls12381g1g2DidDoc(did: string, key: Key) { + const verificationMethods = getBls12381g1g2VerificationMethod(did, key) + + const didDocumentBuilder = new DidDocumentBuilder(did) + + for (const verificationMethod of verificationMethods) { + didDocumentBuilder + .addVerificationMethod(verificationMethod) + .addAuthentication(verificationMethod.id) + .addAssertionMethod(verificationMethod.id) + .addCapabilityDelegation(verificationMethod.id) + .addCapabilityInvocation(verificationMethod.id) + } + + return didDocumentBuilder.addContext(SECURITY_CONTEXT_BBS_URL).build() +} + +export function getJsonWebKey2020DidDocument(did: string, key: Key) { + const verificationMethod = getJsonWebKey2020({ did, key }) + + const didDocumentBuilder = new DidDocumentBuilder(did) + didDocumentBuilder.addContext(SECURITY_JWS_CONTEXT_URL).addVerificationMethod(verificationMethod) + + if (!key.supportsEncrypting && !key.supportsSigning) { + throw new CredoError('Key must support at least signing or encrypting') + } + + if (key.supportsSigning) { + didDocumentBuilder + .addAuthentication(verificationMethod.id) + .addAssertionMethod(verificationMethod.id) + .addCapabilityDelegation(verificationMethod.id) + .addCapabilityInvocation(verificationMethod.id) + } + + if (key.supportsEncrypting) { + didDocumentBuilder.addKeyAgreement(verificationMethod.id) + } + + return didDocumentBuilder.build() +} + +function getEd25519DidDoc(did: string, key: Key) { + const verificationMethod = getEd25519VerificationKey2018({ id: `${did}#${key.fingerprint}`, key, controller: did }) + + const publicKeyX25519 = convertPublicKeyToX25519(key.publicKey) + const didKeyX25519 = Key.fromPublicKey(publicKeyX25519, KeyType.X25519) + const x25519VerificationMethod = getX25519KeyAgreementKey2019({ + id: `${did}#${didKeyX25519.fingerprint}`, + key: didKeyX25519, + controller: did, + }) + + const didDocBuilder = getSignatureKeyBase({ did, key, verificationMethod }) + + didDocBuilder + .addContext(ED25519_SUITE_CONTEXT_URL_2018) + .addContext(SECURITY_X25519_CONTEXT_URL) + .addKeyAgreement(x25519VerificationMethod) + + return didDocBuilder.build() +} + +function getX25519DidDoc(did: string, key: Key) { + const verificationMethod = getX25519KeyAgreementKey2019({ id: `${did}#${key.fingerprint}`, key, controller: did }) + + const document = new DidDocumentBuilder(did) + .addKeyAgreement(verificationMethod) + .addContext(SECURITY_X25519_CONTEXT_URL) + .build() + + return document +} + +function getBls12381g2DidDoc(did: string, key: Key) { + const verificationMethod = getBls12381G2Key2020({ id: `${did}#${key.fingerprint}`, key, controller: did }) + + return getSignatureKeyBase({ + did, + key, + verificationMethod, + }) + .addContext(SECURITY_CONTEXT_BBS_URL) + .build() +} + +function getSignatureKeyBase({ + did, + key, + verificationMethod, +}: { + did: string + key: Key + verificationMethod: VerificationMethod +}) { + const keyId = `${did}#${key.fingerprint}` + + return new DidDocumentBuilder(did) + .addVerificationMethod(verificationMethod) + .addAuthentication(keyId) + .addAssertionMethod(keyId) + .addCapabilityDelegation(keyId) + .addCapabilityInvocation(keyId) +} diff --git a/packages/core/src/modules/dids/domain/parse.ts b/packages/core/src/modules/dids/domain/parse.ts new file mode 100644 index 0000000000..9690749e6b --- /dev/null +++ b/packages/core/src/modules/dids/domain/parse.ts @@ -0,0 +1,19 @@ +import type { ParsedDid } from '../types' + +import { parse } from 'did-resolver' + +import { CredoError } from '../../../error' + +export function parseDid(did: string): ParsedDid { + const parsed = tryParseDid(did) + + if (!parsed) { + throw new CredoError(`Error parsing did '${did}'`) + } + + return parsed +} + +export function tryParseDid(did: string): ParsedDid | null { + return parse(did) +} diff --git a/packages/core/src/modules/dids/domain/service/DidCommV1Service.ts b/packages/core/src/modules/dids/domain/service/DidCommV1Service.ts new file mode 100644 index 0000000000..af39aff270 --- /dev/null +++ b/packages/core/src/modules/dids/domain/service/DidCommV1Service.ts @@ -0,0 +1,50 @@ +import { ArrayNotEmpty, IsOptional, IsString } from 'class-validator' + +import { IsUri } from '../../../../utils' +import { getProtocolScheme } from '../../../../utils/uri' + +import { DidDocumentService } from './DidDocumentService' + +export class DidCommV1Service extends DidDocumentService { + public constructor(options: { + id: string + serviceEndpoint: string + recipientKeys: string[] + routingKeys?: string[] + accept?: string[] + priority?: number + }) { + super({ ...options, type: DidCommV1Service.type }) + + if (options) { + this.recipientKeys = options.recipientKeys + this.routingKeys = options.routingKeys + this.accept = options.accept + if (options.priority) this.priority = options.priority + } + } + + public static type = 'did-communication' + + public get protocolScheme(): string { + return getProtocolScheme(this.serviceEndpoint) + } + + @IsString() + @IsUri() + public serviceEndpoint!: string + + @ArrayNotEmpty() + @IsString({ each: true }) + public recipientKeys!: string[] + + @IsString({ each: true }) + @IsOptional() + public routingKeys?: string[] + + @IsString({ each: true }) + @IsOptional() + public accept?: string[] + + public priority = 0 +} diff --git a/packages/core/src/modules/dids/domain/service/DidCommV2Service.ts b/packages/core/src/modules/dids/domain/service/DidCommV2Service.ts new file mode 100644 index 0000000000..6eb1bf0c47 --- /dev/null +++ b/packages/core/src/modules/dids/domain/service/DidCommV2Service.ts @@ -0,0 +1,53 @@ +import { IsOptional, IsString } from 'class-validator' + +import { IsUri } from '../../../../utils' + +import { DidDocumentService } from './DidDocumentService' +import { NewDidCommV2Service, NewDidCommV2ServiceEndpoint } from './NewDidCommV2Service' + +export interface DidCommV2ServiceOptions { + id: string + serviceEndpoint: string + routingKeys?: string[] + accept?: string[] +} + +/** + * @deprecated use `NewDidCommV2Service` instead. Will be renamed to `LegacyDidCommV2Service` in 0.6 + */ +export class DidCommV2Service extends DidDocumentService { + public constructor(options: DidCommV2ServiceOptions) { + super({ ...options, type: DidCommV2Service.type }) + + if (options) { + this.serviceEndpoint = options.serviceEndpoint + this.accept = options.accept + this.routingKeys = options.routingKeys + } + } + + public static type = 'DIDComm' + + @IsString({ each: true }) + @IsOptional() + public routingKeys?: string[] + + @IsString({ each: true }) + @IsOptional() + public accept?: string[] + + @IsUri() + @IsString() + public serviceEndpoint!: string + + public toNewDidCommV2(): NewDidCommV2Service { + return new NewDidCommV2Service({ + id: this.id, + serviceEndpoint: new NewDidCommV2ServiceEndpoint({ + uri: this.serviceEndpoint, + accept: this.accept, + routingKeys: this.routingKeys, + }), + }) + } +} diff --git a/packages/core/src/modules/dids/domain/service/DidDocumentService.ts b/packages/core/src/modules/dids/domain/service/DidDocumentService.ts new file mode 100644 index 0000000000..25ed0f53c2 --- /dev/null +++ b/packages/core/src/modules/dids/domain/service/DidDocumentService.ts @@ -0,0 +1,63 @@ +import type { ValidationOptions } from 'class-validator' + +import { buildMessage, isString, IsString, ValidateBy } from 'class-validator' + +import { CredoError } from '../../../../error' +import { isJsonObject, SingleOrArray } from '../../../../utils' +import { getProtocolScheme } from '../../../../utils/uri' + +type ServiceEndpointType = SingleOrArray> + +export class DidDocumentService { + public constructor(options: { id: string; serviceEndpoint: ServiceEndpointType; type: string }) { + if (options) { + this.id = options.id + this.serviceEndpoint = options.serviceEndpoint + this.type = options.type + } + } + + /** + * @deprecated will be removed in 0.6, as it's not possible from the base did document service class to determine + * the protocol scheme. It needs to be implemented on a specific did document service class. + */ + public get protocolScheme(): string { + if (typeof this.serviceEndpoint !== 'string') { + throw new CredoError('Unable to extract protocol scheme from serviceEndpoint as it is not a string.') + } + + return getProtocolScheme(this.serviceEndpoint) + } + + @IsString() + public id!: string + + @IsStringOrJsonObjectSingleOrArray() + public serviceEndpoint!: SingleOrArray> + + @IsString() + public type!: string +} + +/** + * Checks if a given value is a string, a json object, or an array of strings and json objects + */ +function IsStringOrJsonObjectSingleOrArray(validationOptions?: Omit): PropertyDecorator { + return ValidateBy( + { + name: 'isStringOrJsonObjectSingleOrArray', + validator: { + validate: (value): boolean => + isString(value) || + isJsonObject(value) || + (Array.isArray(value) && value.every((v) => isString(v) || isJsonObject(v))), + defaultMessage: buildMessage( + (eachPrefix) => + eachPrefix + '$property must be a string, JSON object, or an array consisting of strings and JSON objects', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/modules/dids/domain/service/IndyAgentService.ts b/packages/core/src/modules/dids/domain/service/IndyAgentService.ts new file mode 100644 index 0000000000..3cc95faf46 --- /dev/null +++ b/packages/core/src/modules/dids/domain/service/IndyAgentService.ts @@ -0,0 +1,44 @@ +import { ArrayNotEmpty, IsOptional, IsString } from 'class-validator' + +import { IsUri } from '../../../../utils' +import { getProtocolScheme } from '../../../../utils/uri' + +import { DidDocumentService } from './DidDocumentService' + +export class IndyAgentService extends DidDocumentService { + public constructor(options: { + id: string + serviceEndpoint: string + recipientKeys: string[] + routingKeys?: string[] + priority?: number + }) { + super({ ...options, type: IndyAgentService.type }) + + if (options) { + this.recipientKeys = options.recipientKeys + this.routingKeys = options.routingKeys + if (options.priority) this.priority = options.priority + } + } + + public static type = 'IndyAgent' + + public get protocolScheme(): string { + return getProtocolScheme(this.serviceEndpoint) + } + + @IsString() + @IsUri() + public serviceEndpoint!: string + + @ArrayNotEmpty() + @IsString({ each: true }) + public recipientKeys!: string[] + + @IsString({ each: true }) + @IsOptional() + public routingKeys?: string[] + + public priority = 0 +} diff --git a/packages/core/src/modules/dids/domain/service/NewDidCommV2Service.ts b/packages/core/src/modules/dids/domain/service/NewDidCommV2Service.ts new file mode 100644 index 0000000000..865a107653 --- /dev/null +++ b/packages/core/src/modules/dids/domain/service/NewDidCommV2Service.ts @@ -0,0 +1,74 @@ +import { Type } from 'class-transformer' +import { IsOptional, IsString, ValidateNested } from 'class-validator' + +import { CredoError } from '../../../../error' +import { SingleOrArray, IsInstanceOrArrayOfInstances, IsUri } from '../../../../utils' + +import { DidDocumentService } from './DidDocumentService' + +export interface NewDidCommV2ServiceEndpointOptions { + uri: string + routingKeys?: string[] + accept?: string[] +} + +export class NewDidCommV2ServiceEndpoint { + public constructor(options: NewDidCommV2ServiceEndpointOptions) { + if (options) { + this.uri = options.uri + this.routingKeys = options.routingKeys + this.accept = options.accept + } + } + + @IsString() + @IsUri() + public uri!: string + + @IsString({ each: true }) + @IsOptional() + public routingKeys?: string[] + + @IsString({ each: true }) + @IsOptional() + public accept?: string[]; + + [key: string]: unknown | undefined +} + +export interface DidCommV2ServiceOptions { + id: string + serviceEndpoint: SingleOrArray +} + +/** + * Will be renamed to `DidCommV2Service` in 0.6 (and replace the current `DidCommV2Service`) + */ +export class NewDidCommV2Service extends DidDocumentService { + public constructor(options: DidCommV2ServiceOptions) { + super({ ...options, type: NewDidCommV2Service.type }) + + if (options) { + this.serviceEndpoint = options.serviceEndpoint + } + } + + public static type = 'DIDCommMessaging' + + @IsInstanceOrArrayOfInstances({ classType: [NewDidCommV2ServiceEndpoint] }) + @ValidateNested() + @Type(() => NewDidCommV2ServiceEndpoint) + public serviceEndpoint!: SingleOrArray + + public get firstServiceEndpointUri(): string { + if (Array.isArray(this.serviceEndpoint)) { + if (this.serviceEndpoint.length === 0) { + throw new CredoError('No entries in serviceEndpoint array') + } + + return this.serviceEndpoint[0].uri + } + + return this.serviceEndpoint.uri + } +} diff --git a/packages/core/src/modules/dids/domain/service/ServiceTransformer.ts b/packages/core/src/modules/dids/domain/service/ServiceTransformer.ts new file mode 100644 index 0000000000..ec4ae98d9a --- /dev/null +++ b/packages/core/src/modules/dids/domain/service/ServiceTransformer.ts @@ -0,0 +1,55 @@ +import type { ClassConstructor } from 'class-transformer' + +import { Transform } from 'class-transformer' + +import { JsonTransformer } from '../../../../utils' + +import { DidCommV1Service } from './DidCommV1Service' +import { DidCommV2Service } from './DidCommV2Service' +import { DidDocumentService } from './DidDocumentService' +import { IndyAgentService } from './IndyAgentService' +import { NewDidCommV2Service } from './NewDidCommV2Service' + +export const serviceTypes: { [key: string]: unknown | undefined } = { + [IndyAgentService.type]: IndyAgentService, + [DidCommV1Service.type]: DidCommV1Service, + [NewDidCommV2Service.type]: NewDidCommV2Service, + [DidCommV2Service.type]: DidCommV2Service, +} + +/** + * Decorator that transforms service json to corresponding class instances. See {@link serviceTypes} + * + * @example + * class Example { + * ServiceTransformer() + * private service: Service + * } + */ +export function ServiceTransformer() { + return Transform( + ({ value }: { value?: Array<{ type: string }> }) => { + return value?.map((serviceJson) => { + let serviceClass = (serviceTypes[serviceJson.type] ?? + DidDocumentService) as ClassConstructor + + // NOTE: deal with `DIDCommMessaging` type but using `serviceEndpoint` string value, parse it using the + // legacy class type + if ( + serviceJson.type === NewDidCommV2Service.type && + 'serviceEndpoint' in serviceJson && + typeof serviceJson.serviceEndpoint === 'string' + ) { + serviceClass = DidCommV2Service + } + + const service = JsonTransformer.fromJSON(serviceJson, serviceClass) + + return service + }) + }, + { + toClassOnly: true, + } + ) +} diff --git a/packages/core/src/modules/dids/domain/service/__tests__/DidcommV2Service.test.ts b/packages/core/src/modules/dids/domain/service/__tests__/DidcommV2Service.test.ts new file mode 100644 index 0000000000..230ef604af --- /dev/null +++ b/packages/core/src/modules/dids/domain/service/__tests__/DidcommV2Service.test.ts @@ -0,0 +1,54 @@ +import { JsonTransformer } from '../../../../../utils' +import didExample123DidcommV2 from '../../../__tests__/__fixtures__/didExample123DidcommV2Service.json' +import { DidDocument } from '../../DidDocument' +import { DidCommV2Service } from '../DidCommV2Service' +import { NewDidCommV2Service } from '../NewDidCommV2Service' + +describe('Did | DidDocument | DidCommV2Service', () => { + it('should correctly transforms Json to DidDocument class with didcomm v2 service', () => { + const didDocument = JsonTransformer.fromJSON(didExample123DidcommV2, DidDocument) + + expect(didDocument.service?.[0]).toBeInstanceOf(NewDidCommV2Service) + expect(didDocument.service?.[1]).toBeInstanceOf(DidCommV2Service) + + const didcommV2Service = didDocument.service?.[0] as NewDidCommV2Service + const legacyDidcommV2Service = didDocument.service?.[1] as DidCommV2Service + const didcommV2ServiceArray = didDocument.service?.[2] as NewDidCommV2Service + + expect(didcommV2Service).toEqual({ + id: 'did:example:123#service-1', + type: 'DIDCommMessaging', + serviceEndpoint: { + uri: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + routingKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], + }, + }) + + expect(legacyDidcommV2Service).toEqual({ + id: 'did:example:123#service-2', + type: 'DIDComm', + serviceEndpoint: 'https://agent.com/did-comm', + routingKeys: ['DADEajsDSaksLng9h'], + }) + + expect(didcommV2ServiceArray).toEqual({ + id: 'did:example:123#service-3', + type: 'DIDCommMessaging', + serviceEndpoint: [ + { + uri: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + routingKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], + }, + ], + }) + + expect(legacyDidcommV2Service.toNewDidCommV2()).toEqual({ + id: 'did:example:123#service-2', + type: 'DIDCommMessaging', + serviceEndpoint: { + uri: 'https://agent.com/did-comm', + routingKeys: ['DADEajsDSaksLng9h'], + }, + }) + }) +}) diff --git a/packages/core/src/modules/dids/domain/service/index.ts b/packages/core/src/modules/dids/domain/service/index.ts new file mode 100644 index 0000000000..aa646b0d0d --- /dev/null +++ b/packages/core/src/modules/dids/domain/service/index.ts @@ -0,0 +1,22 @@ +import { DidCommV1Service } from './DidCommV1Service' +import { DidDocumentService } from './DidDocumentService' +import { IndyAgentService } from './IndyAgentService' +import { + NewDidCommV2Service, + NewDidCommV2ServiceEndpoint, + NewDidCommV2ServiceEndpointOptions, +} from './NewDidCommV2Service' +import { ServiceTransformer, serviceTypes } from './ServiceTransformer' + +export { DidCommV2ServiceOptions, DidCommV2Service } from './DidCommV2Service' + +export { + IndyAgentService, + DidCommV1Service, + DidDocumentService, + ServiceTransformer, + serviceTypes, + NewDidCommV2Service, + NewDidCommV2ServiceEndpoint, + NewDidCommV2ServiceEndpointOptions, +} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/Bls12381G1Key2020.ts b/packages/core/src/modules/dids/domain/verificationMethod/Bls12381G1Key2020.ts new file mode 100644 index 0000000000..d362fdfc62 --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/Bls12381G1Key2020.ts @@ -0,0 +1,47 @@ +import { KeyType } from '../../../../crypto' +import { Key } from '../../../../crypto/Key' +import { CredoError } from '../../../../error' + +import { VerificationMethod } from './VerificationMethod' + +export const VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020 = 'Bls12381G1Key2020' +type Bls12381G1Key2020 = VerificationMethod & { + type: typeof VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020 +} + +/** + * Get a Bls12381G1Key2020 verification method. + */ +export function getBls12381G1Key2020({ key, id, controller }: { id: string; key: Key; controller: string }) { + return new VerificationMethod({ + id, + type: VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020, + controller, + publicKeyBase58: key.publicKeyBase58, + }) +} + +/** + * Check whether a verification method is a Bls12381G1Key2020 verification method. + */ +export function isBls12381G1Key2020(verificationMethod: VerificationMethod): verificationMethod is Bls12381G1Key2020 { + return verificationMethod.type === VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020 +} + +/** + * Get a key from a Bls12381G1Key2020 verification method. + */ +export function getKeyFromBls12381G1Key2020(verificationMethod: Bls12381G1Key2020) { + if (verificationMethod.publicKeyBase58) { + return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.Bls12381g1) + } + if (verificationMethod.publicKeyMultibase) { + const key = Key.fromFingerprint(verificationMethod.publicKeyMultibase) + if (key.keyType === KeyType.Bls12381g1) return key + else + throw new CredoError( + `Unexpected key type from resolving multibase encoding, key type was ${key.keyType} but expected ${KeyType.Bls12381g1}}` + ) + } + throw new CredoError('verification method is missing publicKeyBase58 or publicKeyMultibase') +} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/Bls12381G2Key2020.ts b/packages/core/src/modules/dids/domain/verificationMethod/Bls12381G2Key2020.ts new file mode 100644 index 0000000000..96cd3efaa3 --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/Bls12381G2Key2020.ts @@ -0,0 +1,47 @@ +import { KeyType } from '../../../../crypto' +import { Key } from '../../../../crypto/Key' +import { CredoError } from '../../../../error' + +import { VerificationMethod } from './VerificationMethod' + +export const VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020 = 'Bls12381G2Key2020' +type Bls12381G2Key2020 = VerificationMethod & { + type: typeof VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020 +} + +/** + * Get a Bls12381G2Key2020 verification method. + */ +export function getBls12381G2Key2020({ key, id, controller }: { id: string; key: Key; controller: string }) { + return new VerificationMethod({ + id, + type: VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020, + controller, + publicKeyBase58: key.publicKeyBase58, + }) +} + +/** + * Check whether a verification method is a Bls12381G2Key2020 verification method. + */ +export function isBls12381G2Key2020(verificationMethod: VerificationMethod): verificationMethod is Bls12381G2Key2020 { + return verificationMethod.type === VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020 +} + +/** + * Get a key from a Bls12381G2Key2020 verification method. + */ +export function getKeyFromBls12381G2Key2020(verificationMethod: Bls12381G2Key2020) { + if (verificationMethod.publicKeyBase58) { + return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.Bls12381g2) + } + if (verificationMethod.publicKeyMultibase) { + const key = Key.fromFingerprint(verificationMethod.publicKeyMultibase) + if (key.keyType === KeyType.Bls12381g2) return key + else + throw new CredoError( + `Unexpected key type from resolving multibase encoding, key type was ${key.keyType} but expected ${KeyType.Bls12381g2}}` + ) + } + throw new CredoError('verification method is missing publicKeyBase58 or publicKeyMultibase') +} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/EcdsaSecp256k1VerificationKey2019.ts b/packages/core/src/modules/dids/domain/verificationMethod/EcdsaSecp256k1VerificationKey2019.ts new file mode 100644 index 0000000000..e3218d4eea --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/EcdsaSecp256k1VerificationKey2019.ts @@ -0,0 +1,58 @@ +import { KeyType } from '../../../../crypto' +import { Key } from '../../../../crypto/Key' +import { CredoError } from '../../../../error' + +import { VerificationMethod } from './VerificationMethod' + +export const VERIFICATION_METHOD_TYPE_ECDSA_SECP256K1_VERIFICATION_KEY_2019 = 'EcdsaSecp256k1VerificationKey2019' + +type EcdsaSecp256k1VerificationKey2019 = VerificationMethod & { + type: typeof VERIFICATION_METHOD_TYPE_ECDSA_SECP256K1_VERIFICATION_KEY_2019 +} + +/** + * Get a EcdsaSecp256k1VerificationKey2019 verification method. + */ +export function getEcdsaSecp256k1VerificationKey2019({ + key, + id, + controller, +}: { + id: string + key: Key + controller: string +}) { + return new VerificationMethod({ + id, + type: VERIFICATION_METHOD_TYPE_ECDSA_SECP256K1_VERIFICATION_KEY_2019, + controller, + publicKeyBase58: key.publicKeyBase58, + }) +} + +/** + * Check whether a verification method is a EcdsaSecp256k1VerificationKey2019 verification method. + */ +export function isEcdsaSecp256k1VerificationKey2019( + verificationMethod: VerificationMethod +): verificationMethod is EcdsaSecp256k1VerificationKey2019 { + return verificationMethod.type === VERIFICATION_METHOD_TYPE_ECDSA_SECP256K1_VERIFICATION_KEY_2019 +} + +/** + * Get a key from a EcdsaSecp256k1VerificationKey2019 verification method. + */ +export function getKeyFromEcdsaSecp256k1VerificationKey2019(verificationMethod: EcdsaSecp256k1VerificationKey2019) { + if (verificationMethod.publicKeyBase58) { + return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.K256) + } + if (verificationMethod.publicKeyMultibase) { + const key = Key.fromFingerprint(verificationMethod.publicKeyMultibase) + if (key.keyType === KeyType.K256) return key + else + throw new CredoError( + `Unexpected key type from resolving multibase encoding, key type was ${key.keyType} but expected ${KeyType.K256}}` + ) + } + throw new CredoError('verification method is missing publicKeyBase58 or publicKeyMultibase') +} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2018.ts b/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2018.ts new file mode 100644 index 0000000000..0c15344f23 --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2018.ts @@ -0,0 +1,49 @@ +import { KeyType } from '../../../../crypto' +import { Key } from '../../../../crypto/Key' +import { CredoError } from '../../../../error' + +import { VerificationMethod } from './VerificationMethod' + +export const VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018 = 'Ed25519VerificationKey2018' +type Ed25519VerificationKey2018 = VerificationMethod & { + type: typeof VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018 +} + +/** + * Get a Ed25519VerificationKey2018 verification method. + */ +export function getEd25519VerificationKey2018({ key, id, controller }: { id: string; key: Key; controller: string }) { + return new VerificationMethod({ + id, + type: VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + controller, + publicKeyBase58: key.publicKeyBase58, + }) +} + +/** + * Check whether a verification method is a Ed25519VerificationKey2018 verification method. + */ +export function isEd25519VerificationKey2018( + verificationMethod: VerificationMethod +): verificationMethod is Ed25519VerificationKey2018 { + return verificationMethod.type === VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018 +} + +/** + * Get a key from a Ed25519VerificationKey2018 verification method. + */ +export function getKeyFromEd25519VerificationKey2018(verificationMethod: Ed25519VerificationKey2018) { + if (verificationMethod.publicKeyBase58) { + return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.Ed25519) + } + if (verificationMethod.publicKeyMultibase) { + const key = Key.fromFingerprint(verificationMethod.publicKeyMultibase) + if (key.keyType === KeyType.Ed25519) return key + else + throw new CredoError( + `Unexpected key type from resolving multibase encoding, key type was ${key.keyType} but expected ${KeyType.Ed25519}` + ) + } + throw new CredoError('verification method is missing publicKeyBase58 or publicKeyMultibase') +} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2020.ts b/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2020.ts new file mode 100644 index 0000000000..607b47b717 --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2020.ts @@ -0,0 +1,47 @@ +import { KeyType } from '../../../../crypto' +import { Key } from '../../../../crypto/Key' +import { CredoError } from '../../../../error' + +import { VerificationMethod } from './VerificationMethod' + +export const VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020 = 'Ed25519VerificationKey2020' +type Ed25519VerificationKey2020 = VerificationMethod & { + type: typeof VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020 +} + +/** + * Get a Ed25519VerificationKey2020 verification method. + */ +export function getEd25519VerificationKey2020({ key, id, controller }: { id: string; key: Key; controller: string }) { + return new VerificationMethod({ + id, + type: VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + controller, + publicKeyMultibase: key.fingerprint, + }) +} + +/** + * Check whether a verification method is a Ed25519VerificationKey2020 verification method. + */ +export function isEd25519VerificationKey2020( + verificationMethod: VerificationMethod +): verificationMethod is Ed25519VerificationKey2020 { + return verificationMethod.type === VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020 +} + +/** + * Get a key from a Ed25519VerificationKey2020 verification method. + */ +export function getKeyFromEd25519VerificationKey2020(verificationMethod: Ed25519VerificationKey2020) { + if (!verificationMethod.publicKeyMultibase) { + throw new CredoError('verification method is missing publicKeyMultibase') + } + + const key = Key.fromFingerprint(verificationMethod.publicKeyMultibase) + if (key.keyType !== KeyType.Ed25519) { + throw new CredoError(`Verification method publicKeyMultibase is for unexpected key type ${key.keyType}`) + } + + return key +} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/JsonWebKey2020.ts b/packages/core/src/modules/dids/domain/verificationMethod/JsonWebKey2020.ts new file mode 100644 index 0000000000..9401bc512a --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/JsonWebKey2020.ts @@ -0,0 +1,52 @@ +import type { VerificationMethod } from './VerificationMethod' +import type { Key } from '../../../../crypto/Key' +import type { JwkJson } from '../../../../crypto/jose/jwk/Jwk' + +import { getJwkFromJson, getJwkFromKey } from '../../../../crypto/jose/jwk' +import { CredoError } from '../../../../error' + +export const VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020 = 'JsonWebKey2020' + +type JwkOrKey = { jwk: JwkJson; key?: never } | { key: Key; jwk?: never } +type GetJsonWebKey2020Options = { + did: string + + verificationMethodId?: string +} & JwkOrKey + +/** + * Get a JsonWebKey2020 verification method. + */ +export function getJsonWebKey2020(options: GetJsonWebKey2020Options) { + const jwk = options.jwk ? getJwkFromJson(options.jwk) : getJwkFromKey(options.key) + const verificationMethodId = options.verificationMethodId ?? `${options.did}#${jwk.key.fingerprint}` + + return { + id: verificationMethodId, + type: VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, + controller: options.did, + publicKeyJwk: options.jwk ?? jwk.toJson(), + } +} + +/** + * Check whether a verification method is a JsonWebKey2020 verification method. + */ +export function isJsonWebKey2020( + verificationMethod: VerificationMethod +): verificationMethod is VerificationMethod & { type: 'JsonWebKey2020' } { + return verificationMethod.type === VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020 +} + +/** + * Get a key from a JsonWebKey2020 verification method. + */ +export function getKeyFromJsonWebKey2020(verificationMethod: VerificationMethod & { type: 'JsonWebKey2020' }) { + if (!verificationMethod.publicKeyJwk) { + throw new CredoError( + `Missing publicKeyJwk on verification method with type ${VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020}` + ) + } + + return getJwkFromJson(verificationMethod.publicKeyJwk).key +} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/Multikey.ts b/packages/core/src/modules/dids/domain/verificationMethod/Multikey.ts new file mode 100644 index 0000000000..f817e10e8a --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/Multikey.ts @@ -0,0 +1,56 @@ +import type { VerificationMethod } from './VerificationMethod' + +import { Key } from '../../../../crypto/Key' +import { CredoError } from '../../../../error' + +export const VERIFICATION_METHOD_TYPE_MULTIKEY = 'Multikey' + +type GetMultikeyOptions = { + did: string + key: Key + verificationMethodId?: string +} + +/** + * Get a Multikey verification method. + */ +export function getMultikey({ did, key, verificationMethodId }: GetMultikeyOptions) { + if (!verificationMethodId) { + verificationMethodId = `${did}#${key.fingerprint}` + } + + return { + id: verificationMethodId, + type: VERIFICATION_METHOD_TYPE_MULTIKEY, + controller: did, + publicKeyMultibase: key.fingerprint, + } +} + +/** + * Check whether a verification method is a Multikey verification method. + */ +export function isMultikey( + verificationMethod: VerificationMethod +): verificationMethod is + | (VerificationMethod & { type: 'Multikey' }) + | (VerificationMethod & { publicKeyMultibase: string }) { + return verificationMethod.type === VERIFICATION_METHOD_TYPE_MULTIKEY +} + +/** + * Get a key from a Multikey verification method. + */ +export function getKeyFromMultikey( + verificationMethod: + | (VerificationMethod & { type: 'Multikey' }) + | (VerificationMethod & { publicKeyMultibase: string }) +) { + if (!verificationMethod.publicKeyMultibase) { + throw new CredoError( + `Missing publicKeyMultibase on verification method with type ${VERIFICATION_METHOD_TYPE_MULTIKEY}` + ) + } + + return Key.fromFingerprint(verificationMethod.publicKeyMultibase) +} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts b/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts new file mode 100644 index 0000000000..b520a0955c --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts @@ -0,0 +1,75 @@ +import type { JwkJson } from '../../../../crypto/jose/jwk/Jwk' + +import { IsString, IsOptional } from 'class-validator' + +export interface VerificationMethodOptions { + id: string + type: string + controller: string + publicKeyBase58?: string + publicKeyBase64?: string + publicKeyJwk?: JwkJson + publicKeyHex?: string + publicKeyMultibase?: string + publicKeyPem?: string + blockchainAccountId?: string + ethereumAddress?: string +} + +export class VerificationMethod { + public constructor(options: VerificationMethodOptions) { + if (options) { + this.id = options.id + this.type = options.type + this.controller = options.controller + this.publicKeyBase58 = options.publicKeyBase58 + this.publicKeyBase64 = options.publicKeyBase64 + this.publicKeyJwk = options.publicKeyJwk + this.publicKeyHex = options.publicKeyHex + this.publicKeyMultibase = options.publicKeyMultibase + this.publicKeyPem = options.publicKeyPem + this.blockchainAccountId = options.blockchainAccountId + this.ethereumAddress = options.ethereumAddress + } + } + + @IsString() + public id!: string + + @IsString() + public type!: string + + @IsString() + public controller!: string + + @IsOptional() + @IsString() + public publicKeyBase58?: string + + @IsOptional() + @IsString() + public publicKeyBase64?: string + + // TODO: validation of JWK + public publicKeyJwk?: JwkJson + + @IsOptional() + @IsString() + public publicKeyHex?: string + + @IsOptional() + @IsString() + public publicKeyMultibase?: string + + @IsOptional() + @IsString() + public publicKeyPem?: string + + @IsOptional() + @IsString() + public blockchainAccountId?: string + + @IsOptional() + @IsString() + public ethereumAddress?: string +} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethodTransformer.ts b/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethodTransformer.ts new file mode 100644 index 0000000000..43dfd2c1c0 --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethodTransformer.ts @@ -0,0 +1,59 @@ +import type { ValidationOptions } from 'class-validator' + +import { Transform, TransformationType } from 'class-transformer' +import { isString, ValidateBy, isInstance, buildMessage } from 'class-validator' + +import { JsonTransformer } from '../../../../utils/JsonTransformer' + +import { VerificationMethod } from './VerificationMethod' + +/** + * Checks if a given value is a real string. + */ +function IsStringOrVerificationMethod(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'isStringOrVerificationMethod', + validator: { + validate: (value): boolean => isString(value) || isInstance(value, VerificationMethod), + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be a string or instance of VerificationMethod', + validationOptions + ), + }, + }, + validationOptions + ) +} + +/** + * Decorator that transforms authentication json to corresponding class instances + * + * @example + * class Example { + * VerificationMethodTransformer() + * private authentication: VerificationMethod + * } + */ +function VerificationMethodTransformer() { + return Transform(({ value, type }: { value?: Array; type: TransformationType }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + return value?.map((auth) => { + // referenced verification method + if (typeof auth === 'string') { + return String(auth) + } + + // embedded verification method + return JsonTransformer.fromJSON(auth, VerificationMethod) + }) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + return value?.map((auth) => (typeof auth === 'string' ? auth : JsonTransformer.toJSON(auth))) + } + + // PLAIN_TO_PLAIN + return value + }) +} + +export { IsStringOrVerificationMethod, VerificationMethodTransformer } diff --git a/packages/core/src/modules/dids/domain/verificationMethod/X25519KeyAgreementKey2019.ts b/packages/core/src/modules/dids/domain/verificationMethod/X25519KeyAgreementKey2019.ts new file mode 100644 index 0000000000..69b2d2e35a --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/X25519KeyAgreementKey2019.ts @@ -0,0 +1,49 @@ +import { KeyType } from '../../../../crypto' +import { Key } from '../../../../crypto/Key' +import { CredoError } from '../../../../error' + +import { VerificationMethod } from './VerificationMethod' + +export const VERIFICATION_METHOD_TYPE_X25519_KEY_AGREEMENT_KEY_2019 = 'X25519KeyAgreementKey2019' +type X25519KeyAgreementKey2019 = VerificationMethod & { + type: typeof VERIFICATION_METHOD_TYPE_X25519_KEY_AGREEMENT_KEY_2019 +} + +/** + * Get a X25519KeyAgreementKey2019 verification method. + */ +export function getX25519KeyAgreementKey2019({ key, id, controller }: { id: string; key: Key; controller: string }) { + return new VerificationMethod({ + id, + type: VERIFICATION_METHOD_TYPE_X25519_KEY_AGREEMENT_KEY_2019, + controller, + publicKeyBase58: key.publicKeyBase58, + }) +} + +/** + * Check whether a verification method is a X25519KeyAgreementKey2019 verification method. + */ +export function isX25519KeyAgreementKey2019( + verificationMethod: VerificationMethod +): verificationMethod is X25519KeyAgreementKey2019 { + return verificationMethod.type === VERIFICATION_METHOD_TYPE_X25519_KEY_AGREEMENT_KEY_2019 +} + +/** + * Get a key from a X25519KeyAgreementKey2019 verification method. + */ +export function getKeyFromX25519KeyAgreementKey2019(verificationMethod: X25519KeyAgreementKey2019) { + if (verificationMethod.publicKeyBase58) { + return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.X25519) + } + if (verificationMethod.publicKeyMultibase) { + const key = Key.fromFingerprint(verificationMethod.publicKeyMultibase) + if (key.keyType === KeyType.X25519) return key + else + throw new CredoError( + `Unexpected key type from resolving multibase encoding, key type was ${key.keyType} but expected ${KeyType.X25519}` + ) + } + throw new CredoError('verification method is missing publicKeyBase58 or publicKeyMultibase') +} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/index.ts b/packages/core/src/modules/dids/domain/verificationMethod/index.ts new file mode 100644 index 0000000000..53809e4d6f --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/index.ts @@ -0,0 +1,11 @@ +export { VerificationMethod } from './VerificationMethod' +export { VerificationMethodTransformer, IsStringOrVerificationMethod } from './VerificationMethodTransformer' + +export * from './Bls12381G1Key2020' +export * from './Bls12381G2Key2020' +export * from './Ed25519VerificationKey2018' +export * from './Ed25519VerificationKey2020' +export * from './JsonWebKey2020' +export * from './X25519KeyAgreementKey2019' +export * from './Multikey' +export * from './EcdsaSecp256k1VerificationKey2019' diff --git a/packages/core/src/modules/dids/helpers.ts b/packages/core/src/modules/dids/helpers.ts new file mode 100644 index 0000000000..5e02316db5 --- /dev/null +++ b/packages/core/src/modules/dids/helpers.ts @@ -0,0 +1,34 @@ +import { KeyType, Key } from '../../crypto' +import { isDid } from '../../utils' + +import { DidKey } from './methods/key' + +export function isDidKey(key: string) { + return isDid(key, 'key') +} + +export function didKeyToVerkey(key: string) { + if (isDidKey(key)) { + const publicKeyBase58 = DidKey.fromDid(key).key.publicKeyBase58 + return publicKeyBase58 + } + return key +} + +export function verkeyToDidKey(key: string) { + if (isDidKey(key)) return key + const publicKeyBase58 = key + const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519) + const didKey = new DidKey(ed25519Key) + return didKey.did +} + +export function didKeyToInstanceOfKey(key: string) { + const didKey = DidKey.fromDid(key) + return didKey.key +} + +export function verkeyToInstanceOfKey(verkey: string) { + const ed25519Key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) + return ed25519Key +} diff --git a/packages/core/src/modules/dids/index.ts b/packages/core/src/modules/dids/index.ts new file mode 100644 index 0000000000..5f1677eb88 --- /dev/null +++ b/packages/core/src/modules/dids/index.ts @@ -0,0 +1,9 @@ +export * from './types' +export * from './domain' +export * from './DidsApi' +export * from './DidsApiOptions' +export * from './repository' +export * from './services' +export * from './DidsModule' +export * from './methods' +export * from './DidsModuleConfig' diff --git a/packages/core/src/modules/dids/methods/index.ts b/packages/core/src/modules/dids/methods/index.ts new file mode 100644 index 0000000000..4faee9c44b --- /dev/null +++ b/packages/core/src/modules/dids/methods/index.ts @@ -0,0 +1,4 @@ +export * from './key' +export * from './peer' +export * from './web' +export * from './jwk' diff --git a/packages/core/src/modules/dids/methods/jwk/DidJwk.ts b/packages/core/src/modules/dids/methods/jwk/DidJwk.ts new file mode 100644 index 0000000000..81366791e0 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/DidJwk.ts @@ -0,0 +1,67 @@ +import type { Jwk } from '../../../../crypto' + +import { getJwkFromJson } from '../../../../crypto/jose/jwk' +import { JsonEncoder } from '../../../../utils' +import { parseDid } from '../../domain/parse' + +import { getDidJwkDocument } from './didJwkDidDocument' + +export class DidJwk { + public readonly did: string + + private constructor(did: string) { + this.did = did + } + + public get allowsEncrypting() { + return this.jwk.use === 'enc' || this.key.supportsEncrypting + } + + public get allowsSigning() { + return this.jwk.use === 'sig' || this.key.supportsSigning + } + + public static fromDid(did: string) { + const parsed = parseDid(did) + const jwkJson = JsonEncoder.fromBase64(parsed.id) + // This validates the jwk + getJwkFromJson(jwkJson) + + return new DidJwk(did) + } + + /** + * A did:jwk DID can only have one verification method, and the verification method + * id will always be `#0`. + */ + public get verificationMethodId() { + return `${this.did}#0` + } + + public static fromJwk(jwk: Jwk) { + const did = `did:jwk:${JsonEncoder.toBase64URL(jwk.toJson())}` + + return new DidJwk(did) + } + + public get key() { + return this.jwk.key + } + + public get jwk() { + const jwk = getJwkFromJson(this.jwkJson) + + return jwk + } + + public get jwkJson() { + const parsed = parseDid(this.did) + const jwkJson = JsonEncoder.fromBase64(parsed.id) + + return jwkJson + } + + public get didDocument() { + return getDidJwkDocument(this) + } +} diff --git a/packages/core/src/modules/dids/methods/jwk/JwkDidRegistrar.ts b/packages/core/src/modules/dids/methods/jwk/JwkDidRegistrar.ts new file mode 100644 index 0000000000..10991418ac --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/JwkDidRegistrar.ts @@ -0,0 +1,120 @@ +import type { AgentContext } from '../../../../agent' +import type { KeyType } from '../../../../crypto' +import type { Buffer } from '../../../../utils' +import type { DidRegistrar } from '../../domain/DidRegistrar' +import type { DidCreateOptions, DidCreateResult, DidDeactivateResult, DidUpdateResult } from '../../types' + +import { getJwkFromKey } from '../../../../crypto/jose/jwk' +import { DidDocumentRole } from '../../domain/DidDocumentRole' +import { DidRepository, DidRecord } from '../../repository' + +import { DidJwk } from './DidJwk' + +export class JwkDidRegistrar implements DidRegistrar { + public readonly supportedMethods = ['jwk'] + + public async create(agentContext: AgentContext, options: JwkDidCreateOptions): Promise { + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + + const keyType = options.options.keyType + const seed = options.secret?.seed + const privateKey = options.secret?.privateKey + + if (!keyType) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing key type', + }, + } + } + + try { + const key = await agentContext.wallet.createKey({ + keyType, + seed, + privateKey, + }) + + const jwk = getJwkFromKey(key) + const didJwk = DidJwk.fromJwk(jwk) + + // Save the did so we know we created it and can issue with it + const didRecord = new DidRecord({ + did: didJwk.did, + role: DidDocumentRole.Created, + }) + await didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: didJwk.did, + didDocument: didJwk.didDocument, + secret: { + // FIXME: the uni-registrar creates the seed in the registrar method + // if it doesn't exist so the seed can always be returned. Currently + // we can only return it if the seed was passed in by the user. Once + // we have a secure method for generating seeds we should use the same + // approach + seed: options.secret?.seed, + privateKey: options.secret?.privateKey, + }, + }, + } + } catch (error) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot update did:jwk did`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot deactivate did:jwk did`, + }, + } + } +} + +export interface JwkDidCreateOptions extends DidCreateOptions { + method: 'jwk' + // For now we don't support creating a did:jwk with a did or did document + did?: never + didDocument?: never + options: { + keyType: KeyType + } + secret?: { + seed?: Buffer + privateKey?: Buffer + } +} + +// Update and Deactivate not supported for did:jwk +export type JwkDidUpdateOptions = never +export type JwkDidDeactivateOptions = never diff --git a/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts b/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts new file mode 100644 index 0000000000..8bf1e787b8 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts @@ -0,0 +1,43 @@ +import type { AgentContext } from '../../../../agent' +import type { DidResolver } from '../../domain/DidResolver' +import type { DidResolutionResult } from '../../types' + +import { DidJwk } from './DidJwk' + +export class JwkDidResolver implements DidResolver { + public readonly supportedMethods = ['jwk'] + + /** + * No remote resolving done, did document is dynamically constructed. To not pollute the cache we don't allow caching + */ + public readonly allowsCaching = false + + /** + * Easier to calculate for resolving than serving the local did document. Record also doesn't + * have a did document + */ + public readonly allowsLocalDidRecord = false + + public async resolve(agentContext: AgentContext, did: string): Promise { + const didDocumentMetadata = {} + + try { + const didDocument = DidJwk.fromDid(did).didDocument + + return { + didDocument, + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } +} diff --git a/packages/core/src/modules/dids/methods/jwk/__tests__/DidJwk.test.ts b/packages/core/src/modules/dids/methods/jwk/__tests__/DidJwk.test.ts new file mode 100644 index 0000000000..036e0c940d --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/__tests__/DidJwk.test.ts @@ -0,0 +1,24 @@ +import { getJwkFromJson } from '../../../../../crypto/jose/jwk' +import { DidJwk } from '../DidJwk' + +import { p256DidJwkEyJjcnYi0iFixture } from './__fixtures__/p256DidJwkEyJjcnYi0i' +import { x25519DidJwkEyJrdHkiOiJFixture } from './__fixtures__/x25519DidJwkEyJrdHkiOiJ' + +describe('DidJwk', () => { + it('creates a DidJwk instance from a did', async () => { + const documentTypes = [p256DidJwkEyJjcnYi0iFixture, x25519DidJwkEyJrdHkiOiJFixture] + + for (const documentType of documentTypes) { + const didJwk = DidJwk.fromDid(documentType.id) + + expect(didJwk.didDocument.toJSON()).toMatchObject(documentType) + } + }) + + it('creates a DidJwk instance from a jwk instance', async () => { + const didJwk = DidJwk.fromJwk(getJwkFromJson(p256DidJwkEyJjcnYi0iFixture.verificationMethod[0].publicKeyJwk)) + + expect(didJwk.did).toBe(p256DidJwkEyJjcnYi0iFixture.id) + expect(didJwk.didDocument.toJSON()).toMatchObject(p256DidJwkEyJjcnYi0iFixture) + }) +}) diff --git a/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidRegistrar.test.ts b/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidRegistrar.test.ts new file mode 100644 index 0000000000..da69ad0234 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidRegistrar.test.ts @@ -0,0 +1,192 @@ +import type { Wallet } from '../../../../../wallet' + +import { getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { KeyType } from '../../../../../crypto' +import { getJwkFromJson } from '../../../../../crypto/jose/jwk' +import { TypedArrayEncoder } from '../../../../../utils' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { WalletError } from '../../../../../wallet/error' +import { DidDocumentRole } from '../../../domain/DidDocumentRole' +import { DidRepository } from '../../../repository/DidRepository' +import { JwkDidRegistrar } from '../JwkDidRegistrar' + +jest.mock('../../../repository/DidRepository') +const DidRepositoryMock = DidRepository as jest.Mock + +const jwk = getJwkFromJson({ + crv: 'P-256', + kty: 'EC', + x: 'acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0', + y: '_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE', +}) +const walletMock = { + createKey: jest.fn(() => jwk.key), +} as unknown as Wallet + +const didRepositoryMock = new DidRepositoryMock() +const jwkDidRegistrar = new JwkDidRegistrar() + +const agentContext = getAgentContext({ + wallet: walletMock, + registerInstances: [[DidRepository, didRepositoryMock]], +}) + +describe('DidRegistrar', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('JwkDidRegistrar', () => { + it('should correctly create a did:jwk document using P256 key type', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + + const result = await jwkDidRegistrar.create(agentContext, { + method: 'jwk', + options: { + keyType: KeyType.P256, + }, + secret: { + privateKey, + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9', + didDocument: { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9', + verificationMethod: [ + { + id: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + type: 'JsonWebKey2020', + controller: + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9', + publicKeyJwk: { + crv: 'P-256', + kty: 'EC', + x: 'acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0', + y: '_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE', + }, + }, + ], + assertionMethod: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + authentication: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + capabilityInvocation: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + capabilityDelegation: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + keyAgreement: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + }, + secret: { + privateKey, + }, + }, + }) + + expect(walletMock.createKey).toHaveBeenCalledWith({ keyType: KeyType.P256, privateKey }) + }) + + it('should return an error state if no key type is provided', async () => { + const result = await jwkDidRegistrar.create(agentContext, { + method: 'jwk', + // @ts-expect-error - key type is required in interface + options: {}, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing key type', + }, + }) + }) + + it('should return an error state if a key creation error is thrown', async () => { + mockFunction(walletMock.createKey).mockRejectedValueOnce(new WalletError('Invalid private key provided')) + const result = await jwkDidRegistrar.create(agentContext, { + method: 'jwk', + options: { + keyType: KeyType.P256, + }, + secret: { + privateKey: TypedArrayEncoder.fromString('invalid'), + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: expect.stringContaining('Invalid private key provided'), + }, + }) + }) + + it('should store the did document', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + const did = + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9' + + await jwkDidRegistrar.create(agentContext, { + method: 'jwk', + + options: { + keyType: KeyType.P256, + }, + secret: { + privateKey, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + did, + role: DidDocumentRole.Created, + didDocument: undefined, + }) + }) + + it('should return an error state when calling update', async () => { + const result = await jwkDidRegistrar.update() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot update did:jwk did`, + }, + }) + }) + + it('should return an error state when calling deactivate', async () => { + const result = await jwkDidRegistrar.deactivate() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot deactivate did:jwk did`, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidResolver.test.ts b/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidResolver.test.ts new file mode 100644 index 0000000000..28dc34d497 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidResolver.test.ts @@ -0,0 +1,34 @@ +import type { AgentContext } from '../../../../../agent' + +import { getAgentContext } from '../../../../../../tests/helpers' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { DidJwk } from '../DidJwk' +import { JwkDidResolver } from '../JwkDidResolver' + +import { p256DidJwkEyJjcnYi0iFixture } from './__fixtures__/p256DidJwkEyJjcnYi0i' + +describe('DidResolver', () => { + describe('JwkDidResolver', () => { + let keyDidResolver: JwkDidResolver + let agentContext: AgentContext + + beforeEach(() => { + keyDidResolver = new JwkDidResolver() + agentContext = getAgentContext() + }) + + it('should correctly resolve a did:jwk document', async () => { + const fromDidSpy = jest.spyOn(DidJwk, 'fromDid') + const result = await keyDidResolver.resolve(agentContext, p256DidJwkEyJjcnYi0iFixture.id) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: p256DidJwkEyJjcnYi0iFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + }) + expect(result.didDocument) + expect(fromDidSpy).toHaveBeenCalledTimes(1) + expect(fromDidSpy).toHaveBeenCalledWith(p256DidJwkEyJjcnYi0iFixture.id) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/jwk/__tests__/__fixtures__/p256DidJwkEyJjcnYi0i.ts b/packages/core/src/modules/dids/methods/jwk/__tests__/__fixtures__/p256DidJwkEyJjcnYi0i.ts new file mode 100644 index 0000000000..f042e88502 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/__tests__/__fixtures__/p256DidJwkEyJjcnYi0i.ts @@ -0,0 +1,33 @@ +export const p256DidJwkEyJjcnYi0iFixture = { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9', + verificationMethod: [ + { + id: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + type: 'JsonWebKey2020', + controller: + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9', + publicKeyJwk: { + kty: 'EC', + crv: 'P-256', + x: 'acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0', + y: '_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE', + }, + }, + ], + assertionMethod: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + authentication: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + capabilityInvocation: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + capabilityDelegation: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + keyAgreement: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], +} as const diff --git a/packages/core/src/modules/dids/methods/jwk/__tests__/__fixtures__/x25519DidJwkEyJrdHkiOiJ.ts b/packages/core/src/modules/dids/methods/jwk/__tests__/__fixtures__/x25519DidJwkEyJrdHkiOiJ.ts new file mode 100644 index 0000000000..dba397342f --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/__tests__/__fixtures__/x25519DidJwkEyJrdHkiOiJ.ts @@ -0,0 +1,21 @@ +export const x25519DidJwkEyJrdHkiOiJFixture = { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9', + verificationMethod: [ + { + id: 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0', + type: 'JsonWebKey2020', + controller: + 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9', + publicKeyJwk: { + kty: 'OKP', + crv: 'X25519', + use: 'enc', + x: '3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08', + }, + }, + ], + keyAgreement: [ + 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0', + ], +} as const diff --git a/packages/core/src/modules/dids/methods/jwk/didJwkDidDocument.ts b/packages/core/src/modules/dids/methods/jwk/didJwkDidDocument.ts new file mode 100644 index 0000000000..b5b5087ba1 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/didJwkDidDocument.ts @@ -0,0 +1,40 @@ +import type { DidJwk } from './DidJwk' + +import { CredoError } from '../../../../error' +import { JsonEncoder } from '../../../../utils' +import { SECURITY_JWS_CONTEXT_URL } from '../../../vc/constants' +import { getJsonWebKey2020, DidDocumentBuilder } from '../../domain' +import { parseDid } from '../../domain/parse' + +export function getDidJwkDocument(didJwk: DidJwk) { + if (!didJwk.allowsEncrypting && !didJwk.allowsSigning) { + throw new CredoError('At least one of allowsSigning or allowsEncrypting must be enabled') + } + + const parsed = parseDid(didJwk.did) + const jwkJson = JsonEncoder.fromBase64(parsed.id) + + const verificationMethod = getJsonWebKey2020({ + did: didJwk.did, + jwk: jwkJson, + verificationMethodId: didJwk.verificationMethodId, + }) + + const didDocumentBuilder = new DidDocumentBuilder(didJwk.did) + .addContext(SECURITY_JWS_CONTEXT_URL) + .addVerificationMethod(verificationMethod) + + if (didJwk.allowsSigning) { + didDocumentBuilder + .addAuthentication(verificationMethod.id) + .addAssertionMethod(verificationMethod.id) + .addCapabilityDelegation(verificationMethod.id) + .addCapabilityInvocation(verificationMethod.id) + } + + if (didJwk.allowsEncrypting) { + didDocumentBuilder.addKeyAgreement(verificationMethod.id) + } + + return didDocumentBuilder.build() +} diff --git a/packages/core/src/modules/dids/methods/jwk/index.ts b/packages/core/src/modules/dids/methods/jwk/index.ts new file mode 100644 index 0000000000..e377f85f95 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/index.ts @@ -0,0 +1,3 @@ +export { DidJwk } from './DidJwk' +export * from './JwkDidRegistrar' +export * from './JwkDidResolver' diff --git a/packages/core/src/modules/dids/methods/key/DidKey.ts b/packages/core/src/modules/dids/methods/key/DidKey.ts new file mode 100644 index 0000000000..fb377d63c0 --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/DidKey.ts @@ -0,0 +1,26 @@ +import { Key } from '../../../../crypto/Key' +import { getDidDocumentForKey } from '../../domain/keyDidDocument' +import { parseDid } from '../../domain/parse' + +export class DidKey { + public readonly key: Key + + public constructor(key: Key) { + this.key = key + } + + public static fromDid(did: string) { + const parsed = parseDid(did) + + const key = Key.fromFingerprint(parsed.id) + return new DidKey(key) + } + + public get did() { + return `did:key:${this.key.fingerprint}` + } + + public get didDocument() { + return getDidDocumentForKey(this.did, this.key) + } +} diff --git a/packages/core/src/modules/dids/methods/key/KeyDidRegistrar.ts b/packages/core/src/modules/dids/methods/key/KeyDidRegistrar.ts new file mode 100644 index 0000000000..d4c8c239c9 --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/KeyDidRegistrar.ts @@ -0,0 +1,118 @@ +import type { AgentContext } from '../../../../agent' +import type { KeyType } from '../../../../crypto' +import type { Buffer } from '../../../../utils' +import type { DidRegistrar } from '../../domain/DidRegistrar' +import type { DidCreateOptions, DidCreateResult, DidDeactivateResult, DidUpdateResult } from '../../types' + +import { DidDocumentRole } from '../../domain/DidDocumentRole' +import { DidRepository, DidRecord } from '../../repository' + +import { DidKey } from './DidKey' + +export class KeyDidRegistrar implements DidRegistrar { + public readonly supportedMethods = ['key'] + + public async create(agentContext: AgentContext, options: KeyDidCreateOptions): Promise { + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + + const keyType = options.options.keyType + const seed = options.secret?.seed + const privateKey = options.secret?.privateKey + + if (!keyType) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing key type', + }, + } + } + + try { + const key = await agentContext.wallet.createKey({ + keyType, + seed, + privateKey, + }) + + const didKey = new DidKey(key) + + // Save the did so we know we created it and can issue with it + const didRecord = new DidRecord({ + did: didKey.did, + role: DidDocumentRole.Created, + }) + await didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: didKey.did, + didDocument: didKey.didDocument, + secret: { + // FIXME: the uni-registrar creates the seed in the registrar method + // if it doesn't exist so the seed can always be returned. Currently + // we can only return it if the seed was passed in by the user. Once + // we have a secure method for generating seeds we should use the same + // approach + seed: options.secret?.seed, + privateKey: options.secret?.privateKey, + }, + }, + } + } catch (error) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot update did:key did`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot deactivate did:key did`, + }, + } + } +} + +export interface KeyDidCreateOptions extends DidCreateOptions { + method: 'key' + // For now we don't support creating a did:key with a did or did document + did?: never + didDocument?: never + options: { + keyType: KeyType + } + secret?: { + seed?: Buffer + privateKey?: Buffer + } +} + +// Update and Deactivate not supported for did:key +export type KeyDidUpdateOptions = never +export type KeyDidDeactivateOptions = never diff --git a/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts b/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts new file mode 100644 index 0000000000..2f21721d65 --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts @@ -0,0 +1,43 @@ +import type { AgentContext } from '../../../../agent' +import type { DidResolver } from '../../domain/DidResolver' +import type { DidResolutionResult } from '../../types' + +import { DidKey } from './DidKey' + +export class KeyDidResolver implements DidResolver { + public readonly supportedMethods = ['key'] + + /** + * No remote resolving done, did document is dynamically constructed. To not pollute the cache we don't allow caching + */ + public readonly allowsCaching = false + + /** + * Easier to calculate for resolving than serving the local did document. Record also doesn't + * have a did document + */ + public readonly allowsLocalDidRecord = false + + public async resolve(agentContext: AgentContext, did: string): Promise { + const didDocumentMetadata = {} + + try { + const didDocument = DidKey.fromDid(did).didDocument + + return { + didDocument, + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } +} diff --git a/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts b/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts new file mode 100644 index 0000000000..5994e3baeb --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts @@ -0,0 +1,42 @@ +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import didKeyBls12381g1 from '../../../__tests__/__fixtures__/didKeyBls12381g1.json' +import didKeyBls12381g1g2 from '../../../__tests__/__fixtures__/didKeyBls12381g1g2.json' +import didKeyBls12381g2 from '../../../__tests__/__fixtures__/didKeyBls12381g2.json' +import didKeyEd25519 from '../../../__tests__/__fixtures__/didKeyEd25519.json' +import didKeyK256 from '../../../__tests__/__fixtures__/didKeyK256.json' +import didKeyP256 from '../../../__tests__/__fixtures__/didKeyP256.json' +import didKeyP384 from '../../../__tests__/__fixtures__/didKeyP384.json' +import didKeyP521 from '../../../__tests__/__fixtures__/didKeyP521.json' +import didKeyX25519 from '../../../__tests__/__fixtures__/didKeyX25519.json' +import { DidKey } from '../DidKey' + +describe('DidKey', () => { + it('creates a DidKey instance from a did', async () => { + const documentTypes = [ + didKeyX25519, + didKeyEd25519, + didKeyBls12381g1, + didKeyBls12381g2, + didKeyBls12381g1g2, + didKeyP256, + didKeyP384, + didKeyP521, + didKeyK256, + ] + + for (const documentType of documentTypes) { + const didKey = DidKey.fromDid(documentType.id) + + expect(didKey.didDocument.toJSON()).toMatchObject(documentType) + } + }) + + it('creates a DidKey instance from a key instance', async () => { + const key = Key.fromPublicKeyBase58(didKeyX25519.keyAgreement[0].publicKeyBase58, KeyType.X25519) + const didKey = new DidKey(key) + + expect(didKey.did).toBe(didKeyX25519.id) + expect(didKey.didDocument.toJSON()).toMatchObject(didKeyX25519) + }) +}) diff --git a/packages/core/src/modules/dids/methods/key/__tests__/KeyDidRegistrar.test.ts b/packages/core/src/modules/dids/methods/key/__tests__/KeyDidRegistrar.test.ts new file mode 100644 index 0000000000..d3bf481409 --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/__tests__/KeyDidRegistrar.test.ts @@ -0,0 +1,155 @@ +import type { Wallet } from '../../../../../wallet' + +import { getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { TypedArrayEncoder } from '../../../../../utils' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { WalletError } from '../../../../../wallet/error' +import { DidDocumentRole } from '../../../domain/DidDocumentRole' +import { DidRepository } from '../../../repository/DidRepository' +import { KeyDidRegistrar } from '../KeyDidRegistrar' + +import didKeyz6MksLeFixture from './__fixtures__/didKeyz6MksLe.json' + +jest.mock('../../../repository/DidRepository') +const DidRepositoryMock = DidRepository as jest.Mock + +const walletMock = { + createKey: jest.fn(() => Key.fromFingerprint('z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU')), +} as unknown as Wallet + +const didRepositoryMock = new DidRepositoryMock() +const keyDidRegistrar = new KeyDidRegistrar() + +const agentContext = getAgentContext({ + wallet: walletMock, + registerInstances: [[DidRepository, didRepositoryMock]], +}) + +describe('DidRegistrar', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('KeyDidRegistrar', () => { + it('should correctly create a did:key document using Ed25519 key type', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + + const result = await keyDidRegistrar.create(agentContext, { + method: 'key', + options: { + keyType: KeyType.Ed25519, + }, + secret: { + privateKey, + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU', + didDocument: didKeyz6MksLeFixture, + secret: { + privateKey, + }, + }, + }) + + expect(walletMock.createKey).toHaveBeenCalledWith({ keyType: KeyType.Ed25519, privateKey }) + }) + + it('should return an error state if no key type is provided', async () => { + const result = await keyDidRegistrar.create(agentContext, { + method: 'key', + // @ts-expect-error - key type is required in interface + options: {}, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing key type', + }, + }) + }) + + it('should return an error state if a key creation error is thrown', async () => { + mockFunction(walletMock.createKey).mockRejectedValueOnce(new WalletError('Invalid private key provided')) + const result = await keyDidRegistrar.create(agentContext, { + method: 'key', + options: { + keyType: KeyType.Ed25519, + }, + secret: { + privateKey: TypedArrayEncoder.fromString('invalid'), + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: expect.stringContaining('Invalid private key provided'), + }, + }) + }) + + it('should store the did document', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + const did = 'did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU' + + await keyDidRegistrar.create(agentContext, { + method: 'key', + + options: { + keyType: KeyType.Ed25519, + }, + secret: { + privateKey, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + did, + role: DidDocumentRole.Created, + didDocument: undefined, + }) + }) + + it('should return an error state when calling update', async () => { + const result = await keyDidRegistrar.update() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot update did:key did`, + }, + }) + }) + + it('should return an error state when calling deactivate', async () => { + const result = await keyDidRegistrar.deactivate() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot deactivate did:key did`, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/key/__tests__/KeyDidResolver.test.ts b/packages/core/src/modules/dids/methods/key/__tests__/KeyDidResolver.test.ts new file mode 100644 index 0000000000..08157cbdcb --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/__tests__/KeyDidResolver.test.ts @@ -0,0 +1,68 @@ +import type { AgentContext } from '../../../../../agent' + +import { getAgentContext } from '../../../../../../tests/helpers' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import didKeyEd25519Fixture from '../../../__tests__/__fixtures__/didKeyEd25519.json' +import { DidKey } from '../DidKey' +import { KeyDidResolver } from '../KeyDidResolver' + +describe('DidResolver', () => { + describe('KeyDidResolver', () => { + let keyDidResolver: KeyDidResolver + let agentContext: AgentContext + + beforeEach(() => { + keyDidResolver = new KeyDidResolver() + agentContext = getAgentContext() + }) + + it('should correctly resolve a did:key document', async () => { + const fromDidSpy = jest.spyOn(DidKey, 'fromDid') + const result = await keyDidResolver.resolve( + agentContext, + 'did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th' + ) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didKeyEd25519Fixture, + didDocumentMetadata: {}, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + }) + expect(result.didDocument) + expect(fromDidSpy).toHaveBeenCalledTimes(1) + expect(fromDidSpy).toHaveBeenCalledWith('did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th') + }) + + it('should return did resolution metadata with error if the did contains an unsupported multibase', async () => { + const result = await keyDidResolver.resolve( + agentContext, + 'did:key:asdfkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th' + ) + + expect(result).toEqual({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did 'did:key:asdfkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th': Error: No decoder found for multibase prefix 'a'`, + }, + }) + }) + + it('should return did resolution metadata with error if the did contains an unsupported multibase', async () => { + const result = await keyDidResolver.resolve( + agentContext, + 'did:key:z6MkmjYasdfasfd8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th' + ) + + expect(result).toEqual({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did 'did:key:z6MkmjYasdfasfd8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th': Error: Unsupported key type from multicodec code '107'`, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/key/__tests__/__fixtures__/didKeyz6MksLe.json b/packages/core/src/modules/dids/methods/key/__tests__/__fixtures__/didKeyz6MksLe.json new file mode 100644 index 0000000000..4182e6d1ff --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/__tests__/__fixtures__/didKeyz6MksLe.json @@ -0,0 +1,36 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "verificationMethod": [ + { + "id": "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "type": "Ed25519VerificationKey2018", + "controller": "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "publicKeyBase58": "DtPcLpky6Yi6zPecfW8VZH3xNoDkvQfiGWp8u5n9nAj6" + } + ], + "authentication": [ + "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "assertionMethod": [ + "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "keyAgreement": [ + { + "id": "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz", + "type": "X25519KeyAgreementKey2019", + "controller": "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "publicKeyBase58": "7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE" + } + ], + "capabilityInvocation": [ + "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "capabilityDelegation": [ + "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "id": "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" +} diff --git a/packages/core/src/modules/dids/methods/key/index.ts b/packages/core/src/modules/dids/methods/key/index.ts new file mode 100644 index 0000000000..3c5ea1244d --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/index.ts @@ -0,0 +1,3 @@ +export { DidKey } from './DidKey' +export * from './KeyDidRegistrar' +export * from './KeyDidResolver' diff --git a/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts b/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts new file mode 100644 index 0000000000..b1e9172dd9 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts @@ -0,0 +1,227 @@ +import type { AgentContext } from '../../../../agent' +import type { KeyType } from '../../../../crypto' +import type { Buffer } from '../../../../utils' +import type { DidRegistrar } from '../../domain/DidRegistrar' +import type { DidCreateOptions, DidCreateResult, DidDeactivateResult, DidUpdateResult } from '../../types' + +import { JsonTransformer } from '../../../../utils' +import { DidDocument } from '../../domain' +import { DidDocumentRole } from '../../domain/DidDocumentRole' +import { DidRepository, DidRecord } from '../../repository' + +import { PeerDidNumAlgo, getAlternativeDidsForPeerDid } from './didPeer' +import { keyToNumAlgo0DidDocument } from './peerDidNumAlgo0' +import { didDocumentJsonToNumAlgo1Did } from './peerDidNumAlgo1' +import { didDocumentToNumAlgo2Did } from './peerDidNumAlgo2' +import { didDocumentToNumAlgo4Did } from './peerDidNumAlgo4' + +export class PeerDidRegistrar implements DidRegistrar { + public readonly supportedMethods = ['peer'] + + public async create( + agentContext: AgentContext, + options: + | PeerDidNumAlgo0CreateOptions + | PeerDidNumAlgo1CreateOptions + | PeerDidNumAlgo2CreateOptions + | PeerDidNumAlgo4CreateOptions + ): Promise { + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + + let did: string + let didDocument: DidDocument + + try { + if (isPeerDidNumAlgo0CreateOptions(options)) { + const keyType = options.options.keyType + const seed = options.secret?.seed + const privateKey = options.secret?.privateKey + + if (!keyType) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing key type', + }, + } + } + + const key = await agentContext.wallet.createKey({ + keyType, + seed, + privateKey, + }) + + // TODO: validate did:peer document + + didDocument = keyToNumAlgo0DidDocument(key) + did = didDocument.id + } else if (isPeerDidNumAlgo1CreateOptions(options)) { + const didDocumentJson = options.didDocument.toJSON() + did = didDocumentJsonToNumAlgo1Did(didDocumentJson) + + didDocument = JsonTransformer.fromJSON({ ...didDocumentJson, id: did }, DidDocument) + } else if (isPeerDidNumAlgo2CreateOptions(options)) { + const didDocumentJson = options.didDocument.toJSON() + did = didDocumentToNumAlgo2Did(options.didDocument) + + didDocument = JsonTransformer.fromJSON({ ...didDocumentJson, id: did }, DidDocument) + } else if (isPeerDidNumAlgo4CreateOptions(options)) { + const didDocumentJson = options.didDocument.toJSON() + + const { longFormDid, shortFormDid } = didDocumentToNumAlgo4Did(options.didDocument) + + did = longFormDid + didDocument = JsonTransformer.fromJSON( + { ...didDocumentJson, id: longFormDid, alsoKnownAs: [shortFormDid] }, + DidDocument + ) + } else { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `Missing or incorrect numAlgo provided`, + }, + } + } + + // Save the did so we know we created it and can use it for didcomm + const didRecord = new DidRecord({ + did, + role: DidDocumentRole.Created, + didDocument: isPeerDidNumAlgo1CreateOptions(options) ? didDocument : undefined, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + alternativeDids: getAlternativeDidsForPeerDid(did), + }, + }) + await didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: didDocument.id, + didDocument, + secret: { + // FIXME: the uni-registrar creates the seed in the registrar method + // if it doesn't exist so the seed can always be returned. Currently + // we can only return it if the seed was passed in by the user. Once + // we have a secure method for generating seeds we should use the same + // approach + seed: options.secret?.seed, + privateKey: options.secret?.privateKey, + }, + }, + } + } catch (error) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `unknown error: ${error.message}`, + }, + } + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:peer not implemented yet`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:peer not implemented yet`, + }, + } + } +} + +function isPeerDidNumAlgo1CreateOptions(options: PeerDidCreateOptions): options is PeerDidNumAlgo1CreateOptions { + return options.options.numAlgo === PeerDidNumAlgo.GenesisDoc +} + +function isPeerDidNumAlgo0CreateOptions(options: PeerDidCreateOptions): options is PeerDidNumAlgo0CreateOptions { + return options.options.numAlgo === PeerDidNumAlgo.InceptionKeyWithoutDoc +} + +function isPeerDidNumAlgo2CreateOptions(options: PeerDidCreateOptions): options is PeerDidNumAlgo2CreateOptions { + return options.options.numAlgo === PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc +} + +function isPeerDidNumAlgo4CreateOptions(options: PeerDidCreateOptions): options is PeerDidNumAlgo4CreateOptions { + return options.options.numAlgo === PeerDidNumAlgo.ShortFormAndLongForm +} + +export type PeerDidCreateOptions = + | PeerDidNumAlgo0CreateOptions + | PeerDidNumAlgo1CreateOptions + | PeerDidNumAlgo2CreateOptions + | PeerDidNumAlgo4CreateOptions + +export interface PeerDidNumAlgo0CreateOptions extends DidCreateOptions { + method: 'peer' + did?: never + didDocument?: never + options: { + keyType: KeyType.Ed25519 + numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc + } + secret?: { + seed?: Buffer + privateKey?: Buffer + } +} + +export interface PeerDidNumAlgo1CreateOptions extends DidCreateOptions { + method: 'peer' + did?: never + didDocument: DidDocument + options: { + numAlgo: PeerDidNumAlgo.GenesisDoc + } + secret?: undefined +} + +export interface PeerDidNumAlgo2CreateOptions extends DidCreateOptions { + method: 'peer' + did?: never + didDocument: DidDocument + options: { + numAlgo: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc + } + secret?: undefined +} + +export interface PeerDidNumAlgo4CreateOptions extends DidCreateOptions { + method: 'peer' + did?: never + didDocument: DidDocument + options: { + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm + } + secret?: undefined +} + +// Update and Deactivate not supported for did:peer +export type PeerDidUpdateOptions = never +export type PeerDidDeactivateOptions = never diff --git a/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts b/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts new file mode 100644 index 0000000000..1018becba2 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts @@ -0,0 +1,96 @@ +import type { AgentContext } from '../../../../agent' +import type { DidDocument } from '../../domain' +import type { DidResolver } from '../../domain/DidResolver' +import type { DidResolutionResult } from '../../types' + +import { CredoError } from '../../../../error' +import { DidRepository } from '../../repository' + +import { getNumAlgoFromPeerDid, isValidPeerDid, PeerDidNumAlgo } from './didPeer' +import { didToNumAlgo0DidDocument } from './peerDidNumAlgo0' +import { didToNumAlgo2DidDocument } from './peerDidNumAlgo2' +import { didToNumAlgo4DidDocument, isShortFormDidPeer4 } from './peerDidNumAlgo4' + +export class PeerDidResolver implements DidResolver { + public readonly supportedMethods = ['peer'] + + /** + * No remote resolving done, did document is fetched from storage. To not pollute the cache we don't allow caching + */ + public readonly allowsCaching = false + + /** + * Did peer records are often server from local did doucment, but it's easier to handle it in + * the peer did resolver. + */ + public readonly allowsLocalDidRecord = false + + public async resolve(agentContext: AgentContext, did: string): Promise { + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + + const didDocumentMetadata = {} + + try { + let didDocument: DidDocument + + if (!isValidPeerDid(did)) { + throw new CredoError(`did ${did} is not a valid peer did`) + } + + const numAlgo = getNumAlgoFromPeerDid(did) + + // For method 0, generate from did + if (numAlgo === PeerDidNumAlgo.InceptionKeyWithoutDoc) { + didDocument = didToNumAlgo0DidDocument(did) + } + // For Method 1, retrieve from storage + else if (numAlgo === PeerDidNumAlgo.GenesisDoc) { + // We can have multiple did document records stored for a single did (one created and one received). In this case it + // doesn't matter which one we use, and they should be identical. So we just take the first one. + const [didDocumentRecord] = await didRepository.findAllByDid(agentContext, did) + + if (!didDocumentRecord) { + throw new CredoError(`No did record found for peer did ${did}.`) + } + + if (!didDocumentRecord.didDocument) { + throw new CredoError(`Found did record for method 1 peer did (${did}), but no did document.`) + } + + didDocument = didDocumentRecord.didDocument + } + // For Method 2, generate from did + else if (numAlgo === PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc) { + didDocument = didToNumAlgo2DidDocument(did) + } + // For Method 4, if short form is received, attempt to get the didDocument from stored record + else { + if (isShortFormDidPeer4(did)) { + const [didRecord] = await didRepository.findAllByDid(agentContext, did) + + if (!didRecord) { + throw new CredoError(`No did record found for peer did ${did}.`) + } + didDocument = didToNumAlgo4DidDocument(didRecord.did) + } else { + didDocument = didToNumAlgo4DidDocument(did) + } + } + + return { + didDocument, + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts new file mode 100644 index 0000000000..160860bf03 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts @@ -0,0 +1,58 @@ +import { isValidPeerDid, getNumAlgoFromPeerDid, PeerDidNumAlgo } from '../didPeer' + +describe('didPeer', () => { + test('isValidPeerDid', () => { + expect(isValidPeerDid('did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL')).toBe(true) + expect(isValidPeerDid('did:peer:1zQmZMygzYqNwU6Uhmewx5Xepf2VLp5S4HLSwwgf2aiKZuwa')).toBe(true) + expect( + isValidPeerDid( + 'did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' + ) + ).toBe(true) + expect( + isValidPeerDid( + 'did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn:z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + ).toBe(true) + expect(isValidPeerDid('did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn')).toBe(true) + expect( + isValidPeerDid( + 'did:peer:4z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + ).toBe(false) + + expect( + isValidPeerDid( + 'did:peer:5.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' + ) + ).toBe(false) + }) + + describe('getNumAlgoFromPeerDid', () => { + test('extracts the numAlgo from the peer did', async () => { + // NumAlgo 0 + expect(getNumAlgoFromPeerDid('did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL')).toBe( + PeerDidNumAlgo.InceptionKeyWithoutDoc + ) + + // NumAlgo 1 + expect(getNumAlgoFromPeerDid('did:peer:1zQmZMygzYqNwU6Uhmewx5Xepf2VLp5S4HLSwwgf2aiKZuwa')).toBe( + PeerDidNumAlgo.GenesisDoc + ) + + // NumAlgo 2 + expect( + getNumAlgoFromPeerDid( + 'did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' + ) + ).toBe(PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc) + + // NumAlgo 4 + expect( + getNumAlgoFromPeerDid( + 'did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn:z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + ).toBe(PeerDidNumAlgo.ShortFormAndLongForm) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts new file mode 100644 index 0000000000..5201143ddc --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts @@ -0,0 +1,462 @@ +import type { Wallet } from '../../../../../wallet' + +import { getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { TypedArrayEncoder } from '../../../../../utils' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { WalletError } from '../../../../../wallet/error' +import { DidCommV1Service, DidDocumentBuilder, getEd25519VerificationKey2018 } from '../../../domain' +import { DidDocumentRole } from '../../../domain/DidDocumentRole' +import { DidRepository } from '../../../repository/DidRepository' +import { PeerDidRegistrar } from '../PeerDidRegistrar' +import { PeerDidNumAlgo } from '../didPeer' + +import didPeer0z6MksLeFixture from './__fixtures__/didPeer0z6MksLe.json' + +jest.mock('../../../repository/DidRepository') +const DidRepositoryMock = DidRepository as jest.Mock + +const walletMock = { + createKey: jest.fn(() => Key.fromFingerprint('z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU')), +} as unknown as Wallet +const didRepositoryMock = new DidRepositoryMock() + +const agentContext = getAgentContext({ wallet: walletMock, registerInstances: [[DidRepository, didRepositoryMock]] }) +const peerDidRegistrar = new PeerDidRegistrar() + +describe('DidRegistrar', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('PeerDidRegistrar', () => { + describe('did:peer:0', () => { + it('should correctly create a did:peer:0 document using Ed25519 key type', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + options: { + keyType: KeyType.Ed25519, + numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, + }, + secret: { + privateKey, + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU', + didDocument: didPeer0z6MksLeFixture, + secret: { + privateKey, + }, + }, + }) + }) + + it('should return an error state if no key type is provided', async () => { + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + // @ts-expect-error - key type is required in interface + options: { + numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing key type', + }, + }) + }) + + it('should return an error state if a key creation error is thrown', async () => { + mockFunction(walletMock.createKey).mockRejectedValueOnce(new WalletError('Invalid private key provided')) + + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + options: { + keyType: KeyType.Ed25519, + numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, + }, + secret: { + privateKey: TypedArrayEncoder.fromString('invalid'), + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: expect.stringContaining('Invalid private key provided'), + }, + }) + }) + + it('should store the did without the did document', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + const did = 'did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU' + + await peerDidRegistrar.create(agentContext, { + method: 'peer', + options: { + keyType: KeyType.Ed25519, + numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, + }, + secret: { + privateKey, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + did: did, + role: DidDocumentRole.Created, + _tags: { + recipientKeyFingerprints: [], + }, + didDocument: undefined, + }) + }) + }) + + describe('did:peer:1', () => { + const verificationMethod = getEd25519VerificationKey2018({ + key: Key.fromFingerprint('z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz'), + // controller in method 1 did should be #id + controller: '#id', + id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + }) + + const didDocument = new DidDocumentBuilder('') + .addVerificationMethod(verificationMethod) + .addAuthentication(verificationMethod.id) + .addService( + new DidCommV1Service({ + id: '#service-0', + recipientKeys: [verificationMethod.id], + serviceEndpoint: 'https://example.com', + accept: ['didcomm/aip2;env=rfc19'], + }) + ) + .build() + + it('should correctly create a did:peer:1 document from a did document', async () => { + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + didDocument: didDocument, + options: { + numAlgo: PeerDidNumAlgo.GenesisDoc, + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:peer:1zQmUTNcSy2J2sAmX6Ad2bdPvhVnHPUaod8Skpt8DWPpZaiL', + didDocument: { + '@context': ['https://w3id.org/did/v1'], + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + type: 'Ed25519VerificationKey2018', + controller: '#id', + publicKeyBase58: '7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE', + }, + ], + service: [ + { + id: '#service-0', + serviceEndpoint: 'https://example.com', + type: 'did-communication', + priority: 0, + recipientKeys: ['#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16'], + accept: ['didcomm/aip2;env=rfc19'], + }, + ], + authentication: ['#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16'], + assertionMethod: undefined, + keyAgreement: undefined, + capabilityInvocation: undefined, + capabilityDelegation: undefined, + id: 'did:peer:1zQmUTNcSy2J2sAmX6Ad2bdPvhVnHPUaod8Skpt8DWPpZaiL', + }, + }, + }) + }) + + it('should store the did with the did document', async () => { + const did = 'did:peer:1zQmUTNcSy2J2sAmX6Ad2bdPvhVnHPUaod8Skpt8DWPpZaiL' + + const { didState } = await peerDidRegistrar.create(agentContext, { + method: 'peer', + didDocument, + options: { + numAlgo: PeerDidNumAlgo.GenesisDoc, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + did: did, + didDocument: didState.didDocument, + role: DidDocumentRole.Created, + _tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + }) + }) + }) + + describe('did:peer:2', () => { + const key = Key.fromFingerprint('z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz') + const verificationMethod = getEd25519VerificationKey2018({ + key, + // controller in method 1 did should be #id + controller: '#id', + // Use relative id for peer dids with pattern 'key-N' + id: '#key-1', + }) + + const didDocument = new DidDocumentBuilder('') + .addVerificationMethod(verificationMethod) + .addAuthentication(verificationMethod.id) + .addService( + new DidCommV1Service({ + id: '#service-0', + recipientKeys: [verificationMethod.id], + serviceEndpoint: 'https://example.com', + accept: ['didcomm/aip2;env=rfc19'], + }) + ) + .build() + + it('should correctly create a did:peer:2 document from a did document', async () => { + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + didDocument: didDocument, + options: { + numAlgo: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:peer:2.Vz6MkkjPVCX7M8D6jJSCQNzYb4T6giuSN8Fm463gWNZ65DMSc.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbIiNrZXktMSJdLCJhIjpbImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXX0', + didDocument: { + '@context': ['https://w3id.org/did/v1'], + id: 'did:peer:2.Vz6MkkjPVCX7M8D6jJSCQNzYb4T6giuSN8Fm463gWNZ65DMSc.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbIiNrZXktMSJdLCJhIjpbImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXX0', + service: [ + { + serviceEndpoint: 'https://example.com', + type: 'did-communication', + priority: 0, + recipientKeys: ['#key-1'], + accept: ['didcomm/aip2;env=rfc19'], + id: '#service-0', + }, + ], + verificationMethod: [ + { + id: '#key-1', + type: 'Ed25519VerificationKey2018', + controller: '#id', + publicKeyBase58: '7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE', + }, + ], + authentication: ['#key-1'], + }, + secret: {}, + }, + }) + }) + + it('should store the did without the did document', async () => { + const did = + 'did:peer:2.Vz6MkkjPVCX7M8D6jJSCQNzYb4T6giuSN8Fm463gWNZ65DMSc.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbIiNrZXktMSJdLCJhIjpbImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXX0' + + await peerDidRegistrar.create(agentContext, { + method: 'peer', + didDocument, + options: { + numAlgo: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + did: did, + role: DidDocumentRole.Created, + _tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + didDocument: undefined, + }) + }) + }) + + describe('did:peer:4', () => { + const key = Key.fromFingerprint('z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz') + const verificationMethod = getEd25519VerificationKey2018({ + key, + controller: '#id', + // Use relative id for peer dids + id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + }) + + const didDocument = new DidDocumentBuilder('') + .addVerificationMethod(verificationMethod) + .addAuthentication(verificationMethod.id) + .addService( + new DidCommV1Service({ + id: '#service-0', + recipientKeys: [verificationMethod.id], + serviceEndpoint: 'https://example.com', + accept: ['didcomm/aip2;env=rfc19'], + }) + ) + .build() + + it('should correctly create a did:peer:4 document from a did document', async () => { + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + didDocument: didDocument, + options: { + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + }, + }) + + const longFormDid = + 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e' + const shortFormDid = 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4' + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: longFormDid, + didDocument: { + '@context': ['https://w3id.org/did/v1'], + id: longFormDid, + alsoKnownAs: [shortFormDid], + service: [ + { + serviceEndpoint: 'https://example.com', + type: 'did-communication', + priority: 0, + recipientKeys: ['#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16'], + accept: ['didcomm/aip2;env=rfc19'], + id: '#service-0', + }, + ], + verificationMethod: [ + { + id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + type: 'Ed25519VerificationKey2018', + controller: '#id', + publicKeyBase58: '7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE', + }, + ], + authentication: ['#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16'], + }, + secret: {}, + }, + }) + }) + + it('should store the did without the did document', async () => { + const longFormDid = + 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e' + const shortFormDid = 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4' + await peerDidRegistrar.create(agentContext, { + method: 'peer', + didDocument, + options: { + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + did: longFormDid, + role: DidDocumentRole.Created, + _tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + alternativeDids: [shortFormDid], + }, + didDocument: undefined, + }) + }) + }) + + it('should return an error state if an unsupported numAlgo is provided', async () => { + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + options: { + // @ts-expect-error - this is not a valid numAlgo + numAlgo: 5, + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing or incorrect numAlgo provided', + }, + }) + }) + + it('should return an error state when calling update', async () => { + const result = await peerDidRegistrar.update() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:peer not implemented yet`, + }, + }) + }) + + it('should return an error state when calling deactivate', async () => { + const result = await peerDidRegistrar.deactivate() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:peer not implemented yet`, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer0z6MksLe.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer0z6MksLe.json new file mode 100644 index 0000000000..21142434f5 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer0z6MksLe.json @@ -0,0 +1,36 @@ +{ + "id": "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "verificationMethod": [ + { + "id": "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "type": "Ed25519VerificationKey2018", + "controller": "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "publicKeyBase58": "DtPcLpky6Yi6zPecfW8VZH3xNoDkvQfiGWp8u5n9nAj6" + } + ], + "authentication": [ + "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "assertionMethod": [ + "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "keyAgreement": [ + { + "id": "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz", + "type": "X25519KeyAgreementKey2019", + "controller": "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "publicKeyBase58": "7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE" + } + ], + "capabilityInvocation": [ + "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "capabilityDelegation": [ + "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmR-did-comm-service.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmR-did-comm-service.json new file mode 100644 index 0000000000..addf924368 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmR-did-comm-service.json @@ -0,0 +1,23 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmRYBx1pL86DrsxoJ2ZD3w42d7Ng92ErPgFsCSqg8Q1h4i", + "keyAgreement": [ + { + "id": "#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7" + } + ], + "service": [ + { + "id": "#service-0", + "type": "did-communication", + "serviceEndpoint": "https://example.com/endpoint", + "recipientKeys": ["#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V"], + "routingKeys": [ + "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH#z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH" + ], + "accept": ["didcomm/v2", "didcomm/aip2;env=rfc587"] + } + ] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmR.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmR.json new file mode 100644 index 0000000000..f20f5c2aab --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmR.json @@ -0,0 +1,32 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmXv3d2vqC2Q9JrnrFqqj5h8vzcNAumL1UZbb1TGh58j2c", + "authentication": [ + { + "id": "#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7" + }, + { + "id": "#6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "3M5RCDjPTWPkKSN3sxUmmMqHbmRPegYP1tjcKyrDbt9J" + } + ], + "keyAgreement": [ + { + "id": "#6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc", + "type": "X25519KeyAgreementKey2019", + "publicKeyBase58": "JhNWeSVLMYccCk7iopQW4guaSJTojqpMEELgSLhKwRr" + } + ], + "service": [ + { + "id": "#service-0", + "type": "DIDCommMessaging", + "serviceEndpoint": "https://example.com/endpoint", + "routingKeys": ["did:example:somemediator#somekey"], + "accept": ["didcomm/v2", "didcomm/aip2;env=rfc587"] + } + ] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmZ.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmZ.json new file mode 100644 index 0000000000..659ccf98d4 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmZ.json @@ -0,0 +1,27 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/did/v2"], + "id": "did:peer:1zQmZdT2jawCX5T1RKUB7ro83gQuiKbuHwuHi8G1NypB8BTr", + "authentication": [ + { + "id": "did:example:123456789abcdefghs#key3", + "type": "RsaVerificationKey2018", + "controller": "did:example:123456789abcdefghs", + "publicKeyHex": "02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71" + } + ], + "verificationMethod": [ + { + "id": "did:example:123456789abcdefghi#keys-1", + "type": "Secp256k1VerificationKey2018", + "controller": "did:example:123456789abcdefghi", + "publicKeyBase58": "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV" + }, + { + "id": "did:example:123456789abcdefghw#key2", + "type": "RsaVerificationKey2018", + "controller": "did:example:123456789abcdefghw", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAryQICCl6NZ5gDKrnSztO\n3Hy8PEUcuyvg/ikC+VcIo2SFFSf18a3IMYldIugqqqZCs4/4uVW3sbdLs/6PfgdX\n7O9D22ZiFWHPYA2k2N744MNiCD1UE+tJyllUhSblK48bn+v1oZHCM0nYQ2NqUkvS\nj+hwUU3RiWl7x3D2s9wSdNt7XUtW05a/FXehsPSiJfKvHJJnGOX0BgTvkLnkAOTd\nOrUZ/wK69Dzu4IvrN4vs9Nes8vbwPa/ddZEzGR0cQMt0JBkhk9kU/qwqUseP1QRJ\n5I1jR4g8aYPL/ke9K35PxZWuDp3U0UPAZ3PjFAh+5T+fc7gzCs9dPzSHloruU+gl\nFQIDAQAB\n-----END PUBLIC KEY-----" + } + ], + "created": "0001-01-01 00:00:00 +0000 UTC" +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6L.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6L.json new file mode 100644 index 0000000000..f7d141e7b3 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6L.json @@ -0,0 +1,36 @@ +{ + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJzIjp7InVyaSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyb3V0aW5nS2V5cyI6WyJkaWQ6ZXhhbXBsZTpzb21lbWVkaWF0b3Ijc29tZWtleSJdLCJhY2NlcHQiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19LCJ0IjoiZG0ifQ", + "authentication": [ + { + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJzIjp7InVyaSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyb3V0aW5nS2V5cyI6WyJkaWQ6ZXhhbXBsZTpzb21lbWVkaWF0b3Ijc29tZWtleSJdLCJhY2NlcHQiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19LCJ0IjoiZG0ifQ#key-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJzIjp7InVyaSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyb3V0aW5nS2V5cyI6WyJkaWQ6ZXhhbXBsZTpzb21lbWVkaWF0b3Ijc29tZWtleSJdLCJhY2NlcHQiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19LCJ0IjoiZG0ifQ", + "publicKeyBase58": "ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7" + }, + { + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJzIjp7InVyaSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyb3V0aW5nS2V5cyI6WyJkaWQ6ZXhhbXBsZTpzb21lbWVkaWF0b3Ijc29tZWtleSJdLCJhY2NlcHQiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19LCJ0IjoiZG0ifQ#key-3", + "type": "Ed25519VerificationKey2018", + "controller": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJzIjp7InVyaSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyb3V0aW5nS2V5cyI6WyJkaWQ6ZXhhbXBsZTpzb21lbWVkaWF0b3Ijc29tZWtleSJdLCJhY2NlcHQiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19LCJ0IjoiZG0ifQ", + "publicKeyBase58": "3M5RCDjPTWPkKSN3sxUmmMqHbmRPegYP1tjcKyrDbt9J" + } + ], + "keyAgreement": [ + { + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJzIjp7InVyaSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyb3V0aW5nS2V5cyI6WyJkaWQ6ZXhhbXBsZTpzb21lbWVkaWF0b3Ijc29tZWtleSJdLCJhY2NlcHQiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19LCJ0IjoiZG0ifQ#key-2", + "type": "X25519KeyAgreementKey2019", + "controller": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJzIjp7InVyaSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyb3V0aW5nS2V5cyI6WyJkaWQ6ZXhhbXBsZTpzb21lbWVkaWF0b3Ijc29tZWtleSJdLCJhY2NlcHQiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19LCJ0IjoiZG0ifQ", + "publicKeyBase58": "JhNWeSVLMYccCk7iopQW4guaSJTojqpMEELgSLhKwRr" + } + ], + "service": [ + { + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJzIjp7InVyaSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyb3V0aW5nS2V5cyI6WyJkaWQ6ZXhhbXBsZTpzb21lbWVkaWF0b3Ijc29tZWtleSJdLCJhY2NlcHQiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19LCJ0IjoiZG0ifQ#didcommmessaging-0", + "type": "DIDCommMessaging", + "serviceEndpoint": { + "uri": "https://example.com/endpoint", + "routingKeys": ["did:example:somemediator#somekey"], + "accept": ["didcomm/v2", "didcomm/aip2;env=rfc587"] + } + } + ] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6LMoreServices.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6LMoreServices.json new file mode 100644 index 0000000000..fa5de68cf1 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6LMoreServices.json @@ -0,0 +1,34 @@ +{ + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXX0.SeyJ0IjoiZXhhbXBsZSIsInMiOiJodHRwczovL2V4YW1wbGUuY29tL2VuZHBvaW50MiIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkyIl0sImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19", + "authentication": [ + { + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXX0.SeyJ0IjoiZXhhbXBsZSIsInMiOiJodHRwczovL2V4YW1wbGUuY29tL2VuZHBvaW50MiIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkyIl0sImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19#key-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXX0.SeyJ0IjoiZXhhbXBsZSIsInMiOiJodHRwczovL2V4YW1wbGUuY29tL2VuZHBvaW50MiIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkyIl0sImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19", + "publicKeyBase58": "ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7" + } + ], + "keyAgreement": [ + { + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXX0.SeyJ0IjoiZXhhbXBsZSIsInMiOiJodHRwczovL2V4YW1wbGUuY29tL2VuZHBvaW50MiIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkyIl0sImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19#key-2", + "type": "X25519KeyAgreementKey2019", + "controller": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXX0.SeyJ0IjoiZXhhbXBsZSIsInMiOiJodHRwczovL2V4YW1wbGUuY29tL2VuZHBvaW50MiIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkyIl0sImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19", + "publicKeyBase58": "DmgBSHMqaZiYqwNMEJJuxWzsGGC8jUYADrfSdBrC6L8s" + } + ], + "service": [ + { + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXX0.SeyJ0IjoiZXhhbXBsZSIsInMiOiJodHRwczovL2V4YW1wbGUuY29tL2VuZHBvaW50MiIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkyIl0sImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19#didcommmessaging-0", + "type": "DIDCommMessaging", + "serviceEndpoint": "https://example.com/endpoint", + "routingKeys": ["did:example:somemediator#somekey"] + }, + { + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXX0.SeyJ0IjoiZXhhbXBsZSIsInMiOiJodHRwczovL2V4YW1wbGUuY29tL2VuZHBvaW50MiIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkyIl0sImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjNTg3Il19#example-1", + "type": "example", + "serviceEndpoint": "https://example.com/endpoint2", + "routingKeys": ["did:example:somemediator#somekey2"], + "accept": ["didcomm/v2", "didcomm/aip2;env=rfc587"] + } + ] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6LMultipleServicesSingleToken.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6LMultipleServicesSingleToken.json new file mode 100644 index 0000000000..2dbfcdc037 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6LMultipleServicesSingleToken.json @@ -0,0 +1,34 @@ +{ + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0", + "authentication": [ + { + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0#key-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0", + "publicKeyBase58": "ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7" + } + ], + "keyAgreement": [ + { + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0#key-2", + "type": "X25519KeyAgreementKey2019", + "controller": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0", + "publicKeyBase58": "DmgBSHMqaZiYqwNMEJJuxWzsGGC8jUYADrfSdBrC6L8s" + } + ], + "service": [ + { + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0#didcommmessaging-0", + "type": "DIDCommMessaging", + "serviceEndpoint": "https://example.com/endpoint", + "routingKeys": ["did:example:somemediator#somekey"] + }, + { + "id": "did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0#example-1", + "type": "example", + "serviceEndpoint": "https://example.com/endpoint2", + "routingKeys": ["did:example:somemediator#somekey2"], + "accept": ["didcomm/v2", "didcomm/aip2;env=rfc587"] + } + ] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6LSe3YyteKQAcaPy.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6LSe3YyteKQAcaPy.json new file mode 100644 index 0000000000..306a153c92 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6LSe3YyteKQAcaPy.json @@ -0,0 +1,52 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:2.Ez6LSe3YyteKQVXSgGfZyCzbLq9K34zjAfap4EuaWJjbo5Gwk.Vz6MkvLvjZVc9jWJVs8M3qNZeRsnXXZGFjzcRJDH9vvdJMgHB.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmMxOSJdfX0.SeyJzIjogImh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsICJhIjogWyJkaWRjb21tL2FpcDEiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzE5Il0sICJyZWNpcGllbnRLZXlzIjogWyIja2V5LTIiXSwgInQiOiAiZGlkLWNvbW11bmljYXRpb24ifQ.SeyJzIjogIndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwgImEiOiBbImRpZGNvbW0vYWlwMSIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXSwgInJlY2lwaWVudEtleXMiOiBbIiNrZXktMiJdLCAidCI6ICJkaWQtY29tbXVuaWNhdGlvbiJ9", + "service": [ + { + "type": "DIDCommMessaging", + "serviceEndpoint": { + "uri": "https://us-east.proven.mediator.indiciotech.io/message", + "accept": ["didcomm/v2", "didcomm/aip2;env=rfc19"] + }, + "id": "did:peer:2.Ez6LSe3YyteKQVXSgGfZyCzbLq9K34zjAfap4EuaWJjbo5Gwk.Vz6MkvLvjZVc9jWJVs8M3qNZeRsnXXZGFjzcRJDH9vvdJMgHB.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmMxOSJdfX0.SeyJzIjogImh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsICJhIjogWyJkaWRjb21tL2FpcDEiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzE5Il0sICJyZWNpcGllbnRLZXlzIjogWyIja2V5LTIiXSwgInQiOiAiZGlkLWNvbW11bmljYXRpb24ifQ.SeyJzIjogIndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwgImEiOiBbImRpZGNvbW0vYWlwMSIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXSwgInJlY2lwaWVudEtleXMiOiBbIiNrZXktMiJdLCAidCI6ICJkaWQtY29tbXVuaWNhdGlvbiJ9#didcommmessaging-0" + }, + { + "type": "DIDCommMessaging", + "serviceEndpoint": { + "uri": "wss://ws.us-east.proven.mediator.indiciotech.io/ws", + "accept": ["didcomm/v2", "didcomm/aip2;env=rfc19"] + }, + "id": "did:peer:2.Ez6LSe3YyteKQVXSgGfZyCzbLq9K34zjAfap4EuaWJjbo5Gwk.Vz6MkvLvjZVc9jWJVs8M3qNZeRsnXXZGFjzcRJDH9vvdJMgHB.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmMxOSJdfX0.SeyJzIjogImh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsICJhIjogWyJkaWRjb21tL2FpcDEiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzE5Il0sICJyZWNpcGllbnRLZXlzIjogWyIja2V5LTIiXSwgInQiOiAiZGlkLWNvbW11bmljYXRpb24ifQ.SeyJzIjogIndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwgImEiOiBbImRpZGNvbW0vYWlwMSIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXSwgInJlY2lwaWVudEtleXMiOiBbIiNrZXktMiJdLCAidCI6ICJkaWQtY29tbXVuaWNhdGlvbiJ9#didcommmessaging-1" + }, + { + "serviceEndpoint": "https://us-east.proven.mediator.indiciotech.io/message", + "accept": ["didcomm/aip1", "didcomm/aip2;env=rfc19"], + "recipientKeys": ["#key-2"], + "type": "did-communication", + "id": "did:peer:2.Ez6LSe3YyteKQVXSgGfZyCzbLq9K34zjAfap4EuaWJjbo5Gwk.Vz6MkvLvjZVc9jWJVs8M3qNZeRsnXXZGFjzcRJDH9vvdJMgHB.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmMxOSJdfX0.SeyJzIjogImh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsICJhIjogWyJkaWRjb21tL2FpcDEiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzE5Il0sICJyZWNpcGllbnRLZXlzIjogWyIja2V5LTIiXSwgInQiOiAiZGlkLWNvbW11bmljYXRpb24ifQ.SeyJzIjogIndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwgImEiOiBbImRpZGNvbW0vYWlwMSIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXSwgInJlY2lwaWVudEtleXMiOiBbIiNrZXktMiJdLCAidCI6ICJkaWQtY29tbXVuaWNhdGlvbiJ9#did-communication-2" + }, + { + "serviceEndpoint": "wss://ws.us-east.proven.mediator.indiciotech.io/ws", + "accept": ["didcomm/aip1", "didcomm/aip2;env=rfc19"], + "recipientKeys": ["#key-2"], + "type": "did-communication", + "id": "did:peer:2.Ez6LSe3YyteKQVXSgGfZyCzbLq9K34zjAfap4EuaWJjbo5Gwk.Vz6MkvLvjZVc9jWJVs8M3qNZeRsnXXZGFjzcRJDH9vvdJMgHB.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmMxOSJdfX0.SeyJzIjogImh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsICJhIjogWyJkaWRjb21tL2FpcDEiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzE5Il0sICJyZWNpcGllbnRLZXlzIjogWyIja2V5LTIiXSwgInQiOiAiZGlkLWNvbW11bmljYXRpb24ifQ.SeyJzIjogIndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwgImEiOiBbImRpZGNvbW0vYWlwMSIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXSwgInJlY2lwaWVudEtleXMiOiBbIiNrZXktMiJdLCAidCI6ICJkaWQtY29tbXVuaWNhdGlvbiJ9#did-communication-3" + } + ], + "authentication": [ + { + "id": "did:peer:2.Ez6LSe3YyteKQVXSgGfZyCzbLq9K34zjAfap4EuaWJjbo5Gwk.Vz6MkvLvjZVc9jWJVs8M3qNZeRsnXXZGFjzcRJDH9vvdJMgHB.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmMxOSJdfX0.SeyJzIjogImh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsICJhIjogWyJkaWRjb21tL2FpcDEiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzE5Il0sICJyZWNpcGllbnRLZXlzIjogWyIja2V5LTIiXSwgInQiOiAiZGlkLWNvbW11bmljYXRpb24ifQ.SeyJzIjogIndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwgImEiOiBbImRpZGNvbW0vYWlwMSIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXSwgInJlY2lwaWVudEtleXMiOiBbIiNrZXktMiJdLCAidCI6ICJkaWQtY29tbXVuaWNhdGlvbiJ9#key-2", + "type": "Ed25519VerificationKey2018", + "controller": "did:peer:2.Ez6LSe3YyteKQVXSgGfZyCzbLq9K34zjAfap4EuaWJjbo5Gwk.Vz6MkvLvjZVc9jWJVs8M3qNZeRsnXXZGFjzcRJDH9vvdJMgHB.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmMxOSJdfX0.SeyJzIjogImh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsICJhIjogWyJkaWRjb21tL2FpcDEiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzE5Il0sICJyZWNpcGllbnRLZXlzIjogWyIja2V5LTIiXSwgInQiOiAiZGlkLWNvbW11bmljYXRpb24ifQ.SeyJzIjogIndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwgImEiOiBbImRpZGNvbW0vYWlwMSIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXSwgInJlY2lwaWVudEtleXMiOiBbIiNrZXktMiJdLCAidCI6ICJkaWQtY29tbXVuaWNhdGlvbiJ9", + "publicKeyBase58": "GtfgyFMiPxp2kdWM9oboanEXhyzQL7N4cCNE6efHSTVo" + } + ], + "keyAgreement": [ + { + "id": "did:peer:2.Ez6LSe3YyteKQVXSgGfZyCzbLq9K34zjAfap4EuaWJjbo5Gwk.Vz6MkvLvjZVc9jWJVs8M3qNZeRsnXXZGFjzcRJDH9vvdJMgHB.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmMxOSJdfX0.SeyJzIjogImh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsICJhIjogWyJkaWRjb21tL2FpcDEiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzE5Il0sICJyZWNpcGllbnRLZXlzIjogWyIja2V5LTIiXSwgInQiOiAiZGlkLWNvbW11bmljYXRpb24ifQ.SeyJzIjogIndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwgImEiOiBbImRpZGNvbW0vYWlwMSIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXSwgInJlY2lwaWVudEtleXMiOiBbIiNrZXktMiJdLCAidCI6ICJkaWQtY29tbXVuaWNhdGlvbiJ9#key-1", + "type": "X25519KeyAgreementKey2019", + "controller": "did:peer:2.Ez6LSe3YyteKQVXSgGfZyCzbLq9K34zjAfap4EuaWJjbo5Gwk.Vz6MkvLvjZVc9jWJVs8M3qNZeRsnXXZGFjzcRJDH9vvdJMgHB.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsImEiOlsiZGlkY29tbS92MiIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmMxOSJdfX0.SeyJzIjogImh0dHBzOi8vdXMtZWFzdC5wcm92ZW4ubWVkaWF0b3IuaW5kaWNpb3RlY2guaW8vbWVzc2FnZSIsICJhIjogWyJkaWRjb21tL2FpcDEiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzE5Il0sICJyZWNpcGllbnRLZXlzIjogWyIja2V5LTIiXSwgInQiOiAiZGlkLWNvbW11bmljYXRpb24ifQ.SeyJzIjogIndzczovL3dzLnVzLWVhc3QucHJvdmVuLm1lZGlhdG9yLmluZGljaW90ZWNoLmlvL3dzIiwgImEiOiBbImRpZGNvbW0vYWlwMSIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXSwgInJlY2lwaWVudEtleXMiOiBbIiNrZXktMiJdLCAidCI6ICJkaWQtY29tbXVuaWNhdGlvbiJ9", + "publicKeyBase58": "3NNpNLWYQ4iwBHCCgM5PWZ6ZDrC3xyduMvrppGxGMuAz" + } + ] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json new file mode 100644 index 0000000000..79a8c2a0d1 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json @@ -0,0 +1,24 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "verificationMethod": [ + { + "id": "#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE", + "controller": "did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e" + } + ], + "service": [ + { + "id": "#service-0", + "serviceEndpoint": "https://example.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16"], + "accept": ["didcomm/aip2;env=rfc19"] + } + ], + "authentication": ["#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16"], + "id": "did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e", + "alsoKnownAs": ["did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4"] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json new file mode 100644 index 0000000000..fc0529d74e --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json @@ -0,0 +1,39 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/x25519-2020/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "verificationMethod": [ + { + "id": "#6LSqPZfn", + "type": "X25519KeyAgreementKey2020", + "publicKeyMultibase": "z6LSqPZfn9krvgXma2icTMKf2uVcYhKXsudCmPoUzqGYW24U", + "controller": "did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd:z2M1k7h4psgp4CmJcnQn2Ljp7Pz7ktsd7oBhMU3dWY5s4fhFNj17qcRTQ427C7QHNT6cQ7T3XfRh35Q2GhaNFZmWHVFq4vL7F8nm36PA9Y96DvdrUiRUaiCuXnBFrn1o7mxFZAx14JL4t8vUWpuDPwQuddVo1T8myRiVH7wdxuoYbsva5x6idEpCQydJdFjiHGCpNc2UtjzPQ8awSXkctGCnBmgkhrj5gto3D4i3EREXYq4Z8r2cWGBr2UzbSmnxW2BuYddFo9Yfm6mKjtJyLpF74ytqrF5xtf84MnGFg1hMBmh1xVx1JwjZ2BeMJs7mNS8DTZhKC7KH38EgqDtUZzfjhpjmmUfkXg2KFEA3EGbbVm1DPqQXayPYKAsYPS9AyKkcQ3fzWafLPP93UfNhtUPL8JW5pMcSV3P8v6j3vPXqnnGknNyBprD6YGUVtgLiAqDBDUF3LSxFQJCVYYtghMTv8WuSw9h1a1SRFrDQLGHE4UrkgoRvwaGWr64aM87T1eVGkP5Dt4L1AbboeK2ceLArPScrdYGTpi3BpTkLwZCdjdiFSfTy9okL1YNRARqUf2wm8DvkVGUU7u5nQA3ZMaXWJAewk6k1YUxKd7LvofGUK4YEDtoxN5vb6r1Q2godrGqaPkjfL3RoYPpDYymf9XhcgG8Kx3DZaA6cyTs24t45KxYAfeCw4wqUpCH9HbpD78TbEUr9PPAsJgXBvBj2VVsxnr7FKbK4KykGcg1W8M1JPz21Z4Y72LWgGQCmixovrkHktcTX1uNHjAvKBqVD5C7XmVfHgXCHj7djCh3vzLNuVLtEED8J1hhqsB1oCBGiuh3xXr7fZ9wUjJCQ1HYHqxLJKdYKtoCiPmgKM7etVftXkmTFETZmpM19aRyih3bao76LdpQtbw636r7a3qt8v4WfxsXJetSL8c7t24SqQBcAY89FBsbEnFNrQCMK3JEseKHVaU388ctvRD45uQfe5GndFxthj4iSDomk4uRFd1uRbywoP1tRuabHTDX42UxPjz" + }, + { + "id": "#6MkrCD1c", + "type": "Ed25519VerificationKey2020", + "publicKeyMultibase": "z6MkrCD1csqtgdj8sjrsu8jxcbeyP6m7LiK87NzhfWqio5yr", + "controller": "did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd:z2M1k7h4psgp4CmJcnQn2Ljp7Pz7ktsd7oBhMU3dWY5s4fhFNj17qcRTQ427C7QHNT6cQ7T3XfRh35Q2GhaNFZmWHVFq4vL7F8nm36PA9Y96DvdrUiRUaiCuXnBFrn1o7mxFZAx14JL4t8vUWpuDPwQuddVo1T8myRiVH7wdxuoYbsva5x6idEpCQydJdFjiHGCpNc2UtjzPQ8awSXkctGCnBmgkhrj5gto3D4i3EREXYq4Z8r2cWGBr2UzbSmnxW2BuYddFo9Yfm6mKjtJyLpF74ytqrF5xtf84MnGFg1hMBmh1xVx1JwjZ2BeMJs7mNS8DTZhKC7KH38EgqDtUZzfjhpjmmUfkXg2KFEA3EGbbVm1DPqQXayPYKAsYPS9AyKkcQ3fzWafLPP93UfNhtUPL8JW5pMcSV3P8v6j3vPXqnnGknNyBprD6YGUVtgLiAqDBDUF3LSxFQJCVYYtghMTv8WuSw9h1a1SRFrDQLGHE4UrkgoRvwaGWr64aM87T1eVGkP5Dt4L1AbboeK2ceLArPScrdYGTpi3BpTkLwZCdjdiFSfTy9okL1YNRARqUf2wm8DvkVGUU7u5nQA3ZMaXWJAewk6k1YUxKd7LvofGUK4YEDtoxN5vb6r1Q2godrGqaPkjfL3RoYPpDYymf9XhcgG8Kx3DZaA6cyTs24t45KxYAfeCw4wqUpCH9HbpD78TbEUr9PPAsJgXBvBj2VVsxnr7FKbK4KykGcg1W8M1JPz21Z4Y72LWgGQCmixovrkHktcTX1uNHjAvKBqVD5C7XmVfHgXCHj7djCh3vzLNuVLtEED8J1hhqsB1oCBGiuh3xXr7fZ9wUjJCQ1HYHqxLJKdYKtoCiPmgKM7etVftXkmTFETZmpM19aRyih3bao76LdpQtbw636r7a3qt8v4WfxsXJetSL8c7t24SqQBcAY89FBsbEnFNrQCMK3JEseKHVaU388ctvRD45uQfe5GndFxthj4iSDomk4uRFd1uRbywoP1tRuabHTDX42UxPjz" + } + ], + "service": [ + { + "id": "#didcommmessaging-0", + "type": "DIDCommMessaging", + "serviceEndpoint": { + "uri": "didcomm:transport/queue", + "accept": ["didcomm/v2"], + "routingKeys": [] + } + } + ], + "authentication": ["#6MkrCD1c"], + "keyAgreement": ["#6LSqPZfn"], + "assertionMethod": ["#6MkrCD1c"], + "capabilityDelegation": ["#6MkrCD1c"], + "capabilityInvocation": ["#6MkrCD1c"], + "alsoKnownAs": ["did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd"], + "id": "did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd:z2M1k7h4psgp4CmJcnQn2Ljp7Pz7ktsd7oBhMU3dWY5s4fhFNj17qcRTQ427C7QHNT6cQ7T3XfRh35Q2GhaNFZmWHVFq4vL7F8nm36PA9Y96DvdrUiRUaiCuXnBFrn1o7mxFZAx14JL4t8vUWpuDPwQuddVo1T8myRiVH7wdxuoYbsva5x6idEpCQydJdFjiHGCpNc2UtjzPQ8awSXkctGCnBmgkhrj5gto3D4i3EREXYq4Z8r2cWGBr2UzbSmnxW2BuYddFo9Yfm6mKjtJyLpF74ytqrF5xtf84MnGFg1hMBmh1xVx1JwjZ2BeMJs7mNS8DTZhKC7KH38EgqDtUZzfjhpjmmUfkXg2KFEA3EGbbVm1DPqQXayPYKAsYPS9AyKkcQ3fzWafLPP93UfNhtUPL8JW5pMcSV3P8v6j3vPXqnnGknNyBprD6YGUVtgLiAqDBDUF3LSxFQJCVYYtghMTv8WuSw9h1a1SRFrDQLGHE4UrkgoRvwaGWr64aM87T1eVGkP5Dt4L1AbboeK2ceLArPScrdYGTpi3BpTkLwZCdjdiFSfTy9okL1YNRARqUf2wm8DvkVGUU7u5nQA3ZMaXWJAewk6k1YUxKd7LvofGUK4YEDtoxN5vb6r1Q2godrGqaPkjfL3RoYPpDYymf9XhcgG8Kx3DZaA6cyTs24t45KxYAfeCw4wqUpCH9HbpD78TbEUr9PPAsJgXBvBj2VVsxnr7FKbK4KykGcg1W8M1JPz21Z4Y72LWgGQCmixovrkHktcTX1uNHjAvKBqVD5C7XmVfHgXCHj7djCh3vzLNuVLtEED8J1hhqsB1oCBGiuh3xXr7fZ9wUjJCQ1HYHqxLJKdYKtoCiPmgKM7etVftXkmTFETZmpM19aRyih3bao76LdpQtbw636r7a3qt8v4WfxsXJetSL8c7t24SqQBcAY89FBsbEnFNrQCMK3JEseKHVaU388ctvRD45uQfe5GndFxthj4iSDomk4uRFd1uRbywoP1tRuabHTDX42UxPjz" +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo0.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo0.test.ts new file mode 100644 index 0000000000..efc938ae2d --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo0.test.ts @@ -0,0 +1,41 @@ +import { Key } from '../../../../../crypto' +import didKeyBls12381g1 from '../../../__tests__/__fixtures__/didKeyBls12381g1.json' +import didKeyBls12381g1g2 from '../../../__tests__/__fixtures__/didKeyBls12381g1g2.json' +import didKeyBls12381g2 from '../../../__tests__/__fixtures__/didKeyBls12381g2.json' +import didKeyEd25519 from '../../../__tests__/__fixtures__/didKeyEd25519.json' +import didKeyX25519 from '../../../__tests__/__fixtures__/didKeyX25519.json' +import { didToNumAlgo0DidDocument, keyToNumAlgo0DidDocument } from '../peerDidNumAlgo0' + +describe('peerDidNumAlgo0', () => { + describe('keyToNumAlgo0DidDocument', () => { + test('transforms a key correctly into a peer did method 0 did document', async () => { + const didDocuments = [didKeyEd25519, didKeyBls12381g1, didKeyX25519, didKeyBls12381g1g2, didKeyBls12381g2] + + for (const didDocument of didDocuments) { + const key = Key.fromFingerprint(didDocument.id.split(':')[2]) + + const didPeerDocument = keyToNumAlgo0DidDocument(key) + const expectedDidPeerDocument = JSON.parse( + JSON.stringify(didDocument).replace(new RegExp('did:key:', 'g'), 'did:peer:0') + ) + + expect(didPeerDocument.toJSON()).toMatchObject(expectedDidPeerDocument) + } + }) + }) + + describe('didToNumAlgo0DidDocument', () => { + test('transforms a method 0 did correctly into a did document', () => { + const didDocuments = [didKeyEd25519, didKeyBls12381g1, didKeyX25519, didKeyBls12381g1g2, didKeyBls12381g2] + + for (const didDocument of didDocuments) { + const didPeer = didToNumAlgo0DidDocument(didDocument.id.replace('did:key:', 'did:peer:0')) + const expectedDidPeerDocument = JSON.parse( + JSON.stringify(didDocument).replace(new RegExp('did:key:', 'g'), 'did:peer:0') + ) + + expect(didPeer.toJSON()).toMatchObject(expectedDidPeerDocument) + } + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo1.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo1.test.ts new file mode 100644 index 0000000000..c4cd88219f --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo1.test.ts @@ -0,0 +1,17 @@ +import { didDocumentJsonToNumAlgo1Did } from '../peerDidNumAlgo1' + +import didPeer1zQmR from './__fixtures__/didPeer1zQmR.json' +import didPeer1zQmZ from './__fixtures__/didPeer1zQmZ.json' + +describe('peerDidNumAlgo1', () => { + describe('didDocumentJsonToNumAlgo1Did', () => { + test('transforms a did document into a valid method 1 did', async () => { + expect(didDocumentJsonToNumAlgo1Did(didPeer1zQmR)).toEqual(didPeer1zQmR.id) + }) + + // FIXME: we need some input data from AFGO for this test to succeed (we create a hash of the document, so any inconsistency is fatal) + xtest('transforms a did document from aries-framework-go into a valid method 1 did', () => { + expect(didDocumentJsonToNumAlgo1Did(didPeer1zQmZ)).toEqual(didPeer1zQmZ.id) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo2.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo2.test.ts new file mode 100644 index 0000000000..2ae2b24b03 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo2.test.ts @@ -0,0 +1,83 @@ +import { JsonTransformer } from '../../../../../utils' +import { OutOfBandDidCommService } from '../../../../oob/domain/OutOfBandDidCommService' +import { DidDocument } from '../../../domain' +import { isValidPeerDid } from '../didPeer' +import { + didToNumAlgo2DidDocument, + didDocumentToNumAlgo2Did, + outOfBandServiceToNumAlgo2Did, + outOfBandServiceToInlineKeysNumAlgo2Did, +} from '../peerDidNumAlgo2' + +import didPeer2Ez6L from './__fixtures__/didPeer2Ez6L.json' +import didPeer2Ez6LMoreServices from './__fixtures__/didPeer2Ez6LMoreServices.json' +import didPeer2Ez6LMultipleServicesSingleToken from './__fixtures__/didPeer2Ez6LMultipleServicesSingleToken.json' +import didPeer2Ez6LSe3YyteKQAcaPy from './__fixtures__/didPeer2Ez6LSe3YyteKQAcaPy.json' + +describe('peerDidNumAlgo2', () => { + describe('didToNumAlgo2DidDocument', () => { + test('transforms method 2 peer did to a did document', async () => { + expect(didToNumAlgo2DidDocument(didPeer2Ez6L.id).toJSON()).toMatchObject(didPeer2Ez6L) + + // Here we encode each service individually, as clarified in peer did spec + expect(didToNumAlgo2DidDocument(didPeer2Ez6LMoreServices.id).toJSON()).toMatchObject(didPeer2Ez6LMoreServices) + + // In this case, service list is encoded within a single S entry (old way of doing it) + expect(didToNumAlgo2DidDocument(didPeer2Ez6LMultipleServicesSingleToken.id).toJSON()).toMatchObject( + didPeer2Ez6LMultipleServicesSingleToken + ) + }) + + test('transforms method 2 peer did created by aca-py to a did document', async () => { + expect(isValidPeerDid(didPeer2Ez6LSe3YyteKQAcaPy.id)).toEqual(true) + expect(didToNumAlgo2DidDocument(didPeer2Ez6LSe3YyteKQAcaPy.id).toJSON()).toEqual(didPeer2Ez6LSe3YyteKQAcaPy) + }) + }) + + describe('didDocumentToNumAlgo2Did', () => { + test('transforms method 2 peer did document to a did', async () => { + const expectedDid = didPeer2Ez6L.id + + const didDocument = JsonTransformer.fromJSON(didPeer2Ez6L, DidDocument) + + expect(didDocumentToNumAlgo2Did(didDocument)).toBe(expectedDid) + }) + }) + + describe('outOfBandServiceToNumAlgo2Did', () => { + test('transforms a did comm service into a valid method 2 did', () => { + const service = new OutOfBandDidCommService({ + id: '#service-0', + serviceEndpoint: 'https://example.com/endpoint', + recipientKeys: ['did:key:z6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V'], + routingKeys: ['did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'], + accept: ['didcomm/v2', 'didcomm/aip2;env=rfc587'], + }) + const peerDid = outOfBandServiceToNumAlgo2Did(service) + const peerDidDocument = didToNumAlgo2DidDocument(peerDid) + + expect(peerDid).toBe( + 'did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbIiNrZXktMSJdLCJyIjpbImRpZDprZXk6ejZNa3BUSFI4Vk5zQnhZQUFXSHV0MkdlYWRkOWpTd3VCVjh4Um9BbndXc2R2a3RII3o2TWtwVEhSOFZOc0J4WUFBV0h1dDJHZWFkZDlqU3d1QlY4eFJvQW53V3Nkdmt0SCJdfQ' + ) + expect(peerDid).toBe(peerDidDocument.id) + }) + }) + + describe('outOfBandServiceInlineKeysToNumAlgo2Did', () => { + test('transforms a did comm service into a valid method 2 did', () => { + const service = new OutOfBandDidCommService({ + id: '#service-0', + serviceEndpoint: 'https://example.com/endpoint', + recipientKeys: ['did:key:z6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V'], + routingKeys: ['did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'], + accept: ['didcomm/v2', 'didcomm/aip2;env=rfc587'], + }) + const peerDid = outOfBandServiceToInlineKeysNumAlgo2Did(service) + const peerDidDocument = didToNumAlgo2DidDocument(peerDid) + expect(peerDid).toBe( + 'did:peer:2.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3FSWXFRaVNndlpRZG5CeXR3ODZRYnMyWldVa0d2MjJvZDkzNVlGNHM4TTdWI3o2TWtxUllxUWlTZ3ZaUWRuQnl0dzg2UWJzMlpXVWtHdjIyb2Q5MzVZRjRzOE03ViJdLCJyIjpbImRpZDprZXk6ejZNa3BUSFI4Vk5zQnhZQUFXSHV0MkdlYWRkOWpTd3VCVjh4Um9BbndXc2R2a3RII3o2TWtwVEhSOFZOc0J4WUFBV0h1dDJHZWFkZDlqU3d1QlY4eFJvQW53V3Nkdmt0SCJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfQ' + ) + expect(peerDid).toBe(peerDidDocument.id) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts new file mode 100644 index 0000000000..b6c09a663c --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts @@ -0,0 +1,45 @@ +import { JsonTransformer } from '../../../../../utils' +import { OutOfBandDidCommService } from '../../../../oob/domain/OutOfBandDidCommService' +import { DidDocument } from '../../../domain' +import { didDocumentToNumAlgo4Did, didToNumAlgo4DidDocument, outOfBandServiceToNumAlgo4Did } from '../peerDidNumAlgo4' + +import didPeer4zQmUJdJ from './__fixtures__/didPeer4zQmUJdJ.json' +import didPeer4zQmd8Cp from './__fixtures__/didPeer4zQmd8Cp.json' + +describe('peerDidNumAlgo4', () => { + describe('didToNumAlgo4DidDocument', () => { + test('transforms method 4 peer did to a did document', async () => { + expect(didToNumAlgo4DidDocument(didPeer4zQmd8Cp.id).toJSON()).toMatchObject(didPeer4zQmd8Cp) + }) + }) + + describe('didDocumentToNumAlgo4Did', () => { + test('transforms method 4 peer did document to a did', async () => { + const longFormDid = didPeer4zQmUJdJ.id + const shortFormDid = didPeer4zQmUJdJ.alsoKnownAs[0] + + const didDocument = JsonTransformer.fromJSON(didPeer4zQmUJdJ, DidDocument) + + expect(didDocumentToNumAlgo4Did(didDocument)).toEqual({ longFormDid, shortFormDid }) + }) + }) + + describe('outOfBandServiceToNumAlgo4Did', () => { + test('transforms a did comm service into a valid method 4 did', () => { + const service = new OutOfBandDidCommService({ + id: '#service-0', + serviceEndpoint: 'https://example.com/endpoint', + recipientKeys: ['did:key:z6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V'], + routingKeys: ['did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'], + accept: ['didcomm/v2', 'didcomm/aip2;env=rfc587'], + }) + const { longFormDid } = outOfBandServiceToNumAlgo4Did(service) + const peerDidDocument = didToNumAlgo4DidDocument(longFormDid) + + expect(longFormDid).toBe( + 'did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn:z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + expect(longFormDid).toBe(peerDidDocument.id) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts b/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts new file mode 100644 index 0000000000..504555b50d --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts @@ -0,0 +1,72 @@ +import type { ResolvedDidCommService } from '../../../didcomm' + +import { convertPublicKeyToX25519 } from '@stablelib/ed25519' + +import { Key } from '../../../../crypto/Key' +import { KeyType } from '../../../../crypto/KeyType' +import { CredoError } from '../../../../error' +import { getEd25519VerificationKey2018, getX25519KeyAgreementKey2019 } from '../../domain' +import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' +import { DidCommV1Service } from '../../domain/service/DidCommV1Service' +import { DidKey } from '../key' + +export function createPeerDidDocumentFromServices(services: ResolvedDidCommService[]) { + const didDocumentBuilder = new DidDocumentBuilder('') + + // Keep track of all added key id based on the fingerprint so we can add them to the recipientKeys as references + const recipientKeyIdMapping: { [fingerprint: string]: string } = {} + + let keyIndex = 1 + services.forEach((service, index) => { + // Get the local key reference for each of the recipient keys + const recipientKeys = service.recipientKeys.map((recipientKey) => { + // Key already added to the did document + if (recipientKeyIdMapping[recipientKey.fingerprint]) return recipientKeyIdMapping[recipientKey.fingerprint] + + if (recipientKey.keyType !== KeyType.Ed25519) { + throw new CredoError( + `Unable to create did document from services. recipient key type ${recipientKey.keyType} is not supported. Supported key types are ${KeyType.Ed25519}` + ) + } + const x25519Key = Key.fromPublicKey(convertPublicKeyToX25519(recipientKey.publicKey), KeyType.X25519) + + // key ids follow the #key-N pattern to comply with did:peer:2 spec + const ed25519VerificationMethod = getEd25519VerificationKey2018({ + id: `#key-${keyIndex++}`, + key: recipientKey, + controller: '#id', + }) + const x25519VerificationMethod = getX25519KeyAgreementKey2019({ + id: `#key-${keyIndex++}`, + key: x25519Key, + controller: '#id', + }) + + recipientKeyIdMapping[recipientKey.fingerprint] = ed25519VerificationMethod.id + + // We should not add duplicated keys for services + didDocumentBuilder.addAuthentication(ed25519VerificationMethod).addKeyAgreement(x25519VerificationMethod) + + return recipientKeyIdMapping[recipientKey.fingerprint] + }) + + // Transform all routing keys into did:key:xxx#key-id references. This will probably change for didcomm v2 + const routingKeys = service.routingKeys?.map((key) => { + const didKey = new DidKey(key) + + return `${didKey.did}#${key.fingerprint}` + }) + + didDocumentBuilder.addService( + new DidCommV1Service({ + id: service.id, + priority: index, + serviceEndpoint: service.serviceEndpoint, + recipientKeys, + routingKeys, + }) + ) + }) + + return didDocumentBuilder.build() +} diff --git a/packages/core/src/modules/dids/methods/peer/didPeer.ts b/packages/core/src/modules/dids/methods/peer/didPeer.ts new file mode 100644 index 0000000000..7e4b164888 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/didPeer.ts @@ -0,0 +1,47 @@ +import { CredoError } from '../../../../error' + +import { getAlternativeDidsForNumAlgo4Did } from './peerDidNumAlgo4' + +const PEER_DID_REGEX = new RegExp( + '^did:peer:(([01](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))|(2((.[AEVID](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))+(.(S)[0-9a-zA-Z=]*)*))|([4](z[1-9a-km-zA-HJ-NP-Z]{46})(:z[1-9a-km-zA-HJ-NP-Z]{6,}){0,1}))$' +) + +export function isValidPeerDid(did: string): boolean { + const isValid = PEER_DID_REGEX.test(did) + + return isValid +} + +export enum PeerDidNumAlgo { + InceptionKeyWithoutDoc = 0, + GenesisDoc = 1, + MultipleInceptionKeyWithoutDoc = 2, + ShortFormAndLongForm = 4, +} + +export function getNumAlgoFromPeerDid(did: string) { + const numAlgo = Number(did[9]) + + if ( + numAlgo !== PeerDidNumAlgo.InceptionKeyWithoutDoc && + numAlgo !== PeerDidNumAlgo.GenesisDoc && + numAlgo !== PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc && + numAlgo !== PeerDidNumAlgo.ShortFormAndLongForm + ) { + throw new CredoError(`Invalid peer did numAlgo: ${numAlgo}`) + } + + return numAlgo as PeerDidNumAlgo +} + +/** + * Given a peer did, returns any alternative forms equivalent to it. + * + * @param did + * @returns array of alternative dids or undefined if not applicable + */ +export function getAlternativeDidsForPeerDid(did: string) { + if (getNumAlgoFromPeerDid(did) === PeerDidNumAlgo.ShortFormAndLongForm) { + return getAlternativeDidsForNumAlgo4Did(did) + } +} diff --git a/packages/core/src/modules/dids/methods/peer/index.ts b/packages/core/src/modules/dids/methods/peer/index.ts new file mode 100644 index 0000000000..aa2eb72e57 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/index.ts @@ -0,0 +1,4 @@ +export * from './PeerDidRegistrar' +export * from './PeerDidResolver' +export * from './didPeer' +export * from './createPeerDidDocumentFromServices' diff --git a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo0.ts b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo0.ts new file mode 100644 index 0000000000..9ac0495aa0 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo0.ts @@ -0,0 +1,29 @@ +import { Key } from '../../../../crypto/Key' +import { CredoError } from '../../../../error' +import { getDidDocumentForKey } from '../../domain/keyDidDocument' +import { parseDid } from '../../domain/parse' + +import { getNumAlgoFromPeerDid, isValidPeerDid, PeerDidNumAlgo } from './didPeer' + +export function keyToNumAlgo0DidDocument(key: Key) { + const did = `did:peer:0${key.fingerprint}` + + return getDidDocumentForKey(did, key) +} + +export function didToNumAlgo0DidDocument(did: string) { + const parsed = parseDid(did) + const numAlgo = getNumAlgoFromPeerDid(did) + + if (!isValidPeerDid(did)) { + throw new CredoError(`Invalid peer did '${did}'`) + } + + if (numAlgo !== PeerDidNumAlgo.InceptionKeyWithoutDoc) { + throw new CredoError(`Invalid numAlgo ${numAlgo}, expected ${PeerDidNumAlgo.InceptionKeyWithoutDoc}`) + } + + const key = Key.fromFingerprint(parsed.id.substring(1)) + + return getDidDocumentForKey(did, key) +} diff --git a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo1.ts b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo1.ts new file mode 100644 index 0000000000..f9322412bb --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo1.ts @@ -0,0 +1,12 @@ +import { JsonEncoder, MultiBaseEncoder, MultiHashEncoder } from '../../../../utils' + +export function didDocumentJsonToNumAlgo1Did(didDocumentJson: Record): string { + // We need to remove the id property before hashing + const didDocumentBuffer = JsonEncoder.toBuffer({ ...didDocumentJson, id: undefined }) + + const didIdentifier = MultiBaseEncoder.encode(MultiHashEncoder.encode(didDocumentBuffer, 'sha-256'), 'base58btc') + + const did = `did:peer:1${didIdentifier}` + + return did +} diff --git a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo2.ts b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo2.ts new file mode 100644 index 0000000000..d0e7f2fcd4 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo2.ts @@ -0,0 +1,286 @@ +import type { JsonObject } from '../../../../types' +import type { OutOfBandDidCommService } from '../../../oob/domain/OutOfBandDidCommService' +import type { DidDocument, VerificationMethod } from '../../domain' + +import { Key } from '../../../../crypto/Key' +import { CredoError } from '../../../../error' +import { JsonEncoder, JsonTransformer } from '../../../../utils' +import { DidCommV1Service, DidDocumentService } from '../../domain' +import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' +import { getKeyFromVerificationMethod, getKeyDidMappingByKeyType } from '../../domain/key-type' +import { parseDid } from '../../domain/parse' +import { didKeyToInstanceOfKey } from '../../helpers' +import { DidKey } from '../key' + +import { createPeerDidDocumentFromServices } from './createPeerDidDocumentFromServices' + +enum DidPeerPurpose { + Assertion = 'A', + Encryption = 'E', + Verification = 'V', + CapabilityInvocation = 'I', + CapabilityDelegation = 'D', + Service = 'S', +} + +function isDidPeerKeyPurpose(purpose: string): purpose is Exclude { + return purpose !== DidPeerPurpose.Service && Object.values(DidPeerPurpose).includes(purpose as DidPeerPurpose) +} + +const didPeerAbbreviations: { [key: string]: string | undefined } = { + type: 't', + DIDCommMessaging: 'dm', + serviceEndpoint: 's', + routingKeys: 'r', + accept: 'a', +} + +const didPeerExpansions: { [key: string]: string | undefined } = { + t: 'type', + dm: 'DIDCommMessaging', + s: 'serviceEndpoint', + r: 'routingKeys', + a: 'accept', +} + +export function didToNumAlgo2DidDocument(did: string) { + const parsed = parseDid(did) + const identifierWithoutNumAlgo = parsed.id.substring(2) + + // Get a list of all did document entries splitted by . + const entries = identifierWithoutNumAlgo.split('.') + const didDocument = new DidDocumentBuilder(did) + let serviceIndex = 0 + let keyIndex = 1 + + for (const entry of entries) { + // Remove the purpose identifier to get the service or key content + const entryContent = entry.substring(1) + // Get the purpose identifier + const purpose = entry[0] + + // Handle service entry first + if (purpose === DidPeerPurpose.Service) { + let services = JsonEncoder.fromBase64(entryContent) + + // Make sure we have an array of services (can be both json or array) + services = Array.isArray(services) ? services : [services] + + for (let service of services) { + // Expand abbreviations used for service key/values + service = expandServiceAbbreviations(service) + + service.id = `${did}#${service.type.toLowerCase()}-${serviceIndex++}` + + try { + didDocument.addService(JsonTransformer.fromJSON(service, DidDocumentService)) + } catch (e) { + //If it is didCommv2 the json transform will throw + serviceIndex-- + } + } + } + // Otherwise we can be sure it is a key + else { + // Decode the fingerprint, and extract the verification method(s) + const key = Key.fromFingerprint(entryContent) + const { getVerificationMethods } = getKeyDidMappingByKeyType(key.keyType) + const verificationMethods = getVerificationMethods(did, key) + + // Add all verification methods to the did document + for (const verificationMethod of verificationMethods) { + verificationMethod.id = `${did}#key-${keyIndex++}` + addVerificationMethodToDidDocument(didDocument, verificationMethod, purpose) + } + } + } + + return didDocument.build() +} + +export function didDocumentToNumAlgo2Did(didDocument: DidDocument) { + const purposeMapping = { + [DidPeerPurpose.Assertion]: didDocument.assertionMethod, + [DidPeerPurpose.Encryption]: didDocument.keyAgreement, + // FIXME: should verification be authentication or verificationMethod + // verificationMethod is general so it doesn't make a lot of sense to add + // it to the verificationMethod list + [DidPeerPurpose.Verification]: didDocument.authentication, + [DidPeerPurpose.CapabilityInvocation]: didDocument.capabilityInvocation, + [DidPeerPurpose.CapabilityDelegation]: didDocument.capabilityDelegation, + } + + let did = 'did:peer:2' + + const keys: { id: string; encoded: string }[] = [] + + for (const [purpose, entries] of Object.entries(purposeMapping)) { + // Not all entries are required to be defined + if (entries === undefined) continue + + // Dereference all entries to full verification methods + const dereferenced = entries.map((entry) => + typeof entry === 'string' ? didDocument.dereferenceVerificationMethod(entry) : entry + ) + + // Transform all verification methods into a fingerprint (multibase, multicodec) + dereferenced.forEach((entry) => { + const key = getKeyFromVerificationMethod(entry) + + // Encode as '.PurposeFingerprint' + const encoded = `.${purpose}${key.fingerprint}` + + keys.push({ id: entry.id, encoded }) + }) + } + + const prefix = 'key-' + if (!keys.every((key) => key.id.split('#')[1]?.startsWith(prefix))) { + throw new CredoError('Ids for keys within DID Document for did:peer:2 creation must follow the pattern `#key-n`') + } + + // Add all encoded keys ordered by their id (#key-1, #key-2, etc.) + did += keys + .sort((a, b) => { + const aFragment = a.id.split('#')[1] + const bFragment = b.id.split('#')[1] + const aIndex = Number(aFragment.replace(prefix, '')) + const bIndex = Number(bFragment.replace(prefix, '')) + + return aIndex - bIndex + }) + .map((key) => key.encoded) + .join('') + + if (didDocument.service && didDocument.service.length > 0) { + const abbreviatedServices = didDocument.service.map((service) => { + // Transform to JSON, remove id property + const serviceJson = JsonTransformer.toJSON(service) + delete serviceJson.id + + return abbreviateServiceJson(serviceJson) + }) + + for (const abbreviatedService of abbreviatedServices) { + const encodedService = JsonEncoder.toBase64URL(abbreviatedService) + did += `.${DidPeerPurpose.Service}${encodedService}` + } + } + + return did +} + +export function outOfBandServiceToNumAlgo2Did(service: OutOfBandDidCommService) { + const didDocument = createPeerDidDocumentFromServices([ + { + id: service.id, + recipientKeys: service.recipientKeys.map(didKeyToInstanceOfKey), + serviceEndpoint: service.serviceEndpoint, + routingKeys: service.routingKeys?.map(didKeyToInstanceOfKey) ?? [], + }, + ]) + + const did = didDocumentToNumAlgo2Did(didDocument) + + return did +} + +// This method is kept to support searching for existing connections created by +// credo-ts <= 0.5.1 +// TODO: Remove in 0.6.0 (when ConnectionRecord.invitationDid will be migrated) +export function outOfBandServiceToInlineKeysNumAlgo2Did(service: OutOfBandDidCommService) { + const didDocument = new DidDocumentBuilder('') + .addService( + new DidCommV1Service({ + id: service.id, + serviceEndpoint: service.serviceEndpoint, + accept: service.accept, + recipientKeys: service.recipientKeys.map((recipientKey) => { + const did = DidKey.fromDid(recipientKey) + return `${did.did}#${did.key.fingerprint}` + }), + // Map did:key:xxx to actual did:key:xxx#123 + routingKeys: service.routingKeys?.map((routingKey) => { + const did = DidKey.fromDid(routingKey) + return `${did.did}#${did.key.fingerprint}` + }), + }) + ) + .build() + + const did = didDocumentToNumAlgo2Did(didDocument) + + return did +} + +function expandServiceAbbreviations(service: JsonObject) { + const expand = (abbreviated: string) => didPeerExpansions[abbreviated] ?? abbreviated + const expandJson = (json: unknown): unknown => { + if (!json) return json + if (typeof json === 'number') return json + if (typeof json === 'string') return expand(json) + if (Array.isArray(json)) return json.map(expandJson) + if (typeof json === 'object') + return Object.entries(json as Record).reduce( + (jsonBody, [key, value]) => ({ + ...jsonBody, + [expand(key)]: expandJson(value), + }), + {} + ) + } + + const fullService = expandJson(service) as Record + + // Handle the case where a legacy DIDComm v2 service has been encoded in the did:peer:2. + // We use the legacy `DIDComm` type (over `DIDCommMessaging`) + if ('t' in service && service.t === 'dm' && typeof service.serviceEndpoint === 'string') { + return { + ...fullService, + type: 'DIDComm', + } + } + + return fullService +} + +function abbreviateServiceJson(service: JsonObject) { + const abbreviate = (expanded: string) => didPeerAbbreviations[expanded] ?? expanded + + const abbreviatedService = Object.entries(service).reduce( + (serviceBody, [key, value]) => ({ + ...serviceBody, + [abbreviate(key)]: abbreviate(value as string), + }), + {} + ) + + return abbreviatedService +} + +function addVerificationMethodToDidDocument( + didDocument: DidDocumentBuilder, + verificationMethod: VerificationMethod, + purpose: string +) { + const purposeMapping = { + [DidPeerPurpose.Assertion]: didDocument.addAssertionMethod.bind(didDocument), + [DidPeerPurpose.Encryption]: didDocument.addKeyAgreement.bind(didDocument), + // FIXME: should verification be authentication or verificationMethod + // verificationMethod is general so it doesn't make a lot of sense to add + // it to the verificationMethod list + [DidPeerPurpose.Verification]: didDocument.addAuthentication.bind(didDocument), + [DidPeerPurpose.CapabilityInvocation]: didDocument.addCapabilityInvocation.bind(didDocument), + [DidPeerPurpose.CapabilityDelegation]: didDocument.addCapabilityDelegation.bind(didDocument), + } + + // Verify the purpose is a did peer key purpose (service excluded) + if (isDidPeerKeyPurpose(purpose)) { + const addVerificationMethod = purposeMapping[purpose] + + // Add the verification method based on the method from the mapping + addVerificationMethod(verificationMethod) + } else { + throw new CredoError(`Unsupported peer did purpose '${purpose}'`) + } +} diff --git a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts new file mode 100644 index 0000000000..17ad9950c9 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts @@ -0,0 +1,138 @@ +import type { OutOfBandDidCommService } from '../../../oob/domain/OutOfBandDidCommService' + +import { CredoError } from '../../../../error' +import { + JsonEncoder, + JsonTransformer, + MultiBaseEncoder, + MultiHashEncoder, + TypedArrayEncoder, + VarintEncoder, +} from '../../../../utils' +import { Buffer } from '../../../../utils/buffer' +import { DidDocument, DidCommV1Service } from '../../domain' +import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' +import { parseDid } from '../../domain/parse' +import { DidKey } from '../key' + +const LONG_RE = new RegExp(`^did:peer:4(z[1-9a-km-zA-HJ-NP-Z]{46}):(z[1-9a-km-zA-HJ-NP-Z]{6,})$`) +const SHORT_RE = new RegExp(`^did:peer:4(z[1-9a-km-zA-HJ-NP-Z]{46})$`) +const JSON_MULTICODEC_VARINT = 0x0200 + +export const isShortFormDidPeer4 = (did: string) => SHORT_RE.test(did) +export const isLongFormDidPeer4 = (did: string) => LONG_RE.test(did) + +const hashEncodedDocument = (encodedDocument: string) => + MultiBaseEncoder.encode( + MultiHashEncoder.encode(TypedArrayEncoder.fromString(encodedDocument), 'sha-256'), + 'base58btc' + ) + +export function getAlternativeDidsForNumAlgo4Did(did: string) { + const match = did.match(LONG_RE) + if (!match) return + const [, hash] = match + return [`did:peer:4${hash}`] +} + +export function didToNumAlgo4DidDocument(did: string) { + const parsed = parseDid(did) + + const match = parsed.did.match(LONG_RE) + if (!match) { + throw new CredoError(`Invalid long form algo 4 did:peer: ${parsed.did}`) + } + const [, hash, encodedDocument] = match + if (hash !== hashEncodedDocument(encodedDocument)) { + throw new CredoError(`Hash is invalid for did: ${did}`) + } + + const { data } = MultiBaseEncoder.decode(encodedDocument) + const [multiCodecValue] = VarintEncoder.decode(data.subarray(0, 2)) + if (multiCodecValue !== JSON_MULTICODEC_VARINT) { + throw new CredoError(`Not a JSON multicodec data`) + } + const didDocumentJson = JsonEncoder.fromBuffer(data.subarray(2)) + + didDocumentJson.id = parsed.did + didDocumentJson.alsoKnownAs = [parsed.did.slice(0, did.lastIndexOf(':'))] + + // Populate all verification methods without controller + const addControllerIfNotPresent = (item: unknown) => { + if (Array.isArray(item)) item.forEach(addControllerIfNotPresent) + + if (item && typeof item === 'object' && (item as Record).controller === undefined) { + ;(item as Record).controller = parsed.did + } + } + + addControllerIfNotPresent(didDocumentJson.verificationMethod) + addControllerIfNotPresent(didDocumentJson.authentication) + addControllerIfNotPresent(didDocumentJson.assertionMethod) + addControllerIfNotPresent(didDocumentJson.keyAgreement) + addControllerIfNotPresent(didDocumentJson.capabilityDelegation) + addControllerIfNotPresent(didDocumentJson.capabilityInvocation) + + const didDocument = JsonTransformer.fromJSON(didDocumentJson, DidDocument) + return didDocument +} + +export function didDocumentToNumAlgo4Did(didDocument: DidDocument) { + const didDocumentJson = didDocument.toJSON() + + // Build input document based on did document, without any + // reference to controller + const deleteControllerIfPresent = (item: unknown) => { + if (Array.isArray(item)) { + item.forEach((method: { controller?: string }) => { + if (method.controller === '#id' || method.controller === didDocument.id) delete method.controller + }) + } + } + delete didDocumentJson.id + delete didDocumentJson.alsoKnownAs + deleteControllerIfPresent(didDocumentJson.verificationMethod) + deleteControllerIfPresent(didDocumentJson.authentication) + deleteControllerIfPresent(didDocumentJson.assertionMethod) + deleteControllerIfPresent(didDocumentJson.keyAgreement) + deleteControllerIfPresent(didDocumentJson.capabilityDelegation) + deleteControllerIfPresent(didDocumentJson.capabilityInvocation) + + // Construct encoded document by prefixing did document with multicodec prefix for JSON + const buffer = Buffer.concat([ + VarintEncoder.encode(JSON_MULTICODEC_VARINT), + Buffer.from(JSON.stringify(didDocumentJson)), + ]) + + const encodedDocument = MultiBaseEncoder.encode(buffer, 'base58btc') + + const shortFormDid = `did:peer:4${hashEncodedDocument(encodedDocument)}` + const longFormDid = `${shortFormDid}:${encodedDocument}` + + return { shortFormDid, longFormDid } +} + +export function outOfBandServiceToNumAlgo4Did(service: OutOfBandDidCommService) { + // FIXME: add the key entries for the recipientKeys to the did document. + const didDocument = new DidDocumentBuilder('') + .addService( + new DidCommV1Service({ + id: service.id, + serviceEndpoint: service.serviceEndpoint, + accept: service.accept, + // FIXME: this should actually be local key references, not did:key:123#456 references + recipientKeys: service.recipientKeys.map((recipientKey) => { + const did = DidKey.fromDid(recipientKey) + return `${did.did}#${did.key.fingerprint}` + }), + // Map did:key:xxx to actual did:key:xxx#123 + routingKeys: service.routingKeys?.map((routingKey) => { + const did = DidKey.fromDid(routingKey) + return `${did.did}#${did.key.fingerprint}` + }), + }) + ) + .build() + + return didDocumentToNumAlgo4Did(didDocument) +} diff --git a/packages/core/src/modules/dids/methods/web/WebDidResolver.ts b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts new file mode 100644 index 0000000000..468e520f41 --- /dev/null +++ b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts @@ -0,0 +1,50 @@ +import type { AgentContext } from '../../../../agent' +import type { DidResolver } from '../../domain/DidResolver' +import type { ParsedDid, DidResolutionResult, DidResolutionOptions } from '../../types' + +import { Resolver } from 'did-resolver' +import * as didWeb from 'web-did-resolver' + +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { DidDocument } from '../../domain' + +export class WebDidResolver implements DidResolver { + public readonly supportedMethods + + public readonly allowsCaching = true + public readonly allowsLocalDidRecord = true + + // FIXME: Would be nice if we don't have to provide a did resolver instance + private _resolverInstance = new Resolver() + private resolver = didWeb.getResolver() + + public constructor() { + this.supportedMethods = Object.keys(this.resolver) + } + + public async resolve( + agentContext: AgentContext, + did: string, + parsed: ParsedDid, + didResolutionOptions: DidResolutionOptions + ): Promise { + const result = await this.resolver[parsed.method](did, parsed, this._resolverInstance, didResolutionOptions) + + let didDocument = null + + // If the did document uses the deprecated publicKey property + // we map it to the newer verificationMethod property + if (!result.didDocument?.verificationMethod && result.didDocument?.publicKey) { + result.didDocument.verificationMethod = result.didDocument.publicKey + } + + if (result.didDocument) { + didDocument = JsonTransformer.fromJSON(result.didDocument, DidDocument) + } + + return { + ...result, + didDocument, + } + } +} diff --git a/packages/core/src/modules/dids/methods/web/index.ts b/packages/core/src/modules/dids/methods/web/index.ts new file mode 100644 index 0000000000..59e66593dd --- /dev/null +++ b/packages/core/src/modules/dids/methods/web/index.ts @@ -0,0 +1 @@ +export * from './WebDidResolver' diff --git a/packages/core/src/modules/dids/repository/DidRecord.ts b/packages/core/src/modules/dids/repository/DidRecord.ts new file mode 100644 index 0000000000..5431465c33 --- /dev/null +++ b/packages/core/src/modules/dids/repository/DidRecord.ts @@ -0,0 +1,90 @@ +import type { DidRecordMetadata } from './didRecordMetadataTypes' +import type { TagsBase } from '../../../storage/BaseRecord' + +import { Type } from 'class-transformer' +import { IsEnum, ValidateNested } from 'class-validator' + +import { BaseRecord } from '../../../storage/BaseRecord' +import { uuid } from '../../../utils/uuid' +import { DidDocument } from '../domain' +import { DidDocumentRole } from '../domain/DidDocumentRole' +import { parseDid } from '../domain/parse' + +import { DidRecordMetadataKeys } from './didRecordMetadataTypes' + +export interface DidRecordProps { + id?: string + did: string + role: DidDocumentRole + didDocument?: DidDocument + createdAt?: Date + tags?: CustomDidTags +} + +export interface CustomDidTags extends TagsBase { + recipientKeyFingerprints?: string[] + + // Alternative forms of the did, allowed to be queried by them. + // Relationship must be verified both ways before setting this tag. + alternativeDids?: string[] +} + +type DefaultDidTags = { + // We set the recipientKeyFingeprints as a default tag, if the did record has a did document + // If the did record does not have a did document, we can't calculate it, and it needs to be + // handled by the creator of the did record + recipientKeyFingerprints?: string[] + + role: DidDocumentRole + method: string + legacyUnqualifiedDid?: string + methodSpecificIdentifier: string + did: string +} + +export class DidRecord extends BaseRecord implements DidRecordProps { + @Type(() => DidDocument) + @ValidateNested() + public didDocument?: DidDocument + + public did!: string + + @IsEnum(DidDocumentRole) + public role!: DidDocumentRole + + public static readonly type = 'DidRecord' + public readonly type = DidRecord.type + + public constructor(props: DidRecordProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.did = props.did + this.role = props.role + this.didDocument = props.didDocument + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags ?? {} + } + } + + public getTags() { + const did = parseDid(this.did) + + const legacyDid = this.metadata.get(DidRecordMetadataKeys.LegacyDid) + + return { + ...this._tags, + role: this.role, + method: did.method, + legacyUnqualifiedDid: legacyDid?.unqualifiedDid, + did: this.did, + methodSpecificIdentifier: did.id, + + // Calculate if we have a did document, otherwise use the already present recipient keys + recipientKeyFingerprints: this.didDocument + ? this.didDocument.recipientKeys.map((recipientKey) => recipientKey.fingerprint) + : this._tags.recipientKeyFingerprints, + } + } +} diff --git a/packages/core/src/modules/dids/repository/DidRepository.ts b/packages/core/src/modules/dids/repository/DidRepository.ts new file mode 100644 index 0000000000..0851390d87 --- /dev/null +++ b/packages/core/src/modules/dids/repository/DidRepository.ts @@ -0,0 +1,107 @@ +import type { CustomDidTags } from './DidRecord' +import type { AgentContext } from '../../../agent' +import type { Key } from '../../../crypto' +import type { DidDocument } from '../domain' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' +import { DidDocumentRole } from '../domain/DidDocumentRole' + +import { DidRecord } from './DidRecord' + +@injectable() +export class DidRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(DidRecord, storageService, eventEmitter) + } + + /** + * Finds a {@link DidRecord}, containing the specified recipientKey that was received by this agent. + * To find a {@link DidRecord} that was created by this agent, use {@link DidRepository.findCreatedDidByRecipientKey}. + */ + public findReceivedDidByRecipientKey(agentContext: AgentContext, recipientKey: Key) { + return this.findSingleByQuery(agentContext, { + recipientKeyFingerprints: [recipientKey.fingerprint], + role: DidDocumentRole.Received, + }) + } + + /** + * Finds a {@link DidRecord}, containing the specified recipientKey that was created by this agent. + * To find a {@link DidRecord} that was received by this agent, use {@link DidRepository.findReceivedDidByRecipientKey}. + */ + public findCreatedDidByRecipientKey(agentContext: AgentContext, recipientKey: Key) { + return this.findSingleByQuery(agentContext, { + recipientKeyFingerprints: [recipientKey.fingerprint], + role: DidDocumentRole.Created, + }) + } + + public findAllByRecipientKey(agentContext: AgentContext, recipientKey: Key) { + return this.findByQuery(agentContext, { recipientKeyFingerprints: [recipientKey.fingerprint] }) + } + + public findAllByDid(agentContext: AgentContext, did: string) { + return this.findByQuery(agentContext, { $or: [{ alternativeDids: [did] }, { did }] }) + } + + public findReceivedDid(agentContext: AgentContext, receivedDid: string) { + return this.findSingleByQuery(agentContext, { + $or: [{ alternativeDids: [receivedDid] }, { did: receivedDid }], + role: DidDocumentRole.Received, + }) + } + + public findCreatedDid(agentContext: AgentContext, createdDid: string) { + return this.findSingleByQuery(agentContext, { + $or: [{ alternativeDids: [createdDid] }, { did: createdDid }], + role: DidDocumentRole.Created, + }) + } + + public getCreatedDids(agentContext: AgentContext, { method, did }: { method?: string; did?: string }) { + return this.findByQuery(agentContext, { + role: DidDocumentRole.Created, + method, + $or: did ? [{ alternativeDids: [did] }, { did }] : undefined, + }) + } + + public async storeCreatedDid(agentContext: AgentContext, { did, didDocument, tags }: StoreDidOptions) { + const didRecord = new DidRecord({ + did, + didDocument, + role: DidDocumentRole.Created, + tags, + }) + + await this.save(agentContext, didRecord) + + return didRecord + } + + public async storeReceivedDid(agentContext: AgentContext, { did, didDocument, tags }: StoreDidOptions) { + const didRecord = new DidRecord({ + did, + didDocument, + role: DidDocumentRole.Received, + tags, + }) + + await this.save(agentContext, didRecord) + + return didRecord + } +} + +interface StoreDidOptions { + did: string + didDocument?: DidDocument + tags?: CustomDidTags +} diff --git a/packages/core/src/modules/dids/repository/__tests__/DidRecord.test.ts b/packages/core/src/modules/dids/repository/__tests__/DidRecord.test.ts new file mode 100644 index 0000000000..981b070720 --- /dev/null +++ b/packages/core/src/modules/dids/repository/__tests__/DidRecord.test.ts @@ -0,0 +1,27 @@ +import { DidDocumentRole } from '../../domain/DidDocumentRole' +import { DidRecord } from '../DidRecord' +import { DidRecordMetadataKeys } from '../didRecordMetadataTypes' + +describe('DidRecord', () => { + describe('getTags', () => { + it('should return default tags', () => { + const didRecord = new DidRecord({ + did: 'did:example:123456789abcdefghi', + role: DidDocumentRole.Created, + }) + + didRecord.metadata.set(DidRecordMetadataKeys.LegacyDid, { + didDocumentString: '{}', + unqualifiedDid: 'unqualifiedDid', + }) + + expect(didRecord.getTags()).toEqual({ + role: DidDocumentRole.Created, + method: 'example', + legacyUnqualifiedDid: 'unqualifiedDid', + did: 'did:example:123456789abcdefghi', + methodSpecificIdentifier: '123456789abcdefghi', + }) + }) + }) +}) diff --git a/packages/core/src/modules/dids/repository/didRecordMetadataTypes.ts b/packages/core/src/modules/dids/repository/didRecordMetadataTypes.ts new file mode 100644 index 0000000000..fbab647239 --- /dev/null +++ b/packages/core/src/modules/dids/repository/didRecordMetadataTypes.ts @@ -0,0 +1,10 @@ +export enum DidRecordMetadataKeys { + LegacyDid = '_internal/legacyDid', +} + +export type DidRecordMetadata = { + [DidRecordMetadataKeys.LegacyDid]: { + unqualifiedDid: string + didDocumentString: string + } +} diff --git a/packages/core/src/modules/dids/repository/index.ts b/packages/core/src/modules/dids/repository/index.ts new file mode 100644 index 0000000000..2d9119d818 --- /dev/null +++ b/packages/core/src/modules/dids/repository/index.ts @@ -0,0 +1,2 @@ +export * from './DidRepository' +export * from './DidRecord' diff --git a/packages/core/src/modules/dids/services/DidRegistrarService.ts b/packages/core/src/modules/dids/services/DidRegistrarService.ts new file mode 100644 index 0000000000..3952edd43d --- /dev/null +++ b/packages/core/src/modules/dids/services/DidRegistrarService.ts @@ -0,0 +1,177 @@ +import type { AgentContext } from '../../../agent' +import type { DidRegistrar } from '../domain/DidRegistrar' +import type { + DidCreateOptions, + DidCreateResult, + DidDeactivateOptions, + DidDeactivateResult, + DidUpdateOptions, + DidUpdateResult, +} from '../types' + +import { InjectionSymbols } from '../../../constants' +import { Logger } from '../../../logger' +import { inject, injectable } from '../../../plugins' +import { DidsModuleConfig } from '../DidsModuleConfig' +import { tryParseDid } from '../domain/parse' + +import { DidResolverService } from './DidResolverService' + +@injectable() +export class DidRegistrarService { + private logger: Logger + private didsModuleConfig: DidsModuleConfig + private didResolverService: DidResolverService + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + didsModuleConfig: DidsModuleConfig, + didResolverService: DidResolverService + ) { + this.logger = logger + this.didsModuleConfig = didsModuleConfig + this.didResolverService = didResolverService + } + + public async create( + agentContext: AgentContext, + options: CreateOptions + ): Promise { + this.logger.debug(`creating did ${options.did ?? options.method}`) + + const errorResult = { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: options.did, + }, + } as const + + if ((!options.did && !options.method) || (options.did && options.method)) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: 'Either did OR method must be specified', + }, + } + } + + const method = options.method ?? tryParseDid(options.did as string)?.method + if (!method) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: `Could not extract method from did ${options.did}`, + }, + } + } + + const registrar = this.findRegistrarForMethod(method) + if (!registrar) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: `Unsupported did method: '${method}'`, + }, + } + } + + return await registrar.create(agentContext, options) + } + + public async update(agentContext: AgentContext, options: DidUpdateOptions): Promise { + this.logger.debug(`updating did ${options.did}`) + + const method = tryParseDid(options.did)?.method + + const errorResult = { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: options.did, + }, + } as const + + if (!method) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: `Could not extract method from did ${options.did}`, + }, + } + } + + const registrar = this.findRegistrarForMethod(method) + if (!registrar) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: `Unsupported did method: '${method}'`, + }, + } + } + + // Invalidate cache before updating + await this.didResolverService.invalidateCacheForDid(agentContext, options.did) + + return await registrar.update(agentContext, options) + } + + public async deactivate(agentContext: AgentContext, options: DidDeactivateOptions): Promise { + this.logger.debug(`deactivating did ${options.did}`) + + const errorResult = { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: options.did, + }, + } as const + + const method = tryParseDid(options.did)?.method + if (!method) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: `Could not extract method from did ${options.did}`, + }, + } + } + + const registrar = this.findRegistrarForMethod(method) + if (!registrar) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: `Unsupported did method: '${method}'`, + }, + } + } + + // Invalidate cache before deactivating + await this.didResolverService.invalidateCacheForDid(agentContext, options.did) + + return await registrar.deactivate(agentContext, options) + } + + private findRegistrarForMethod(method: string): DidRegistrar | null { + return this.didsModuleConfig.registrars.find((r) => r.supportedMethods.includes(method)) ?? null + } + + /** + * Get all supported did methods for the did registrar. + */ + public get supportedMethods() { + return Array.from(new Set(this.didsModuleConfig.registrars.flatMap((r) => r.supportedMethods))) + } +} diff --git a/packages/core/src/modules/dids/services/DidResolverService.ts b/packages/core/src/modules/dids/services/DidResolverService.ts new file mode 100644 index 0000000000..f3ad3c41d9 --- /dev/null +++ b/packages/core/src/modules/dids/services/DidResolverService.ts @@ -0,0 +1,183 @@ +import type { AgentContext } from '../../../agent' +import type { DidResolver } from '../domain/DidResolver' +import type { DidResolutionOptions, DidResolutionResult, ParsedDid } from '../types' + +import { InjectionSymbols } from '../../../constants' +import { CredoError } from '../../../error' +import { Logger } from '../../../logger' +import { injectable, inject } from '../../../plugins' +import { JsonTransformer } from '../../../utils' +import { CacheModuleConfig } from '../../cache' +import { DidsModuleConfig } from '../DidsModuleConfig' +import { DidDocument } from '../domain' +import { parseDid } from '../domain/parse' +import { DidRepository } from '../repository' + +@injectable() +export class DidResolverService { + private logger: Logger + private didsModuleConfig: DidsModuleConfig + private didRepository: DidRepository + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + didsModuleConfig: DidsModuleConfig, + didRepository: DidRepository + ) { + this.logger = logger + this.didsModuleConfig = didsModuleConfig + this.didRepository = didRepository + } + + public async resolve( + agentContext: AgentContext, + didUrl: string, + options: DidResolutionOptions = {} + ): Promise { + this.logger.debug(`resolving didUrl ${didUrl}`) + + const result = { + didResolutionMetadata: {}, + didDocument: null, + didDocumentMetadata: {}, + } + + let parsed: ParsedDid + try { + parsed = parseDid(didUrl) + } catch (error) { + return { + ...result, + didResolutionMetadata: { error: 'invalidDid' }, + } + } + + const resolver = this.findResolver(parsed) + if (!resolver) { + return { + ...result, + didResolutionMetadata: { + error: 'unsupportedDidMethod', + message: `No did resolver registered for did method ${parsed.method}`, + }, + } + } + + // extract caching options and set defaults + const { + useCache = true, + cacheDurationInSeconds = 300, + persistInCache = true, + useLocalCreatedDidRecord = true, + } = options + const cacheKey = this.getCacheKey(parsed.did) + + if (resolver.allowsCaching && useCache) { + const cache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache + // FIXME: in multi-tenancy it can be that the same cache is used for different agent contexts + // This may become a problem when resolving dids, as you can get back a cache hit for a different + // tenant. did:peer has disabled caching, and I think we should just recommend disabling caching + // for these private dids + // We could allow providing a custom cache prefix in the resolver options, so that the cache key + // can be recognized in the cache implementation + const cachedDidDocument = await cache.get }>( + agentContext, + cacheKey + ) + + if (cachedDidDocument) { + return { + ...cachedDidDocument, + didDocument: JsonTransformer.fromJSON(cachedDidDocument.didDocument, DidDocument), + didResolutionMetadata: { + ...cachedDidDocument.didResolutionMetadata, + servedFromCache: true, + }, + } + } + } + + if (resolver.allowsLocalDidRecord && useLocalCreatedDidRecord) { + // TODO: did should have tag whether a did document is present in the did record + const [didRecord] = await this.didRepository.getCreatedDids(agentContext, { + did: parsed.did, + }) + + if (didRecord && didRecord.didDocument) { + return { + didDocument: didRecord.didDocument, + didDocumentMetadata: {}, + didResolutionMetadata: { + servedFromCache: false, + servedFromDidRecord: true, + }, + } + } + } + + let resolutionResult = await resolver.resolve(agentContext, parsed.did, parsed, options) + // Avoid overwriting existing document + resolutionResult = { + ...resolutionResult, + didResolutionMetadata: { + ...resolutionResult.didResolutionMetadata, + resolutionTime: Date.now(), + // Did resolver implementation might use did method specific caching strategy + // We only set to false if not defined by the resolver + servedFromCache: resolutionResult.didResolutionMetadata.servedFromCache ?? false, + }, + } + + if (resolutionResult.didDocument && resolver.allowsCaching && persistInCache) { + const cache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache + await cache.set( + agentContext, + cacheKey, + { + ...resolutionResult, + didDocument: resolutionResult.didDocument.toJSON(), + }, + // Set cache duration + cacheDurationInSeconds + ) + } + + return resolutionResult + } + + /** + * Resolve a did document. This uses the default resolution options, and thus + * will use caching if available. + */ + public async resolveDidDocument(agentContext: AgentContext, did: string) { + const { + didDocument, + didResolutionMetadata: { error, message }, + } = await this.resolve(agentContext, did) + + if (!didDocument) { + throw new CredoError(`Unable to resolve did document for did '${did}': ${error} ${message}`) + } + return didDocument + } + + public async invalidateCacheForDid(agentContext: AgentContext, did: string) { + const cache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache + await cache.remove(agentContext, this.getCacheKey(did)) + } + + private getCacheKey(did: string) { + return `did:resolver:${did}` + } + + private findResolver(parsed: ParsedDid): DidResolver | null { + return this.didsModuleConfig.resolvers.find((r) => r.supportedMethods.includes(parsed.method)) ?? null + } + + /** + * Get all supported did methods for the did resolver. + */ + public get supportedMethods() { + return Array.from(new Set(this.didsModuleConfig.resolvers.flatMap((r) => r.supportedMethods))) + } +} diff --git a/packages/core/src/modules/dids/services/__tests__/DidRegistrarService.test.ts b/packages/core/src/modules/dids/services/__tests__/DidRegistrarService.test.ts new file mode 100644 index 0000000000..30fa7507d4 --- /dev/null +++ b/packages/core/src/modules/dids/services/__tests__/DidRegistrarService.test.ts @@ -0,0 +1,213 @@ +import type { DidDocument, DidRegistrar } from '../../domain' +import type { DidResolverService } from '../DidResolverService' + +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../tests/helpers' +import { DidsModuleConfig } from '../../DidsModuleConfig' +import { DidRegistrarService } from '../DidRegistrarService' + +const agentConfig = getAgentConfig('DidResolverService') +const agentContext = getAgentContext() + +const didRegistrarMock = { + supportedMethods: ['key'], + create: jest.fn(), + update: jest.fn(), + deactivate: jest.fn(), +} as DidRegistrar + +const didResolverMock = { + invalidateCacheForDid: jest.fn(), +} as unknown as DidResolverService + +const didRegistrarService = new DidRegistrarService( + agentConfig.logger, + new DidsModuleConfig({ + registrars: [didRegistrarMock], + }), + didResolverMock +) + +describe('DidResolverService', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('create', () => { + it('should correctly find and call the correct registrar for a specified did', async () => { + const returnValue = { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: ':(', + }, + } as const + mockFunction(didRegistrarMock.create).mockResolvedValue(returnValue) + + const result = await didRegistrarService.create(agentContext, { did: 'did:key:xxxx' }) + expect(result).toEqual(returnValue) + + expect(didRegistrarMock.create).toHaveBeenCalledTimes(1) + expect(didRegistrarMock.create).toHaveBeenCalledWith(agentContext, { did: 'did:key:xxxx' }) + }) + + it('should return error state failed if no did or method is provided', async () => { + const result = await didRegistrarService.create(agentContext, {}) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: undefined, + reason: 'Either did OR method must be specified', + }, + }) + }) + + it('should return error state failed if both did and method are provided', async () => { + const result = await didRegistrarService.create(agentContext, { did: 'did:key:xxxx', method: 'key' }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:key:xxxx', + reason: 'Either did OR method must be specified', + }, + }) + }) + + it('should return error state failed if no method could be extracted from the did or method', async () => { + const result = await didRegistrarService.create(agentContext, { did: 'did:a' }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:a', + reason: 'Could not extract method from did did:a', + }, + }) + }) + + it('should return error with state failed if the did has no registrar', async () => { + const result = await didRegistrarService.create(agentContext, { did: 'did:something:123' }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:something:123', + reason: "Unsupported did method: 'something'", + }, + }) + }) + }) + + describe('update', () => { + it('should correctly find and call the correct registrar for a specified did', async () => { + const returnValue = { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: ':(', + }, + } as const + mockFunction(didRegistrarMock.update).mockResolvedValue(returnValue) + + const didDocument = {} as unknown as DidDocument + + const result = await didRegistrarService.update(agentContext, { did: 'did:key:xxxx', didDocument }) + expect(result).toEqual(returnValue) + + expect(didResolverMock.invalidateCacheForDid).toHaveBeenCalledTimes(1) + expect(didRegistrarMock.update).toHaveBeenCalledTimes(1) + expect(didRegistrarMock.update).toHaveBeenCalledWith(agentContext, { did: 'did:key:xxxx', didDocument }) + }) + + it('should return error state failed if no method could be extracted from the did', async () => { + const result = await didRegistrarService.update(agentContext, { did: 'did:a', didDocument: {} as DidDocument }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:a', + reason: 'Could not extract method from did did:a', + }, + }) + }) + + it('should return error with state failed if the did has no registrar', async () => { + const result = await didRegistrarService.update(agentContext, { + did: 'did:something:123', + didDocument: {} as DidDocument, + }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:something:123', + reason: "Unsupported did method: 'something'", + }, + }) + }) + }) + + describe('deactivate', () => { + it('should correctly find and call the correct registrar for a specified did', async () => { + const returnValue = { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: ':(', + }, + } as const + mockFunction(didRegistrarMock.deactivate).mockResolvedValue(returnValue) + + const result = await didRegistrarService.deactivate(agentContext, { did: 'did:key:xxxx' }) + expect(result).toEqual(returnValue) + + expect(didResolverMock.invalidateCacheForDid).toHaveBeenCalledTimes(1) + expect(didRegistrarMock.deactivate).toHaveBeenCalledTimes(1) + expect(didRegistrarMock.deactivate).toHaveBeenCalledWith(agentContext, { did: 'did:key:xxxx' }) + }) + + it('should return error state failed if no method could be extracted from the did', async () => { + const result = await didRegistrarService.deactivate(agentContext, { did: 'did:a' }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:a', + reason: 'Could not extract method from did did:a', + }, + }) + }) + + it('should return error with state failed if the did has no registrar', async () => { + const result = await didRegistrarService.deactivate(agentContext, { did: 'did:something:123' }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:something:123', + reason: "Unsupported did method: 'something'", + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts b/packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts new file mode 100644 index 0000000000..6419c170d6 --- /dev/null +++ b/packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts @@ -0,0 +1,184 @@ +import type { DidResolver } from '../../domain' +import type { DidRepository } from '../../repository' + +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../tests/helpers' +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { CacheModuleConfig, InMemoryLruCache } from '../../../cache' +import { DidsModuleConfig } from '../../DidsModuleConfig' +import didKeyEd25519Fixture from '../../__tests__/__fixtures__/didKeyEd25519.json' +import { DidDocumentRole, DidDocument } from '../../domain' +import { parseDid } from '../../domain/parse' +import { DidRecord } from '../../repository' +import { DidResolverService } from '../DidResolverService' + +const didResolverMock = { + allowsCaching: true, + allowsLocalDidRecord: false, + supportedMethods: ['key'], + resolve: jest.fn(), +} as DidResolver + +const recordResolverMock = { + allowsCaching: false, + allowsLocalDidRecord: true, + supportedMethods: ['record'], + resolve: jest.fn(), +} as DidResolver + +const didRepositoryMock = { + getCreatedDids: jest.fn(), +} as unknown as DidRepository + +const cache = new InMemoryLruCache({ limit: 10 }) +const agentConfig = getAgentConfig('DidResolverService') +const agentContext = getAgentContext({ + registerInstances: [[CacheModuleConfig, new CacheModuleConfig({ cache })]], +}) + +describe('DidResolverService', () => { + const didResolverService = new DidResolverService( + agentConfig.logger, + new DidsModuleConfig({ resolvers: [didResolverMock, recordResolverMock] }), + didRepositoryMock + ) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should correctly find and call the correct resolver for a specified did', async () => { + const returnValue = { + didDocument: JsonTransformer.fromJSON(didKeyEd25519Fixture, DidDocument), + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + } + mockFunction(didResolverMock.resolve).mockResolvedValue(returnValue) + + const result = await didResolverService.resolve(agentContext, 'did:key:xxxx', { someKey: 'string' }) + expect(result).toEqual({ + ...returnValue, + didResolutionMetadata: { + ...returnValue.didResolutionMetadata, + resolutionTime: expect.any(Number), + servedFromCache: false, + }, + }) + + expect(didResolverMock.resolve).toHaveBeenCalledTimes(1) + expect(didResolverMock.resolve).toHaveBeenCalledWith(agentContext, 'did:key:xxxx', parseDid('did:key:xxxx'), { + someKey: 'string', + }) + }) + + it('should return cached did document when resolved multiple times within caching duration', async () => { + const returnValue = { + didDocument: JsonTransformer.fromJSON(didKeyEd25519Fixture, DidDocument), + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + } + mockFunction(didResolverMock.resolve).mockResolvedValue(returnValue) + + const result = await didResolverService.resolve(agentContext, 'did:key:cached', { someKey: 'string' }) + const cachedValue = await cache.get(agentContext, 'did:resolver:did:key:cached') + + expect(result).toEqual({ + ...returnValue, + didResolutionMetadata: { + ...returnValue.didResolutionMetadata, + resolutionTime: expect.any(Number), + servedFromCache: false, + }, + }) + + expect(cachedValue).toEqual({ + ...returnValue, + didDocument: returnValue.didDocument.toJSON(), + didResolutionMetadata: { + ...returnValue.didResolutionMetadata, + resolutionTime: expect.any(Number), + servedFromCache: false, + }, + }) + + expect(didResolverMock.resolve).toHaveBeenCalledTimes(1) + expect(didResolverMock.resolve).toHaveBeenCalledWith(agentContext, 'did:key:cached', parseDid('did:key:cached'), { + someKey: 'string', + }) + + const resultCached = await didResolverService.resolve(agentContext, 'did:key:cached', { someKey: 'string' }) + expect(resultCached).toEqual({ + ...returnValue, + didResolutionMetadata: { + ...returnValue.didResolutionMetadata, + resolutionTime: expect.any(Number), + servedFromCache: true, + }, + }) + + // Still called once because served from cache + expect(didResolverMock.resolve).toHaveBeenCalledTimes(1) + }) + + it('should return local did document from did record when enabled on resolver and present in storage', async () => { + const didDocument = new DidDocument({ + id: 'did:record:stored', + }) + + mockFunction(didRepositoryMock.getCreatedDids).mockResolvedValue([ + new DidRecord({ + did: 'did:record:stored', + didDocument, + role: DidDocumentRole.Created, + }), + ]) + + const result = await didResolverService.resolve(agentContext, 'did:record:stored', { someKey: 'string' }) + + expect(result).toEqual({ + didDocument, + didDocumentMetadata: {}, + didResolutionMetadata: { + servedFromCache: false, + servedFromDidRecord: true, + }, + }) + + expect(didRepositoryMock.getCreatedDids).toHaveBeenCalledTimes(1) + expect(didRepositoryMock.getCreatedDids).toHaveBeenCalledWith(agentContext, { + did: 'did:record:stored', + }) + }) + + it("should return an error with 'invalidDid' if the did string couldn't be parsed", async () => { + const did = 'did:__Asd:asdfa' + + const result = await didResolverService.resolve(agentContext, did) + + expect(result).toEqual({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'invalidDid', + }, + }) + }) + + it("should return an error with 'unsupportedDidMethod' if the did has no resolver", async () => { + const did = 'did:example:asdfa' + + const result = await didResolverService.resolve(agentContext, did) + + expect(result).toEqual({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'unsupportedDidMethod', + message: 'No did resolver registered for did method example', + }, + }) + }) +}) diff --git a/packages/core/src/modules/dids/services/index.ts b/packages/core/src/modules/dids/services/index.ts new file mode 100644 index 0000000000..9c86ace87a --- /dev/null +++ b/packages/core/src/modules/dids/services/index.ts @@ -0,0 +1,2 @@ +export * from './DidResolverService' +export * from './DidRegistrarService' diff --git a/packages/core/src/modules/dids/types.ts b/packages/core/src/modules/dids/types.ts new file mode 100644 index 0000000000..a4d1ee244d --- /dev/null +++ b/packages/core/src/modules/dids/types.ts @@ -0,0 +1,140 @@ +import type { DidDocument } from './domain' +import type { DIDDocumentMetadata, DIDResolutionMetadata, DIDResolutionOptions, ParsedDID } from 'did-resolver' + +export type ParsedDid = ParsedDID +export type DidDocumentMetadata = DIDDocumentMetadata + +export interface DidResolutionOptions extends DIDResolutionOptions { + /** + * Whether to resolve the did document from the cache. + * + * @default true + */ + useCache?: boolean + + /** + * Whether to resolve the did from a local created did document in a DidRecord. + * Cache has precendence over local records, as they're often fater. Records + * served from DidRecords will not be added to the cache. + * + * The resolver must have enabled `allowsLocalDidRecord` (default false) to use this + * feature. + * + * @default true + */ + useLocalCreatedDidRecord?: boolean + + /** + * Whether to persist the did document in the cache. + * + * @default true + */ + persistInCache?: boolean + + /** + * How many seconds to persist the resolved document + * + * @default 3600 + */ + cacheDurationInSeconds?: number +} + +export interface DidResolutionMetadata extends DIDResolutionMetadata { + message?: string + + /** + * Whether the did document was served from the cache + */ + servedFromCache?: boolean + + /** + * Whether the did document was served from a local did record + */ + servedFromDidRecord?: boolean +} + +export interface DidResolutionResult { + didResolutionMetadata: DidResolutionMetadata + didDocument: DidDocument | null + didDocumentMetadata: DidDocumentMetadata +} + +// Based on https://identity.foundation/did-registration +export type DidRegistrationExtraOptions = Record +export type DidRegistrationSecretOptions = Record +export type DidRegistrationMetadata = Record +export type DidDocumentOperation = 'setDidDocument' | 'addToDidDocument' | 'removeFromDidDocument' + +export interface DidOperationStateFinished { + state: 'finished' + did: string + secret?: DidRegistrationSecretOptions + didDocument: DidDocument +} + +export interface DidOperationStateFailed { + state: 'failed' + did?: string + secret?: DidRegistrationSecretOptions + didDocument?: DidDocument + reason: string +} + +export interface DidOperationStateWait { + state: 'wait' + did?: string + secret?: DidRegistrationSecretOptions + didDocument?: DidDocument +} + +export interface DidOperationStateActionBase { + state: 'action' + action: string + did?: string + secret?: DidRegistrationSecretOptions + didDocument?: DidDocument +} +export interface DidCreateOptions { + method?: string + did?: string + options?: DidRegistrationExtraOptions + secret?: DidRegistrationSecretOptions + didDocument?: DidDocument +} + +export interface DidCreateResult< + DidOperationStateAction extends DidOperationStateActionBase = DidOperationStateActionBase +> { + jobId?: string + didState: DidOperationStateWait | DidOperationStateAction | DidOperationStateFinished | DidOperationStateFailed + didRegistrationMetadata: DidRegistrationMetadata + didDocumentMetadata: DidResolutionMetadata +} + +export interface DidUpdateOptions { + did: string + options?: DidRegistrationExtraOptions + secret?: DidRegistrationSecretOptions + didDocumentOperation?: DidDocumentOperation + didDocument: DidDocument | Partial +} + +export interface DidUpdateResult { + jobId?: string + didState: DidOperationStateWait | DidOperationStateActionBase | DidOperationStateFinished | DidOperationStateFailed + didRegistrationMetadata: DidRegistrationMetadata + didDocumentMetadata: DidResolutionMetadata +} + +export interface DidDeactivateOptions { + did: string + options?: DidRegistrationExtraOptions + secret?: DidRegistrationSecretOptions +} + +export interface DidDeactivateResult { + jobId?: string + didState: DidOperationStateWait | DidOperationStateActionBase | DidOperationStateFinished | DidOperationStateFailed + didRegistrationMetadata: DidRegistrationMetadata + didDocumentMetadata: DidResolutionMetadata +} diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts new file mode 100644 index 0000000000..9ba7bd72f4 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts @@ -0,0 +1,13 @@ +import { CredoError } from '../../error' + +export class DifPresentationExchangeError extends CredoError { + public additionalMessages?: Array + + public constructor( + message: string, + { cause, additionalMessages }: { cause?: Error; additionalMessages?: Array } = {} + ) { + super(message, { cause }) + this.additionalMessages = additionalMessages + } +} diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts new file mode 100644 index 0000000000..ee426de40f --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts @@ -0,0 +1,25 @@ +import type { DependencyManager, Module } from '../../plugins' + +import { AgentConfig } from '../../agent/AgentConfig' + +import { DifPresentationExchangeService } from './DifPresentationExchangeService' + +/** + * @public + */ +export class DifPresentationExchangeModule implements Module { + /** + * Registers the dependencies of the presentation-exchange module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The 'DifPresentationExchangeModule' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." + ) + + // service + dependencyManager.registerSingleton(DifPresentationExchangeService) + } +} diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts new file mode 100644 index 0000000000..395159cefd --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -0,0 +1,640 @@ +import type { + DifPexCredentialsForRequest, + DifPexInputDescriptorToCredentials, + DifPresentationExchangeDefinition, + DifPresentationExchangeDefinitionV1, + DifPresentationExchangeDefinitionV2, + DifPresentationExchangeSubmission, + VerifiablePresentation, +} from './models' +import type { PresentationToCreate } from './utils' +import type { AgentContext } from '../../agent' +import type { Query } from '../../storage/StorageService' +import type { VerificationMethod } from '../dids' +import type { SdJwtVcRecord } from '../sd-jwt-vc' +import type { W3cCredentialRecord } from '../vc' +import type { IAnonCredsDataIntegrityService } from '../vc/data-integrity/models/IAnonCredsDataIntegrityService' +import type { + PresentationSignCallBackParams, + SdJwtDecodedVerifiableCredentialWithKbJwtInput, + Validated, + VerifiablePresentationResult, +} from '@sphereon/pex' +import type { InputDescriptorV2 } from '@sphereon/pex-models' +import type { + W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, + W3CVerifiablePresentation, +} from '@sphereon/ssi-types' + +import { PEVersion, PEX, Status } from '@sphereon/pex' +import { injectable } from 'tsyringe' + +import { getJwkFromKey } from '../../crypto' +import { CredoError } from '../../error' +import { Hasher, JsonTransformer } from '../../utils' +import { DidsApi, getKeyFromVerificationMethod } from '../dids' +import { SdJwtVcApi } from '../sd-jwt-vc' +import { + ClaimFormat, + SignatureSuiteRegistry, + W3cCredentialRepository, + W3cCredentialService, + W3cPresentation, +} from '../vc' +import { + AnonCredsDataIntegrityServiceSymbol, + ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE, +} from '../vc/data-integrity/models/IAnonCredsDataIntegrityService' + +import { DifPresentationExchangeError } from './DifPresentationExchangeError' +import { DifPresentationExchangeSubmissionLocation } from './models' +import { + getVerifiablePresentationFromEncoded, + getSphereonOriginalVerifiablePresentation, + getCredentialsForRequest, + getPresentationsToCreate, + getSphereonOriginalVerifiableCredential, +} from './utils' + +/** + * @todo create a public api for using dif presentation exchange + */ +@injectable() +export class DifPresentationExchangeService { + private pex = new PEX({ hasher: Hasher.hash }) + + public constructor(private w3cCredentialService: W3cCredentialService) {} + + public async getCredentialsForRequest( + agentContext: AgentContext, + presentationDefinition: DifPresentationExchangeDefinition + ): Promise { + const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) + return getCredentialsForRequest(this.pex, presentationDefinition, credentialRecords) + } + + /** + * Selects the credentials to use based on the output from `getCredentialsForRequest` + * Use this method if you don't want to manually select the credentials yourself. + */ + public selectCredentialsForRequest( + credentialsForRequest: DifPexCredentialsForRequest + ): DifPexInputDescriptorToCredentials { + if (!credentialsForRequest.areRequirementsSatisfied) { + throw new CredoError('Could not find the required credentials for the presentation submission') + } + + const credentials: DifPexInputDescriptorToCredentials = {} + + for (const requirement of credentialsForRequest.requirements) { + for (const submission of requirement.submissionEntry) { + if (!credentials[submission.inputDescriptorId]) { + credentials[submission.inputDescriptorId] = [] + } + + // We pick the first matching VC if we are auto-selecting + credentials[submission.inputDescriptorId].push(submission.verifiableCredentials[0].credentialRecord) + } + } + + return credentials + } + + public validatePresentationDefinition(presentationDefinition: DifPresentationExchangeDefinition) { + const validation = PEX.validateDefinition(presentationDefinition) + const errorMessages = this.formatValidated(validation) + if (errorMessages.length > 0) { + throw new DifPresentationExchangeError(`Invalid presentation definition`, { additionalMessages: errorMessages }) + } + } + + public validatePresentationSubmission(presentationSubmission: DifPresentationExchangeSubmission) { + const validation = PEX.validateSubmission(presentationSubmission) + const errorMessages = this.formatValidated(validation) + if (errorMessages.length > 0) { + throw new DifPresentationExchangeError(`Invalid presentation submission`, { additionalMessages: errorMessages }) + } + } + + public validatePresentation( + presentationDefinition: DifPresentationExchangeDefinition, + presentation: VerifiablePresentation + ) { + const { errors } = this.pex.evaluatePresentation( + presentationDefinition, + getSphereonOriginalVerifiablePresentation(presentation), + { + limitDisclosureSignatureSuites: ['BbsBlsSignatureProof2020', 'DataIntegrityProof.anoncreds-2023'], + } + ) + + if (errors) { + const errorMessages = this.formatValidated(errors as Validated) + if (errorMessages.length > 0) { + throw new DifPresentationExchangeError(`Invalid presentation`, { additionalMessages: errorMessages }) + } + } + } + + private formatValidated(v: Validated) { + const validated = Array.isArray(v) ? v : [v] + return validated + .filter((r) => r.tag === Status.ERROR) + .map((r) => r.message) + .filter((r): r is string => Boolean(r)) + } + + public async createPresentation( + agentContext: AgentContext, + options: { + credentialsForInputDescriptor: DifPexInputDescriptorToCredentials + presentationDefinition: DifPresentationExchangeDefinition + /** + * Defaults to {@link DifPresentationExchangeSubmissionLocation.PRESENTATION} + */ + presentationSubmissionLocation?: DifPresentationExchangeSubmissionLocation + challenge: string + domain?: string + } + ) { + const { presentationDefinition, domain, challenge } = options + const presentationSubmissionLocation = + options.presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION + + const verifiablePresentationResultsWithFormat: Array<{ + verifiablePresentationResult: VerifiablePresentationResult + claimFormat: PresentationToCreate['claimFormat'] + }> = [] + + const presentationsToCreate = getPresentationsToCreate(options.credentialsForInputDescriptor) + for (const presentationToCreate of presentationsToCreate) { + // We create a presentation for each subject + // Thus for each subject we need to filter all the related input descriptors and credentials + // FIXME: cast to V1, as tsc errors for strange reasons if not + const inputDescriptorIds = presentationToCreate.verifiableCredentials.map((c) => c.inputDescriptorId) + const inputDescriptorsForPresentation = ( + presentationDefinition as DifPresentationExchangeDefinitionV1 + ).input_descriptors.filter((inputDescriptor) => inputDescriptorIds.includes(inputDescriptor.id)) + + // Get all the credentials for the presentation + const credentialsForPresentation = presentationToCreate.verifiableCredentials.map((c) => + getSphereonOriginalVerifiableCredential(c.credential) + ) + + const presentationDefinitionForSubject: DifPresentationExchangeDefinition = { + ...presentationDefinition, + input_descriptors: inputDescriptorsForPresentation, + + // We remove the submission requirements, as it will otherwise fail to create the VP + submission_requirements: undefined, + } + + const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( + presentationDefinitionForSubject, + credentialsForPresentation, + this.getPresentationSignCallback(agentContext, presentationToCreate), + { + proofOptions: { + challenge, + domain, + }, + signatureOptions: {}, + presentationSubmissionLocation: + presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION, + } + ) + + verifiablePresentationResultsWithFormat.push({ + verifiablePresentationResult, + claimFormat: presentationToCreate.claimFormat, + }) + } + + if (verifiablePresentationResultsWithFormat.length === 0) { + throw new DifPresentationExchangeError('No verifiable presentations created') + } + + if (presentationsToCreate.length !== verifiablePresentationResultsWithFormat.length) { + throw new DifPresentationExchangeError('Invalid amount of verifiable presentations created') + } + + const presentationSubmission: DifPresentationExchangeSubmission = { + id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, + definition_id: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.definition_id, + descriptor_map: [], + } + + verifiablePresentationResultsWithFormat.forEach(({ verifiablePresentationResult }, index) => { + const descriptorMap = verifiablePresentationResult.presentationSubmission.descriptor_map.map((d) => { + const descriptor = { ...d } + + // when multiple presentations are submitted, path should be $[0], $[1] + // FIXME: this should be addressed in the PEX/OID4VP lib. + // See https://github.com/Sphereon-Opensource/SIOP-OID4VP/issues/62 + if ( + presentationSubmissionLocation === DifPresentationExchangeSubmissionLocation.EXTERNAL && + verifiablePresentationResultsWithFormat.length > 1 + ) { + descriptor.path = `$[${index}]` + } + + return descriptor + }) + + presentationSubmission.descriptor_map.push(...descriptorMap) + }) + + return { + verifiablePresentations: verifiablePresentationResultsWithFormat.map((resultWithFormat) => + getVerifiablePresentationFromEncoded( + agentContext, + resultWithFormat.verifiablePresentationResult.verifiablePresentation + ) + ), + presentationSubmission, + presentationSubmissionLocation: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmissionLocation, + } + } + + private getSigningAlgorithmFromVerificationMethod( + verificationMethod: VerificationMethod, + suitableAlgorithms?: Array + ) { + const key = getKeyFromVerificationMethod(verificationMethod) + const jwk = getJwkFromKey(key) + + if (suitableAlgorithms) { + const possibleAlgorithms = jwk.supportedSignatureAlgorithms.filter((alg) => suitableAlgorithms?.includes(alg)) + if (!possibleAlgorithms || possibleAlgorithms.length === 0) { + throw new DifPresentationExchangeError( + [ + `Found no suitable signing algorithm.`, + `Algorithms supported by Verification method: ${jwk.supportedSignatureAlgorithms.join(', ')}`, + `Suitable algorithms: ${suitableAlgorithms.join(', ')}`, + ].join('\n') + ) + } + } + + const alg = jwk.supportedSignatureAlgorithms[0] + if (!alg) throw new DifPresentationExchangeError(`No supported algs for key type: ${key.keyType}`) + return alg + } + + private getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition: Array, + inputDescriptorAlgorithms: Array> + ) { + const allDescriptorAlgorithms = inputDescriptorAlgorithms.flat() + const algorithmsSatisfyingDescriptors = allDescriptorAlgorithms.filter((alg) => + inputDescriptorAlgorithms.every((descriptorAlgorithmSet) => descriptorAlgorithmSet.includes(alg)) + ) + + const algorithmsSatisfyingPdAndDescriptorRestrictions = algorithmsSatisfyingDefinition.filter((alg) => + algorithmsSatisfyingDescriptors.includes(alg) + ) + + if ( + algorithmsSatisfyingDefinition.length > 0 && + algorithmsSatisfyingDescriptors.length > 0 && + algorithmsSatisfyingPdAndDescriptorRestrictions.length === 0 + ) { + throw new DifPresentationExchangeError( + `No signature algorithm found for satisfying restrictions of the presentation definition and input descriptors` + ) + } + + if (allDescriptorAlgorithms.length > 0 && algorithmsSatisfyingDescriptors.length === 0) { + throw new DifPresentationExchangeError( + `No signature algorithm found for satisfying restrictions of the input descriptors` + ) + } + + let suitableAlgorithms: Array | undefined + if (algorithmsSatisfyingPdAndDescriptorRestrictions.length > 0) { + suitableAlgorithms = algorithmsSatisfyingPdAndDescriptorRestrictions + } else if (algorithmsSatisfyingDescriptors.length > 0) { + suitableAlgorithms = algorithmsSatisfyingDescriptors + } else if (algorithmsSatisfyingDefinition.length > 0) { + suitableAlgorithms = algorithmsSatisfyingDefinition + } + + return suitableAlgorithms + } + + private getSigningAlgorithmForJwtVc( + presentationDefinition: DifPresentationExchangeDefinitionV1 | DifPresentationExchangeDefinitionV2, + verificationMethod: VerificationMethod + ) { + const algorithmsSatisfyingDefinition = presentationDefinition.format?.jwt_vc?.alg ?? [] + + const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors + .map((descriptor) => (descriptor as InputDescriptorV2).format?.jwt_vc?.alg ?? []) + .filter((alg) => alg.length > 0) + + const suitableAlgorithms = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition, + inputDescriptorAlgorithms + ) + + return this.getSigningAlgorithmFromVerificationMethod(verificationMethod, suitableAlgorithms) + } + + private getProofTypeForLdpVc( + agentContext: AgentContext, + presentationDefinition: DifPresentationExchangeDefinitionV1 | DifPresentationExchangeDefinitionV2, + verificationMethod: VerificationMethod + ) { + const algorithmsSatisfyingDefinition = presentationDefinition.format?.ldp_vc?.proof_type ?? [] + + const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors + .map((descriptor) => (descriptor as InputDescriptorV2).format?.ldp_vc?.proof_type ?? []) + .filter((alg) => alg.length > 0) + + const suitableSignatureSuites = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition, + inputDescriptorAlgorithms + ) + + // For each of the supported algs, find the key types, then find the proof types + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + + const key = getKeyFromVerificationMethod(verificationMethod) + const supportedSignatureSuites = signatureSuiteRegistry.getAllByKeyType(key.keyType) + if (supportedSignatureSuites.length === 0) { + throw new DifPresentationExchangeError( + `Couldn't find a supported signature suite for the given key type '${key.keyType}'` + ) + } + + if (suitableSignatureSuites) { + const foundSignatureSuite = supportedSignatureSuites.find((suite) => + suitableSignatureSuites.includes(suite.proofType) + ) + + if (!foundSignatureSuite) { + throw new DifPresentationExchangeError( + [ + 'No possible signature suite found for the given verification method.', + `Verification method type: ${verificationMethod.type}`, + `Key type: ${key.keyType}`, + `SupportedSignatureSuites: '${supportedSignatureSuites.map((s) => s.proofType).join(', ')}'`, + `SuitableSignatureSuites: ${suitableSignatureSuites.join(', ')}`, + ].join('\n') + ) + } + + return supportedSignatureSuites[0].proofType + } + + return supportedSignatureSuites[0].proofType + } + + /** + * if all submission descriptors have a format of di | ldp, + * and all credentials have an ANONCREDS_DATA_INTEGRITY proof we default to + * signing the presentation using the ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE + */ + private shouldSignUsingAnonCredsDataIntegrity( + presentationToCreate: PresentationToCreate, + presentationSubmission: DifPresentationExchangeSubmission + ) { + if (presentationToCreate.claimFormat !== ClaimFormat.LdpVp) return undefined + + const validDescriptorFormat = presentationSubmission.descriptor_map.every((descriptor) => + [ClaimFormat.DiVc, ClaimFormat.DiVp, ClaimFormat.LdpVc, ClaimFormat.LdpVp].includes( + descriptor.format as ClaimFormat + ) + ) + + const credentialAreSignedUsingAnonCredsDataIntegrity = presentationToCreate.verifiableCredentials.every( + ({ credential }) => { + if (credential.credential.claimFormat !== ClaimFormat.LdpVc) return false + return credential.credential.dataIntegrityCryptosuites.includes(ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE) + } + ) + + return validDescriptorFormat && credentialAreSignedUsingAnonCredsDataIntegrity + } + + private getPresentationSignCallback(agentContext: AgentContext, presentationToCreate: PresentationToCreate) { + return async (callBackParams: PresentationSignCallBackParams) => { + // The created partial proof and presentation, as well as original supplied options + const { + presentation: presentationInput, + options, + presentationDefinition, + presentationSubmission, + } = callBackParams + const { challenge, domain } = options.proofOptions ?? {} + + if (!challenge) { + throw new CredoError('challenge MUST be provided when signing a Verifiable Presentation') + } + + if (presentationToCreate.claimFormat === ClaimFormat.JwtVp) { + if (!presentationToCreate.subjectIds) { + throw new DifPresentationExchangeError(`Cannot create presentation for credentials without subject id`) + } + + // Determine a suitable verification method for the presentation + const verificationMethod = await this.getVerificationMethodForSubjectId( + agentContext, + presentationToCreate.subjectIds[0] + ) + + const w3cPresentation = JsonTransformer.fromJSON(presentationInput, W3cPresentation) + w3cPresentation.holder = verificationMethod.controller + + const signedPresentation = await this.w3cCredentialService.signPresentation(agentContext, { + format: ClaimFormat.JwtVp, + alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), + verificationMethod: verificationMethod.id, + presentation: w3cPresentation, + challenge, + domain, + }) + + return signedPresentation.encoded as W3CVerifiablePresentation + } else if (presentationToCreate.claimFormat === ClaimFormat.LdpVp) { + if (this.shouldSignUsingAnonCredsDataIntegrity(presentationToCreate, presentationSubmission)) { + // make sure the descriptors format properties are set correctly + presentationSubmission.descriptor_map = presentationSubmission.descriptor_map.map((descriptor) => ({ + ...descriptor, + format: 'di_vp', + })) + const anoncredsDataIntegrityService = agentContext.dependencyManager.resolve( + AnonCredsDataIntegrityServiceSymbol + ) + const presentation = await anoncredsDataIntegrityService.createPresentation(agentContext, { + presentationDefinition, + presentationSubmission, + selectedCredentialRecords: presentationToCreate.verifiableCredentials.map((vc) => vc.credential), + challenge, + }) + return { + ...presentation.toJSON(), + presentation_submission: presentationSubmission, + } as unknown as SphereonW3cVerifiablePresentation + } + + if (!presentationToCreate.subjectIds) { + throw new DifPresentationExchangeError(`Cannot create presentation for credentials without subject id`) + } + // Determine a suitable verification method for the presentation + const verificationMethod = await this.getVerificationMethodForSubjectId( + agentContext, + presentationToCreate.subjectIds[0] + ) + + const w3cPresentation = JsonTransformer.fromJSON(presentationInput, W3cPresentation) + w3cPresentation.holder = verificationMethod.controller + + const signedPresentation = await this.w3cCredentialService.signPresentation(agentContext, { + format: ClaimFormat.LdpVp, + // TODO: we should move the check for which proof to use for a presentation to earlier + // as then we know when determining which VPs to submit already if the proof types are supported + // by the verifier, and we can then just add this to the vpToCreate interface + proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), + proofPurpose: 'authentication', + verificationMethod: verificationMethod.id, + presentation: w3cPresentation, + challenge, + domain, + }) + + return signedPresentation.encoded as W3CVerifiablePresentation + } else if (presentationToCreate.claimFormat === ClaimFormat.SdJwtVc) { + const sdJwtInput = presentationInput as SdJwtDecodedVerifiableCredentialWithKbJwtInput + + if (!domain) { + throw new CredoError("Missing 'domain' property, unable to set required 'aud' property in SD-JWT KB-JWT") + } + + const sdJwtVcApi = this.getSdJwtVcApi(agentContext) + const sdJwtVc = await sdJwtVcApi.present({ + compactSdJwtVc: sdJwtInput.compactSdJwtVc, + // SD is already handled by PEX, so we presents all keys + presentationFrame: undefined, + verifierMetadata: { + audience: domain, + nonce: challenge, + // TODO: we should make this optional + issuedAt: Math.floor(Date.now() / 1000), + }, + }) + + return sdJwtVc + } else { + throw new DifPresentationExchangeError( + `Only JWT, SD-JWT-VC, JSONLD credentials are supported for a single presentation` + ) + } + } + } + + private async getVerificationMethodForSubjectId(agentContext: AgentContext, subjectId: string) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + + if (!subjectId.startsWith('did:')) { + throw new DifPresentationExchangeError( + `Only dids are supported as credentialSubject id. ${subjectId} is not a valid did` + ) + } + + const didDocument = await didsApi.resolveDidDocument(subjectId) + + if (!didDocument.authentication || didDocument.authentication.length === 0) { + throw new DifPresentationExchangeError( + `No authentication verificationMethods found for did ${subjectId} in did document` + ) + } + + // the signature suite to use for the presentation is dependant on the credentials we share. + // 1. Get the verification method for this given proof purpose in this DID document + let [verificationMethod] = didDocument.authentication + if (typeof verificationMethod === 'string') { + verificationMethod = didDocument.dereferenceKey(verificationMethod, ['authentication']) + } + + return verificationMethod + } + + /** + * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the + * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. + */ + private async queryCredentialForPresentationDefinition( + agentContext: AgentContext, + presentationDefinition: DifPresentationExchangeDefinition + ): Promise> { + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const w3cQuery: Array> = [] + const sdJwtVcQuery: Array> = [] + const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) + + if (!presentationDefinitionVersion.version) { + throw new DifPresentationExchangeError( + `Unable to determine the Presentation Exchange version from the presentation definition`, + presentationDefinitionVersion.error ? { additionalMessages: [presentationDefinitionVersion.error] } : {} + ) + } + + // FIXME: in the query we should take into account the supported proof types of the verifier + // this could help enormously in the amount of credentials we have to retrieve from storage. + if (presentationDefinitionVersion.version === PEVersion.v1) { + const pd = presentationDefinition as DifPresentationExchangeDefinitionV1 + + // The schema.uri can contain either an expanded type, or a context uri + for (const inputDescriptor of pd.input_descriptors) { + for (const schema of inputDescriptor.schema) { + sdJwtVcQuery.push({ + vct: schema.uri, + }) + w3cQuery.push({ + $or: [{ expandedTypes: [schema.uri] }, { contexts: [schema.uri] }, { types: [schema.uri] }], + }) + } + } + } else if (presentationDefinitionVersion.version === PEVersion.v2) { + // FIXME: As PE version 2 does not have the `schema` anymore, we can't query by schema anymore. + // We probably need + // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. + } else { + throw new DifPresentationExchangeError( + `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` + ) + } + + const allRecords: Array = [] + + // query the wallet ourselves first to avoid the need to query the pex library for all + // credentials for every proof request + const w3cCredentialRecords = + w3cQuery.length > 0 + ? await w3cCredentialRepository.findByQuery(agentContext, { + $or: w3cQuery, + }) + : await w3cCredentialRepository.getAll(agentContext) + + allRecords.push(...w3cCredentialRecords) + + const sdJwtVcApi = this.getSdJwtVcApi(agentContext) + const sdJwtVcRecords = + sdJwtVcQuery.length > 0 + ? await sdJwtVcApi.findAllByQuery({ + $or: sdJwtVcQuery, + }) + : await sdJwtVcApi.getAll() + + allRecords.push(...sdJwtVcRecords) + + return allRecords + } + + private getSdJwtVcApi(agentContext: AgentContext) { + return agentContext.dependencyManager.resolve(SdJwtVcApi) + } +} diff --git a/packages/core/src/modules/dif-presentation-exchange/index.ts b/packages/core/src/modules/dif-presentation-exchange/index.ts new file mode 100644 index 0000000000..4f4e4b3923 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/index.ts @@ -0,0 +1,4 @@ +export * from './DifPresentationExchangeError' +export * from './DifPresentationExchangeModule' +export * from './DifPresentationExchangeService' +export * from './models' diff --git a/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts new file mode 100644 index 0000000000..70dcf4bbfe --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts @@ -0,0 +1,136 @@ +import type { SdJwtVcRecord } from '../../sd-jwt-vc' +import type { ClaimFormat, W3cCredentialRecord } from '../../vc' + +export interface DifPexCredentialsForRequest { + /** + * Whether all requirements have been satisfied by the credentials in the wallet. + */ + areRequirementsSatisfied: boolean + + /** + * The requirements for the presentation definition. If the `areRequirementsSatisfied` value + * is `false`, this list will still be populated with requirements, but won't contain credentials + * for all requirements. This can be useful to display the missing credentials for a presentation + * definition to be satisfied. + * + * NOTE: Presentation definition requirements can be really complex as there's a lot of different + * combinations that are possible. The structure doesn't include all possible combinations yet that + * could satisfy a presentation definition. + */ + requirements: DifPexCredentialsForRequestRequirement[] + + /** + * Name of the presentation definition + */ + name?: string + + /** + * Purpose of the presentation definition. + */ + purpose?: string +} + +/** + * A requirement for the presentation submission. A requirement + * is a group of input descriptors that together fulfill a requirement + * from the presentation definition. + * + * Each submission represents a input descriptor. + */ +export interface DifPexCredentialsForRequestRequirement { + /** + * Whether the requirement is satisfied. + * + * If the requirement is not satisfied, the submission will still contain + * entries, but the `verifiableCredentials` list will be empty. + */ + isRequirementSatisfied: boolean + + /** + * Name of the requirement + */ + name?: string + + /** + * Purpose of the requirement + */ + purpose?: string + + /** + * Array of objects, where each entry contains one or more credentials that will be part + * of the submission. + * + * NOTE: if the `isRequirementSatisfied` is `false` the submission list will + * contain entries where the verifiable credential list is empty. In this case it could also + * contain more entries than are actually needed (as you sometimes can choose from + * e.g. 4 types of credentials and need to submit at least two). If + * `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value + * to see how many of those submissions needed. + */ + submissionEntry: DifPexCredentialsForRequestSubmissionEntry[] + + /** + * The number of submission entries that are needed to fulfill the requirement. + * If `isRequirementSatisfied` is `true`, the submission list will always be equal + * to the number of `needsCount`. If `isRequirementSatisfied` is `false` the list of + * submissions could be longer. + */ + needsCount: number + + /** + * The rule that is used to select the credentials for the submission. + * If the rule is `pick`, the user can select which credentials to use for the submission. + * If the rule is `all`, all credentials that satisfy the input descriptor will be used. + */ + rule: 'pick' | 'all' +} + +/** + * A submission entry that satisfies a specific input descriptor from the + * presentation definition. + */ +export interface DifPexCredentialsForRequestSubmissionEntry { + /** + * The id of the input descriptor + */ + inputDescriptorId: string + + /** + * Name of the input descriptor + */ + name?: string + + /** + * Purpose of the input descriptor + */ + purpose?: string + + /** + * The verifiable credentials that satisfy the input descriptor. + * + * If the value is an empty list, it means the input descriptor could + * not be satisfied. + */ + verifiableCredentials: SubmissionEntryCredential[] +} + +export type SubmissionEntryCredential = + | { + type: ClaimFormat.SdJwtVc + credentialRecord: SdJwtVcRecord + + /** + * The payload that will be disclosed, including always disclosed attributes + * and disclosures for the presentation definition + */ + disclosedPayload: Record + } + | { + type: ClaimFormat.JwtVc | ClaimFormat.LdpVc + credentialRecord: W3cCredentialRecord + } + +/** + * Mapping of selected credentials for an input descriptor + */ +export type DifPexInputDescriptorToCredentials = Record> diff --git a/packages/core/src/modules/dif-presentation-exchange/models/index.ts b/packages/core/src/modules/dif-presentation-exchange/models/index.ts new file mode 100644 index 0000000000..a94c88e6c9 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/models/index.ts @@ -0,0 +1,17 @@ +export * from './DifPexCredentialsForRequest' +import type { SdJwtVc } from '../../sd-jwt-vc' +import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '../../vc' +import type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' + +import { PresentationSubmissionLocation } from '@sphereon/pex' + +// Re-export some types from sphereon library, but under more explicit names +export type DifPresentationExchangeDefinition = PresentationDefinitionV1 | PresentationDefinitionV2 +export type DifPresentationExchangeDefinitionV1 = PresentationDefinitionV1 +export type DifPresentationExchangeDefinitionV2 = PresentationDefinitionV2 +export type DifPresentationExchangeSubmission = PresentationSubmission +export { PresentationSubmissionLocation as DifPresentationExchangeSubmissionLocation } + +// TODO: we might want to move this to another place at some point +export type VerifiablePresentation = W3cVerifiablePresentation | SdJwtVc +export type VerifiableCredential = W3cVerifiableCredential | SdJwtVc diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts new file mode 100644 index 0000000000..606fbcbfb2 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts @@ -0,0 +1,339 @@ +import type { + DifPexCredentialsForRequest, + DifPexCredentialsForRequestRequirement, + DifPexCredentialsForRequestSubmissionEntry, + SubmissionEntryCredential, +} from '../models' +import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch, PEX } from '@sphereon/pex' +import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' + +import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode' +import { Rules } from '@sphereon/pex-models' +import { default as jp } from 'jsonpath' + +import { CredoError } from '../../../error' +import { deepEquality, Hasher } from '../../../utils' +import { SdJwtVcRecord } from '../../sd-jwt-vc' +import { ClaimFormat, W3cCredentialRecord } from '../../vc' +import { DifPresentationExchangeError } from '../DifPresentationExchangeError' + +import { getSphereonOriginalVerifiableCredential } from './transform' + +export async function getCredentialsForRequest( + // PEX instance with hasher defined + pex: PEX, + presentationDefinition: IPresentationDefinition, + credentialRecords: Array +): Promise { + const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c)) + const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials) + + const selectResults = { + ...selectResultsRaw, + // Map the encoded credential to their respective w3c credential record + verifiableCredential: selectResultsRaw.verifiableCredential?.map((selectedEncoded): SubmissionEntryCredential => { + const credentialRecordIndex = encodedCredentials.findIndex((encoded) => { + if ( + typeof selectedEncoded === 'string' && + selectedEncoded.includes('~') && + typeof encoded === 'string' && + encoded.includes('~') + ) { + // FIXME: pex applies SD-JWT, so we actually can't match the record anymore :( + // We take the first part of the sd-jwt, as that will never change, and should + // be unique on it's own + const [encodedJwt] = encoded.split('~') + const [selectedEncodedJwt] = selectedEncoded.split('~') + + return encodedJwt === selectedEncodedJwt + } else { + return deepEquality(selectedEncoded, encoded) + } + }) + + if (credentialRecordIndex === -1) { + throw new DifPresentationExchangeError('Unable to find credential in credential records.') + } + + const credentialRecord = credentialRecords[credentialRecordIndex] + if (credentialRecord instanceof SdJwtVcRecord) { + // selectedEncoded always string when SdJwtVcRecord + // Get the decoded payload from the the selected credential, this already has SD applied + const { jwt, disclosures } = decodeSdJwtSync(selectedEncoded as string, Hasher.hash) + const prettyClaims = getClaimsSync(jwt.payload, disclosures, Hasher.hash) + + return { + type: ClaimFormat.SdJwtVc, + credentialRecord, + disclosedPayload: prettyClaims as Record, + } + } else if (credentialRecord instanceof W3cCredentialRecord) { + return { + type: credentialRecord.credential.claimFormat, + credentialRecord, + } + } else { + throw new CredoError(`Unrecognized credential record type`) + } + }), + } + + const presentationSubmission: DifPexCredentialsForRequest = { + requirements: [], + areRequirementsSatisfied: false, + name: presentationDefinition.name, + purpose: presentationDefinition.purpose, + } + + // If there's no submission requirements, ALL input descriptors MUST be satisfied + if (!presentationDefinition.submission_requirements || presentationDefinition.submission_requirements.length === 0) { + presentationSubmission.requirements = getSubmissionRequirementsForAllInputDescriptors( + presentationDefinition.input_descriptors, + selectResults + ) + } else { + presentationSubmission.requirements = getSubmissionRequirements(presentationDefinition, selectResults) + } + + // There may be no requirements if we filter out all optional ones. To not makes things too complicated, we see it as an error + // for now if a request is made that has no required requirements (but only e.g. min: 0, which means we don't need to disclose anything) + // I see this more as the fault of the presentation definition, as it should have at least some requirements. + if (presentationSubmission.requirements.length === 0) { + throw new DifPresentationExchangeError( + 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' + ) + } + if (selectResultsRaw.areRequiredCredentialsPresent === 'error') { + return presentationSubmission + } + + return { + ...presentationSubmission, + + // If all requirements are satisfied, the presentation submission is satisfied + areRequirementsSatisfied: presentationSubmission.requirements.every( + (requirement) => requirement.isRequirementSatisfied + ), + } +} + +function getSubmissionRequirements( + presentationDefinition: IPresentationDefinition, + selectResults: CredentialRecordSelectResults +): Array { + const submissionRequirements: Array = [] + + // There are submission requirements, so we need to select the input_descriptors + // based on the submission requirements + for (const submissionRequirement of presentationDefinition.submission_requirements ?? []) { + // Check: if the submissionRequirement uses `from_nested`, as we don't support this yet + if (submissionRequirement.from_nested) { + throw new DifPresentationExchangeError( + "Presentation definition contains requirement using 'from_nested', which is not supported yet." + ) + } + + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) { + throw new DifPresentationExchangeError("Missing 'from' in submission requirement match") + } + + if (submissionRequirement.rule === Rules.All) { + const selectedSubmission = getSubmissionRequirementRuleAll( + submissionRequirement, + presentationDefinition, + selectResults + ) + submissionRequirements.push(selectedSubmission) + } else { + const selectedSubmission = getSubmissionRequirementRulePick( + submissionRequirement, + presentationDefinition, + selectResults + ) + + submissionRequirements.push(selectedSubmission) + } + } + + // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) + // We use minimization strategy, and thus only disclose the minimum amount of information + const requirementsWithCredentials = submissionRequirements.filter((requirement) => requirement.needsCount > 0) + + return requirementsWithCredentials +} + +function getSubmissionRequirementsForAllInputDescriptors( + inputDescriptors: Array | Array, + selectResults: CredentialRecordSelectResults +): Array { + const submissionRequirements: Array = [] + + for (const inputDescriptor of inputDescriptors) { + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + submissionRequirements.push({ + rule: Rules.Pick, + needsCount: 1, // Every input descriptor is a distinct requirement, so the count is always 1, + submissionEntry: [submission], + isRequirementSatisfied: submission.verifiableCredentials.length >= 1, + }) + } + + return submissionRequirements +} + +function getSubmissionRequirementRuleAll( + submissionRequirement: SubmissionRequirement, + presentationDefinition: IPresentationDefinition, + selectResults: CredentialRecordSelectResults +) { + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) + throw new DifPresentationExchangeError("Missing 'from' in submission requirement match.") + + const selectedSubmission: DifPexCredentialsForRequestRequirement = { + rule: Rules.All, + needsCount: 0, + name: submissionRequirement.name, + purpose: submissionRequirement.purpose, + submissionEntry: [], + isRequirementSatisfied: false, + } + + for (const inputDescriptor of presentationDefinition.input_descriptors) { + // We only want to get the submission if the input descriptor belongs to the group + if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + // Rule ALL, so for every input descriptor that matches in this group, we need to add it + selectedSubmission.needsCount += 1 + selectedSubmission.submissionEntry.push(submission) + } + + return { + ...selectedSubmission, + + // If all submissions have a credential, the requirement is satisfied + isRequirementSatisfied: selectedSubmission.submissionEntry.every( + (submission) => submission.verifiableCredentials.length >= 1 + ), + } +} + +function getSubmissionRequirementRulePick( + submissionRequirement: SubmissionRequirement, + presentationDefinition: IPresentationDefinition, + selectResults: CredentialRecordSelectResults +) { + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) { + throw new DifPresentationExchangeError("Missing 'from' in submission requirement match.") + } + + const selectedSubmission: DifPexCredentialsForRequestRequirement = { + rule: Rules.Pick, + needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1, + name: submissionRequirement.name, + purpose: submissionRequirement.purpose, + // If there's no count, min, or max we assume one credential is required for submission + // however, the exact behavior is not specified in the spec + submissionEntry: [], + isRequirementSatisfied: false, + } + + const satisfiedSubmissions: Array = [] + const unsatisfiedSubmissions: Array = [] + + for (const inputDescriptor of presentationDefinition.input_descriptors) { + // We only want to get the submission if the input descriptor belongs to the group + if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + if (submission.verifiableCredentials.length >= 1) { + satisfiedSubmissions.push(submission) + } else { + unsatisfiedSubmissions.push(submission) + } + + // If we have found enough credentials to satisfy the requirement, we could stop + // but the user may not want the first x that match, so we continue and return all matches + // if (satisfiedSubmissions.length === selectedSubmission.needsCount) break + } + + return { + ...selectedSubmission, + + // If there are enough satisfied submissions, the requirement is satisfied + isRequirementSatisfied: satisfiedSubmissions.length >= selectedSubmission.needsCount, + + // if the requirement is satisfied, we only need to return the satisfied submissions + // however if the requirement is not satisfied, we include all entries so the wallet could + // render which credentials are missing. + submission: + satisfiedSubmissions.length >= selectedSubmission.needsCount + ? satisfiedSubmissions + : [...satisfiedSubmissions, ...unsatisfiedSubmissions], + } +} + +function getSubmissionForInputDescriptor( + inputDescriptor: InputDescriptorV1 | InputDescriptorV2, + selectResults: CredentialRecordSelectResults +): DifPexCredentialsForRequestSubmissionEntry { + // https://github.com/Sphereon-Opensource/PEX/issues/116 + // If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it + const matchesForInputDescriptor = selectResults.matches?.filter( + (m) => + m.name === inputDescriptor.id || + // FIXME: this is not collision proof as the name doesn't have to be unique + m.name === inputDescriptor.name + ) + + const submissionEntry: DifPexCredentialsForRequestSubmissionEntry = { + inputDescriptorId: inputDescriptor.id, + name: inputDescriptor.name, + purpose: inputDescriptor.purpose, + verifiableCredentials: [], + } + + // return early if no matches. + if (!matchesForInputDescriptor?.length) return submissionEntry + + // FIXME: This can return multiple credentials for multiple input_descriptors, + // which I think is a bug in the PEX library + // Extract all credentials from the match + const verifiableCredentials = matchesForInputDescriptor.flatMap((matchForInputDescriptor) => + extractCredentialsFromMatch(matchForInputDescriptor, selectResults.verifiableCredential) + ) + + submissionEntry.verifiableCredentials = verifiableCredentials + + return submissionEntry +} + +function extractCredentialsFromMatch( + match: SubmissionRequirementMatch, + availableCredentials?: SubmissionEntryCredential[] +) { + const verifiableCredentials: SubmissionEntryCredential[] = [] + + for (const vcPath of match.vc_path) { + const [verifiableCredential] = jp.query( + { verifiableCredential: availableCredentials }, + vcPath + ) as SubmissionEntryCredential[] + verifiableCredentials.push(verifiableCredential) + } + + return verifiableCredentials +} + +/** + * Custom SelectResults that includes the Credo records instead of the encoded verifiable credential + */ +type CredentialRecordSelectResults = Omit & { + verifiableCredential?: SubmissionEntryCredential[] +} diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/index.ts b/packages/core/src/modules/dif-presentation-exchange/utils/index.ts new file mode 100644 index 0000000000..18fe3ad53c --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/utils/index.ts @@ -0,0 +1,3 @@ +export * from './transform' +export * from './credentialSelection' +export * from './presentationsToCreate' diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/presentationsToCreate.ts b/packages/core/src/modules/dif-presentation-exchange/utils/presentationsToCreate.ts new file mode 100644 index 0000000000..17c17e01dd --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/utils/presentationsToCreate.ts @@ -0,0 +1,86 @@ +import type { SdJwtVcRecord } from '../../sd-jwt-vc' +import type { DifPexInputDescriptorToCredentials } from '../models' + +import { W3cCredentialRecord, ClaimFormat } from '../../vc' + +// - the credentials included in the presentation +export interface SdJwtVcPresentationToCreate { + claimFormat: ClaimFormat.SdJwtVc + subjectIds: [] // subject is included in the cnf of the sd-jwt and automatically extracted by PEX + verifiableCredentials: [ + { + credential: SdJwtVcRecord + inputDescriptorId: string + } + ] // only one credential supported for SD-JWT-VC +} + +export interface JwtVpPresentationToCreate { + claimFormat: ClaimFormat.JwtVp + subjectIds: [string] // only one subject id supported for JWT VP + verifiableCredentials: Array<{ + credential: W3cCredentialRecord + inputDescriptorId: string + }> // multiple credentials supported for JWT VP +} + +export interface LdpVpPresentationToCreate { + claimFormat: ClaimFormat.LdpVp + // NOTE: we only support one subject id at the moment as we don't have proper + // support yet for adding multiple proofs to an LDP-VP + subjectIds: undefined | [string] + verifiableCredentials: Array<{ + credential: W3cCredentialRecord + inputDescriptorId: string + }> // multiple credentials supported for LDP VP +} + +export type PresentationToCreate = SdJwtVcPresentationToCreate | JwtVpPresentationToCreate | LdpVpPresentationToCreate + +// FIXME: we should extract supported format form top-level presentation definition, and input_descriptor as well +// to make sure the presentation we are going to create is a presentation format supported by the verifier. +// In addition we should allow to pass an override 'format' object, as specification like OID4VP do not use the +// PD formats, but define their own. +export function getPresentationsToCreate(credentialsForInputDescriptor: DifPexInputDescriptorToCredentials) { + const presentationsToCreate: Array = [] + + // We map all credentials for a input descriptor to the different subject ids. Each subjectId will need + // to create a separate proof (either on the same presentation or if not allowed by proof format on separate) + // presentations + for (const [inputDescriptorId, credentials] of Object.entries(credentialsForInputDescriptor)) { + for (const credential of credentials) { + if (credential instanceof W3cCredentialRecord) { + const subjectId = credential.credential.credentialSubjectIds[0] + + // NOTE: we only support one subjectId per VP -- once we have proper support + // for multiple proofs on an LDP-VP we can add multiple subjectIds to a single VP for LDP-vp only + const expectedClaimFormat = + credential.credential.claimFormat === ClaimFormat.LdpVc ? ClaimFormat.LdpVp : ClaimFormat.JwtVp + + const matchingClaimFormatAndSubject = presentationsToCreate.find( + (p): p is JwtVpPresentationToCreate => + p.claimFormat === expectedClaimFormat && Boolean(p.subjectIds?.includes(subjectId)) + ) + + if (matchingClaimFormatAndSubject) { + matchingClaimFormatAndSubject.verifiableCredentials.push({ inputDescriptorId, credential }) + } else { + presentationsToCreate.push({ + claimFormat: expectedClaimFormat, + subjectIds: [subjectId], + verifiableCredentials: [{ credential, inputDescriptorId }], + }) + } + } else { + // SD-JWT-VC always needs it's own presentation + presentationsToCreate.push({ + claimFormat: ClaimFormat.SdJwtVc, + subjectIds: [], + verifiableCredentials: [{ inputDescriptorId, credential }], + }) + } + } + } + + return presentationsToCreate +} diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts new file mode 100644 index 0000000000..7748ec7d65 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts @@ -0,0 +1,54 @@ +import type { AgentContext } from '../../../agent' +import type { SdJwtVcRecord } from '../../sd-jwt-vc' +import type { W3cJsonPresentation } from '../../vc/models/presentation/W3cJsonPresentation' +import type { VerifiablePresentation } from '../models' +import type { + OriginalVerifiableCredential as SphereonOriginalVerifiableCredential, + OriginalVerifiablePresentation as SphereonOriginalVerifiablePresentation, + W3CVerifiablePresentation as SphereonW3CVerifiablePresentation, +} from '@sphereon/ssi-types' + +import { CredoError } from '../../../error' +import { JsonTransformer } from '../../../utils' +import { SdJwtVcApi } from '../../sd-jwt-vc' +import { W3cCredentialRecord, W3cJsonLdVerifiablePresentation, W3cJwtVerifiablePresentation } from '../../vc' + +export function getSphereonOriginalVerifiableCredential( + credentialRecord: W3cCredentialRecord | SdJwtVcRecord +): SphereonOriginalVerifiableCredential { + if (credentialRecord instanceof W3cCredentialRecord) { + return credentialRecord.credential.encoded as SphereonOriginalVerifiableCredential + } else { + return credentialRecord.compactSdJwtVc + } +} + +export function getSphereonOriginalVerifiablePresentation( + verifiablePresentation: VerifiablePresentation +): SphereonOriginalVerifiablePresentation { + if ( + verifiablePresentation instanceof W3cJwtVerifiablePresentation || + verifiablePresentation instanceof W3cJsonLdVerifiablePresentation + ) { + return verifiablePresentation.encoded as SphereonOriginalVerifiablePresentation + } else { + return verifiablePresentation.compact + } +} + +// TODO: we might want to move this to some generic vc transformation util +export function getVerifiablePresentationFromEncoded( + agentContext: AgentContext, + encodedVerifiablePresentation: string | W3cJsonPresentation | SphereonW3CVerifiablePresentation +) { + if (typeof encodedVerifiablePresentation === 'string' && encodedVerifiablePresentation.includes('~')) { + const sdJwtVcApi = agentContext.dependencyManager.resolve(SdJwtVcApi) + return sdJwtVcApi.fromCompact(encodedVerifiablePresentation) + } else if (typeof encodedVerifiablePresentation === 'string') { + return W3cJwtVerifiablePresentation.fromSerializedJwt(encodedVerifiablePresentation) + } else if (typeof encodedVerifiablePresentation === 'object' && '@context' in encodedVerifiablePresentation) { + return JsonTransformer.fromJSON(encodedVerifiablePresentation, W3cJsonLdVerifiablePresentation) + } else { + throw new CredoError('Unsupported verifiable presentation format') + } +} diff --git a/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts b/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts new file mode 100644 index 0000000000..e2b7487b92 --- /dev/null +++ b/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts @@ -0,0 +1,168 @@ +import type { + DiscloseFeaturesOptions, + QueryFeaturesOptions, + DiscoverFeaturesServiceMap, +} from './DiscoverFeaturesApiOptions' +import type { DiscoverFeaturesDisclosureReceivedEvent } from './DiscoverFeaturesEvents' +import type { DiscoverFeaturesService } from './services' +import type { Feature } from '../../agent/models' + +import { firstValueFrom, of, ReplaySubject, Subject } from 'rxjs' +import { catchError, filter, first, map, takeUntil, timeout } from 'rxjs/operators' + +import { AgentContext } from '../../agent' +import { EventEmitter } from '../../agent/EventEmitter' +import { MessageSender } from '../../agent/MessageSender' +import { OutboundMessageContext } from '../../agent/models' +import { InjectionSymbols } from '../../constants' +import { CredoError } from '../../error' +import { inject, injectable } from '../../plugins' +import { ConnectionService } from '../connections/services' + +import { DiscoverFeaturesEventTypes } from './DiscoverFeaturesEvents' +import { DiscoverFeaturesModuleConfig } from './DiscoverFeaturesModuleConfig' +import { V1DiscoverFeaturesService, V2DiscoverFeaturesService } from './protocol' + +export interface QueryFeaturesReturnType { + features?: Feature[] +} + +export interface DiscoverFeaturesApi { + queryFeatures(options: QueryFeaturesOptions): Promise + discloseFeatures(options: DiscloseFeaturesOptions): Promise +} +@injectable() +export class DiscoverFeaturesApi< + DFSs extends DiscoverFeaturesService[] = [V1DiscoverFeaturesService, V2DiscoverFeaturesService] +> implements DiscoverFeaturesApi +{ + /** + * Configuration for Discover Features module + */ + public readonly config: DiscoverFeaturesModuleConfig + + private connectionService: ConnectionService + private messageSender: MessageSender + private eventEmitter: EventEmitter + private stop$: Subject + private agentContext: AgentContext + private serviceMap: DiscoverFeaturesServiceMap + + public constructor( + connectionService: ConnectionService, + messageSender: MessageSender, + v1Service: V1DiscoverFeaturesService, + v2Service: V2DiscoverFeaturesService, + eventEmitter: EventEmitter, + @inject(InjectionSymbols.Stop$) stop$: Subject, + agentContext: AgentContext, + config: DiscoverFeaturesModuleConfig + ) { + this.connectionService = connectionService + this.messageSender = messageSender + this.eventEmitter = eventEmitter + this.stop$ = stop$ + this.agentContext = agentContext + this.config = config + + // Dynamically build service map. This will be extracted once services are registered dynamically + this.serviceMap = [v1Service, v2Service].reduce( + (serviceMap, service) => ({ + ...serviceMap, + [service.version]: service, + }), + {} + ) as DiscoverFeaturesServiceMap + } + + public getService(protocolVersion: PVT): DiscoverFeaturesService { + if (!this.serviceMap[protocolVersion]) { + throw new CredoError(`No discover features service registered for protocol version ${protocolVersion}`) + } + + return this.serviceMap[protocolVersion] as unknown as DiscoverFeaturesService + } + + /** + * Send a query to an existing connection for discovering supported features of any kind. If desired, do the query synchronously, + * meaning that it will await the response (or timeout) before resolving. Otherwise, response can be hooked by subscribing to + * {DiscoverFeaturesDisclosureReceivedEvent}. + * + * Note: V1 protocol only supports a single query and is limited to protocols + * + * @param options feature queries to perform, protocol version and optional comment string (only used + * in V1 protocol). If awaitDisclosures is set, perform the query synchronously with awaitDisclosuresTimeoutMs timeout. + */ + public async queryFeatures(options: QueryFeaturesOptions) { + const service = this.getService(options.protocolVersion) + + const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + + const { message: queryMessage } = await service.createQuery({ + queries: options.queries, + comment: options.comment, + }) + + const outboundMessageContext = new OutboundMessageContext(queryMessage, { + agentContext: this.agentContext, + connection, + }) + + const replaySubject = new ReplaySubject(1) + if (options.awaitDisclosures) { + // Listen for response to our feature query + this.eventEmitter + .observable(DiscoverFeaturesEventTypes.DisclosureReceived) + .pipe( + // Stop when the agent shuts down + takeUntil(this.stop$), + // filter by connection id + filter((e) => e.payload.connection?.id === connection.id), + // Return disclosures + map((e) => e.payload.disclosures), + // Only wait for first event that matches the criteria + first(), + // If we don't have an answer in timeoutMs miliseconds (no response, not supported, etc...) error + timeout({ + first: options.awaitDisclosuresTimeoutMs ?? 7000, + meta: 'DiscoverFeaturesApi.queryFeatures', + }), // TODO: Harmonize default timeouts across the framework + // We want to return false if an error occurred + catchError(() => of([])) + ) + .subscribe(replaySubject) + } + + await this.messageSender.sendMessage(outboundMessageContext) + + return { features: options.awaitDisclosures ? await firstValueFrom(replaySubject) : undefined } + } + + /** + * Disclose features to a connection, either proactively or as a response to a query. + * + * Features are disclosed based on queries that will be performed to Agent's Feature Registry, + * meaning that they should be registered prior to disclosure. When sending disclosure as response, + * these queries will usually match those from the corresponding Query or Queries message. + * + * Note: V1 protocol only supports sending disclosures as a response to a query. + * + * @param options multiple properties like protocol version to use, disclosure queries and thread id + * (in case of disclosure as response to query) + */ + public async discloseFeatures(options: DiscloseFeaturesOptions) { + const service = this.getService(options.protocolVersion) + + const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + const { message: disclosuresMessage } = await service.createDisclosure({ + disclosureQueries: options.disclosureQueries, + threadId: options.threadId, + }) + + const outboundMessageContext = new OutboundMessageContext(disclosuresMessage, { + agentContext: this.agentContext, + connection, + }) + await this.messageSender.sendMessage(outboundMessageContext) + } +} diff --git a/packages/core/src/modules/discover-features/DiscoverFeaturesApiOptions.ts b/packages/core/src/modules/discover-features/DiscoverFeaturesApiOptions.ts new file mode 100644 index 0000000000..11bdf538b7 --- /dev/null +++ b/packages/core/src/modules/discover-features/DiscoverFeaturesApiOptions.ts @@ -0,0 +1,45 @@ +import type { DiscoverFeaturesService } from './services' +import type { FeatureQueryOptions } from '../../agent/models' + +/** + * Get the supported protocol versions based on the provided discover features services. + */ +export type DiscoverFeaturesProtocolVersionType = DFSs[number]['version'] + +/** + * Get the service map for usage in the discover features module. Will return a type mapping of protocol version to service. + * + * @example + * ``` + * type ServiceMap = DiscoverFeaturesServiceMap<[V1DiscoverFeaturesService,V2DiscoverFeaturesService]> + * + * // equal to + * type ServiceMap = { + * v1: V1DiscoverFeatureService + * v2: V2DiscoverFeaturesService + * } + * ``` + */ +export type DiscoverFeaturesServiceMap = { + [DFS in DFSs[number] as DFS['version']]: DiscoverFeaturesService +} + +interface BaseOptions { + connectionId: string +} + +export interface QueryFeaturesOptions + extends BaseOptions { + protocolVersion: DiscoverFeaturesProtocolVersionType + queries: FeatureQueryOptions[] + awaitDisclosures?: boolean + awaitDisclosuresTimeoutMs?: number + comment?: string +} + +export interface DiscloseFeaturesOptions + extends BaseOptions { + protocolVersion: DiscoverFeaturesProtocolVersionType + disclosureQueries: FeatureQueryOptions[] + threadId?: string +} diff --git a/packages/core/src/modules/discover-features/DiscoverFeaturesEvents.ts b/packages/core/src/modules/discover-features/DiscoverFeaturesEvents.ts new file mode 100644 index 0000000000..2e4340ffbc --- /dev/null +++ b/packages/core/src/modules/discover-features/DiscoverFeaturesEvents.ts @@ -0,0 +1,31 @@ +import type { AgentMessage } from '../../agent/AgentMessage' +import type { BaseEvent } from '../../agent/Events' +import type { Feature, FeatureQueryOptions } from '../../agent/models' +import type { ConnectionRecord } from '../connections' + +export enum DiscoverFeaturesEventTypes { + QueryReceived = 'QueryReceived', + DisclosureReceived = 'DisclosureReceived', +} + +export interface DiscoverFeaturesQueryReceivedEvent extends BaseEvent { + type: typeof DiscoverFeaturesEventTypes.QueryReceived + payload: { + message: AgentMessage + queries: FeatureQueryOptions[] + protocolVersion: string + connection: ConnectionRecord + threadId: string + } +} + +export interface DiscoverFeaturesDisclosureReceivedEvent extends BaseEvent { + type: typeof DiscoverFeaturesEventTypes.DisclosureReceived + payload: { + message: AgentMessage + disclosures: Feature[] + protocolVersion: string + connection: ConnectionRecord + threadId: string + } +} diff --git a/packages/core/src/modules/discover-features/DiscoverFeaturesModule.ts b/packages/core/src/modules/discover-features/DiscoverFeaturesModule.ts new file mode 100644 index 0000000000..25c67f6d1d --- /dev/null +++ b/packages/core/src/modules/discover-features/DiscoverFeaturesModule.ts @@ -0,0 +1,43 @@ +import type { DiscoverFeaturesModuleConfigOptions } from './DiscoverFeaturesModuleConfig' +import type { FeatureRegistry } from '../../agent/FeatureRegistry' +import type { DependencyManager, Module } from '../../plugins' + +import { Protocol } from '../../agent/models' + +import { DiscoverFeaturesApi } from './DiscoverFeaturesApi' +import { DiscoverFeaturesModuleConfig } from './DiscoverFeaturesModuleConfig' +import { V1DiscoverFeaturesService } from './protocol/v1' +import { V2DiscoverFeaturesService } from './protocol/v2' + +export class DiscoverFeaturesModule implements Module { + public readonly api = DiscoverFeaturesApi + public readonly config: DiscoverFeaturesModuleConfig + + public constructor(config?: DiscoverFeaturesModuleConfigOptions) { + this.config = new DiscoverFeaturesModuleConfig(config) + } + + /** + * Registers the dependencies of the discover features module on the dependency manager. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Config + dependencyManager.registerInstance(DiscoverFeaturesModuleConfig, this.config) + + // Services + dependencyManager.registerSingleton(V1DiscoverFeaturesService) + dependencyManager.registerSingleton(V2DiscoverFeaturesService) + + // Features + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/discover-features/1.0', + roles: ['requester', 'responder'], + }), + new Protocol({ + id: 'https://didcomm.org/discover-features/2.0', + roles: ['requester', 'responder'], + }) + ) + } +} diff --git a/packages/core/src/modules/discover-features/DiscoverFeaturesModuleConfig.ts b/packages/core/src/modules/discover-features/DiscoverFeaturesModuleConfig.ts new file mode 100644 index 0000000000..9417b5c213 --- /dev/null +++ b/packages/core/src/modules/discover-features/DiscoverFeaturesModuleConfig.ts @@ -0,0 +1,25 @@ +/** + * DiscoverFeaturesModuleConfigOptions defines the interface for the options of the DiscoverFeaturesModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface DiscoverFeaturesModuleConfigOptions { + /** + * Whether to automatically accept feature queries. Applies to all protocol versions. + * + * @default true + */ + autoAcceptQueries?: boolean +} + +export class DiscoverFeaturesModuleConfig { + private options: DiscoverFeaturesModuleConfigOptions + + public constructor(options?: DiscoverFeaturesModuleConfigOptions) { + this.options = options ?? {} + } + + /** {@inheritDoc DiscoverFeaturesModuleConfigOptions.autoAcceptQueries} */ + public get autoAcceptQueries() { + return this.options.autoAcceptQueries ?? true + } +} diff --git a/packages/core/src/modules/discover-features/DiscoverFeaturesServiceOptions.ts b/packages/core/src/modules/discover-features/DiscoverFeaturesServiceOptions.ts new file mode 100644 index 0000000000..5dcbb04bdc --- /dev/null +++ b/packages/core/src/modules/discover-features/DiscoverFeaturesServiceOptions.ts @@ -0,0 +1,16 @@ +import type { AgentMessage } from '../../agent/AgentMessage' +import type { FeatureQueryOptions } from '../../agent/models' + +export interface CreateQueryOptions { + queries: FeatureQueryOptions[] + comment?: string +} + +export interface CreateDisclosureOptions { + disclosureQueries: FeatureQueryOptions[] + threadId?: string +} + +export interface DiscoverFeaturesProtocolMsgReturnType { + message: MessageType +} diff --git a/packages/core/src/modules/discover-features/__tests__/DiscoverFeaturesModule.test.ts b/packages/core/src/modules/discover-features/__tests__/DiscoverFeaturesModule.test.ts new file mode 100644 index 0000000000..d7aa96511e --- /dev/null +++ b/packages/core/src/modules/discover-features/__tests__/DiscoverFeaturesModule.test.ts @@ -0,0 +1,36 @@ +import { FeatureRegistry } from '../../../agent/FeatureRegistry' +import { Protocol } from '../../../agent/models' +import { DependencyManager } from '../../../plugins/DependencyManager' +import { DiscoverFeaturesModule } from '../DiscoverFeaturesModule' +import { V1DiscoverFeaturesService } from '../protocol/v1' +import { V2DiscoverFeaturesService } from '../protocol/v2' + +jest.mock('../../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +jest.mock('../../../agent/FeatureRegistry') +const FeatureRegistryMock = FeatureRegistry as jest.Mock + +const dependencyManager = new DependencyManagerMock() +const featureRegistry = new FeatureRegistryMock() + +describe('DiscoverFeaturesModule', () => { + test('registers dependencies on the dependency manager', () => { + new DiscoverFeaturesModule().register(dependencyManager, featureRegistry) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(V1DiscoverFeaturesService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(V2DiscoverFeaturesService) + + expect(featureRegistry.register).toHaveBeenCalledWith( + new Protocol({ + id: 'https://didcomm.org/discover-features/1.0', + roles: ['requester', 'responder'], + }), + new Protocol({ + id: 'https://didcomm.org/discover-features/2.0', + roles: ['requester', 'responder'], + }) + ) + }) +}) diff --git a/packages/core/src/modules/discover-features/__tests__/FeatureRegistry.test.ts b/packages/core/src/modules/discover-features/__tests__/FeatureRegistry.test.ts new file mode 100644 index 0000000000..f353b36752 --- /dev/null +++ b/packages/core/src/modules/discover-features/__tests__/FeatureRegistry.test.ts @@ -0,0 +1,53 @@ +import { FeatureRegistry } from '../../../agent/FeatureRegistry' +import { Feature, GoalCode, Protocol } from '../../../agent/models' +import { JsonTransformer } from '../../../utils/JsonTransformer' + +describe('Feature Registry', () => { + test('register goal codes', () => { + const featureRegistry = new FeatureRegistry() + + const goalCode = new GoalCode({ id: 'aries.vc.issue' }) + + expect(JsonTransformer.toJSON(goalCode)).toMatchObject({ id: 'aries.vc.issue', 'feature-type': 'goal-code' }) + + featureRegistry.register(goalCode) + const found = featureRegistry.query({ featureType: GoalCode.type, match: 'aries.*' }) + + expect(found.map((t) => t.toJSON())).toStrictEqual([{ id: 'aries.vc.issue', 'feature-type': 'goal-code' }]) + }) + + test('register generic feature', () => { + const featureRegistry = new FeatureRegistry() + + class GenericFeature extends Feature { + public customFieldString: string + public customFieldNumber: number + public constructor(id: string, customFieldString: string, customFieldNumber: number) { + super({ id, type: 'generic' }) + this.customFieldString = customFieldString + this.customFieldNumber = customFieldNumber + } + } + featureRegistry.register(new GenericFeature('myId', 'myString', 42)) + const found = featureRegistry.query({ featureType: 'generic', match: '*' }) + + expect(found.map((t) => t.toJSON())).toStrictEqual([ + { id: 'myId', 'feature-type': 'generic', customFieldString: 'myString', customFieldNumber: 42 }, + ]) + }) + + test('register combined features', () => { + const featureRegistry = new FeatureRegistry() + + featureRegistry.register( + new Protocol({ id: 'https://didcomm.org/dummy/1.0', roles: ['requester'] }), + new Protocol({ id: 'https://didcomm.org/dummy/1.0', roles: ['responder'] }), + new Protocol({ id: 'https://didcomm.org/dummy/1.0', roles: ['responder'] }) + ) + const found = featureRegistry.query({ featureType: Protocol.type, match: 'https://didcomm.org/dummy/1.0' }) + + expect(found.map((t) => t.toJSON())).toStrictEqual([ + { id: 'https://didcomm.org/dummy/1.0', 'feature-type': 'protocol', roles: ['requester', 'responder'] }, + ]) + }) +}) diff --git a/packages/core/src/modules/discover-features/__tests__/helpers.ts b/packages/core/src/modules/discover-features/__tests__/helpers.ts new file mode 100644 index 0000000000..209bf83b05 --- /dev/null +++ b/packages/core/src/modules/discover-features/__tests__/helpers.ts @@ -0,0 +1,41 @@ +import type { + DiscoverFeaturesDisclosureReceivedEvent, + DiscoverFeaturesQueryReceivedEvent, +} from '../DiscoverFeaturesEvents' +import type { Observable } from 'rxjs' + +import { map, catchError, timeout, firstValueFrom, ReplaySubject } from 'rxjs' + +export function waitForDisclosureSubject( + subject: ReplaySubject | Observable, + { timeoutMs = 10000 }: { timeoutMs: number } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + + return firstValueFrom( + observable.pipe( + timeout(timeoutMs), + catchError(() => { + throw new Error(`DiscoverFeaturesDisclosureReceivedEvent event not emitted within specified timeout`) + }), + map((e) => e.payload) + ) + ) +} + +export function waitForQuerySubject( + subject: ReplaySubject | Observable, + { timeoutMs = 10000 }: { timeoutMs: number } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + + return firstValueFrom( + observable.pipe( + timeout(timeoutMs), + catchError(() => { + throw new Error(`DiscoverFeaturesQueryReceivedEvent event not emitted within specified timeout`) + }), + map((e) => e.payload) + ) + ) +} diff --git a/packages/core/src/modules/discover-features/__tests__/v1-discover-features.test.ts b/packages/core/src/modules/discover-features/__tests__/v1-discover-features.test.ts new file mode 100644 index 0000000000..9ca14efd37 --- /dev/null +++ b/packages/core/src/modules/discover-features/__tests__/v1-discover-features.test.ts @@ -0,0 +1,97 @@ +import type { ConnectionRecord } from '../../connections' +import type { + DiscoverFeaturesDisclosureReceivedEvent, + DiscoverFeaturesQueryReceivedEvent, +} from '../DiscoverFeaturesEvents' + +import { ReplaySubject } from 'rxjs' + +import { setupSubjectTransports } from '../../../../tests' +import { getInMemoryAgentOptions, makeConnection } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { DiscoverFeaturesEventTypes } from '../DiscoverFeaturesEvents' + +import { waitForDisclosureSubject, waitForQuerySubject } from './helpers' + +const faberAgentOptions = getInMemoryAgentOptions('Faber Discover Features V1 E2E', { + endpoints: ['rxjs:faber'], +}) + +const aliceAgentOptions = getInMemoryAgentOptions('Alice Discover Features V1 E2E', { + endpoints: ['rxjs:alice'], +}) + +describe('v1 discover features', () => { + let faberAgent: Agent + let aliceAgent: Agent + let faberConnection: ConnectionRecord + + beforeAll(async () => { + faberAgent = new Agent(faberAgentOptions) + aliceAgent = new Agent(aliceAgentOptions) + + setupSubjectTransports([faberAgent, aliceAgent]) + + await faberAgent.initialize() + await aliceAgent.initialize() + ;[faberConnection] = await makeConnection(faberAgent, aliceAgent) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Faber asks Alice for revocation notification protocol support', async () => { + const faberReplay = new ReplaySubject() + const aliceReplay = new ReplaySubject() + + faberAgent.events + .observable(DiscoverFeaturesEventTypes.DisclosureReceived) + .subscribe(faberReplay) + aliceAgent.events + .observable(DiscoverFeaturesEventTypes.QueryReceived) + .subscribe(aliceReplay) + + await faberAgent.discovery.queryFeatures({ + connectionId: faberConnection.id, + protocolVersion: 'v1', + queries: [{ featureType: 'protocol', match: 'https://didcomm.org/revocation_notification/*' }], + }) + + const query = await waitForQuerySubject(aliceReplay, { timeoutMs: 10000 }) + + expect(query).toMatchObject({ + protocolVersion: 'v1', + queries: [{ featureType: 'protocol', match: 'https://didcomm.org/revocation_notification/*' }], + }) + + const disclosure = await waitForDisclosureSubject(faberReplay, { timeoutMs: 10000 }) + + expect(disclosure).toMatchObject({ + protocolVersion: 'v1', + disclosures: [ + { type: 'protocol', id: 'https://didcomm.org/revocation_notification/1.0', roles: ['holder'] }, + { type: 'protocol', id: 'https://didcomm.org/revocation_notification/2.0', roles: ['holder'] }, + ], + }) + }) + + test('Faber asks Alice for revocation notification protocol support synchronously', async () => { + const matchingFeatures = await faberAgent.discovery.queryFeatures({ + connectionId: faberConnection.id, + protocolVersion: 'v1', + queries: [{ featureType: 'protocol', match: 'https://didcomm.org/revocation_notification/*' }], + awaitDisclosures: true, + }) + + expect(matchingFeatures).toMatchObject({ + features: [ + { type: 'protocol', id: 'https://didcomm.org/revocation_notification/1.0', roles: ['holder'] }, + { type: 'protocol', id: 'https://didcomm.org/revocation_notification/2.0', roles: ['holder'] }, + ], + }) + }) +}) diff --git a/packages/core/src/modules/discover-features/__tests__/v2-discover-features.test.ts b/packages/core/src/modules/discover-features/__tests__/v2-discover-features.test.ts new file mode 100644 index 0000000000..c76a574992 --- /dev/null +++ b/packages/core/src/modules/discover-features/__tests__/v2-discover-features.test.ts @@ -0,0 +1,224 @@ +import type { ConnectionRecord } from '../../connections' +import type { + DiscoverFeaturesDisclosureReceivedEvent, + DiscoverFeaturesQueryReceivedEvent, +} from '../DiscoverFeaturesEvents' + +import { ReplaySubject } from 'rxjs' + +import { setupSubjectTransports } from '../../../../tests' +import { getInMemoryAgentOptions, makeConnection } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { GoalCode, Feature } from '../../../agent/models' +import { DiscoverFeaturesEventTypes } from '../DiscoverFeaturesEvents' + +import { waitForDisclosureSubject, waitForQuerySubject } from './helpers' + +const faberAgentOptions = getInMemoryAgentOptions('Faber Discover Features V2 E2E', { + endpoints: ['rxjs:faber'], +}) + +const aliceAgentOptions = getInMemoryAgentOptions('Alice Discover Features V2 E2E', { + endpoints: ['rxjs:alice'], +}) + +describe('v2 discover features', () => { + let faberAgent: Agent + let aliceAgent: Agent + let aliceConnection: ConnectionRecord + let faberConnection: ConnectionRecord + + beforeAll(async () => { + faberAgent = new Agent(faberAgentOptions) + aliceAgent = new Agent(aliceAgentOptions) + setupSubjectTransports([faberAgent, aliceAgent]) + + await faberAgent.initialize() + await aliceAgent.initialize() + ;[faberConnection, aliceConnection] = await makeConnection(faberAgent, aliceAgent) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Faber asks Alice for issue credential protocol support', async () => { + const faberReplay = new ReplaySubject() + const aliceReplay = new ReplaySubject() + + faberAgent.discovery.config.autoAcceptQueries + faberAgent.events + .observable(DiscoverFeaturesEventTypes.DisclosureReceived) + .subscribe(faberReplay) + aliceAgent.events + .observable(DiscoverFeaturesEventTypes.QueryReceived) + .subscribe(aliceReplay) + + await faberAgent.discovery.queryFeatures({ + connectionId: faberConnection.id, + protocolVersion: 'v2', + queries: [{ featureType: 'protocol', match: 'https://didcomm.org/revocation_notification/*' }], + }) + + const query = await waitForQuerySubject(aliceReplay, { timeoutMs: 10000 }) + + expect(query).toMatchObject({ + protocolVersion: 'v2', + queries: [{ featureType: 'protocol', match: 'https://didcomm.org/revocation_notification/*' }], + }) + + const disclosure = await waitForDisclosureSubject(faberReplay, { timeoutMs: 10000 }) + + expect(disclosure).toMatchObject({ + protocolVersion: 'v2', + disclosures: [ + { type: 'protocol', id: 'https://didcomm.org/revocation_notification/1.0', roles: ['holder'] }, + { type: 'protocol', id: 'https://didcomm.org/revocation_notification/2.0', roles: ['holder'] }, + ], + }) + }) + + test('Faber defines a supported goal code and Alice queries', async () => { + const faberReplay = new ReplaySubject() + const aliceReplay = new ReplaySubject() + + aliceAgent.events + .observable(DiscoverFeaturesEventTypes.DisclosureReceived) + .subscribe(aliceReplay) + faberAgent.events + .observable(DiscoverFeaturesEventTypes.QueryReceived) + .subscribe(faberReplay) + + // Register some goal codes + faberAgent.features.register(new GoalCode({ id: 'faber.vc.issuance' }), new GoalCode({ id: 'faber.vc.query' })) + + await aliceAgent.discovery.queryFeatures({ + connectionId: aliceConnection.id, + protocolVersion: 'v2', + queries: [{ featureType: 'goal-code', match: '*' }], + }) + + const query = await waitForQuerySubject(faberReplay, { timeoutMs: 10000 }) + + expect(query).toMatchObject({ + protocolVersion: 'v2', + queries: [{ featureType: 'goal-code', match: '*' }], + }) + + const disclosure = await waitForDisclosureSubject(aliceReplay, { timeoutMs: 10000 }) + + expect(disclosure).toMatchObject({ + protocolVersion: 'v2', + disclosures: [ + { type: 'goal-code', id: 'faber.vc.issuance' }, + { type: 'goal-code', id: 'faber.vc.query' }, + ], + }) + }) + + test('Faber defines a custom feature and Alice queries', async () => { + const faberReplay = new ReplaySubject() + const aliceReplay = new ReplaySubject() + + aliceAgent.events + .observable(DiscoverFeaturesEventTypes.DisclosureReceived) + .subscribe(aliceReplay) + faberAgent.events + .observable(DiscoverFeaturesEventTypes.QueryReceived) + .subscribe(faberReplay) + + // Define a custom feature type + class GenericFeature extends Feature { + public 'generic-field'!: string + + public constructor(options: { id: string; genericField: string }) { + super({ id: options.id, type: 'generic' }) + this['generic-field'] = options.genericField + } + } + + // Register a custom feature + faberAgent.features.register(new GenericFeature({ id: 'custom-feature', genericField: 'custom-field' })) + + await aliceAgent.discovery.queryFeatures({ + connectionId: aliceConnection.id, + protocolVersion: 'v2', + queries: [{ featureType: 'generic', match: 'custom-feature' }], + }) + + const query = await waitForQuerySubject(faberReplay, { timeoutMs: 10000 }) + + expect(query).toMatchObject({ + protocolVersion: 'v2', + queries: [{ featureType: 'generic', match: 'custom-feature' }], + }) + + const disclosure = await waitForDisclosureSubject(aliceReplay, { timeoutMs: 10000 }) + + expect(disclosure).toMatchObject({ + protocolVersion: 'v2', + disclosures: [ + { + type: 'generic', + id: 'custom-feature', + 'generic-field': 'custom-field', + }, + ], + }) + }) + + test('Faber proactively sends a set of features to Alice', async () => { + const faberReplay = new ReplaySubject() + const aliceReplay = new ReplaySubject() + + aliceAgent.events + .observable(DiscoverFeaturesEventTypes.DisclosureReceived) + .subscribe(aliceReplay) + faberAgent.events + .observable(DiscoverFeaturesEventTypes.QueryReceived) + .subscribe(faberReplay) + + // Register a custom feature + faberAgent.features.register( + new Feature({ id: 'AIP2.0', type: 'aip' }), + new Feature({ id: 'AIP2.0/INDYCRED', type: 'aip' }), + new Feature({ id: 'AIP2.0/MEDIATE', type: 'aip' }) + ) + + await faberAgent.discovery.discloseFeatures({ + connectionId: faberConnection.id, + protocolVersion: 'v2', + disclosureQueries: [{ featureType: 'aip', match: '*' }], + }) + + const disclosure = await waitForDisclosureSubject(aliceReplay, { timeoutMs: 10000 }) + + expect(disclosure).toMatchObject({ + protocolVersion: 'v2', + disclosures: [ + { type: 'aip', id: 'AIP2.0' }, + { type: 'aip', id: 'AIP2.0/INDYCRED' }, + { type: 'aip', id: 'AIP2.0/MEDIATE' }, + ], + }) + }) + + test('Faber asks Alice for issue credential protocol support synchronously', async () => { + const matchingFeatures = await faberAgent.discovery.queryFeatures({ + connectionId: faberConnection.id, + protocolVersion: 'v2', + queries: [{ featureType: 'protocol', match: 'https://didcomm.org/revocation_notification/*' }], + awaitDisclosures: true, + }) + + expect(matchingFeatures).toMatchObject({ + features: [ + { type: 'protocol', id: 'https://didcomm.org/revocation_notification/1.0', roles: ['holder'] }, + { type: 'protocol', id: 'https://didcomm.org/revocation_notification/2.0', roles: ['holder'] }, + ], + }) + }) +}) diff --git a/packages/core/src/modules/discover-features/index.ts b/packages/core/src/modules/discover-features/index.ts new file mode 100644 index 0000000000..acdaae6633 --- /dev/null +++ b/packages/core/src/modules/discover-features/index.ts @@ -0,0 +1,4 @@ +export * from './DiscoverFeaturesApi' +export * from './DiscoverFeaturesEvents' +export * from './DiscoverFeaturesModule' +export * from './protocol' diff --git a/packages/core/src/modules/discover-features/protocol/index.ts b/packages/core/src/modules/discover-features/protocol/index.ts new file mode 100644 index 0000000000..4d9da63573 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/index.ts @@ -0,0 +1,2 @@ +export * from './v1' +export * from './v2' diff --git a/packages/core/src/modules/discover-features/protocol/v1/V1DiscoverFeaturesService.ts b/packages/core/src/modules/discover-features/protocol/v1/V1DiscoverFeaturesService.ts new file mode 100644 index 0000000000..8a7c830d1b --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v1/V1DiscoverFeaturesService.ts @@ -0,0 +1,142 @@ +import type { AgentMessage } from '../../../../agent/AgentMessage' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { + DiscoverFeaturesDisclosureReceivedEvent, + DiscoverFeaturesQueryReceivedEvent, +} from '../../DiscoverFeaturesEvents' +import type { + CreateDisclosureOptions, + CreateQueryOptions, + DiscoverFeaturesProtocolMsgReturnType, +} from '../../DiscoverFeaturesServiceOptions' + +import { EventEmitter } from '../../../../agent/EventEmitter' +import { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import { MessageHandlerRegistry } from '../../../../agent/MessageHandlerRegistry' +import { Protocol } from '../../../../agent/models' +import { InjectionSymbols } from '../../../../constants' +import { CredoError } from '../../../../error' +import { Logger } from '../../../../logger' +import { inject, injectable } from '../../../../plugins' +import { DiscoverFeaturesEventTypes } from '../../DiscoverFeaturesEvents' +import { DiscoverFeaturesModuleConfig } from '../../DiscoverFeaturesModuleConfig' +import { DiscoverFeaturesService } from '../../services' + +import { V1DiscloseMessageHandler, V1QueryMessageHandler } from './handlers' +import { V1QueryMessage, V1DiscloseMessage, DiscloseProtocol } from './messages' + +@injectable() +export class V1DiscoverFeaturesService extends DiscoverFeaturesService { + public constructor( + featureRegistry: FeatureRegistry, + eventEmitter: EventEmitter, + messageHandlerRegistry: MessageHandlerRegistry, + @inject(InjectionSymbols.Logger) logger: Logger, + discoverFeaturesConfig: DiscoverFeaturesModuleConfig + ) { + super(featureRegistry, eventEmitter, logger, discoverFeaturesConfig) + + this.registerMessageHandlers(messageHandlerRegistry) + } + + /** + * The version of the discover features protocol this service supports + */ + public readonly version = 'v1' + + private registerMessageHandlers(messageHandlerRegistry: MessageHandlerRegistry) { + messageHandlerRegistry.registerMessageHandler(new V1DiscloseMessageHandler(this)) + messageHandlerRegistry.registerMessageHandler(new V1QueryMessageHandler(this)) + } + + public async createQuery( + options: CreateQueryOptions + ): Promise> { + if (options.queries.length > 1) { + throw new CredoError('Discover Features V1 only supports a single query') + } + + if (options.queries[0].featureType !== 'protocol') { + throw new CredoError('Discover Features V1 only supports querying for protocol support') + } + + const queryMessage = new V1QueryMessage({ + query: options.queries[0].match, + comment: options.comment, + }) + + return { message: queryMessage } + } + + public async processQuery( + messageContext: InboundMessageContext + ): Promise | void> { + const { query, threadId } = messageContext.message + + const connection = messageContext.assertReadyConnection() + + this.eventEmitter.emit(messageContext.agentContext, { + type: DiscoverFeaturesEventTypes.QueryReceived, + payload: { + message: messageContext.message, + connection, + queries: [{ featureType: 'protocol', match: query }], + protocolVersion: this.version, + threadId, + }, + }) + + // Process query and send responde automatically if configured to do so, otherwise + // just send the event and let controller decide + if (this.discoverFeaturesModuleConfig.autoAcceptQueries) { + return await this.createDisclosure({ + threadId, + disclosureQueries: [{ featureType: 'protocol', match: query }], + }) + } + } + + public async createDisclosure( + options: CreateDisclosureOptions + ): Promise> { + if (options.disclosureQueries.some((item) => item.featureType !== 'protocol')) { + throw new CredoError('Discover Features V1 only supports protocols') + } + + if (!options.threadId) { + throw new CredoError('Thread Id is required for Discover Features V1 disclosure') + } + + const matches = this.featureRegistry.query(...options.disclosureQueries) + + const discloseMessage = new V1DiscloseMessage({ + threadId: options.threadId, + protocols: matches.map( + (item) => + new DiscloseProtocol({ + protocolId: (item as Protocol).id, + roles: (item as Protocol).roles, + }) + ), + }) + + return { message: discloseMessage } + } + + public async processDisclosure(messageContext: InboundMessageContext): Promise { + const { protocols, threadId } = messageContext.message + + const connection = messageContext.assertReadyConnection() + + this.eventEmitter.emit(messageContext.agentContext, { + type: DiscoverFeaturesEventTypes.DisclosureReceived, + payload: { + message: messageContext.message, + connection, + disclosures: protocols.map((item) => new Protocol({ id: item.protocolId, roles: item.roles })), + protocolVersion: this.version, + threadId, + }, + }) + } +} diff --git a/packages/core/src/modules/discover-features/protocol/v1/__tests__/V1DiscoverFeaturesService.test.ts b/packages/core/src/modules/discover-features/protocol/v1/__tests__/V1DiscoverFeaturesService.test.ts new file mode 100644 index 0000000000..03db2cf74a --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v1/__tests__/V1DiscoverFeaturesService.test.ts @@ -0,0 +1,277 @@ +import type { + DiscoverFeaturesDisclosureReceivedEvent, + DiscoverFeaturesQueryReceivedEvent, +} from '../../../DiscoverFeaturesEvents' +import type { DiscoverFeaturesProtocolMsgReturnType } from '../../../DiscoverFeaturesServiceOptions' + +import { Subject } from 'rxjs' + +import { agentDependencies, getAgentContext, getMockConnection } from '../../../../../../tests/helpers' +import { EventEmitter } from '../../../../../agent/EventEmitter' +import { FeatureRegistry } from '../../../../../agent/FeatureRegistry' +import { MessageHandlerRegistry } from '../../../../../agent/MessageHandlerRegistry' +import { Protocol } from '../../../../../agent/models' +import { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import { ConsoleLogger } from '../../../../../logger/ConsoleLogger' +import { DidExchangeState } from '../../../../../modules/connections' +import { DiscoverFeaturesEventTypes } from '../../../DiscoverFeaturesEvents' +import { DiscoverFeaturesModuleConfig } from '../../../DiscoverFeaturesModuleConfig' +import { V1DiscoverFeaturesService } from '../V1DiscoverFeaturesService' +import { V1DiscloseMessage, V1QueryMessage } from '../messages' + +jest.mock('../../../../../agent/MessageHandlerRegistry') +const MessageHandlerRegistryMock = MessageHandlerRegistry as jest.Mock +const eventEmitter = new EventEmitter(agentDependencies, new Subject()) +const featureRegistry = new FeatureRegistry() +featureRegistry.register(new Protocol({ id: 'https://didcomm.org/connections/1.0' })) +featureRegistry.register(new Protocol({ id: 'https://didcomm.org/notification/1.0', roles: ['role-1', 'role-2'] })) +featureRegistry.register(new Protocol({ id: 'https://didcomm.org/issue-credential/1.0' })) + +jest.mock('../../../../../logger/Logger') +const LoggerMock = ConsoleLogger as jest.Mock + +describe('V1DiscoverFeaturesService - auto accept queries', () => { + const discoverFeaturesModuleConfig = new DiscoverFeaturesModuleConfig({ autoAcceptQueries: true }) + + const discoverFeaturesService = new V1DiscoverFeaturesService( + featureRegistry, + eventEmitter, + new MessageHandlerRegistryMock(), + new LoggerMock(), + discoverFeaturesModuleConfig + ) + describe('createDisclosure', () => { + it('should return all protocols when query is *', async () => { + const queryMessage = new V1QueryMessage({ + query: '*', + }) + + const { message } = await discoverFeaturesService.createDisclosure({ + disclosureQueries: [{ featureType: 'protocol', match: queryMessage.query }], + threadId: queryMessage.threadId, + }) + + expect(message.protocols.map((p) => p.protocolId)).toStrictEqual([ + 'https://didcomm.org/connections/1.0', + 'https://didcomm.org/notification/1.0', + 'https://didcomm.org/issue-credential/1.0', + ]) + }) + + it('should return only one protocol if the query specifies a specific protocol', async () => { + const queryMessage = new V1QueryMessage({ + query: 'https://didcomm.org/connections/1.0', + }) + + const { message } = await discoverFeaturesService.createDisclosure({ + disclosureQueries: [{ featureType: 'protocol', match: queryMessage.query }], + threadId: queryMessage.threadId, + }) + + expect(message.protocols.map((p) => p.protocolId)).toStrictEqual(['https://didcomm.org/connections/1.0']) + }) + + it('should respect a wild card at the end of the query', async () => { + const queryMessage = new V1QueryMessage({ + query: 'https://didcomm.org/connections/*', + }) + + const { message } = await discoverFeaturesService.createDisclosure({ + disclosureQueries: [{ featureType: 'protocol', match: queryMessage.query }], + threadId: queryMessage.threadId, + }) + + expect(message.protocols.map((p) => p.protocolId)).toStrictEqual(['https://didcomm.org/connections/1.0']) + }) + + it('should send an empty array if no feature matches query', async () => { + const queryMessage = new V1QueryMessage({ + query: 'not-supported', + }) + + const { message } = await discoverFeaturesService.createDisclosure({ + disclosureQueries: [{ featureType: 'protocol', match: queryMessage.query }], + threadId: queryMessage.threadId, + }) + + expect(message.protocols.map((p) => p.protocolId)).toStrictEqual([]) + }) + + it('should throw error if features other than protocols are disclosed', async () => { + expect( + discoverFeaturesService.createDisclosure({ + disclosureQueries: [ + { featureType: 'protocol', match: '1' }, + { featureType: 'goal-code', match: '2' }, + ], + threadId: '1234', + }) + ).rejects.toThrow('Discover Features V1 only supports protocols') + }) + + it('should throw error if no thread id is provided', async () => { + expect( + discoverFeaturesService.createDisclosure({ + disclosureQueries: [{ featureType: 'protocol', match: '1' }], + }) + ).rejects.toThrow('Thread Id is required for Discover Features V1 disclosure') + }) + }) + + describe('createQuery', () => { + it('should return a query message with the query and comment', async () => { + const { message } = await discoverFeaturesService.createQuery({ + queries: [{ featureType: 'protocol', match: '*' }], + comment: 'Hello', + }) + + expect(message.query).toBe('*') + expect(message.comment).toBe('Hello') + }) + + it('should throw error if multiple features are queried', async () => { + expect( + discoverFeaturesService.createQuery({ + queries: [ + { featureType: 'protocol', match: '1' }, + { featureType: 'protocol', match: '2' }, + ], + }) + ).rejects.toThrow('Discover Features V1 only supports a single query') + }) + + it('should throw error if a feature other than protocol is queried', async () => { + expect( + discoverFeaturesService.createQuery({ + queries: [{ featureType: 'goal-code', match: '1' }], + }) + ).rejects.toThrow('Discover Features V1 only supports querying for protocol support') + }) + }) + + describe('processQuery', () => { + it('should emit event and create disclosure message', async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(DiscoverFeaturesEventTypes.QueryReceived, eventListenerMock) + + const queryMessage = new V1QueryMessage({ query: '*' }) + + const connection = getMockConnection({ state: DidExchangeState.Completed }) + const messageContext = new InboundMessageContext(queryMessage, { + agentContext: getAgentContext(), + connection, + }) + const outboundMessage = await discoverFeaturesService.processQuery(messageContext) + + eventEmitter.off(DiscoverFeaturesEventTypes.QueryReceived, eventListenerMock) + + expect(eventListenerMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: DiscoverFeaturesEventTypes.QueryReceived, + payload: expect.objectContaining({ + connection, + protocolVersion: 'v1', + queries: [{ featureType: 'protocol', match: queryMessage.query }], + threadId: queryMessage.threadId, + }), + }) + ) + expect(outboundMessage).toBeDefined() + expect( + (outboundMessage as DiscoverFeaturesProtocolMsgReturnType).message.protocols.map( + (p) => p.protocolId + ) + ).toStrictEqual([ + 'https://didcomm.org/connections/1.0', + 'https://didcomm.org/notification/1.0', + 'https://didcomm.org/issue-credential/1.0', + ]) + }) + }) + + describe('processDisclosure', () => { + it('should emit event', async () => { + const eventListenerMock = jest.fn() + eventEmitter.on( + DiscoverFeaturesEventTypes.DisclosureReceived, + eventListenerMock + ) + + const discloseMessage = new V1DiscloseMessage({ + protocols: [{ protocolId: 'prot1', roles: ['role1', 'role2'] }, { protocolId: 'prot2' }], + threadId: '1234', + }) + + const connection = getMockConnection({ state: DidExchangeState.Completed }) + const messageContext = new InboundMessageContext(discloseMessage, { + agentContext: getAgentContext(), + connection, + }) + await discoverFeaturesService.processDisclosure(messageContext) + + eventEmitter.off( + DiscoverFeaturesEventTypes.DisclosureReceived, + eventListenerMock + ) + + expect(eventListenerMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: DiscoverFeaturesEventTypes.DisclosureReceived, + payload: expect.objectContaining({ + connection, + protocolVersion: 'v1', + disclosures: [ + { type: 'protocol', id: 'prot1', roles: ['role1', 'role2'] }, + { type: 'protocol', id: 'prot2' }, + ], + + threadId: discloseMessage.threadId, + }), + }) + ) + }) + }) +}) + +describe('V1DiscoverFeaturesService - auto accept disabled', () => { + const discoverFeaturesModuleConfig = new DiscoverFeaturesModuleConfig({ autoAcceptQueries: false }) + + const discoverFeaturesService = new V1DiscoverFeaturesService( + featureRegistry, + eventEmitter, + new MessageHandlerRegistry(), + new LoggerMock(), + discoverFeaturesModuleConfig + ) + + describe('processQuery', () => { + it('should emit event and not send any message', async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(DiscoverFeaturesEventTypes.QueryReceived, eventListenerMock) + + const queryMessage = new V1QueryMessage({ query: '*' }) + + const connection = getMockConnection({ state: DidExchangeState.Completed }) + const messageContext = new InboundMessageContext(queryMessage, { + agentContext: getAgentContext(), + connection, + }) + const outboundMessage = await discoverFeaturesService.processQuery(messageContext) + + eventEmitter.off(DiscoverFeaturesEventTypes.QueryReceived, eventListenerMock) + + expect(eventListenerMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: DiscoverFeaturesEventTypes.QueryReceived, + payload: expect.objectContaining({ + connection, + protocolVersion: 'v1', + queries: [{ featureType: 'protocol', match: queryMessage.query }], + threadId: queryMessage.threadId, + }), + }) + ) + expect(outboundMessage).toBeUndefined() + }) + }) +}) diff --git a/packages/core/src/modules/discover-features/protocol/v1/handlers/V1DiscloseMessageHandler.ts b/packages/core/src/modules/discover-features/protocol/v1/handlers/V1DiscloseMessageHandler.ts new file mode 100644 index 0000000000..5a66a4a527 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v1/handlers/V1DiscloseMessageHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V1DiscoverFeaturesService } from '../V1DiscoverFeaturesService' + +import { V1DiscloseMessage } from '../messages' + +export class V1DiscloseMessageHandler implements MessageHandler { + public supportedMessages = [V1DiscloseMessage] + private discoverFeaturesService: V1DiscoverFeaturesService + + public constructor(discoverFeaturesService: V1DiscoverFeaturesService) { + this.discoverFeaturesService = discoverFeaturesService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + await this.discoverFeaturesService.processDisclosure(inboundMessage) + } +} diff --git a/packages/core/src/modules/discover-features/protocol/v1/handlers/V1QueryMessageHandler.ts b/packages/core/src/modules/discover-features/protocol/v1/handlers/V1QueryMessageHandler.ts new file mode 100644 index 0000000000..cd1db2d885 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v1/handlers/V1QueryMessageHandler.ts @@ -0,0 +1,27 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V1DiscoverFeaturesService } from '../V1DiscoverFeaturesService' + +import { OutboundMessageContext } from '../../../../../agent/models' +import { V1QueryMessage } from '../messages' + +export class V1QueryMessageHandler implements MessageHandler { + private discoverFeaturesService: V1DiscoverFeaturesService + public supportedMessages = [V1QueryMessage] + + public constructor(discoverFeaturesService: V1DiscoverFeaturesService) { + this.discoverFeaturesService = discoverFeaturesService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + const connection = inboundMessage.assertReadyConnection() + + const discloseMessage = await this.discoverFeaturesService.processQuery(inboundMessage) + + if (discloseMessage) { + return new OutboundMessageContext(discloseMessage.message, { + agentContext: inboundMessage.agentContext, + connection, + }) + } + } +} diff --git a/packages/core/src/modules/discover-features/protocol/v1/handlers/index.ts b/packages/core/src/modules/discover-features/protocol/v1/handlers/index.ts new file mode 100644 index 0000000000..73f3391154 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v1/handlers/index.ts @@ -0,0 +1,2 @@ +export * from './V1DiscloseMessageHandler' +export * from './V1QueryMessageHandler' diff --git a/packages/core/src/modules/discover-features/protocol/v1/index.ts b/packages/core/src/modules/discover-features/protocol/v1/index.ts new file mode 100644 index 0000000000..e13fec27de --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v1/index.ts @@ -0,0 +1,2 @@ +export * from './V1DiscoverFeaturesService' +export * from './messages' diff --git a/packages/core/src/modules/discover-features/protocol/v1/messages/DiscloseMessage.ts b/packages/core/src/modules/discover-features/protocol/v1/messages/DiscloseMessage.ts new file mode 100644 index 0000000000..800900424b --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v1/messages/DiscloseMessage.ts @@ -0,0 +1,55 @@ +import { Expose, Type } from 'class-transformer' +import { IsInstance, IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface DiscloseProtocolOptions { + protocolId: string + roles?: string[] +} + +export class DiscloseProtocol { + public constructor(options: DiscloseProtocolOptions) { + if (options) { + this.protocolId = options.protocolId + this.roles = options.roles + } + } + + @Expose({ name: 'pid' }) + @IsString() + public protocolId!: string + + @IsString({ each: true }) + @IsOptional() + public roles?: string[] +} + +export interface DiscoverFeaturesDiscloseMessageOptions { + id?: string + threadId: string + protocols: DiscloseProtocolOptions[] +} + +export class V1DiscloseMessage extends AgentMessage { + public constructor(options: DiscoverFeaturesDiscloseMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.protocols = options.protocols.map((p) => new DiscloseProtocol(p)) + this.setThread({ + threadId: options.threadId, + }) + } + } + + @IsValidMessageType(V1DiscloseMessage.type) + public readonly type = V1DiscloseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/discover-features/1.0/disclose') + + @IsInstance(DiscloseProtocol, { each: true }) + @Type(() => DiscloseProtocol) + public protocols!: DiscloseProtocol[] +} diff --git a/packages/core/src/modules/discover-features/protocol/v1/messages/QueryMessage.ts b/packages/core/src/modules/discover-features/protocol/v1/messages/QueryMessage.ts new file mode 100644 index 0000000000..7b8d5e26b4 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v1/messages/QueryMessage.ts @@ -0,0 +1,33 @@ +import { IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface DiscoverFeaturesQueryMessageOptions { + id?: string + query: string + comment?: string +} + +export class V1QueryMessage extends AgentMessage { + public constructor(options: DiscoverFeaturesQueryMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.query = options.query + this.comment = options.comment + } + } + + @IsValidMessageType(V1QueryMessage.type) + public readonly type = V1QueryMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/discover-features/1.0/query') + + @IsString() + public query!: string + + @IsString() + @IsOptional() + public comment?: string +} diff --git a/packages/core/src/modules/discover-features/protocol/v1/messages/index.ts b/packages/core/src/modules/discover-features/protocol/v1/messages/index.ts new file mode 100644 index 0000000000..9f08fe74bd --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v1/messages/index.ts @@ -0,0 +1,2 @@ +export * from './DiscloseMessage' +export * from './QueryMessage' diff --git a/packages/core/src/modules/discover-features/protocol/v2/V2DiscoverFeaturesService.ts b/packages/core/src/modules/discover-features/protocol/v2/V2DiscoverFeaturesService.ts new file mode 100644 index 0000000000..0196a351c4 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v2/V2DiscoverFeaturesService.ts @@ -0,0 +1,113 @@ +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { + DiscoverFeaturesDisclosureReceivedEvent, + DiscoverFeaturesQueryReceivedEvent, +} from '../../DiscoverFeaturesEvents' +import type { + CreateQueryOptions, + DiscoverFeaturesProtocolMsgReturnType, + CreateDisclosureOptions, +} from '../../DiscoverFeaturesServiceOptions' + +import { EventEmitter } from '../../../../agent/EventEmitter' +import { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import { MessageHandlerRegistry } from '../../../../agent/MessageHandlerRegistry' +import { InjectionSymbols } from '../../../../constants' +import { Logger } from '../../../../logger' +import { inject, injectable } from '../../../../plugins' +import { DiscoverFeaturesEventTypes } from '../../DiscoverFeaturesEvents' +import { DiscoverFeaturesModuleConfig } from '../../DiscoverFeaturesModuleConfig' +import { DiscoverFeaturesService } from '../../services' + +import { V2DisclosuresMessageHandler, V2QueriesMessageHandler } from './handlers' +import { V2QueriesMessage, V2DisclosuresMessage } from './messages' + +@injectable() +export class V2DiscoverFeaturesService extends DiscoverFeaturesService { + public constructor( + featureRegistry: FeatureRegistry, + eventEmitter: EventEmitter, + messageHandlerRegistry: MessageHandlerRegistry, + @inject(InjectionSymbols.Logger) logger: Logger, + discoverFeaturesModuleConfig: DiscoverFeaturesModuleConfig + ) { + super(featureRegistry, eventEmitter, logger, discoverFeaturesModuleConfig) + this.registerMessageHandlers(messageHandlerRegistry) + } + + /** + * The version of the discover features protocol this service supports + */ + public readonly version = 'v2' + + private registerMessageHandlers(messageHandlerRegistry: MessageHandlerRegistry) { + messageHandlerRegistry.registerMessageHandler(new V2DisclosuresMessageHandler(this)) + messageHandlerRegistry.registerMessageHandler(new V2QueriesMessageHandler(this)) + } + + public async createQuery( + options: CreateQueryOptions + ): Promise> { + const queryMessage = new V2QueriesMessage({ queries: options.queries }) + + return { message: queryMessage } + } + + public async processQuery( + messageContext: InboundMessageContext + ): Promise | void> { + const { queries, threadId } = messageContext.message + + const connection = messageContext.assertReadyConnection() + + this.eventEmitter.emit(messageContext.agentContext, { + type: DiscoverFeaturesEventTypes.QueryReceived, + payload: { + message: messageContext.message, + connection, + queries, + protocolVersion: this.version, + threadId, + }, + }) + + // Process query and send responde automatically if configured to do so, otherwise + // just send the event and let controller decide + if (this.discoverFeaturesModuleConfig.autoAcceptQueries) { + return await this.createDisclosure({ + threadId, + disclosureQueries: queries, + }) + } + } + + public async createDisclosure( + options: CreateDisclosureOptions + ): Promise> { + const matches = this.featureRegistry.query(...options.disclosureQueries) + + const discloseMessage = new V2DisclosuresMessage({ + threadId: options.threadId, + features: matches, + }) + + return { message: discloseMessage } + } + + public async processDisclosure(messageContext: InboundMessageContext): Promise { + const { disclosures, threadId } = messageContext.message + + const connection = messageContext.assertReadyConnection() + + this.eventEmitter.emit(messageContext.agentContext, { + type: DiscoverFeaturesEventTypes.DisclosureReceived, + payload: { + message: messageContext.message, + connection, + disclosures, + protocolVersion: this.version, + threadId, + }, + }) + } +} diff --git a/packages/core/src/modules/discover-features/protocol/v2/__tests__/V2DiscoverFeaturesService.test.ts b/packages/core/src/modules/discover-features/protocol/v2/__tests__/V2DiscoverFeaturesService.test.ts new file mode 100644 index 0000000000..897fe5d1b4 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v2/__tests__/V2DiscoverFeaturesService.test.ts @@ -0,0 +1,288 @@ +import type { + DiscoverFeaturesDisclosureReceivedEvent, + DiscoverFeaturesQueryReceivedEvent, +} from '../../../DiscoverFeaturesEvents' +import type { DiscoverFeaturesProtocolMsgReturnType } from '../../../DiscoverFeaturesServiceOptions' + +import { Subject } from 'rxjs' + +import { agentDependencies, getAgentContext, getMockConnection } from '../../../../../../tests/helpers' +import { EventEmitter } from '../../../../../agent/EventEmitter' +import { FeatureRegistry } from '../../../../../agent/FeatureRegistry' +import { MessageHandlerRegistry } from '../../../../../agent/MessageHandlerRegistry' +import { InboundMessageContext, Protocol, GoalCode } from '../../../../../agent/models' +import { ConsoleLogger } from '../../../../../logger/ConsoleLogger' +import { DidExchangeState } from '../../../../connections' +import { DiscoverFeaturesEventTypes } from '../../../DiscoverFeaturesEvents' +import { DiscoverFeaturesModuleConfig } from '../../../DiscoverFeaturesModuleConfig' +import { V2DiscoverFeaturesService } from '../V2DiscoverFeaturesService' +import { V2DisclosuresMessage, V2QueriesMessage } from '../messages' + +jest.mock('../../../../../agent/MessageHandlerRegistry') +const MessageHandlerRegistryMock = MessageHandlerRegistry as jest.Mock +const eventEmitter = new EventEmitter(agentDependencies, new Subject()) +const featureRegistry = new FeatureRegistry() +featureRegistry.register(new Protocol({ id: 'https://didcomm.org/connections/1.0' })) +featureRegistry.register(new Protocol({ id: 'https://didcomm.org/notification/1.0', roles: ['role-1', 'role-2'] })) +featureRegistry.register(new Protocol({ id: 'https://didcomm.org/issue-credential/1.0' })) +featureRegistry.register(new GoalCode({ id: 'aries.vc.1' })) +featureRegistry.register(new GoalCode({ id: 'aries.vc.2' })) +featureRegistry.register(new GoalCode({ id: 'caries.vc.3' })) + +jest.mock('../../../../../logger/Logger') +const LoggerMock = ConsoleLogger as jest.Mock + +describe('V2DiscoverFeaturesService - auto accept queries', () => { + const discoverFeaturesModuleConfig = new DiscoverFeaturesModuleConfig({ autoAcceptQueries: true }) + + const discoverFeaturesService = new V2DiscoverFeaturesService( + featureRegistry, + eventEmitter, + new MessageHandlerRegistryMock(), + new LoggerMock(), + discoverFeaturesModuleConfig + ) + describe('createDisclosure', () => { + it('should return all items when query is *', async () => { + const queryMessage = new V2QueriesMessage({ + queries: [ + { featureType: Protocol.type, match: '*' }, + { featureType: GoalCode.type, match: '*' }, + ], + }) + + const { message } = await discoverFeaturesService.createDisclosure({ + disclosureQueries: queryMessage.queries, + threadId: queryMessage.threadId, + }) + + expect(message.disclosures.map((p) => p.id)).toStrictEqual([ + 'https://didcomm.org/connections/1.0', + 'https://didcomm.org/notification/1.0', + 'https://didcomm.org/issue-credential/1.0', + 'aries.vc.1', + 'aries.vc.2', + 'caries.vc.3', + ]) + }) + + it('should return only one protocol if the query specifies a specific protocol', async () => { + const queryMessage = new V2QueriesMessage({ + queries: [{ featureType: 'protocol', match: 'https://didcomm.org/connections/1.0' }], + }) + + const { message } = await discoverFeaturesService.createDisclosure({ + disclosureQueries: queryMessage.queries, + threadId: queryMessage.threadId, + }) + + expect(message.disclosures).toEqual([{ type: 'protocol', id: 'https://didcomm.org/connections/1.0' }]) + }) + + it('should respect a wild card at the end of the query', async () => { + const queryMessage = new V2QueriesMessage({ + queries: [ + { featureType: 'protocol', match: 'https://didcomm.org/connections/*' }, + { featureType: 'goal-code', match: 'aries*' }, + ], + }) + + const { message } = await discoverFeaturesService.createDisclosure({ + disclosureQueries: queryMessage.queries, + threadId: queryMessage.threadId, + }) + + expect(message.disclosures.map((p) => p.id)).toStrictEqual([ + 'https://didcomm.org/connections/1.0', + 'aries.vc.1', + 'aries.vc.2', + ]) + }) + + it('should send an empty array if no feature matches query', async () => { + const queryMessage = new V2QueriesMessage({ + queries: [{ featureType: 'anything', match: 'not-supported' }], + }) + + const { message } = await discoverFeaturesService.createDisclosure({ + disclosureQueries: queryMessage.queries, + threadId: queryMessage.threadId, + }) + + expect(message.disclosures).toStrictEqual([]) + }) + + it('should accept an empty queries object', async () => { + const { message } = await discoverFeaturesService.createDisclosure({ + disclosureQueries: [], + threadId: '1234', + }) + + expect(message.disclosures).toStrictEqual([]) + }) + + it('should accept no thread Id', async () => { + const { message } = await discoverFeaturesService.createDisclosure({ + disclosureQueries: [{ featureType: 'goal-code', match: 'caries*' }], + }) + + expect(message.disclosures).toEqual([ + { + type: 'goal-code', + id: 'caries.vc.3', + }, + ]) + expect(message.threadId).toEqual(message.id) + }) + }) + + describe('createQuery', () => { + it('should return a queries message with the query and comment', async () => { + const { message } = await discoverFeaturesService.createQuery({ + queries: [{ featureType: 'protocol', match: '*' }], + }) + + expect(message.queries).toEqual([{ featureType: 'protocol', match: '*' }]) + }) + + it('should accept multiple features', async () => { + const { message } = await discoverFeaturesService.createQuery({ + queries: [ + { featureType: 'protocol', match: '1' }, + { featureType: 'anything', match: '2' }, + ], + }) + + expect(message.queries).toEqual([ + { featureType: 'protocol', match: '1' }, + { featureType: 'anything', match: '2' }, + ]) + }) + }) + + describe('processQuery', () => { + it('should emit event and create disclosure message', async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(DiscoverFeaturesEventTypes.QueryReceived, eventListenerMock) + + const queryMessage = new V2QueriesMessage({ queries: [{ featureType: 'protocol', match: '*' }] }) + + const connection = getMockConnection({ state: DidExchangeState.Completed }) + const messageContext = new InboundMessageContext(queryMessage, { + agentContext: getAgentContext(), + connection, + }) + const outboundMessage = await discoverFeaturesService.processQuery(messageContext) + + eventEmitter.off(DiscoverFeaturesEventTypes.QueryReceived, eventListenerMock) + + expect(eventListenerMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: DiscoverFeaturesEventTypes.QueryReceived, + payload: expect.objectContaining({ + connection, + protocolVersion: 'v2', + queries: queryMessage.queries, + threadId: queryMessage.threadId, + }), + }) + ) + expect(outboundMessage).toBeDefined() + expect( + (outboundMessage as DiscoverFeaturesProtocolMsgReturnType).message.disclosures.map( + (p) => p.id + ) + ).toStrictEqual([ + 'https://didcomm.org/connections/1.0', + 'https://didcomm.org/notification/1.0', + 'https://didcomm.org/issue-credential/1.0', + ]) + }) + }) + + describe('processDisclosure', () => { + it('should emit event', async () => { + const eventListenerMock = jest.fn() + eventEmitter.on( + DiscoverFeaturesEventTypes.DisclosureReceived, + eventListenerMock + ) + + const discloseMessage = new V2DisclosuresMessage({ + features: [new Protocol({ id: 'prot1', roles: ['role1', 'role2'] }), new Protocol({ id: 'prot2' })], + threadId: '1234', + }) + + const connection = getMockConnection({ state: DidExchangeState.Completed }) + const messageContext = new InboundMessageContext(discloseMessage, { + agentContext: getAgentContext(), + connection, + }) + await discoverFeaturesService.processDisclosure(messageContext) + + eventEmitter.off( + DiscoverFeaturesEventTypes.DisclosureReceived, + eventListenerMock + ) + + expect(eventListenerMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: DiscoverFeaturesEventTypes.DisclosureReceived, + payload: expect.objectContaining({ + connection, + protocolVersion: 'v2', + disclosures: [ + { type: 'protocol', id: 'prot1', roles: ['role1', 'role2'] }, + { type: 'protocol', id: 'prot2' }, + ], + + threadId: discloseMessage.threadId, + }), + }) + ) + }) + }) +}) + +describe('V2DiscoverFeaturesService - auto accept disabled', () => { + const discoverFeaturesModuleConfig = new DiscoverFeaturesModuleConfig({ autoAcceptQueries: false }) + + const discoverFeaturesService = new V2DiscoverFeaturesService( + featureRegistry, + eventEmitter, + new MessageHandlerRegistryMock(), + new LoggerMock(), + discoverFeaturesModuleConfig + ) + + describe('processQuery', () => { + it('should emit event and not send any message', async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(DiscoverFeaturesEventTypes.QueryReceived, eventListenerMock) + + const queryMessage = new V2QueriesMessage({ queries: [{ featureType: 'protocol', match: '*' }] }) + + const connection = getMockConnection({ state: DidExchangeState.Completed }) + const messageContext = new InboundMessageContext(queryMessage, { + agentContext: getAgentContext(), + connection, + }) + const outboundMessage = await discoverFeaturesService.processQuery(messageContext) + + eventEmitter.off(DiscoverFeaturesEventTypes.QueryReceived, eventListenerMock) + + expect(eventListenerMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: DiscoverFeaturesEventTypes.QueryReceived, + payload: expect.objectContaining({ + connection, + protocolVersion: 'v2', + queries: queryMessage.queries, + threadId: queryMessage.threadId, + }), + }) + ) + expect(outboundMessage).toBeUndefined() + }) + }) +}) diff --git a/packages/core/src/modules/discover-features/protocol/v2/handlers/V2DisclosuresMessageHandler.ts b/packages/core/src/modules/discover-features/protocol/v2/handlers/V2DisclosuresMessageHandler.ts new file mode 100644 index 0000000000..1691e7a5a8 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v2/handlers/V2DisclosuresMessageHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V2DiscoverFeaturesService } from '../V2DiscoverFeaturesService' + +import { V2DisclosuresMessage } from '../messages' + +export class V2DisclosuresMessageHandler implements MessageHandler { + private discoverFeaturesService: V2DiscoverFeaturesService + public supportedMessages = [V2DisclosuresMessage] + + public constructor(discoverFeaturesService: V2DiscoverFeaturesService) { + this.discoverFeaturesService = discoverFeaturesService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + await this.discoverFeaturesService.processDisclosure(inboundMessage) + } +} diff --git a/packages/core/src/modules/discover-features/protocol/v2/handlers/V2QueriesMessageHandler.ts b/packages/core/src/modules/discover-features/protocol/v2/handlers/V2QueriesMessageHandler.ts new file mode 100644 index 0000000000..45798397be --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v2/handlers/V2QueriesMessageHandler.ts @@ -0,0 +1,27 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V2DiscoverFeaturesService } from '../V2DiscoverFeaturesService' + +import { OutboundMessageContext } from '../../../../../agent/models' +import { V2QueriesMessage } from '../messages' + +export class V2QueriesMessageHandler implements MessageHandler { + private discoverFeaturesService: V2DiscoverFeaturesService + public supportedMessages = [V2QueriesMessage] + + public constructor(discoverFeaturesService: V2DiscoverFeaturesService) { + this.discoverFeaturesService = discoverFeaturesService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + const connection = inboundMessage.assertReadyConnection() + + const discloseMessage = await this.discoverFeaturesService.processQuery(inboundMessage) + + if (discloseMessage) { + return new OutboundMessageContext(discloseMessage.message, { + agentContext: inboundMessage.agentContext, + connection, + }) + } + } +} diff --git a/packages/core/src/modules/discover-features/protocol/v2/handlers/index.ts b/packages/core/src/modules/discover-features/protocol/v2/handlers/index.ts new file mode 100644 index 0000000000..e4e6ce65a4 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v2/handlers/index.ts @@ -0,0 +1,2 @@ +export * from './V2DisclosuresMessageHandler' +export * from './V2QueriesMessageHandler' diff --git a/packages/core/src/modules/discover-features/protocol/v2/index.ts b/packages/core/src/modules/discover-features/protocol/v2/index.ts new file mode 100644 index 0000000000..f3bc1281ae --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v2/index.ts @@ -0,0 +1,2 @@ +export * from './V2DiscoverFeaturesService' +export * from './messages' diff --git a/packages/core/src/modules/discover-features/protocol/v2/messages/V2DisclosuresMessage.ts b/packages/core/src/modules/discover-features/protocol/v2/messages/V2DisclosuresMessage.ts new file mode 100644 index 0000000000..de029d7b29 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v2/messages/V2DisclosuresMessage.ts @@ -0,0 +1,36 @@ +import { Type } from 'class-transformer' +import { IsInstance } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Feature } from '../../../../../agent/models' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface V2DisclosuresMessageOptions { + id?: string + threadId?: string + features?: Feature[] +} + +export class V2DisclosuresMessage extends AgentMessage { + public constructor(options: V2DisclosuresMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.disclosures = options.features ?? [] + if (options.threadId) { + this.setThread({ + threadId: options.threadId, + }) + } + } + } + + @IsValidMessageType(V2DisclosuresMessage.type) + public readonly type = V2DisclosuresMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/discover-features/2.0/disclosures') + + @IsInstance(Feature, { each: true }) + @Type(() => Feature) + public disclosures!: Feature[] +} diff --git a/packages/core/src/modules/discover-features/protocol/v2/messages/V2QueriesMessage.ts b/packages/core/src/modules/discover-features/protocol/v2/messages/V2QueriesMessage.ts new file mode 100644 index 0000000000..b5de37fa20 --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v2/messages/V2QueriesMessage.ts @@ -0,0 +1,34 @@ +import type { FeatureQueryOptions } from '../../../../../agent/models' + +import { Type } from 'class-transformer' +import { ArrayNotEmpty, IsInstance } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { FeatureQuery } from '../../../../../agent/models' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface V2DiscoverFeaturesQueriesMessageOptions { + id?: string + queries: FeatureQueryOptions[] + comment?: string +} + +export class V2QueriesMessage extends AgentMessage { + public constructor(options: V2DiscoverFeaturesQueriesMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.queries = options.queries.map((q) => new FeatureQuery(q)) + } + } + + @IsValidMessageType(V2QueriesMessage.type) + public readonly type = V2QueriesMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/discover-features/2.0/queries') + + @IsInstance(FeatureQuery, { each: true }) + @Type(() => FeatureQuery) + @ArrayNotEmpty() + public queries!: FeatureQuery[] +} diff --git a/packages/core/src/modules/discover-features/protocol/v2/messages/index.ts b/packages/core/src/modules/discover-features/protocol/v2/messages/index.ts new file mode 100644 index 0000000000..ec88209bce --- /dev/null +++ b/packages/core/src/modules/discover-features/protocol/v2/messages/index.ts @@ -0,0 +1,2 @@ +export * from './V2DisclosuresMessage' +export * from './V2QueriesMessage' diff --git a/packages/core/src/modules/discover-features/services/DiscoverFeaturesService.ts b/packages/core/src/modules/discover-features/services/DiscoverFeaturesService.ts new file mode 100644 index 0000000000..c9e532b4c7 --- /dev/null +++ b/packages/core/src/modules/discover-features/services/DiscoverFeaturesService.ts @@ -0,0 +1,42 @@ +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { EventEmitter } from '../../../agent/EventEmitter' +import type { FeatureRegistry } from '../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { Logger } from '../../../logger' +import type { DiscoverFeaturesModuleConfig } from '../DiscoverFeaturesModuleConfig' +import type { + CreateDisclosureOptions, + CreateQueryOptions, + DiscoverFeaturesProtocolMsgReturnType, +} from '../DiscoverFeaturesServiceOptions' + +export abstract class DiscoverFeaturesService { + protected featureRegistry: FeatureRegistry + protected eventEmitter: EventEmitter + protected logger: Logger + protected discoverFeaturesModuleConfig: DiscoverFeaturesModuleConfig + + public constructor( + featureRegistry: FeatureRegistry, + eventEmitter: EventEmitter, + logger: Logger, + discoverFeaturesModuleConfig: DiscoverFeaturesModuleConfig + ) { + this.featureRegistry = featureRegistry + this.eventEmitter = eventEmitter + this.logger = logger + this.discoverFeaturesModuleConfig = discoverFeaturesModuleConfig + } + + public abstract readonly version: string + + public abstract createQuery(options: CreateQueryOptions): Promise> + public abstract processQuery( + messageContext: InboundMessageContext + ): Promise | void> + + public abstract createDisclosure( + options: CreateDisclosureOptions + ): Promise> + public abstract processDisclosure(messageContext: InboundMessageContext): Promise +} diff --git a/packages/core/src/modules/discover-features/services/index.ts b/packages/core/src/modules/discover-features/services/index.ts new file mode 100644 index 0000000000..2a245ed256 --- /dev/null +++ b/packages/core/src/modules/discover-features/services/index.ts @@ -0,0 +1 @@ +export * from './DiscoverFeaturesService' diff --git a/packages/core/src/modules/generic-records/GenericRecordsApi.ts b/packages/core/src/modules/generic-records/GenericRecordsApi.ts new file mode 100644 index 0000000000..26013dfdc7 --- /dev/null +++ b/packages/core/src/modules/generic-records/GenericRecordsApi.ts @@ -0,0 +1,90 @@ +import type { GenericRecord, SaveGenericRecordOption } from './repository/GenericRecord' +import type { Query, QueryOptions } from '../../storage/StorageService' + +import { AgentContext } from '../../agent' +import { InjectionSymbols } from '../../constants' +import { Logger } from '../../logger' +import { inject, injectable } from '../../plugins' + +import { GenericRecordService } from './services/GenericRecordService' + +export type ContentType = { + content: string +} + +@injectable() +export class GenericRecordsApi { + private genericRecordsService: GenericRecordService + private logger: Logger + private agentContext: AgentContext + + public constructor( + genericRecordsService: GenericRecordService, + @inject(InjectionSymbols.Logger) logger: Logger, + agentContext: AgentContext + ) { + this.genericRecordsService = genericRecordsService + this.logger = logger + this.agentContext = agentContext + } + + public async save({ content, tags, id }: SaveGenericRecordOption) { + try { + const record = await this.genericRecordsService.save(this.agentContext, { + id, + content: content, + tags: tags, + }) + return record + } catch (error) { + this.logger.error('Error while saving generic-record', { + error, + content, + errorMessage: error instanceof Error ? error.message : error, + }) + throw error + } + } + + public async delete(record: GenericRecord): Promise { + try { + await this.genericRecordsService.delete(this.agentContext, record) + } catch (error) { + this.logger.error('Error while saving generic-record', { + error, + content: record.content, + errorMessage: error instanceof Error ? error.message : error, + }) + throw error + } + } + + public async deleteById(id: string): Promise { + await this.genericRecordsService.deleteById(this.agentContext, id) + } + + public async update(record: GenericRecord): Promise { + try { + await this.genericRecordsService.update(this.agentContext, record) + } catch (error) { + this.logger.error('Error while update generic-record', { + error, + content: record.content, + errorMessage: error instanceof Error ? error.message : error, + }) + throw error + } + } + + public async findById(id: string) { + return this.genericRecordsService.findById(this.agentContext, id) + } + + public async findAllByQuery(query: Query, queryOptions?: QueryOptions): Promise { + return this.genericRecordsService.findAllByQuery(this.agentContext, query, queryOptions) + } + + public async getAll(): Promise { + return this.genericRecordsService.getAll(this.agentContext) + } +} diff --git a/packages/core/src/modules/generic-records/GenericRecordsModule.ts b/packages/core/src/modules/generic-records/GenericRecordsModule.ts new file mode 100644 index 0000000000..9ba63eecc4 --- /dev/null +++ b/packages/core/src/modules/generic-records/GenericRecordsModule.ts @@ -0,0 +1,20 @@ +import type { DependencyManager, Module } from '../../plugins' + +import { GenericRecordsApi } from './GenericRecordsApi' +import { GenericRecordsRepository } from './repository/GenericRecordsRepository' +import { GenericRecordService } from './services/GenericRecordService' + +export class GenericRecordsModule implements Module { + public readonly api = GenericRecordsApi + + /** + * Registers the dependencies of the generic records module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Services + dependencyManager.registerSingleton(GenericRecordService) + + // Repositories + dependencyManager.registerSingleton(GenericRecordsRepository) + } +} diff --git a/packages/core/src/modules/generic-records/__tests__/GenericRecordsModule.test.ts b/packages/core/src/modules/generic-records/__tests__/GenericRecordsModule.test.ts new file mode 100644 index 0000000000..8913cc0a8b --- /dev/null +++ b/packages/core/src/modules/generic-records/__tests__/GenericRecordsModule.test.ts @@ -0,0 +1,19 @@ +import { DependencyManager } from '../../../plugins/DependencyManager' +import { GenericRecordsModule } from '../GenericRecordsModule' +import { GenericRecordsRepository } from '../repository/GenericRecordsRepository' +import { GenericRecordService } from '../services/GenericRecordService' + +jest.mock('../../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +describe('GenericRecordsModule', () => { + test('registers dependencies on the dependency manager', () => { + new GenericRecordsModule().register(dependencyManager) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(GenericRecordService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(GenericRecordsRepository) + }) +}) diff --git a/packages/core/src/modules/generic-records/index.ts b/packages/core/src/modules/generic-records/index.ts new file mode 100644 index 0000000000..48d68d003f --- /dev/null +++ b/packages/core/src/modules/generic-records/index.ts @@ -0,0 +1,2 @@ +export * from './GenericRecordsApi' +export * from './GenericRecordsModule' diff --git a/packages/core/src/modules/generic-records/repository/GenericRecord.ts b/packages/core/src/modules/generic-records/repository/GenericRecord.ts new file mode 100644 index 0000000000..9c23266c9a --- /dev/null +++ b/packages/core/src/modules/generic-records/repository/GenericRecord.ts @@ -0,0 +1,43 @@ +import type { TagsBase } from '../../../storage/BaseRecord' + +import { BaseRecord } from '../../../storage/BaseRecord' +import { uuid } from '../../../utils/uuid' + +export type GenericRecordTags = TagsBase + +export interface GenericRecordStorageProps { + id?: string + createdAt?: Date + tags?: GenericRecordTags + content: Record +} + +export interface SaveGenericRecordOption { + content: Record + id?: string + tags?: GenericRecordTags +} + +export class GenericRecord extends BaseRecord { + public content!: Record + + public static readonly type = 'GenericRecord' + public readonly type = GenericRecord.type + + public constructor(props: GenericRecordStorageProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.content = props.content + this._tags = props.tags ?? {} + } + } + + public getTags() { + return { + ...this._tags, + } + } +} diff --git a/packages/core/src/modules/generic-records/repository/GenericRecordsRepository.ts b/packages/core/src/modules/generic-records/repository/GenericRecordsRepository.ts new file mode 100644 index 0000000000..bcac57ab64 --- /dev/null +++ b/packages/core/src/modules/generic-records/repository/GenericRecordsRepository.ts @@ -0,0 +1,17 @@ +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { GenericRecord } from './GenericRecord' + +@injectable() +export class GenericRecordsRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(GenericRecord, storageService, eventEmitter) + } +} diff --git a/packages/core/src/modules/generic-records/services/GenericRecordService.ts b/packages/core/src/modules/generic-records/services/GenericRecordService.ts new file mode 100644 index 0000000000..1a5335f377 --- /dev/null +++ b/packages/core/src/modules/generic-records/services/GenericRecordService.ts @@ -0,0 +1,64 @@ +import type { AgentContext } from '../../../agent' +import type { Query, QueryOptions } from '../../../storage/StorageService' +import type { SaveGenericRecordOption } from '../repository/GenericRecord' + +import { CredoError } from '../../../error' +import { injectable } from '../../../plugins' +import { GenericRecord } from '../repository/GenericRecord' +import { GenericRecordsRepository } from '../repository/GenericRecordsRepository' + +@injectable() +export class GenericRecordService { + private genericRecordsRepository: GenericRecordsRepository + + public constructor(genericRecordsRepository: GenericRecordsRepository) { + this.genericRecordsRepository = genericRecordsRepository + } + + public async save(agentContext: AgentContext, { content, tags, id }: SaveGenericRecordOption) { + const genericRecord = new GenericRecord({ + id, + content, + tags, + }) + + try { + await this.genericRecordsRepository.save(agentContext, genericRecord) + return genericRecord + } catch (error) { + throw new CredoError(`Unable to store the genericRecord record with id ${genericRecord.id}. Message: ${error}`) + } + } + + public async delete(agentContext: AgentContext, record: GenericRecord): Promise { + try { + await this.genericRecordsRepository.delete(agentContext, record) + } catch (error) { + throw new CredoError(`Unable to delete the genericRecord record with id ${record.id}. Message: ${error}`) + } + } + + public async deleteById(agentContext: AgentContext, id: string): Promise { + await this.genericRecordsRepository.deleteById(agentContext, id) + } + + public async update(agentContext: AgentContext, record: GenericRecord): Promise { + try { + await this.genericRecordsRepository.update(agentContext, record) + } catch (error) { + throw new CredoError(`Unable to update the genericRecord record with id ${record.id}. Message: ${error}`) + } + } + + public async findAllByQuery(agentContext: AgentContext, query: Query, queryOptions?: QueryOptions) { + return this.genericRecordsRepository.findByQuery(agentContext, query, queryOptions) + } + + public async findById(agentContext: AgentContext, id: string): Promise { + return this.genericRecordsRepository.findById(agentContext, id) + } + + public async getAll(agentContext: AgentContext) { + return this.genericRecordsRepository.getAll(agentContext) + } +} diff --git a/packages/core/src/modules/message-pickup/MessagePickupApi.ts b/packages/core/src/modules/message-pickup/MessagePickupApi.ts new file mode 100644 index 0000000000..e0d4de87d9 --- /dev/null +++ b/packages/core/src/modules/message-pickup/MessagePickupApi.ts @@ -0,0 +1,275 @@ +import type { + DeliverMessagesOptions, + DeliverMessagesFromQueueOptions, + PickupMessagesOptions, + PickupMessagesReturnType, + QueueMessageOptions, + QueueMessageReturnType, + SetLiveDeliveryModeOptions, + SetLiveDeliveryModeReturnType, + DeliverMessagesReturnType, + DeliverMessagesFromQueueReturnType, +} from './MessagePickupApiOptions' +import type { MessagePickupCompletedEvent } from './MessagePickupEvents' +import type { MessagePickupSession, MessagePickupSessionRole } from './MessagePickupSession' +import type { V1MessagePickupProtocol, V2MessagePickupProtocol } from './protocol' +import type { MessagePickupProtocol } from './protocol/MessagePickupProtocol' +import type { MessagePickupRepository } from './storage/MessagePickupRepository' + +import { ReplaySubject, Subject, filter, first, firstValueFrom, takeUntil, timeout } from 'rxjs' + +import { AgentContext } from '../../agent' +import { EventEmitter } from '../../agent/EventEmitter' +import { MessageSender } from '../../agent/MessageSender' +import { OutboundMessageContext } from '../../agent/models' +import { InjectionSymbols } from '../../constants' +import { CredoError } from '../../error' +import { Logger } from '../../logger/Logger' +import { inject, injectable } from '../../plugins' +import { ConnectionService } from '../connections/services' + +import { MessagePickupEventTypes } from './MessagePickupEvents' +import { MessagePickupModuleConfig } from './MessagePickupModuleConfig' +import { MessagePickupSessionService } from './services/MessagePickupSessionService' + +export interface MessagePickupApi { + queueMessage(options: QueueMessageOptions): Promise + pickupMessages(options: PickupMessagesOptions): Promise + getLiveModeSession(options: { + connectionId: string + role?: MessagePickupSessionRole + }): Promise + deliverMessages(options: DeliverMessagesOptions): Promise + deliverMessagesFromQueue(options: DeliverMessagesFromQueueOptions): Promise + setLiveDeliveryMode(options: SetLiveDeliveryModeOptions): Promise +} + +@injectable() +export class MessagePickupApi + implements MessagePickupApi +{ + public config: MessagePickupModuleConfig + + private messageSender: MessageSender + private agentContext: AgentContext + private eventEmitter: EventEmitter + private connectionService: ConnectionService + private messagePickupSessionService: MessagePickupSessionService + private logger: Logger + private stop$: Subject + + public constructor( + messageSender: MessageSender, + agentContext: AgentContext, + connectionService: ConnectionService, + eventEmitter: EventEmitter, + messagePickupSessionService: MessagePickupSessionService, + config: MessagePickupModuleConfig, + @inject(InjectionSymbols.Stop$) stop$: Subject, + @inject(InjectionSymbols.Logger) logger: Logger + ) { + this.messageSender = messageSender + this.connectionService = connectionService + this.agentContext = agentContext + this.eventEmitter = eventEmitter + this.config = config + this.messagePickupSessionService = messagePickupSessionService + this.stop$ = stop$ + this.logger = logger + } + + public async initialize() { + this.messagePickupSessionService.start(this.agentContext) + } + + private getProtocol(protocolVersion: MPP): MessagePickupProtocol { + const protocol = this.config.protocols.find((protocol) => protocol.version === protocolVersion) + + if (!protocol) { + throw new CredoError(`No message pickup protocol registered for protocol version ${protocolVersion}`) + } + + return protocol + } + + /** + * Add an encrypted message to the message pickup queue + * + * @param options: connectionId associated to the message and the encrypted message itself + */ + public async queueMessage(options: QueueMessageOptions): Promise { + this.logger.debug('Queuing message...') + const { connectionId, message, recipientDids } = options + const connectionRecord = await this.connectionService.getById(this.agentContext, connectionId) + + const messagePickupRepository = this.agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository + ) + + await messagePickupRepository.addMessage({ connectionId: connectionRecord.id, recipientDids, payload: message }) + } + + /** + * Get current active live mode message pickup session for a given connection. Undefined if no active session found + * + * @param options connection id and optional role + * @returns live mode session + */ + public async getLiveModeSession(options: { connectionId: string; role?: MessagePickupSessionRole }) { + const { connectionId, role } = options + return this.messagePickupSessionService.getLiveSessionByConnectionId(this.agentContext, { connectionId, role }) + } + + /** + * Deliver specific messages to an active live mode pickup session through message pickup protocol. + * + * This will deliver the messages regardless of the state of the message pickup queue, meaning that + * any message stuck there should be sent separately (e.g. using deliverQU). + * + * @param options: pickup session id and the messages to deliver + */ + public async deliverMessages(options: DeliverMessagesOptions) { + const { pickupSessionId, messages } = options + + const session = this.messagePickupSessionService.getLiveSession(this.agentContext, pickupSessionId) + + if (!session) { + throw new CredoError(`No active live mode session found with id ${pickupSessionId}`) + } + + const connectionRecord = await this.connectionService.getById(this.agentContext, session.connectionId) + + const protocol = this.getProtocol(session.protocolVersion) + + const createDeliveryReturn = await protocol.createDeliveryMessage(this.agentContext, { + connectionRecord, + messages, + }) + + if (createDeliveryReturn) { + await this.messageSender.sendMessage( + new OutboundMessageContext(createDeliveryReturn.message, { + agentContext: this.agentContext, + connection: connectionRecord, + }) + ) + } + } + + /** + * Deliver messages in the Message Pickup Queue for a given live mode session and key (if specified). + * + * This will retrieve messages up to 'batchSize' messages from the queue and deliver it through the + * corresponding Message Pickup protocol. If there are more than 'batchSize' messages in the queue, + * the recipient may request remaining messages after receiving the first batch of messages. + * + */ + public async deliverMessagesFromQueue(options: DeliverMessagesFromQueueOptions) { + this.logger.debug('Delivering queued messages') + + const { pickupSessionId, recipientDid: recipientKey, batchSize } = options + + const session = this.messagePickupSessionService.getLiveSession(this.agentContext, pickupSessionId) + + if (!session) { + throw new CredoError(`No active live mode session found with id ${pickupSessionId}`) + } + const connectionRecord = await this.connectionService.getById(this.agentContext, session.connectionId) + + const protocol = this.getProtocol(session.protocolVersion) + + const deliverMessagesReturn = await protocol.createDeliveryMessage(this.agentContext, { + connectionRecord, + recipientKey, + batchSize, + }) + + if (deliverMessagesReturn) { + await this.messageSender.sendMessage( + new OutboundMessageContext(deliverMessagesReturn.message, { + agentContext: this.agentContext, + connection: connectionRecord, + }) + ) + } + } + + /** + * Pickup queued messages from a message holder. It attempts to retrieve all current messages from the + * queue, receiving up to `batchSize` messages per batch retrieval. + * + * By default, this method only waits until the initial pick-up request is sent. Use `options.awaitCompletion` + * if you want to wait until all messages are effectively retrieved. + * + * @param options connectionId, protocol version to use and batch size, awaitCompletion, + * awaitCompletionTimeoutMs + */ + public async pickupMessages(options: PickupMessagesOptions): Promise { + const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) + + const protocol = this.getProtocol(options.protocolVersion) + const { message } = await protocol.createPickupMessage(this.agentContext, { + connectionRecord, + batchSize: options.batchSize, + recipientDid: options.recipientDid, + }) + + const outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connectionRecord, + }) + + const replaySubject = new ReplaySubject(1) + + if (options.awaitCompletion) { + this.eventEmitter + .observable(MessagePickupEventTypes.MessagePickupCompleted) + .pipe( + // Stop when the agent shuts down + takeUntil(this.stop$), + // filter by connection id + filter((e) => e.payload.connection.id === connectionRecord.id), + // Only wait for first event that matches the criteria + first(), + // If we don't receive all messages within timeoutMs miliseconds (no response, not supported, etc...) error + timeout({ + first: options.awaitCompletionTimeoutMs ?? 10000, + meta: 'MessagePickupApi.pickupMessages', + }) + ) + .subscribe(replaySubject) + } + + // For picking up messages we prefer a long-lived transport session, so we will set a higher priority to + // WebSocket endpoints. However, it is not extrictly required. + await this.messageSender.sendMessage(outboundMessageContext, { transportPriority: { schemes: ['wss', 'ws'] } }) + + if (options.awaitCompletion) { + await firstValueFrom(replaySubject) + } + } + + /** + * Enable or disable Live Delivery mode as a recipient. Depending on the message pickup protocol used, + * after receiving a response from the mediator the agent might retrieve any pending message. + * + * @param options connectionId, protocol version to use and boolean to enable/disable Live Mode + */ + public async setLiveDeliveryMode(options: SetLiveDeliveryModeOptions): Promise { + const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) + const protocol = this.getProtocol(options.protocolVersion) + const { message } = await protocol.setLiveDeliveryMode(this.agentContext, { + connectionRecord, + liveDelivery: options.liveDelivery, + }) + + // Live mode requires a long-lived transport session, so we'll require WebSockets to send this message + await this.messageSender.sendMessage( + new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connectionRecord, + }), + { transportPriority: { schemes: ['wss', 'ws'], restrictive: options.liveDelivery } } + ) + } +} diff --git a/packages/core/src/modules/message-pickup/MessagePickupApiOptions.ts b/packages/core/src/modules/message-pickup/MessagePickupApiOptions.ts new file mode 100644 index 0000000000..351753b2fb --- /dev/null +++ b/packages/core/src/modules/message-pickup/MessagePickupApiOptions.ts @@ -0,0 +1,50 @@ +import type { MessagePickupProtocol } from './protocol/MessagePickupProtocol' +import type { QueuedMessage } from './storage' +import type { EncryptedMessage } from '../../types' + +/** + * Get the supported protocol versions based on the provided message pickup protocols + */ +export type MessagePickupProtocolVersionType = MPPs[number]['version'] + +export interface QueueMessageOptions { + connectionId: string + recipientDids: string[] + message: EncryptedMessage +} + +export interface DeliverMessagesFromQueueOptions { + pickupSessionId: string + recipientDid?: string + batchSize?: number +} + +export interface DeliverMessagesOptions { + pickupSessionId: string + messages: QueuedMessage[] +} + +export interface PickupMessagesOptions { + connectionId: string + protocolVersion: MessagePickupProtocolVersionType + recipientDid?: string + batchSize?: number + awaitCompletion?: boolean + awaitCompletionTimeoutMs?: number +} + +export interface SetLiveDeliveryModeOptions { + connectionId: string + protocolVersion: MessagePickupProtocolVersionType + liveDelivery: boolean +} + +export type QueueMessageReturnType = void + +export type PickupMessagesReturnType = void + +export type DeliverMessagesReturnType = void + +export type DeliverMessagesFromQueueReturnType = void + +export type SetLiveDeliveryModeReturnType = void diff --git a/packages/core/src/modules/message-pickup/MessagePickupEvents.ts b/packages/core/src/modules/message-pickup/MessagePickupEvents.ts new file mode 100644 index 0000000000..bc95e70d29 --- /dev/null +++ b/packages/core/src/modules/message-pickup/MessagePickupEvents.ts @@ -0,0 +1,31 @@ +import type { MessagePickupSession } from './MessagePickupSession' +import type { BaseEvent } from '../../agent/Events' +import type { ConnectionRecord } from '../connections' + +export enum MessagePickupEventTypes { + LiveSessionSaved = 'LiveSessionSaved', + LiveSessionRemoved = 'LiveSessionRemoved', + MessagePickupCompleted = 'MessagePickupCompleted', +} + +export interface MessagePickupLiveSessionSavedEvent extends BaseEvent { + type: typeof MessagePickupEventTypes.LiveSessionSaved + payload: { + session: MessagePickupSession + } +} + +export interface MessagePickupLiveSessionRemovedEvent extends BaseEvent { + type: typeof MessagePickupEventTypes.LiveSessionRemoved + payload: { + session: MessagePickupSession + } +} + +export interface MessagePickupCompletedEvent extends BaseEvent { + type: typeof MessagePickupEventTypes.MessagePickupCompleted + payload: { + connection: ConnectionRecord + threadId?: string + } +} diff --git a/packages/core/src/modules/message-pickup/MessagePickupModule.ts b/packages/core/src/modules/message-pickup/MessagePickupModule.ts new file mode 100644 index 0000000000..a108392287 --- /dev/null +++ b/packages/core/src/modules/message-pickup/MessagePickupModule.ts @@ -0,0 +1,66 @@ +import type { MessagePickupModuleConfigOptions } from './MessagePickupModuleConfig' +import type { MessagePickupProtocol } from './protocol/MessagePickupProtocol' +import type { FeatureRegistry } from '../../agent/FeatureRegistry' +import type { ApiModule, DependencyManager } from '../../plugins' +import type { Optional } from '../../utils' +import type { Constructor } from '../../utils/mixins' + +import { InjectionSymbols } from '../../constants' + +import { MessagePickupApi } from './MessagePickupApi' +import { MessagePickupModuleConfig } from './MessagePickupModuleConfig' +import { V1MessagePickupProtocol, V2MessagePickupProtocol } from './protocol' +import { MessagePickupSessionService } from './services' +import { InMemoryMessagePickupRepository } from './storage' + +/** + * Default protocols that will be registered if the `protocols` property is not configured. + */ +export type DefaultMessagePickupProtocols = [V1MessagePickupProtocol, V2MessagePickupProtocol] + +// MessagePickupModuleOptions makes the protocols property optional from the config, as it will set it when not provided. +export type MessagePickupModuleOptions = Optional< + MessagePickupModuleConfigOptions, + 'protocols' +> + +export class MessagePickupModule + implements ApiModule +{ + public readonly config: MessagePickupModuleConfig + + // Infer Api type from the config + public readonly api: Constructor> = MessagePickupApi + + public constructor(config?: MessagePickupModuleOptions) { + this.config = new MessagePickupModuleConfig({ + ...config, + protocols: config?.protocols ?? [new V1MessagePickupProtocol(), new V2MessagePickupProtocol()], + }) as MessagePickupModuleConfig + } + + /** + * Registers the dependencies of the message pickup answer module on the dependency manager. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Config + dependencyManager.registerInstance(MessagePickupModuleConfig, this.config) + + // Services + dependencyManager.registerSingleton(MessagePickupSessionService) + + // Message Pickup queue: use provided one or in-memory one if no injection symbol is yet defined + if (this.config.messagePickupRepository) { + dependencyManager.registerInstance(InjectionSymbols.MessagePickupRepository, this.config.messagePickupRepository) + } else { + if (!dependencyManager.isRegistered(InjectionSymbols.MessagePickupRepository)) { + dependencyManager.registerSingleton(InjectionSymbols.MessagePickupRepository, InMemoryMessagePickupRepository) + } + } + + // Protocol needs to register feature registry items and handlers + for (const protocol of this.config.protocols) { + protocol.register(dependencyManager, featureRegistry) + } + } +} diff --git a/packages/core/src/modules/message-pickup/MessagePickupModuleConfig.ts b/packages/core/src/modules/message-pickup/MessagePickupModuleConfig.ts new file mode 100644 index 0000000000..1a2d9adc83 --- /dev/null +++ b/packages/core/src/modules/message-pickup/MessagePickupModuleConfig.ts @@ -0,0 +1,57 @@ +import type { MessagePickupProtocol } from './protocol/MessagePickupProtocol' +import type { MessagePickupRepository } from './storage/MessagePickupRepository' + +/** + * MessagePickupModuleConfigOptions defines the interface for the options of the MessagePickupModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface MessagePickupModuleConfigOptions { + /** + * Maximum number of messages to retrieve in a single batch message pickup + * + * @default 10 + */ + maximumBatchSize?: number + + /** + * Message pickup protocols to make available to the message pickup module. Only one protocol should be registered for each + * protocol version. + * + * When not provided, V1MessagePickupProtocol and V2MessagePickupProtocol` are registered by default. + * + * @default + * ``` + * [V1MessagePickupProtocol, V2MessagePickupProtocol] + * ``` + */ + protocols: MessagePickupProtocols + + /** + * Allows to specify a custom pickup message queue. It defaults to an in-memory queue + * + */ + messagePickupRepository?: MessagePickupRepository +} + +export class MessagePickupModuleConfig { + private options: MessagePickupModuleConfigOptions + + public constructor(options: MessagePickupModuleConfigOptions) { + this.options = options + } + + /** See {@link MessagePickupModuleConfig.maximumBatchSize} */ + public get maximumBatchSize() { + return this.options.maximumBatchSize ?? 10 + } + + /** See {@link MessagePickupModuleConfig.protocols} */ + public get protocols() { + return this.options.protocols + } + + /** See {@link MessagePickupModuleConfig.messagePickupRepository} */ + public get messagePickupRepository() { + return this.options.messagePickupRepository + } +} diff --git a/packages/core/src/modules/message-pickup/MessagePickupSession.ts b/packages/core/src/modules/message-pickup/MessagePickupSession.ts new file mode 100644 index 0000000000..26cf49ff1c --- /dev/null +++ b/packages/core/src/modules/message-pickup/MessagePickupSession.ts @@ -0,0 +1,13 @@ +import type { MessagePickupProtocolVersionType } from './MessagePickupApiOptions' +import type { MessagePickupProtocol } from './protocol/MessagePickupProtocol' + +export enum MessagePickupSessionRole { + Recipient = 'Recipient', + MessageHolder = 'MessageHolder', +} +export type MessagePickupSession = { + id: string + connectionId: string + protocolVersion: MessagePickupProtocolVersionType + role: MessagePickupSessionRole +} diff --git a/packages/core/src/modules/message-pickup/__tests__/MessagePickupModule.test.ts b/packages/core/src/modules/message-pickup/__tests__/MessagePickupModule.test.ts new file mode 100644 index 0000000000..6bb3c4f992 --- /dev/null +++ b/packages/core/src/modules/message-pickup/__tests__/MessagePickupModule.test.ts @@ -0,0 +1,47 @@ +import { FeatureRegistry } from '../../../agent/FeatureRegistry' +import { Protocol } from '../../../agent/models' +import { InjectionSymbols } from '../../../constants' +import { DependencyManager } from '../../../plugins/DependencyManager' +import { MessagePickupModule } from '../MessagePickupModule' +import { MessagePickupModuleConfig } from '../MessagePickupModuleConfig' +import { MessagePickupSessionService } from '../services' +import { InMemoryMessagePickupRepository } from '../storage' + +jest.mock('../../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +jest.mock('../../../agent/FeatureRegistry') +const FeatureRegistryMock = FeatureRegistry as jest.Mock + +const dependencyManager = new DependencyManagerMock() +const featureRegistry = new FeatureRegistryMock() + +describe('MessagePickupModule', () => { + test('registers dependencies on the dependency manager', () => { + const module = new MessagePickupModule() + module.register(dependencyManager, featureRegistry) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(MessagePickupModuleConfig, module.config) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( + InjectionSymbols.MessagePickupRepository, + InMemoryMessagePickupRepository + ) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(MessagePickupSessionService) + expect(featureRegistry.register).toHaveBeenCalledTimes(2) + expect(featureRegistry.register).toHaveBeenCalledWith( + new Protocol({ + id: 'https://didcomm.org/messagepickup/1.0', + roles: ['message_holder', 'recipient', 'batch_sender', 'batch_recipient'], + }) + ) + expect(featureRegistry.register).toHaveBeenCalledWith( + new Protocol({ + id: 'https://didcomm.org/messagepickup/2.0', + roles: ['mediator', 'recipient'], + }) + ) + }) +}) diff --git a/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts b/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts new file mode 100644 index 0000000000..c54b4a6184 --- /dev/null +++ b/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts @@ -0,0 +1,310 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../../../../tests/transport/SubjectInboundTransport' + +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' +import { + getInMemoryAgentOptions, + waitForAgentMessageProcessedEvent, + waitForBasicMessage, +} from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { HandshakeProtocol } from '../../connections' +import { MediatorModule } from '../../routing' +import { MessageForwardingStrategy } from '../../routing/MessageForwardingStrategy' +import { V2MessagesReceivedMessage, V2StatusMessage } from '../protocol' + +const recipientOptions = getInMemoryAgentOptions('Mediation Pickup Loop Recipient') +const mediatorOptions = getInMemoryAgentOptions( + 'Mediation Pickup Loop Mediator', + { + endpoints: ['wss://mediator'], + }, + { + mediator: new MediatorModule({ + autoAcceptMediationRequests: true, + messageForwardingStrategy: MessageForwardingStrategy.QueueAndLiveModeDelivery, + }), + } +) + +describe('E2E Pick Up protocol', () => { + let recipientAgent: Agent + let mediatorAgent: Agent + + afterEach(async () => { + await recipientAgent.mediationRecipient.stopMessagePickup() + + await recipientAgent.shutdown() + await recipientAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + }) + + test('E2E manual Pick Up V1 loop', async () => { + const mediatorMessages = new Subject() + + const subjectMap = { + 'wss://mediator': mediatorMessages, + } + + // Initialize mediatorReceived message + mediatorAgent = new Agent(mediatorOptions) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + await mediatorAgent.initialize() + + // Create connection to use for recipient + const mediatorOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'mediator invitation', + handshake: true, + handshakeProtocols: [HandshakeProtocol.DidExchange], + }) + + // Initialize recipient + recipientAgent = new Agent(recipientOptions) + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await recipientAgent.initialize() + + // Connect + const mediatorInvitation = mediatorOutOfBandRecord.outOfBandInvitation + + let { connectionRecord: recipientMediatorConnection } = await recipientAgent.oob.receiveInvitationFromUrl( + mediatorInvitation.toUrl({ domain: 'https://example.com/ssi' }) + ) + + recipientMediatorConnection = await recipientAgent.connections.returnWhenIsConnected( + recipientMediatorConnection!.id + ) + + let [mediatorRecipientConnection] = await mediatorAgent.connections.findAllByOutOfBandId(mediatorOutOfBandRecord.id) + + mediatorRecipientConnection = await mediatorAgent.connections.returnWhenIsConnected(mediatorRecipientConnection!.id) + + // Now they are connected, reinitialize recipient agent in order to lose the session (as with SubjectTransport it remains open) + await recipientAgent.shutdown() + await recipientAgent.initialize() + + const message = 'hello pickup V1' + await mediatorAgent.basicMessages.sendMessage(mediatorRecipientConnection.id, message) + + await recipientAgent.messagePickup.pickupMessages({ + connectionId: recipientMediatorConnection.id, + protocolVersion: 'v1', + }) + + const basicMessage = await waitForBasicMessage(recipientAgent, { + content: message, + }) + + expect(basicMessage.content).toBe(message) + }) + + test('E2E manual Pick Up V1 loop - waiting for completion', async () => { + const mediatorMessages = new Subject() + + const subjectMap = { + 'wss://mediator': mediatorMessages, + } + + // Initialize mediatorReceived message + mediatorAgent = new Agent(mediatorOptions) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + await mediatorAgent.initialize() + + // Create connection to use for recipient + const mediatorOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'mediator invitation', + handshake: true, + handshakeProtocols: [HandshakeProtocol.DidExchange], + }) + + // Initialize recipient + recipientAgent = new Agent(recipientOptions) + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await recipientAgent.initialize() + + // Connect + const mediatorInvitation = mediatorOutOfBandRecord.outOfBandInvitation + + let { connectionRecord: recipientMediatorConnection } = await recipientAgent.oob.receiveInvitationFromUrl( + mediatorInvitation.toUrl({ domain: 'https://example.com/ssi' }) + ) + + recipientMediatorConnection = await recipientAgent.connections.returnWhenIsConnected( + recipientMediatorConnection!.id + ) + + let [mediatorRecipientConnection] = await mediatorAgent.connections.findAllByOutOfBandId(mediatorOutOfBandRecord.id) + + mediatorRecipientConnection = await mediatorAgent.connections.returnWhenIsConnected(mediatorRecipientConnection!.id) + + // Now they are connected, reinitialize recipient agent in order to lose the session (as with SubjectTransport it remains open) + await recipientAgent.shutdown() + await recipientAgent.initialize() + + const message = 'hello pickup V1' + await mediatorAgent.basicMessages.sendMessage(mediatorRecipientConnection.id, message) + + const basicMessagePromise = waitForBasicMessage(recipientAgent, { + content: message, + }) + await recipientAgent.messagePickup.pickupMessages({ + connectionId: recipientMediatorConnection.id, + protocolVersion: 'v1', + awaitCompletion: true, + }) + + const basicMessage = await basicMessagePromise + expect(basicMessage.content).toBe(message) + }) + + test('E2E manual Pick Up V2 loop', async () => { + const mediatorMessages = new Subject() + + // FIXME: we harcoded that pickup of messages MUST be using ws(s) scheme when doing implicit pickup + // For liver delivery we need a duplex transport. however that means we can't test it with the subject transport. Using wss here to 'hack' this. We should + // extend the API to allow custom schemes (or maybe add a `supportsDuplex` transport / `supportMultiReturnMessages`) + // For pickup v2 pickup message (which we're testing here) we could just as well use `http` as it is just request/response. + const subjectMap = { + 'wss://mediator': mediatorMessages, + } + + // Initialize mediatorReceived message + mediatorAgent = new Agent(mediatorOptions) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + await mediatorAgent.initialize() + + // Create connection to use for recipient + const mediatorOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'mediator invitation', + handshake: true, + handshakeProtocols: [HandshakeProtocol.DidExchange], + }) + + // Initialize recipient + recipientAgent = new Agent(recipientOptions) + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await recipientAgent.initialize() + + // Connect + const mediatorInvitation = mediatorOutOfBandRecord.outOfBandInvitation + + let { connectionRecord: recipientMediatorConnection } = await recipientAgent.oob.receiveInvitationFromUrl( + mediatorInvitation.toUrl({ domain: 'https://example.com/ssi' }) + ) + + recipientMediatorConnection = await recipientAgent.connections.returnWhenIsConnected( + recipientMediatorConnection!.id + ) + + let [mediatorRecipientConnection] = await mediatorAgent.connections.findAllByOutOfBandId(mediatorOutOfBandRecord.id) + + mediatorRecipientConnection = await mediatorAgent.connections.returnWhenIsConnected(mediatorRecipientConnection!.id) + + // Now they are connected, reinitialize recipient agent in order to lose the session (as with SubjectTransport it remains open) + await recipientAgent.shutdown() + await recipientAgent.initialize() + + const message = 'hello pickup V2' + + await mediatorAgent.basicMessages.sendMessage(mediatorRecipientConnection.id, message) + + const basicMessagePromise = waitForBasicMessage(recipientAgent, { + content: message, + }) + await recipientAgent.messagePickup.pickupMessages({ + connectionId: recipientMediatorConnection.id, + protocolVersion: 'v2', + }) + const firstStatusMessage = await waitForAgentMessageProcessedEvent(recipientAgent, { + messageType: V2StatusMessage.type.messageTypeUri, + }) + + expect((firstStatusMessage as V2StatusMessage).messageCount).toBe(1) + + const basicMessage = await basicMessagePromise + expect(basicMessage.content).toBe(message) + + const messagesReceived = await waitForAgentMessageProcessedEvent(mediatorAgent, { + messageType: V2MessagesReceivedMessage.type.messageTypeUri, + }) + + expect((messagesReceived as V2MessagesReceivedMessage).messageIdList.length).toBe(1) + + const secondStatusMessage = await waitForAgentMessageProcessedEvent(recipientAgent, { + messageType: V2StatusMessage.type.messageTypeUri, + }) + + expect((secondStatusMessage as V2StatusMessage).messageCount).toBe(0) + }) + + test('E2E manual Pick Up V2 loop - waiting for completion', async () => { + const mediatorMessages = new Subject() + + // FIXME: we harcoded that pickup of messages MUST be using ws(s) scheme when doing implicit pickup + // For liver delivery we need a duplex transport. however that means we can't test it with the subject transport. Using wss here to 'hack' this. We should + // extend the API to allow custom schemes (or maybe add a `supportsDuplex` transport / `supportMultiReturnMessages`) + // For pickup v2 pickup message (which we're testing here) we could just as well use `http` as it is just request/response. + const subjectMap = { + 'wss://mediator': mediatorMessages, + } + + // Initialize mediatorReceived message + mediatorAgent = new Agent(mediatorOptions) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + await mediatorAgent.initialize() + + // Create connection to use for recipient + const mediatorOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'mediator invitation', + handshake: true, + handshakeProtocols: [HandshakeProtocol.DidExchange], + }) + + // Initialize recipient + recipientAgent = new Agent(recipientOptions) + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await recipientAgent.initialize() + + // Connect + const mediatorInvitation = mediatorOutOfBandRecord.outOfBandInvitation + + let { connectionRecord: recipientMediatorConnection } = await recipientAgent.oob.receiveInvitationFromUrl( + mediatorInvitation.toUrl({ domain: 'https://example.com/ssi' }) + ) + + recipientMediatorConnection = await recipientAgent.connections.returnWhenIsConnected( + recipientMediatorConnection!.id + ) + + let [mediatorRecipientConnection] = await mediatorAgent.connections.findAllByOutOfBandId(mediatorOutOfBandRecord.id) + + mediatorRecipientConnection = await mediatorAgent.connections.returnWhenIsConnected(mediatorRecipientConnection!.id) + + // Now they are connected, reinitialize recipient agent in order to lose the session (as with SubjectTransport it remains open) + await recipientAgent.shutdown() + await recipientAgent.initialize() + + const message = 'hello pickup V2' + + await mediatorAgent.basicMessages.sendMessage(mediatorRecipientConnection.id, message) + + const basicMessagePromise = waitForBasicMessage(recipientAgent, { + content: message, + }) + await recipientAgent.messagePickup.pickupMessages({ + connectionId: recipientMediatorConnection.id, + protocolVersion: 'v2', + awaitCompletion: true, + }) + + const basicMessage = await basicMessagePromise + expect(basicMessage.content).toBe(message) + }) +}) diff --git a/packages/core/src/modules/message-pickup/index.ts b/packages/core/src/modules/message-pickup/index.ts new file mode 100644 index 0000000000..b2b05ba8ee --- /dev/null +++ b/packages/core/src/modules/message-pickup/index.ts @@ -0,0 +1,8 @@ +export * from './MessagePickupApi' +export * from './MessagePickupApiOptions' +export * from './MessagePickupEvents' +export * from './MessagePickupModule' +export * from './MessagePickupModuleConfig' +export * from './protocol' +export * from './storage' +export { MessagePickupSessionService } from './services' diff --git a/packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts new file mode 100644 index 0000000000..686cdccc90 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts @@ -0,0 +1,38 @@ +import type { MessagePickupProtocol } from './MessagePickupProtocol' +import type { + DeliverMessagesProtocolOptions, + DeliverMessagesProtocolReturnType, + PickupMessagesProtocolOptions, + PickupMessagesProtocolReturnType, + SetLiveDeliveryModeProtocolOptions, + SetLiveDeliveryModeProtocolReturnType, +} from './MessagePickupProtocolOptions' +import type { AgentContext } from '../../../agent' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { FeatureRegistry } from '../../../agent/FeatureRegistry' +import type { DependencyManager } from '../../../plugins' + +/** + * Base implementation of the MessagePickupProtocol that can be used as a foundation for implementing + * the MessagePickupProtocol interface. + */ +export abstract class BaseMessagePickupProtocol implements MessagePickupProtocol { + public abstract readonly version: string + + public abstract createPickupMessage( + agentContext: AgentContext, + options: PickupMessagesProtocolOptions + ): Promise> + + public abstract createDeliveryMessage( + agentContext: AgentContext, + options: DeliverMessagesProtocolOptions + ): Promise | void> + + public abstract setLiveDeliveryMode( + agentContext: AgentContext, + options: SetLiveDeliveryModeProtocolOptions + ): Promise> + + public abstract register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void +} diff --git a/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts new file mode 100644 index 0000000000..df11b80547 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts @@ -0,0 +1,33 @@ +import type { + DeliverMessagesProtocolOptions, + DeliverMessagesProtocolReturnType, + PickupMessagesProtocolOptions, + PickupMessagesProtocolReturnType, + SetLiveDeliveryModeProtocolOptions, + SetLiveDeliveryModeProtocolReturnType, +} from './MessagePickupProtocolOptions' +import type { AgentContext } from '../../../agent' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { FeatureRegistry } from '../../../agent/FeatureRegistry' +import type { DependencyManager } from '../../../plugins' + +export interface MessagePickupProtocol { + readonly version: string + + createPickupMessage( + agentContext: AgentContext, + options: PickupMessagesProtocolOptions + ): Promise> + + createDeliveryMessage( + agentContext: AgentContext, + options: DeliverMessagesProtocolOptions + ): Promise | void> + + setLiveDeliveryMode( + agentContext: AgentContext, + options: SetLiveDeliveryModeProtocolOptions + ): Promise> + + register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void +} diff --git a/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts new file mode 100644 index 0000000000..4f4409c501 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts @@ -0,0 +1,33 @@ +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { ConnectionRecord } from '../../connections' +import type { QueuedMessage } from '../storage' + +export interface PickupMessagesProtocolOptions { + connectionRecord: ConnectionRecord + recipientDid?: string + batchSize?: number +} + +export interface DeliverMessagesProtocolOptions { + connectionRecord: ConnectionRecord + messages?: QueuedMessage[] + recipientKey?: string + batchSize?: number +} + +export interface SetLiveDeliveryModeProtocolOptions { + connectionRecord: ConnectionRecord + liveDelivery: boolean +} + +export type PickupMessagesProtocolReturnType = { + message: MessageType +} + +export type DeliverMessagesProtocolReturnType = { + message: MessageType +} + +export type SetLiveDeliveryModeProtocolReturnType = { + message: MessageType +} diff --git a/packages/core/src/modules/message-pickup/protocol/index.ts b/packages/core/src/modules/message-pickup/protocol/index.ts new file mode 100644 index 0000000000..4d9da63573 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/index.ts @@ -0,0 +1,2 @@ +export * from './v1' +export * from './v2' diff --git a/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts new file mode 100644 index 0000000000..f661b04ba2 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts @@ -0,0 +1,173 @@ +import type { AgentContext } from '../../../../agent' +import type { AgentMessage } from '../../../../agent/AgentMessage' +import type { AgentMessageReceivedEvent } from '../../../../agent/Events' +import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../../plugins' +import type { MessagePickupCompletedEvent } from '../../MessagePickupEvents' +import type { MessagePickupRepository } from '../../storage/MessagePickupRepository' +import type { + DeliverMessagesProtocolOptions, + DeliverMessagesProtocolReturnType, + PickupMessagesProtocolOptions, + PickupMessagesProtocolReturnType, + SetLiveDeliveryModeProtocolReturnType, +} from '../MessagePickupProtocolOptions' + +import { EventEmitter } from '../../../../agent/EventEmitter' +import { AgentEventTypes } from '../../../../agent/Events' +import { OutboundMessageContext, Protocol } from '../../../../agent/models' +import { InjectionSymbols } from '../../../../constants' +import { CredoError } from '../../../../error' +import { injectable } from '../../../../plugins' +import { MessagePickupEventTypes } from '../../MessagePickupEvents' +import { MessagePickupModuleConfig } from '../../MessagePickupModuleConfig' +import { BaseMessagePickupProtocol } from '../BaseMessagePickupProtocol' + +import { V1BatchHandler, V1BatchPickupHandler } from './handlers' +import { V1BatchMessage, BatchMessageMessage, V1BatchPickupMessage } from './messages' + +@injectable() +export class V1MessagePickupProtocol extends BaseMessagePickupProtocol { + /** + * The version of the message pickup protocol this class supports + */ + public readonly version = 'v1' as const + + /** + * Registers the protocol implementation (handlers, feature registry) on the agent. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void { + dependencyManager.registerMessageHandlers([new V1BatchPickupHandler(this), new V1BatchHandler(this)]) + + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/messagepickup/1.0', + roles: ['message_holder', 'recipient', 'batch_sender', 'batch_recipient'], + }) + ) + } + + public async createPickupMessage( + agentContext: AgentContext, + options: PickupMessagesProtocolOptions + ): Promise> { + const { connectionRecord, batchSize } = options + connectionRecord.assertReady() + + const config = agentContext.dependencyManager.resolve(MessagePickupModuleConfig) + const message = new V1BatchPickupMessage({ + batchSize: batchSize ?? config.maximumBatchSize, + }) + + return { message } + } + + public async createDeliveryMessage( + agentContext: AgentContext, + options: DeliverMessagesProtocolOptions + ): Promise | void> { + const { connectionRecord, batchSize, messages } = options + connectionRecord.assertReady() + + const pickupMessageQueue = agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository + ) + + const messagesToDeliver = + messages ?? + (await pickupMessageQueue.takeFromQueue({ + connectionId: connectionRecord.id, + limit: batchSize, // TODO: Define as config parameter for message holder side + deleteMessages: true, + })) + + const batchMessages = messagesToDeliver.map( + (msg) => + new BatchMessageMessage({ + id: msg.id, + message: msg.encryptedMessage, + }) + ) + + if (messagesToDeliver.length > 0) { + const message = new V1BatchMessage({ + messages: batchMessages, + }) + + return { message } + } + } + + public async setLiveDeliveryMode(): Promise> { + throw new CredoError('Live Delivery mode not supported in Message Pickup V1 protocol') + } + + public async processBatchPickup(messageContext: InboundMessageContext) { + // Assert ready connection + const connection = messageContext.assertReadyConnection() + + const { message } = messageContext + + const pickupMessageQueue = messageContext.agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository + ) + + const messages = await pickupMessageQueue.takeFromQueue({ + connectionId: connection.id, + limit: message.batchSize, + deleteMessages: true, + }) + + const batchMessages = messages.map( + (msg) => + new BatchMessageMessage({ + id: msg.id, + message: msg.encryptedMessage, + }) + ) + + const batchMessage = new V1BatchMessage({ + messages: batchMessages, + threadId: message.threadId, + }) + + return new OutboundMessageContext(batchMessage, { agentContext: messageContext.agentContext, connection }) + } + + public async processBatch(messageContext: InboundMessageContext) { + const { message: batchMessage, agentContext } = messageContext + const { messages } = batchMessage + + const connection = messageContext.assertReadyConnection() + + const eventEmitter = messageContext.agentContext.dependencyManager.resolve(EventEmitter) + + messages.forEach((message) => { + eventEmitter.emit(messageContext.agentContext, { + type: AgentEventTypes.AgentMessageReceived, + payload: { + message: message.message, + contextCorrelationId: messageContext.agentContext.contextCorrelationId, + }, + }) + }) + + // A Batch message without messages at all means that we are done with the + // message pickup process (Note: this is not optimal since we'll always doing an extra + // Batch Pickup. However, it is safer and should be faster than waiting an entire loop + // interval to retrieve more messages) + if (messages.length === 0) { + eventEmitter.emit(messageContext.agentContext, { + type: MessagePickupEventTypes.MessagePickupCompleted, + payload: { + connection, + threadId: batchMessage.threadId, + }, + }) + return null + } + + return (await this.createPickupMessage(agentContext, { connectionRecord: connection })).message + } +} diff --git a/packages/core/src/modules/message-pickup/protocol/v1/handlers/V1BatchHandler.ts b/packages/core/src/modules/message-pickup/protocol/v1/handlers/V1BatchHandler.ts new file mode 100644 index 0000000000..f49e8130d1 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v1/handlers/V1BatchHandler.ts @@ -0,0 +1,26 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V1MessagePickupProtocol } from '../V1MessagePickupProtocol' + +import { OutboundMessageContext } from '../../../../../agent/models' +import { V1BatchMessage } from '../messages' + +export class V1BatchHandler implements MessageHandler { + public supportedMessages = [V1BatchMessage] + private messagePickupProtocol: V1MessagePickupProtocol + + public constructor(messagePickupProtocol: V1MessagePickupProtocol) { + this.messagePickupProtocol = messagePickupProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const connection = messageContext.assertReadyConnection() + const batchRequestMessage = await this.messagePickupProtocol.processBatch(messageContext) + + if (batchRequestMessage) { + return new OutboundMessageContext(batchRequestMessage, { + agentContext: messageContext.agentContext, + connection, + }) + } + } +} diff --git a/packages/core/src/modules/message-pickup/protocol/v1/handlers/V1BatchPickupHandler.ts b/packages/core/src/modules/message-pickup/protocol/v1/handlers/V1BatchPickupHandler.ts new file mode 100644 index 0000000000..d9eee7c4d9 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v1/handlers/V1BatchPickupHandler.ts @@ -0,0 +1,19 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V1MessagePickupProtocol } from '../V1MessagePickupProtocol' + +import { V1BatchPickupMessage } from '../messages' + +export class V1BatchPickupHandler implements MessageHandler { + private messagePickupService: V1MessagePickupProtocol + public supportedMessages = [V1BatchPickupMessage] + + public constructor(messagePickupService: V1MessagePickupProtocol) { + this.messagePickupService = messagePickupService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + messageContext.assertReadyConnection() + + return this.messagePickupService.processBatchPickup(messageContext) + } +} diff --git a/packages/core/src/modules/message-pickup/protocol/v1/handlers/index.ts b/packages/core/src/modules/message-pickup/protocol/v1/handlers/index.ts new file mode 100644 index 0000000000..b8aef88046 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v1/handlers/index.ts @@ -0,0 +1,2 @@ +export * from './V1BatchHandler' +export * from './V1BatchPickupHandler' diff --git a/packages/core/src/modules/message-pickup/protocol/v1/index.ts b/packages/core/src/modules/message-pickup/protocol/v1/index.ts new file mode 100644 index 0000000000..abf43d6b2a --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v1/index.ts @@ -0,0 +1,2 @@ +export * from './V1MessagePickupProtocol' +export * from './messages' diff --git a/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchMessage.ts b/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchMessage.ts new file mode 100644 index 0000000000..37b8c775e2 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchMessage.ts @@ -0,0 +1,64 @@ +import { Type, Expose } from 'class-transformer' +import { Matches, IsArray, ValidateNested, IsObject, IsInstance } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { MessageIdRegExp } from '../../../../../agent/BaseMessage' +import { EncryptedMessage } from '../../../../../types' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { uuid } from '../../../../../utils/uuid' + +export class BatchMessageMessage { + public constructor(options: { id?: string; message: EncryptedMessage }) { + if (options) { + this.id = options.id || uuid() + this.message = options.message + } + } + + @Matches(MessageIdRegExp) + public id!: string + + @IsObject() + public message!: EncryptedMessage +} + +export interface BatchMessageOptions { + id?: string + messages: BatchMessageMessage[] + threadId?: string +} + +/** + * A message that contains multiple waiting messages. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0212-pickup/README.md#batch + */ +export class V1BatchMessage extends AgentMessage { + public readonly allowQueueTransport = false + + public constructor(options: BatchMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.messages = options.messages + + if (options.threadId) { + this.setThread({ + threadId: options.threadId, + }) + } + } + } + + @IsValidMessageType(V1BatchMessage.type) + public readonly type = V1BatchMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/1.0/batch') + + @Type(() => BatchMessageMessage) + @IsArray() + @ValidateNested() + @IsInstance(BatchMessageMessage, { each: true }) + @Expose({ name: 'messages~attach' }) + public messages!: BatchMessageMessage[] +} diff --git a/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchPickupMessage.ts b/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchPickupMessage.ts new file mode 100644 index 0000000000..950c700b3d --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchPickupMessage.ts @@ -0,0 +1,41 @@ +import { Expose } from 'class-transformer' +import { IsInt } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface BatchPickupMessageOptions { + id?: string + batchSize: number +} + +/** + * A message to request to have multiple waiting messages sent inside a `batch` message. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0212-pickup/README.md#batch-pickup + */ +export class V1BatchPickupMessage extends AgentMessage { + public readonly allowQueueTransport = false + + /** + * Create new BatchPickupMessage instance. + * + * @param options + */ + public constructor(options: BatchPickupMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.batchSize = options.batchSize + } + } + + @IsValidMessageType(V1BatchPickupMessage.type) + public readonly type = V1BatchPickupMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/1.0/batch-pickup') + + @IsInt() + @Expose({ name: 'batch_size' }) + public batchSize!: number +} diff --git a/packages/core/src/modules/message-pickup/protocol/v1/messages/index.ts b/packages/core/src/modules/message-pickup/protocol/v1/messages/index.ts new file mode 100644 index 0000000000..19c16cf1d8 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v1/messages/index.ts @@ -0,0 +1,2 @@ +export * from './V1BatchMessage' +export * from './V1BatchPickupMessage' diff --git a/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts new file mode 100644 index 0000000000..3c2470c34a --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts @@ -0,0 +1,337 @@ +import type { AgentContext } from '../../../../agent' +import type { AgentMessage } from '../../../../agent/AgentMessage' +import type { AgentMessageReceivedEvent } from '../../../../agent/Events' +import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../../plugins' +import type { EncryptedMessage } from '../../../../types' +import type { MessagePickupCompletedEvent } from '../../MessagePickupEvents' +import type { MessagePickupRepository } from '../../storage/MessagePickupRepository' +import type { + DeliverMessagesProtocolOptions, + DeliverMessagesProtocolReturnType, + PickupMessagesProtocolOptions, + PickupMessagesProtocolReturnType, + SetLiveDeliveryModeProtocolOptions, + SetLiveDeliveryModeProtocolReturnType, +} from '../MessagePickupProtocolOptions' + +import { EventEmitter } from '../../../../agent/EventEmitter' +import { AgentEventTypes } from '../../../../agent/Events' +import { OutboundMessageContext, Protocol } from '../../../../agent/models' +import { InjectionSymbols } from '../../../../constants' +import { Attachment } from '../../../../decorators/attachment/Attachment' +import { injectable } from '../../../../plugins' +import { verkeyToDidKey } from '../../../dids/helpers' +import { ProblemReportError } from '../../../problem-reports' +import { RoutingProblemReportReason } from '../../../routing/error' +import { MessagePickupEventTypes } from '../../MessagePickupEvents' +import { MessagePickupModuleConfig } from '../../MessagePickupModuleConfig' +import { MessagePickupSessionRole } from '../../MessagePickupSession' +import { MessagePickupSessionService } from '../../services' +import { BaseMessagePickupProtocol } from '../BaseMessagePickupProtocol' + +import { + V2DeliveryRequestHandler, + V2LiveDeliveryChangeHandler, + V2MessageDeliveryHandler, + V2MessagesReceivedHandler, + V2StatusHandler, + V2StatusRequestHandler, +} from './handlers' +import { + V2MessageDeliveryMessage, + V2StatusMessage, + V2DeliveryRequestMessage, + V2MessagesReceivedMessage, + V2StatusRequestMessage, + V2LiveDeliveryChangeMessage, +} from './messages' + +@injectable() +export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { + /** + * The version of the message pickup protocol this class supports + */ + public readonly version = 'v2' as const + + /** + * Registers the protocol implementation (handlers, feature registry) on the agent. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void { + dependencyManager.registerMessageHandlers([ + new V2StatusRequestHandler(this), + new V2DeliveryRequestHandler(this), + new V2MessagesReceivedHandler(this), + new V2StatusHandler(this), + new V2MessageDeliveryHandler(this), + new V2LiveDeliveryChangeHandler(this), + ]) + + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/messagepickup/2.0', + roles: ['mediator', 'recipient'], + }) + ) + } + + public async createPickupMessage( + agentContext: AgentContext, + options: PickupMessagesProtocolOptions + ): Promise> { + const { connectionRecord, recipientDid: recipientKey } = options + connectionRecord.assertReady() + + const message = new V2StatusRequestMessage({ + recipientKey, + }) + + return { message } + } + + public async createDeliveryMessage( + agentContext: AgentContext, + options: DeliverMessagesProtocolOptions + ): Promise | void> { + const { connectionRecord, recipientKey, messages } = options + connectionRecord.assertReady() + + const messagePickupRepository = agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository + ) + + // Get available messages from queue, but don't delete them + const messagesToDeliver = + messages ?? + (await messagePickupRepository.takeFromQueue({ + connectionId: connectionRecord.id, + recipientDid: recipientKey, + limit: 10, // TODO: Define as config parameter + })) + + if (messagesToDeliver.length === 0) { + return + } + + const attachments = messagesToDeliver.map( + (msg) => + new Attachment({ + id: msg.id, + lastmodTime: msg.receivedAt, + data: { + json: msg.encryptedMessage, + }, + }) + ) + + return { + message: new V2MessageDeliveryMessage({ + attachments, + }), + } + } + + public async setLiveDeliveryMode( + agentContext: AgentContext, + options: SetLiveDeliveryModeProtocolOptions + ): Promise> { + const { connectionRecord, liveDelivery } = options + connectionRecord.assertReady() + return { + message: new V2LiveDeliveryChangeMessage({ + liveDelivery, + }), + } + } + + public async processStatusRequest(messageContext: InboundMessageContext) { + // Assert ready connection + const connection = messageContext.assertReadyConnection() + const recipientKey = messageContext.message.recipientKey + + const messagePickupRepository = messageContext.agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository + ) + + const statusMessage = new V2StatusMessage({ + threadId: messageContext.message.threadId, + recipientKey, + messageCount: await messagePickupRepository.getAvailableMessageCount({ + connectionId: connection.id, + recipientDid: recipientKey ? verkeyToDidKey(recipientKey) : undefined, + }), + }) + + return new OutboundMessageContext(statusMessage, { + agentContext: messageContext.agentContext, + connection, + }) + } + + public async processDeliveryRequest(messageContext: InboundMessageContext) { + // Assert ready connection + const connection = messageContext.assertReadyConnection() + const recipientKey = messageContext.message.recipientKey + + const { message } = messageContext + + const messagePickupRepository = messageContext.agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository + ) + + // Get available messages from queue, but don't delete them + const messages = await messagePickupRepository.takeFromQueue({ + connectionId: connection.id, + recipientDid: recipientKey ? verkeyToDidKey(recipientKey) : undefined, + limit: message.limit, + }) + + const attachments = messages.map( + (msg) => + new Attachment({ + id: msg.id, + lastmodTime: msg.receivedAt, + data: { + json: msg.encryptedMessage, + }, + }) + ) + + const outboundMessageContext = + messages.length > 0 + ? new V2MessageDeliveryMessage({ + threadId: messageContext.message.threadId, + recipientKey, + attachments, + }) + : new V2StatusMessage({ + threadId: messageContext.message.threadId, + recipientKey, + messageCount: 0, + }) + + return new OutboundMessageContext(outboundMessageContext, { + agentContext: messageContext.agentContext, + connection, + }) + } + + public async processMessagesReceived(messageContext: InboundMessageContext) { + // Assert ready connection + const connection = messageContext.assertReadyConnection() + + const { message } = messageContext + + const messageRepository = messageContext.agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository + ) + + if (message.messageIdList.length) { + await messageRepository.removeMessages({ connectionId: connection.id, messageIds: message.messageIdList }) + } + + const statusMessage = new V2StatusMessage({ + threadId: messageContext.message.threadId, + messageCount: await messageRepository.getAvailableMessageCount({ connectionId: connection.id }), + }) + + return new OutboundMessageContext(statusMessage, { + agentContext: messageContext.agentContext, + connection, + }) + } + + public async processStatus(messageContext: InboundMessageContext) { + const { message: statusMessage } = messageContext + const { messageCount, recipientKey } = statusMessage + + const connection = messageContext.assertReadyConnection() + + const messagePickupModuleConfig = messageContext.agentContext.dependencyManager.resolve(MessagePickupModuleConfig) + + const eventEmitter = messageContext.agentContext.dependencyManager.resolve(EventEmitter) + + //No messages to be retrieved: message pick-up is completed + if (messageCount === 0) { + eventEmitter.emit(messageContext.agentContext, { + type: MessagePickupEventTypes.MessagePickupCompleted, + payload: { + connection, + threadId: statusMessage.threadId, + }, + }) + return null + } + + const { maximumBatchSize: maximumMessagePickup } = messagePickupModuleConfig + const limit = messageCount < maximumMessagePickup ? messageCount : maximumMessagePickup + + const deliveryRequestMessage = new V2DeliveryRequestMessage({ + limit, + recipientKey, + }) + + return deliveryRequestMessage + } + + public async processLiveDeliveryChange(messageContext: InboundMessageContext) { + const { agentContext, message } = messageContext + + const connection = messageContext.assertReadyConnection() + + const messagePickupRepository = messageContext.agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository + ) + const sessionService = messageContext.agentContext.dependencyManager.resolve(MessagePickupSessionService) + + if (message.liveDelivery) { + sessionService.saveLiveSession(agentContext, { + connectionId: connection.id, + protocolVersion: 'v2', + role: MessagePickupSessionRole.MessageHolder, + }) + } else { + sessionService.removeLiveSession(agentContext, { connectionId: connection.id }) + } + + const statusMessage = new V2StatusMessage({ + threadId: message.threadId, + liveDelivery: message.liveDelivery, + messageCount: await messagePickupRepository.getAvailableMessageCount({ connectionId: connection.id }), + }) + + return new OutboundMessageContext(statusMessage, { agentContext: messageContext.agentContext, connection }) + } + + public async processDelivery(messageContext: InboundMessageContext) { + messageContext.assertReadyConnection() + + const { appendedAttachments } = messageContext.message + + const eventEmitter = messageContext.agentContext.dependencyManager.resolve(EventEmitter) + + if (!appendedAttachments) + throw new ProblemReportError('Error processing attachments', { + problemCode: RoutingProblemReportReason.ErrorProcessingAttachments, + }) + + const ids: string[] = [] + for (const attachment of appendedAttachments) { + ids.push(attachment.id) + + eventEmitter.emit(messageContext.agentContext, { + type: AgentEventTypes.AgentMessageReceived, + payload: { + message: attachment.getDataAsJson(), + contextCorrelationId: messageContext.agentContext.contextCorrelationId, + receivedAt: attachment.lastmodTime, + }, + }) + } + + return new V2MessagesReceivedMessage({ + messageIdList: ids, + }) + } +} diff --git a/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts b/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts new file mode 100644 index 0000000000..7a4bc3dc22 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts @@ -0,0 +1,432 @@ +import type { EncryptedMessage } from '../../../../../types' + +import { getAgentContext, getMockConnection, mockFunction } from '../../../../../../tests/helpers' +import { EventEmitter } from '../../../../../agent/EventEmitter' +import { AgentEventTypes } from '../../../../../agent/Events' +import { MessageSender } from '../../../../../agent/MessageSender' +import { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import { InjectionSymbols } from '../../../../../constants' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { CredoError } from '../../../../../error' +import { uuid } from '../../../../../utils/uuid' +import { DidExchangeState, TrustPingMessage } from '../../../../connections' +import { ConnectionService } from '../../../../connections/services/ConnectionService' +import { verkeyToDidKey } from '../../../../dids/helpers' +import { MessagePickupModuleConfig } from '../../../MessagePickupModuleConfig' +import { InMemoryMessagePickupRepository } from '../../../storage/InMemoryMessagePickupRepository' +import { V1MessagePickupProtocol } from '../../v1' +import { V2MessagePickupProtocol } from '../V2MessagePickupProtocol' +import { + V2DeliveryRequestMessage, + V2MessageDeliveryMessage, + V2MessagesReceivedMessage, + V2StatusMessage, + V2StatusRequestMessage, +} from '../messages' + +const mockConnection = getMockConnection({ + state: DidExchangeState.Completed, +}) + +// Mock classes +jest.mock('../../../storage/InMemoryMessagePickupRepository') +jest.mock('../../../../../agent/EventEmitter') +jest.mock('../../../../../agent/MessageSender') +jest.mock('../../../../connections/services/ConnectionService') + +// Mock typed object +const InMessageRepositoryMock = InMemoryMessagePickupRepository as jest.Mock +const EventEmitterMock = EventEmitter as jest.Mock +const MessageSenderMock = MessageSender as jest.Mock +const ConnectionServiceMock = ConnectionService as jest.Mock + +const messagePickupModuleConfig = new MessagePickupModuleConfig({ + maximumBatchSize: 10, + protocols: [new V1MessagePickupProtocol(), new V2MessagePickupProtocol()], +}) +const messageSender = new MessageSenderMock() +const eventEmitter = new EventEmitterMock() +const connectionService = new ConnectionServiceMock() +const messagePickupRepository = new InMessageRepositoryMock() + +const agentContext = getAgentContext({ + registerInstances: [ + [InjectionSymbols.MessagePickupRepository, messagePickupRepository], + [EventEmitter, eventEmitter], + [MessageSender, messageSender], + [ConnectionService, connectionService], + [MessagePickupModuleConfig, messagePickupModuleConfig], + ], +}) + +const encryptedMessage: EncryptedMessage = { + protected: 'base64url', + iv: 'base64url', + ciphertext: 'base64url', + tag: 'base64url', +} +const queuedMessages = [ + { id: '1', encryptedMessage }, + { id: '2', encryptedMessage }, + { id: '3', encryptedMessage }, +] + +describe('V2MessagePickupProtocol', () => { + let pickupProtocol: V2MessagePickupProtocol + + beforeEach(async () => { + pickupProtocol = new V2MessagePickupProtocol() + }) + + describe('processStatusRequest', () => { + test('no available messages in queue', async () => { + mockFunction(messagePickupRepository.getAvailableMessageCount).mockResolvedValue(0) + + const statusRequest = new V2StatusRequestMessage({}) + + const messageContext = new InboundMessageContext(statusRequest, { connection: mockConnection, agentContext }) + + const { connection, message } = await pickupProtocol.processStatusRequest(messageContext) + + expect(connection).toEqual(mockConnection) + expect(message).toEqual( + new V2StatusMessage({ + id: message.id, + threadId: statusRequest.threadId, + messageCount: 0, + }) + ) + expect(messagePickupRepository.getAvailableMessageCount).toHaveBeenCalledWith({ connectionId: mockConnection.id }) + }) + + test('multiple messages in queue', async () => { + mockFunction(messagePickupRepository.getAvailableMessageCount).mockResolvedValue(5) + const statusRequest = new V2StatusRequestMessage({}) + + const messageContext = new InboundMessageContext(statusRequest, { connection: mockConnection, agentContext }) + + const { connection, message } = await pickupProtocol.processStatusRequest(messageContext) + + expect(connection).toEqual(mockConnection) + expect(message).toEqual( + new V2StatusMessage({ + id: message.id, + threadId: statusRequest.threadId, + messageCount: 5, + }) + ) + expect(messagePickupRepository.getAvailableMessageCount).toHaveBeenCalledWith({ connectionId: mockConnection.id }) + }) + + test('status request specifying recipient key', async () => { + mockFunction(messagePickupRepository.getAvailableMessageCount).mockResolvedValue(10) + + const statusRequest = new V2StatusRequestMessage({ + recipientKey: '79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', + }) + + const messageContext = new InboundMessageContext(statusRequest, { connection: mockConnection, agentContext }) + + await pickupProtocol.processStatusRequest(messageContext) + expect(messagePickupRepository.getAvailableMessageCount).toHaveBeenCalledWith({ connectionId: mockConnection.id }) + }) + }) + + describe('processDeliveryRequest', () => { + test('no available messages in queue', async () => { + mockFunction(messagePickupRepository.takeFromQueue).mockReturnValue([]) + + const deliveryRequest = new V2DeliveryRequestMessage({ limit: 10 }) + + const messageContext = new InboundMessageContext(deliveryRequest, { connection: mockConnection, agentContext }) + + const { connection, message } = await pickupProtocol.processDeliveryRequest(messageContext) + + expect(connection).toEqual(mockConnection) + expect(message).toEqual( + new V2StatusMessage({ + id: message.id, + threadId: deliveryRequest.threadId, + messageCount: 0, + }) + ) + expect(messagePickupRepository.takeFromQueue).toHaveBeenCalledWith({ + connectionId: mockConnection.id, + limit: 10, + }) + }) + + test('less messages in queue than limit', async () => { + mockFunction(messagePickupRepository.takeFromQueue).mockReturnValue(queuedMessages) + + const deliveryRequest = new V2DeliveryRequestMessage({ limit: 10 }) + + const messageContext = new InboundMessageContext(deliveryRequest, { connection: mockConnection, agentContext }) + + const { connection, message } = await pickupProtocol.processDeliveryRequest(messageContext) + + expect(connection).toEqual(mockConnection) + expect(message).toBeInstanceOf(V2MessageDeliveryMessage) + expect(message.threadId).toEqual(deliveryRequest.threadId) + expect(message.appendedAttachments?.length).toEqual(3) + expect(message.appendedAttachments).toEqual( + expect.arrayContaining( + queuedMessages.map((msg) => + expect.objectContaining({ + id: msg.id, + data: { + json: msg.encryptedMessage, + }, + }) + ) + ) + ) + expect(messagePickupRepository.takeFromQueue).toHaveBeenCalledWith({ + connectionId: mockConnection.id, + limit: 10, + }) + }) + + test('more messages in queue than limit', async () => { + mockFunction(messagePickupRepository.takeFromQueue).mockReturnValue(queuedMessages.slice(0, 2)) + + const deliveryRequest = new V2DeliveryRequestMessage({ limit: 2 }) + + const messageContext = new InboundMessageContext(deliveryRequest, { connection: mockConnection, agentContext }) + + const { connection, message } = await pickupProtocol.processDeliveryRequest(messageContext) + + expect(connection).toEqual(mockConnection) + expect(message).toBeInstanceOf(V2MessageDeliveryMessage) + expect(message.threadId).toEqual(deliveryRequest.threadId) + expect(message.appendedAttachments?.length).toEqual(2) + expect(message.appendedAttachments).toEqual( + expect.arrayContaining( + queuedMessages.slice(0, 2).map((msg) => + expect.objectContaining({ + id: msg.id, + data: { + json: msg.encryptedMessage, + }, + }) + ) + ) + ) + expect(messagePickupRepository.takeFromQueue).toHaveBeenCalledWith({ + connectionId: mockConnection.id, + limit: 2, + }) + }) + + test('delivery request specifying recipient key', async () => { + mockFunction(messagePickupRepository.takeFromQueue).mockReturnValue(queuedMessages) + + const deliveryRequest = new V2DeliveryRequestMessage({ + limit: 10, + recipientKey: 'recipientKey', + }) + + const messageContext = new InboundMessageContext(deliveryRequest, { connection: mockConnection, agentContext }) + + await pickupProtocol.processDeliveryRequest(messageContext) + + expect(messagePickupRepository.takeFromQueue).toHaveBeenCalledWith({ + connectionId: mockConnection.id, + limit: 10, + recipientDid: verkeyToDidKey('recipientKey'), + }) + }) + }) + + describe('processMessagesReceived', () => { + test('messages received partially', async () => { + mockFunction(messagePickupRepository.takeFromQueue).mockReturnValue(queuedMessages) + mockFunction(messagePickupRepository.getAvailableMessageCount).mockResolvedValue(4) + + const messagesReceived = new V2MessagesReceivedMessage({ + messageIdList: ['1', '2'], + }) + + const messageContext = new InboundMessageContext(messagesReceived, { connection: mockConnection, agentContext }) + + const { connection, message } = await pickupProtocol.processMessagesReceived(messageContext) + + expect(connection).toEqual(mockConnection) + expect(message).toEqual( + new V2StatusMessage({ + id: message.id, + threadId: messagesReceived.threadId, + messageCount: 4, + }) + ) + expect(messagePickupRepository.getAvailableMessageCount).toHaveBeenCalledWith({ connectionId: mockConnection.id }) + expect(messagePickupRepository.removeMessages).toHaveBeenCalledWith({ + connectionId: mockConnection.id, + messageIds: ['1', '2'], + }) + }) + + test('all messages have been received', async () => { + mockFunction(messagePickupRepository.takeFromQueue).mockReturnValue(queuedMessages) + mockFunction(messagePickupRepository.getAvailableMessageCount).mockResolvedValue(0) + + const messagesReceived = new V2MessagesReceivedMessage({ + messageIdList: ['1', '2'], + }) + + const messageContext = new InboundMessageContext(messagesReceived, { connection: mockConnection, agentContext }) + + const { connection, message } = await pickupProtocol.processMessagesReceived(messageContext) + + expect(connection).toEqual(mockConnection) + expect(message).toEqual( + new V2StatusMessage({ + id: message.id, + threadId: messagesReceived.threadId, + messageCount: 0, + }) + ) + + expect(messagePickupRepository.getAvailableMessageCount).toHaveBeenCalledWith({ connectionId: mockConnection.id }) + expect(messagePickupRepository.removeMessages).toHaveBeenCalledWith({ + connectionId: mockConnection.id, + messageIds: ['1', '2'], + }) + }) + }) + + describe('createPickupMessage', () => { + it('creates a status request message', async () => { + const { message: statusRequestMessage } = await pickupProtocol.createPickupMessage(agentContext, { + connectionRecord: mockConnection, + recipientDid: 'a-key', + }) + + expect(statusRequestMessage).toMatchObject({ + id: expect.any(String), + recipientKey: 'a-key', + }) + }) + }) + + describe('processStatus', () => { + it('if status request has a message count of zero returns nothing', async () => { + const status = new V2StatusMessage({ + threadId: uuid(), + messageCount: 0, + }) + + mockFunction(connectionService.createTrustPing).mockResolvedValueOnce({ + message: new TrustPingMessage({}), + connectionRecord: mockConnection, + }) + + const messageContext = new InboundMessageContext(status, { connection: mockConnection, agentContext }) + const deliveryRequestMessage = await pickupProtocol.processStatus(messageContext) + expect(deliveryRequestMessage).toBeNull() + }) + + it('if it has a message count greater than zero return a valid delivery request', async () => { + const status = new V2StatusMessage({ + threadId: uuid(), + messageCount: 1, + }) + const messageContext = new InboundMessageContext(status, { connection: mockConnection, agentContext }) + + const deliveryRequestMessage = await pickupProtocol.processStatus(messageContext) + expect(deliveryRequestMessage) + expect(deliveryRequestMessage).toEqual(new V2DeliveryRequestMessage({ id: deliveryRequestMessage?.id, limit: 1 })) + }) + }) + + describe('processDelivery', () => { + it('if the delivery has no attachments expect an error', async () => { + const messageContext = new InboundMessageContext({} as V2MessageDeliveryMessage, { + connection: mockConnection, + agentContext, + }) + + await expect(pickupProtocol.processDelivery(messageContext)).rejects.toThrowError( + new CredoError('Error processing attachments') + ) + }) + + it('should return a message received with an message id list in it', async () => { + const messageDeliveryMessage = new V2MessageDeliveryMessage({ + threadId: uuid(), + attachments: [ + new Attachment({ + id: '1', + data: { + json: { + a: 'value', + }, + }, + }), + ], + }) + const messageContext = new InboundMessageContext(messageDeliveryMessage, { + connection: mockConnection, + agentContext, + }) + + const messagesReceivedMessage = await pickupProtocol.processDelivery(messageContext) + + expect(messagesReceivedMessage).toEqual( + new V2MessagesReceivedMessage({ + id: messagesReceivedMessage.id, + messageIdList: ['1'], + }) + ) + }) + + it('calls the event emitter for each message', async () => { + // This is to not take into account events previously emitted + jest.clearAllMocks() + + const messageDeliveryMessage = new V2MessageDeliveryMessage({ + threadId: uuid(), + attachments: [ + new Attachment({ + id: '1', + data: { + json: { + first: 'value', + }, + }, + }), + new Attachment({ + id: '2', + data: { + json: { + second: 'value', + }, + }, + }), + ], + }) + const messageContext = new InboundMessageContext(messageDeliveryMessage, { + connection: mockConnection, + agentContext, + }) + + await pickupProtocol.processDelivery(messageContext) + + expect(eventEmitter.emit).toHaveBeenCalledTimes(2) + expect(eventEmitter.emit).toHaveBeenNthCalledWith(1, agentContext, { + type: AgentEventTypes.AgentMessageReceived, + payload: { + message: { first: 'value' }, + contextCorrelationId: agentContext.contextCorrelationId, + }, + }) + expect(eventEmitter.emit).toHaveBeenNthCalledWith(2, agentContext, { + type: AgentEventTypes.AgentMessageReceived, + payload: { + message: { second: 'value' }, + contextCorrelationId: agentContext.contextCorrelationId, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2DeliveryRequestHandler.ts b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2DeliveryRequestHandler.ts new file mode 100644 index 0000000000..b935dcd512 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2DeliveryRequestHandler.ts @@ -0,0 +1,19 @@ +import type { MessageHandler } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { V2MessagePickupProtocol } from '../V2MessagePickupProtocol' + +import { V2DeliveryRequestMessage } from '../messages' + +export class V2DeliveryRequestHandler implements MessageHandler { + public supportedMessages = [V2DeliveryRequestMessage] + private messagePickupService: V2MessagePickupProtocol + + public constructor(messagePickupService: V2MessagePickupProtocol) { + this.messagePickupService = messagePickupService + } + + public async handle(messageContext: InboundMessageContext) { + messageContext.assertReadyConnection() + return this.messagePickupService.processDeliveryRequest(messageContext) + } +} diff --git a/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts new file mode 100644 index 0000000000..30eeaf035f --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts @@ -0,0 +1,19 @@ +import type { MessageHandler } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { V2MessagePickupProtocol } from '../V2MessagePickupProtocol' + +import { V2LiveDeliveryChangeMessage } from '../messages' + +export class V2LiveDeliveryChangeHandler implements MessageHandler { + public supportedMessages = [V2LiveDeliveryChangeMessage] + private messagePickupService: V2MessagePickupProtocol + + public constructor(messagePickupService: V2MessagePickupProtocol) { + this.messagePickupService = messagePickupService + } + + public async handle(messageContext: InboundMessageContext) { + messageContext.assertReadyConnection() + return this.messagePickupService.processLiveDeliveryChange(messageContext) + } +} diff --git a/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2MessageDeliveryHandler.ts b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2MessageDeliveryHandler.ts new file mode 100644 index 0000000000..918b3f37b8 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2MessageDeliveryHandler.ts @@ -0,0 +1,27 @@ +import type { MessageHandler } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { V2MessagePickupProtocol } from '../V2MessagePickupProtocol' + +import { OutboundMessageContext } from '../../../../../agent/models' +import { V2MessageDeliveryMessage } from '../messages/V2MessageDeliveryMessage' + +export class V2MessageDeliveryHandler implements MessageHandler { + public supportedMessages = [V2MessageDeliveryMessage] + private messagePickupService: V2MessagePickupProtocol + + public constructor(messagePickupService: V2MessagePickupProtocol) { + this.messagePickupService = messagePickupService + } + + public async handle(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + const deliveryReceivedMessage = await this.messagePickupService.processDelivery(messageContext) + + if (deliveryReceivedMessage) { + return new OutboundMessageContext(deliveryReceivedMessage, { + agentContext: messageContext.agentContext, + connection, + }) + } + } +} diff --git a/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2MessagesReceivedHandler.ts b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2MessagesReceivedHandler.ts new file mode 100644 index 0000000000..5820c4878c --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2MessagesReceivedHandler.ts @@ -0,0 +1,19 @@ +import type { MessageHandler } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { V2MessagePickupProtocol } from '../V2MessagePickupProtocol' + +import { V2MessagesReceivedMessage } from '../messages' + +export class V2MessagesReceivedHandler implements MessageHandler { + public supportedMessages = [V2MessagesReceivedMessage] + private messagePickupService: V2MessagePickupProtocol + + public constructor(messagePickupService: V2MessagePickupProtocol) { + this.messagePickupService = messagePickupService + } + + public async handle(messageContext: InboundMessageContext) { + messageContext.assertReadyConnection() + return this.messagePickupService.processMessagesReceived(messageContext) + } +} diff --git a/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2StatusHandler.ts b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2StatusHandler.ts new file mode 100644 index 0000000000..598c4a447f --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2StatusHandler.ts @@ -0,0 +1,27 @@ +import type { MessageHandler } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { V2MessagePickupProtocol } from '../V2MessagePickupProtocol' + +import { OutboundMessageContext } from '../../../../../agent/models' +import { V2StatusMessage } from '../messages' + +export class V2StatusHandler implements MessageHandler { + public supportedMessages = [V2StatusMessage] + private messagePickupProtocol: V2MessagePickupProtocol + + public constructor(messagePickupProtocol: V2MessagePickupProtocol) { + this.messagePickupProtocol = messagePickupProtocol + } + + public async handle(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + const deliveryRequestMessage = await this.messagePickupProtocol.processStatus(messageContext) + + if (deliveryRequestMessage) { + return new OutboundMessageContext(deliveryRequestMessage, { + agentContext: messageContext.agentContext, + connection, + }) + } + } +} diff --git a/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2StatusRequestHandler.ts b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2StatusRequestHandler.ts new file mode 100644 index 0000000000..b9e365b8a4 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2StatusRequestHandler.ts @@ -0,0 +1,19 @@ +import type { MessageHandler } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { V2MessagePickupProtocol } from '../V2MessagePickupProtocol' + +import { V2StatusRequestMessage } from '../messages' + +export class V2StatusRequestHandler implements MessageHandler { + public supportedMessages = [V2StatusRequestMessage] + private messagePickupService: V2MessagePickupProtocol + + public constructor(messagePickupService: V2MessagePickupProtocol) { + this.messagePickupService = messagePickupService + } + + public async handle(messageContext: InboundMessageContext) { + messageContext.assertReadyConnection() + return this.messagePickupService.processStatusRequest(messageContext) + } +} diff --git a/packages/core/src/modules/message-pickup/protocol/v2/handlers/index.ts b/packages/core/src/modules/message-pickup/protocol/v2/handlers/index.ts new file mode 100644 index 0000000000..f8a173669b --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/handlers/index.ts @@ -0,0 +1,6 @@ +export * from './V2DeliveryRequestHandler' +export * from './V2LiveDeliveryChangeHandler' +export * from './V2MessageDeliveryHandler' +export * from './V2MessagesReceivedHandler' +export * from './V2StatusHandler' +export * from './V2StatusRequestHandler' diff --git a/packages/core/src/modules/message-pickup/protocol/v2/index.ts b/packages/core/src/modules/message-pickup/protocol/v2/index.ts new file mode 100644 index 0000000000..90567cdaf4 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/index.ts @@ -0,0 +1,2 @@ +export * from './V2MessagePickupProtocol' +export * from './messages' diff --git a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2DeliveryRequestMessage.ts b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2DeliveryRequestMessage.ts new file mode 100644 index 0000000000..2a1e73f867 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2DeliveryRequestMessage.ts @@ -0,0 +1,39 @@ +import { Expose } from 'class-transformer' +import { IsInt, IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { ReturnRouteTypes } from '../../../../../decorators/transport/TransportDecorator' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface V2DeliveryRequestMessageOptions { + id?: string + recipientKey?: string + limit: number +} + +export class V2DeliveryRequestMessage extends AgentMessage { + public readonly allowQueueTransport = false + + public constructor(options: V2DeliveryRequestMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.recipientKey = options.recipientKey + this.limit = options.limit + } + this.setReturnRouting(ReturnRouteTypes.all) + } + + @IsValidMessageType(V2DeliveryRequestMessage.type) + public readonly type = V2DeliveryRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/2.0/delivery-request') + + @IsString() + @IsOptional() + @Expose({ name: 'recipient_key' }) + public recipientKey?: string + + @IsInt() + public limit!: number +} diff --git a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts new file mode 100644 index 0000000000..3b14501f6b --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts @@ -0,0 +1,33 @@ +import { Expose } from 'class-transformer' +import { IsBoolean } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { ReturnRouteTypes } from '../../../../../decorators/transport/TransportDecorator' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface V2LiveDeliveryChangeMessageOptions { + id?: string + liveDelivery: boolean +} + +export class V2LiveDeliveryChangeMessage extends AgentMessage { + public readonly allowQueueTransport = false + + public constructor(options: V2LiveDeliveryChangeMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.liveDelivery = options.liveDelivery + } + this.setReturnRouting(ReturnRouteTypes.all) + } + + @IsValidMessageType(V2LiveDeliveryChangeMessage.type) + public readonly type = V2LiveDeliveryChangeMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/2.0/live-delivery-change') + + @IsBoolean() + @Expose({ name: 'live_delivery' }) + public liveDelivery!: boolean +} diff --git a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessageDeliveryMessage.ts b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessageDeliveryMessage.ts new file mode 100644 index 0000000000..4523c5d54b --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessageDeliveryMessage.ts @@ -0,0 +1,44 @@ +import type { Attachment } from '../../../../../decorators/attachment/Attachment' + +import { Expose } from 'class-transformer' +import { IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { ReturnRouteTypes } from '../../../../../decorators/transport/TransportDecorator' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface V2MessageDeliveryMessageOptions { + id?: string + recipientKey?: string + threadId?: string + attachments: Attachment[] +} + +export class V2MessageDeliveryMessage extends AgentMessage { + public readonly allowQueueTransport = false + + public constructor(options: V2MessageDeliveryMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.recipientKey = options.recipientKey + this.appendedAttachments = options.attachments + if (this.threadId) { + this.setThread({ + threadId: options.threadId, + }) + } + } + this.setReturnRouting(ReturnRouteTypes.all) + } + + @IsValidMessageType(V2MessageDeliveryMessage.type) + public readonly type = V2MessageDeliveryMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/2.0/delivery') + + @IsString() + @IsOptional() + @Expose({ name: 'recipient_key' }) + public recipientKey?: string +} diff --git a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessagesReceivedMessage.ts b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessagesReceivedMessage.ts new file mode 100644 index 0000000000..889e08853c --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessagesReceivedMessage.ts @@ -0,0 +1,33 @@ +import { Expose } from 'class-transformer' +import { IsArray } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { ReturnRouteTypes } from '../../../../../decorators/transport/TransportDecorator' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface V2MessagesReceivedMessageOptions { + id?: string + messageIdList: string[] +} + +export class V2MessagesReceivedMessage extends AgentMessage { + public readonly allowQueueTransport = false + + public constructor(options: V2MessagesReceivedMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.messageIdList = options.messageIdList + } + this.setReturnRouting(ReturnRouteTypes.all) + } + + @IsValidMessageType(V2MessagesReceivedMessage.type) + public readonly type = V2MessagesReceivedMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/2.0/messages-received') + + @IsArray() + @Expose({ name: 'message_id_list' }) + public messageIdList!: string[] +} diff --git a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusMessage.ts b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusMessage.ts new file mode 100644 index 0000000000..46d3a8c226 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusMessage.ts @@ -0,0 +1,81 @@ +import { Expose, Transform } from 'class-transformer' +import { IsBoolean, IsDate, IsInt, IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { ReturnRouteTypes } from '../../../../../decorators/transport/TransportDecorator' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { DateParser } from '../../../../../utils/transformers' + +export interface V2StatusMessageOptions { + id?: string + recipientKey?: string + threadId: string + messageCount: number + longestWaitedSeconds?: number + newestReceivedTime?: Date + oldestReceivedTime?: Date + totalBytes?: number + liveDelivery?: boolean +} + +export class V2StatusMessage extends AgentMessage { + public readonly allowQueueTransport = false + + public constructor(options: V2StatusMessageOptions) { + super() + if (options) { + this.id = options.id || this.generateId() + this.recipientKey = options.recipientKey + this.messageCount = options.messageCount + this.longestWaitedSeconds = options.longestWaitedSeconds + this.newestReceivedTime = options.newestReceivedTime + this.oldestReceivedTime = options.oldestReceivedTime + this.totalBytes = options.totalBytes + this.liveDelivery = options.liveDelivery + this.setThread({ + threadId: options.threadId, + }) + } + this.setReturnRouting(ReturnRouteTypes.all) + } + + @IsValidMessageType(V2StatusMessage.type) + public readonly type = V2StatusMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/2.0/status') + + @IsString() + @IsOptional() + @Expose({ name: 'recipient_key' }) + public recipientKey?: string + + @IsInt() + @Expose({ name: 'message_count' }) + public messageCount!: number + + @IsInt() + @IsOptional() + @Expose({ name: 'longest_waited_seconds' }) + public longestWaitedSeconds?: number + + @Expose({ name: 'newest_received_time' }) + @Transform(({ value }) => DateParser(value)) + @IsDate() + @IsOptional() + public newestReceivedTime?: Date + + @IsOptional() + @Transform(({ value }) => DateParser(value)) + @IsDate() + @Expose({ name: 'oldest_received_time' }) + public oldestReceivedTime?: Date + + @IsOptional() + @IsInt() + @Expose({ name: 'total_bytes' }) + public totalBytes?: number + + @IsOptional() + @IsBoolean() + @Expose({ name: 'live_delivery' }) + public liveDelivery?: boolean +} diff --git a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusRequestMessage.ts b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusRequestMessage.ts new file mode 100644 index 0000000000..c10acf8b75 --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusRequestMessage.ts @@ -0,0 +1,32 @@ +import { Expose } from 'class-transformer' +import { IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface V2StatusRequestMessageOptions { + id?: string + recipientKey?: string +} + +export class V2StatusRequestMessage extends AgentMessage { + public readonly allowQueueTransport = false + + public constructor(options: V2StatusRequestMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.recipientKey = options.recipientKey + } + } + + @IsValidMessageType(V2StatusRequestMessage.type) + public readonly type = V2StatusRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/2.0/status-request') + + @IsString() + @IsOptional() + @Expose({ name: 'recipient_key' }) + public recipientKey?: string +} diff --git a/packages/core/src/modules/message-pickup/protocol/v2/messages/index.ts b/packages/core/src/modules/message-pickup/protocol/v2/messages/index.ts new file mode 100644 index 0000000000..df70290a6f --- /dev/null +++ b/packages/core/src/modules/message-pickup/protocol/v2/messages/index.ts @@ -0,0 +1,6 @@ +export * from './V2DeliveryRequestMessage' +export * from './V2LiveDeliveryChangeMessage' +export * from './V2MessageDeliveryMessage' +export * from './V2MessagesReceivedMessage' +export * from './V2StatusMessage' +export * from './V2StatusRequestMessage' diff --git a/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts b/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts new file mode 100644 index 0000000000..7e726c7c8a --- /dev/null +++ b/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts @@ -0,0 +1,103 @@ +import type { AgentContext } from '../../../agent' +import type { TransportSessionRemovedEvent } from '../../../transport' +import type { MessagePickupLiveSessionRemovedEvent, MessagePickupLiveSessionSavedEvent } from '../MessagePickupEvents' +import type { MessagePickupSession, MessagePickupSessionRole } from '../MessagePickupSession' + +import { takeUntil, type Subject } from 'rxjs' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { injectable } from '../../../plugins' +import { TransportEventTypes } from '../../../transport' +import { uuid } from '../../../utils/uuid' +import { MessagePickupEventTypes } from '../MessagePickupEvents' + +/** + * @internal + * The Message Pickup session service keeps track of all {@link MessagePickupSession} + * + * It is initially intended for Message Holder/Mediator role, where only Live Mode sessions are + * considered. + */ +@injectable() +export class MessagePickupSessionService { + private sessions: MessagePickupSession[] + + public constructor() { + this.sessions = [] + } + + public start(agentContext: AgentContext) { + const stop$ = agentContext.dependencyManager.resolve>(InjectionSymbols.Stop$) + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + this.sessions = [] + + eventEmitter + .observable(TransportEventTypes.TransportSessionRemoved) + .pipe(takeUntil(stop$)) + .subscribe({ + next: (e) => { + const connectionId = e.payload.session.connectionId + if (connectionId) this.removeLiveSession(agentContext, { connectionId }) + }, + }) + } + + public getLiveSession(agentContext: AgentContext, sessionId: string) { + return this.sessions.find((session) => session.id === sessionId) + } + + public getLiveSessionByConnectionId( + agentContext: AgentContext, + options: { connectionId: string; role?: MessagePickupSessionRole } + ) { + const { connectionId, role } = options + + return this.sessions.find( + (session) => session.connectionId === connectionId && (role === undefined || role === session.role) + ) + } + + public saveLiveSession( + agentContext: AgentContext, + options: { connectionId: string; protocolVersion: string; role: MessagePickupSessionRole } + ) { + const { connectionId, protocolVersion, role } = options + + // First remove any live session for the given connection Id + this.removeLiveSession(agentContext, { connectionId }) + + const session = { + id: uuid(), + connectionId, + protocolVersion, + role, + } + + this.sessions.push(session) + + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + eventEmitter.emit(agentContext, { + type: MessagePickupEventTypes.LiveSessionSaved, + payload: { + session, + }, + }) + } + + public removeLiveSession(agentContext: AgentContext, options: { connectionId: string }) { + const itemIndex = this.sessions.findIndex((session) => session.connectionId === options.connectionId) + + if (itemIndex > -1) { + const [session] = this.sessions.splice(itemIndex, 1) + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + + eventEmitter.emit(agentContext, { + type: MessagePickupEventTypes.LiveSessionRemoved, + payload: { + session, + }, + }) + } + } +} diff --git a/packages/core/src/modules/message-pickup/services/index.ts b/packages/core/src/modules/message-pickup/services/index.ts new file mode 100644 index 0000000000..e91435bfaf --- /dev/null +++ b/packages/core/src/modules/message-pickup/services/index.ts @@ -0,0 +1 @@ +export * from './MessagePickupSessionService' diff --git a/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts new file mode 100644 index 0000000000..f066899369 --- /dev/null +++ b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts @@ -0,0 +1,95 @@ +import type { MessagePickupRepository } from './MessagePickupRepository' +import type { + AddMessageOptions, + GetAvailableMessageCountOptions, + RemoveMessagesOptions, + TakeFromQueueOptions, +} from './MessagePickupRepositoryOptions' +import type { QueuedMessage } from './QueuedMessage' + +import { InjectionSymbols } from '../../../constants' +import { Logger } from '../../../logger' +import { injectable, inject } from '../../../plugins' +import { uuid } from '../../../utils/uuid' + +interface InMemoryQueuedMessage extends QueuedMessage { + connectionId: string + recipientDids: string[] + state: 'pending' | 'sending' +} + +@injectable() +export class InMemoryMessagePickupRepository implements MessagePickupRepository { + private logger: Logger + private messages: InMemoryQueuedMessage[] + + public constructor(@inject(InjectionSymbols.Logger) logger: Logger) { + this.logger = logger + this.messages = [] + } + + public getAvailableMessageCount(options: GetAvailableMessageCountOptions): number | Promise { + const { connectionId, recipientDid } = options + + const messages = this.messages.filter( + (msg) => + msg.connectionId === connectionId && + (recipientDid === undefined || msg.recipientDids.includes(recipientDid)) && + msg.state === 'pending' + ) + return messages.length + } + + public takeFromQueue(options: TakeFromQueueOptions): QueuedMessage[] { + const { connectionId, recipientDid, limit, deleteMessages } = options + + let messages = this.messages.filter( + (msg) => + msg.connectionId === connectionId && + msg.state === 'pending' && + (recipientDid === undefined || msg.recipientDids.includes(recipientDid)) + ) + + const messagesToTake = limit ?? messages.length + + messages = messages.slice(0, messagesToTake) + + this.logger.debug(`Taking ${messagesToTake} messages from queue for connection ${connectionId}`) + + // Mark taken messages in order to prevent them of being retrieved again + messages.forEach((msg) => { + const index = this.messages.findIndex((item) => item.id === msg.id) + if (index !== -1) this.messages[index].state = 'sending' + }) + + if (deleteMessages) { + this.removeMessages({ connectionId, messageIds: messages.map((msg) => msg.id) }) + } + + return messages + } + + public addMessage(options: AddMessageOptions) { + const { connectionId, recipientDids, payload } = options + + const id = uuid() + this.messages.push({ + id, + connectionId, + encryptedMessage: payload, + recipientDids, + state: 'pending', + }) + + return id + } + + public removeMessages(options: RemoveMessagesOptions) { + const { messageIds } = options + + for (const messageId of messageIds) { + const messageIndex = this.messages.findIndex((item) => item.id === messageId) + if (messageIndex > -1) this.messages.splice(messageIndex, 1) + } + } +} diff --git a/packages/core/src/modules/message-pickup/storage/MessagePickupRepository.ts b/packages/core/src/modules/message-pickup/storage/MessagePickupRepository.ts new file mode 100644 index 0000000000..6b234918ce --- /dev/null +++ b/packages/core/src/modules/message-pickup/storage/MessagePickupRepository.ts @@ -0,0 +1,14 @@ +import type { + AddMessageOptions, + GetAvailableMessageCountOptions, + RemoveMessagesOptions, + TakeFromQueueOptions, +} from './MessagePickupRepositoryOptions' +import type { QueuedMessage } from './QueuedMessage' + +export interface MessagePickupRepository { + getAvailableMessageCount(options: GetAvailableMessageCountOptions): number | Promise + takeFromQueue(options: TakeFromQueueOptions): QueuedMessage[] | Promise + addMessage(options: AddMessageOptions): string | Promise + removeMessages(options: RemoveMessagesOptions): void | Promise +} diff --git a/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts new file mode 100644 index 0000000000..e586d5756a --- /dev/null +++ b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts @@ -0,0 +1,24 @@ +import type { EncryptedMessage } from '../../../types' + +export interface GetAvailableMessageCountOptions { + connectionId: string + recipientDid?: string +} + +export interface TakeFromQueueOptions { + connectionId: string + recipientDid?: string + limit?: number + deleteMessages?: boolean +} + +export interface AddMessageOptions { + connectionId: string + recipientDids: string[] + payload: EncryptedMessage +} + +export interface RemoveMessagesOptions { + connectionId: string + messageIds: string[] +} diff --git a/packages/core/src/modules/message-pickup/storage/QueuedMessage.ts b/packages/core/src/modules/message-pickup/storage/QueuedMessage.ts new file mode 100644 index 0000000000..1c22dfdf69 --- /dev/null +++ b/packages/core/src/modules/message-pickup/storage/QueuedMessage.ts @@ -0,0 +1,13 @@ +import type { EncryptedMessage } from '../../../types' + +/** + * Basic representation of an encrypted message in a Message Pickup Queue + * - id: Message Pickup repository's specific queued message id (unrelated to DIDComm message id) + * - receivedAt: reception time (i.e. time when the message has been added to the queue) + * - encryptedMessage: packed message + */ +export type QueuedMessage = { + id: string + receivedAt?: Date + encryptedMessage: EncryptedMessage +} diff --git a/packages/core/src/modules/message-pickup/storage/index.ts b/packages/core/src/modules/message-pickup/storage/index.ts new file mode 100644 index 0000000000..1894b67d72 --- /dev/null +++ b/packages/core/src/modules/message-pickup/storage/index.ts @@ -0,0 +1,4 @@ +export * from './InMemoryMessagePickupRepository' +export * from './MessagePickupRepository' +export * from './MessagePickupRepositoryOptions' +export * from './QueuedMessage' diff --git a/packages/core/src/modules/oob/OutOfBandApi.ts b/packages/core/src/modules/oob/OutOfBandApi.ts new file mode 100644 index 0000000000..aa1c7f0d3c --- /dev/null +++ b/packages/core/src/modules/oob/OutOfBandApi.ts @@ -0,0 +1,958 @@ +import type { HandshakeReusedEvent } from './domain/OutOfBandEvents' +import type { AgentMessage } from '../../agent/AgentMessage' +import type { AgentMessageReceivedEvent } from '../../agent/Events' +import type { Attachment } from '../../decorators/attachment/Attachment' +import type { Query, QueryOptions } from '../../storage/StorageService' +import type { PlaintextMessage } from '../../types' +import type { ConnectionInvitationMessage, ConnectionRecord, Routing } from '../connections' + +import { catchError, EmptyError, first, firstValueFrom, map, of, timeout } from 'rxjs' + +import { AgentContext } from '../../agent' +import { EventEmitter } from '../../agent/EventEmitter' +import { filterContextCorrelationId, AgentEventTypes } from '../../agent/Events' +import { MessageHandlerRegistry } from '../../agent/MessageHandlerRegistry' +import { MessageSender } from '../../agent/MessageSender' +import { OutboundMessageContext } from '../../agent/models' +import { InjectionSymbols } from '../../constants' +import { Key } from '../../crypto' +import { ServiceDecorator } from '../../decorators/service/ServiceDecorator' +import { CredoError } from '../../error' +import { Logger } from '../../logger' +import { inject, injectable } from '../../plugins' +import { JsonEncoder, JsonTransformer } from '../../utils' +import { + parseDidCommProtocolUri, + parseMessageType, + supportsIncomingDidCommProtocolUri, + supportsIncomingMessageType, +} from '../../utils/messageType' +import { parseInvitationShortUrl } from '../../utils/parseInvitation' +import { ConnectionsApi, DidExchangeState, HandshakeProtocol } from '../connections' +import { DidCommDocumentService } from '../didcomm' +import { DidKey } from '../dids' +import { outOfBandServiceToInlineKeysNumAlgo2Did } from '../dids/methods/peer/peerDidNumAlgo2' +import { RoutingService } from '../routing/services/RoutingService' + +import { OutOfBandService } from './OutOfBandService' +import { OutOfBandDidCommService } from './domain/OutOfBandDidCommService' +import { OutOfBandEventTypes } from './domain/OutOfBandEvents' +import { OutOfBandRole } from './domain/OutOfBandRole' +import { OutOfBandState } from './domain/OutOfBandState' +import { HandshakeReuseHandler } from './handlers' +import { HandshakeReuseAcceptedHandler } from './handlers/HandshakeReuseAcceptedHandler' +import { convertToNewInvitation, convertToOldInvitation } from './helpers' +import { InvitationType, OutOfBandInvitation } from './messages' +import { OutOfBandRepository } from './repository' +import { OutOfBandRecord } from './repository/OutOfBandRecord' +import { OutOfBandRecordMetadataKeys } from './repository/outOfBandRecordMetadataTypes' + +const didCommProfiles = ['didcomm/aip1', 'didcomm/aip2;env=rfc19'] + +export interface CreateOutOfBandInvitationConfig { + label?: string + alias?: string // alias for a connection record to be created + imageUrl?: string + goalCode?: string + goal?: string + handshake?: boolean + handshakeProtocols?: HandshakeProtocol[] + messages?: AgentMessage[] + multiUseInvitation?: boolean + autoAcceptConnection?: boolean + routing?: Routing + appendedAttachments?: Attachment[] + + /** + * Did to use in the invitation. Cannot be used in combination with `routing`. + */ + invitationDid?: string +} + +export interface CreateLegacyInvitationConfig { + label?: string + alias?: string // alias for a connection record to be created + imageUrl?: string + multiUseInvitation?: boolean + autoAcceptConnection?: boolean + routing?: Routing +} + +interface BaseReceiveOutOfBandInvitationConfig { + label?: string + alias?: string + imageUrl?: string + autoAcceptInvitation?: boolean + autoAcceptConnection?: boolean + reuseConnection?: boolean + routing?: Routing + acceptInvitationTimeoutMs?: number + isImplicit?: boolean + ourDid?: string +} + +export type ReceiveOutOfBandInvitationConfig = Omit + +export interface ReceiveOutOfBandImplicitInvitationConfig + extends Omit { + did: string + handshakeProtocols?: HandshakeProtocol[] +} + +@injectable() +export class OutOfBandApi { + private outOfBandService: OutOfBandService + private routingService: RoutingService + private connectionsApi: ConnectionsApi + private messageHandlerRegistry: MessageHandlerRegistry + private didCommDocumentService: DidCommDocumentService + private messageSender: MessageSender + private eventEmitter: EventEmitter + private agentContext: AgentContext + private logger: Logger + + public constructor( + messageHandlerRegistry: MessageHandlerRegistry, + didCommDocumentService: DidCommDocumentService, + outOfBandService: OutOfBandService, + routingService: RoutingService, + connectionsApi: ConnectionsApi, + messageSender: MessageSender, + eventEmitter: EventEmitter, + @inject(InjectionSymbols.Logger) logger: Logger, + agentContext: AgentContext + ) { + this.messageHandlerRegistry = messageHandlerRegistry + this.didCommDocumentService = didCommDocumentService + this.agentContext = agentContext + this.logger = logger + this.outOfBandService = outOfBandService + this.routingService = routingService + this.connectionsApi = connectionsApi + this.messageSender = messageSender + this.eventEmitter = eventEmitter + this.registerMessageHandlers(messageHandlerRegistry) + } + + /** + * Creates an outbound out-of-band record containing out-of-band invitation message defined in + * Aries RFC 0434: Out-of-Band Protocol 1.1. + * + * It automatically adds all supported handshake protocols by agent to `handshake_protocols`. You + * can modify this by setting `handshakeProtocols` in `config` parameter. If you want to create + * invitation without handshake, you can set `handshake` to `false`. + * + * If `config` parameter contains `messages` it adds them to `requests~attach` attribute. + * + * Agent role: sender (inviter) + * + * @param config configuration of how out-of-band invitation should be created + * @returns out-of-band record + */ + public async createInvitation(config: CreateOutOfBandInvitationConfig = {}): Promise { + const multiUseInvitation = config.multiUseInvitation ?? false + const handshake = config.handshake ?? true + const customHandshakeProtocols = config.handshakeProtocols + const autoAcceptConnection = config.autoAcceptConnection ?? this.connectionsApi.config.autoAcceptConnections + // We don't want to treat an empty array as messages being provided + const messages = config.messages && config.messages.length > 0 ? config.messages : undefined + const label = config.label ?? this.agentContext.config.label + const imageUrl = config.imageUrl ?? this.agentContext.config.connectionImageUrl + const appendedAttachments = + config.appendedAttachments && config.appendedAttachments.length > 0 ? config.appendedAttachments : undefined + + if (!handshake && !messages) { + throw new CredoError('One or both of handshake_protocols and requests~attach MUST be included in the message.') + } + + if (!handshake && customHandshakeProtocols) { + throw new CredoError(`Attribute 'handshake' can not be 'false' when 'handshakeProtocols' is defined.`) + } + + // For now we disallow creating multi-use invitation with attachments. This would mean we need multi-use + // credential and presentation exchanges. + if (messages && multiUseInvitation) { + throw new CredoError("Attribute 'multiUseInvitation' can not be 'true' when 'messages' is defined.") + } + + let handshakeProtocols: string[] | undefined + if (handshake) { + // Assert ALL custom handshake protocols are supported + if (customHandshakeProtocols) { + this.assertHandshakeProtocolsSupported(customHandshakeProtocols) + } + + // Find supported handshake protocol preserving the order of handshake protocols defined by agent or in config + handshakeProtocols = this.getSupportedHandshakeProtocols(customHandshakeProtocols).map( + (p) => p.parsedProtocolUri.protocolUri + ) + } + + let mediatorId: string | undefined = undefined + let services: [string] | OutOfBandDidCommService[] + if (config.routing && config.invitationDid) { + throw new CredoError("Both 'routing' and 'invitationDid' cannot be provided at the same time.") + } + + if (config.invitationDid) { + services = [config.invitationDid] + } else { + const routing = config.routing ?? (await this.routingService.getRouting(this.agentContext, {})) + mediatorId = routing?.mediatorId + services = routing.endpoints.map((endpoint, index) => { + return new OutOfBandDidCommService({ + id: `#inline-${index}`, + serviceEndpoint: endpoint, + recipientKeys: [routing.recipientKey].map((key) => new DidKey(key).did), + routingKeys: routing.routingKeys.map((key) => new DidKey(key).did), + }) + }) + } + + const outOfBandInvitation = new OutOfBandInvitation({ + label, + goal: config.goal, + goalCode: config.goalCode, + imageUrl, + accept: didCommProfiles, + services, + handshakeProtocols, + appendedAttachments, + }) + + if (messages) { + messages.forEach((message) => { + if (message.service) { + // We can remove `~service` attribute from message. Newer OOB messages have `services` attribute instead. + message.service = undefined + } + outOfBandInvitation.addRequest(message) + }) + } + + const recipientKeyFingerprints = await this.resolveInvitationRecipientKeyFingerprints(outOfBandInvitation) + const outOfBandRecord = new OutOfBandRecord({ + mediatorId: mediatorId, + role: OutOfBandRole.Sender, + state: OutOfBandState.AwaitResponse, + alias: config.alias, + outOfBandInvitation: outOfBandInvitation, + reusable: multiUseInvitation, + autoAcceptConnection, + tags: { + recipientKeyFingerprints, + }, + }) + + await this.outOfBandService.save(this.agentContext, outOfBandRecord) + this.outOfBandService.emitStateChangedEvent(this.agentContext, outOfBandRecord, null) + + return outOfBandRecord + } + + /** + * Creates an outbound out-of-band record in the same way how `createInvitation` method does it, + * but it also converts out-of-band invitation message to an "legacy" invitation message defined + * in RFC 0160: Connection Protocol and returns it together with out-of-band record. + * + * Agent role: sender (inviter) + * + * @param config configuration of how a connection invitation should be created + * @returns out-of-band record and connection invitation + */ + public async createLegacyInvitation(config: CreateLegacyInvitationConfig = {}) { + const outOfBandRecord = await this.createInvitation({ + ...config, + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + // Set legacy invitation type + outOfBandRecord.metadata.set(OutOfBandRecordMetadataKeys.LegacyInvitation, { + legacyInvitationType: InvitationType.Connection, + }) + const outOfBandRepository = this.agentContext.dependencyManager.resolve(OutOfBandRepository) + await outOfBandRepository.update(this.agentContext, outOfBandRecord) + + return { outOfBandRecord, invitation: convertToOldInvitation(outOfBandRecord.outOfBandInvitation) } + } + + public async createLegacyConnectionlessInvitation(config: { + /** + * @deprecated this value is not used anymore, as the legacy connection-less exchange is now + * integrated with the out of band protocol. The value is kept to not break the API, but will + * be removed in a future version, and has no effect. + */ + recordId?: string + message: Message + domain: string + routing?: Routing + }): Promise<{ message: Message; invitationUrl: string; outOfBandRecord: OutOfBandRecord }> { + const outOfBandRecord = await this.createInvitation({ + messages: [config.message], + routing: config.routing, + }) + + // Set legacy invitation type + outOfBandRecord.metadata.set(OutOfBandRecordMetadataKeys.LegacyInvitation, { + legacyInvitationType: InvitationType.Connectionless, + }) + const outOfBandRepository = this.agentContext.dependencyManager.resolve(OutOfBandRepository) + await outOfBandRepository.update(this.agentContext, outOfBandRecord) + + // Resolve the service and set it on the message + const resolvedService = await this.outOfBandService.getResolvedServiceForOutOfBandServices( + this.agentContext, + outOfBandRecord.outOfBandInvitation.getServices() + ) + config.message.service = ServiceDecorator.fromResolvedDidCommService(resolvedService) + + return { + message: config.message, + invitationUrl: `${config.domain}?d_m=${JsonEncoder.toBase64URL(JsonTransformer.toJSON(config.message))}`, + outOfBandRecord, + } + } + + /** + * Parses URL, decodes invitation and calls `receiveMessage` with parsed invitation message. + * + * Agent role: receiver (invitee) + * + * @param invitationUrl url containing a base64 encoded invitation to receive + * @param config configuration of how out-of-band invitation should be processed + * @returns out-of-band record and connection record if one has been created + */ + public async receiveInvitationFromUrl(invitationUrl: string, config: ReceiveOutOfBandInvitationConfig = {}) { + const message = await this.parseInvitation(invitationUrl) + + return this.receiveInvitation(message, config) + } + + /** + * Parses URL containing encoded invitation and returns invitation message. + * + * Will fetch the url if the url does not contain a base64 encoded invitation. + * + * @param invitationUrl URL containing encoded invitation + * + * @returns OutOfBandInvitation + */ + public async parseInvitation(invitationUrl: string): Promise { + return parseInvitationShortUrl(invitationUrl, this.agentContext.config.agentDependencies) + } + + /** + * Creates inbound out-of-band record and assigns out-of-band invitation message to it if the + * message is valid. It automatically passes out-of-band invitation for further processing to + * `acceptInvitation` method. If you don't want to do that you can set `autoAcceptInvitation` + * attribute in `config` parameter to `false` and accept the message later by calling + * `acceptInvitation`. + * + * It supports both OOB (Aries RFC 0434: Out-of-Band Protocol 1.1) and Connection Invitation + * (0160: Connection Protocol). + * + * Agent role: receiver (invitee) + * + * @param invitation either OutOfBandInvitation or ConnectionInvitationMessage + * @param config config for handling of invitation + * + * @returns out-of-band record and connection record if one has been created. + */ + public async receiveInvitation( + invitation: OutOfBandInvitation | ConnectionInvitationMessage, + config: ReceiveOutOfBandInvitationConfig = {} + ): Promise<{ outOfBandRecord: OutOfBandRecord; connectionRecord?: ConnectionRecord }> { + return this._receiveInvitation(invitation, config) + } + + /** + * Creates inbound out-of-band record from an implicit invitation, given as a public DID the agent + * should be capable of resolving. It automatically passes out-of-band invitation for further + * processing to `acceptInvitation` method. If you don't want to do that you can set + * `autoAcceptInvitation` attribute in `config` parameter to `false` and accept the message later by + * calling `acceptInvitation`. + * + * It supports both OOB (Aries RFC 0434: Out-of-Band Protocol 1.1) and Connection Invitation + * (0160: Connection Protocol). Handshake protocol to be used depends on handshakeProtocols + * (DID Exchange by default) + * + * Agent role: receiver (invitee) + * + * @param config config for creating and handling invitation + * + * @returns out-of-band record and connection record if one has been created. + */ + public async receiveImplicitInvitation(config: ReceiveOutOfBandImplicitInvitationConfig) { + const handshakeProtocols = this.getSupportedHandshakeProtocols( + config.handshakeProtocols ?? [HandshakeProtocol.DidExchange] + ).map((p) => p.parsedProtocolUri.protocolUri) + + const invitation = new OutOfBandInvitation({ + id: config.did, + label: config.label ?? '', + services: [config.did], + handshakeProtocols, + }) + + return this._receiveInvitation(invitation, { ...config, isImplicit: true }) + } + + /** + * Internal receive invitation method, for both explicit and implicit OOB invitations + */ + private async _receiveInvitation( + invitation: OutOfBandInvitation | ConnectionInvitationMessage, + config: BaseReceiveOutOfBandInvitationConfig = {} + ): Promise<{ outOfBandRecord: OutOfBandRecord; connectionRecord?: ConnectionRecord }> { + // Convert to out of band invitation if needed + const outOfBandInvitation = + invitation instanceof OutOfBandInvitation ? invitation : convertToNewInvitation(invitation) + + const { handshakeProtocols } = outOfBandInvitation + const { routing } = config + + const autoAcceptInvitation = config.autoAcceptInvitation ?? true + const autoAcceptConnection = config.autoAcceptConnection ?? true + const reuseConnection = config.reuseConnection ?? false + const label = config.label ?? this.agentContext.config.label + const alias = config.alias + const imageUrl = config.imageUrl ?? this.agentContext.config.connectionImageUrl + + const messages = outOfBandInvitation.getRequests() + + const isConnectionless = handshakeProtocols === undefined || handshakeProtocols.length === 0 + + if ((!handshakeProtocols || handshakeProtocols.length === 0) && (!messages || messages?.length === 0)) { + throw new CredoError('One or both of handshake_protocols and requests~attach MUST be included in the message.') + } + + const recipientKeyFingerprints = await this.resolveInvitationRecipientKeyFingerprints(outOfBandInvitation) + const outOfBandRecord = new OutOfBandRecord({ + role: OutOfBandRole.Receiver, + state: OutOfBandState.Initial, + outOfBandInvitation: outOfBandInvitation, + autoAcceptConnection, + tags: { recipientKeyFingerprints }, + mediatorId: routing?.mediatorId, + }) + + // If we have routing, and this is a connectionless exchange, or we are not auto accepting the connection + // we need to store the routing, so it can be used when we send the first message in response to this invitation + if (routing && (isConnectionless || !autoAcceptInvitation)) { + this.logger.debug('Storing routing for out of band invitation.') + outOfBandRecord.metadata.set(OutOfBandRecordMetadataKeys.RecipientRouting, { + recipientKeyFingerprint: routing.recipientKey.fingerprint, + routingKeyFingerprints: routing.routingKeys.map((key) => key.fingerprint), + endpoints: routing.endpoints, + mediatorId: routing.mediatorId, + }) + } + + // If the invitation was converted from another legacy format, we store this, as its needed for some flows + if (outOfBandInvitation.invitationType && outOfBandInvitation.invitationType !== InvitationType.OutOfBand) { + outOfBandRecord.metadata.set(OutOfBandRecordMetadataKeys.LegacyInvitation, { + legacyInvitationType: outOfBandInvitation.invitationType, + }) + } + + await this.outOfBandService.save(this.agentContext, outOfBandRecord) + this.outOfBandService.emitStateChangedEvent(this.agentContext, outOfBandRecord, null) + + if (autoAcceptInvitation) { + return await this.acceptInvitation(outOfBandRecord.id, { + label, + alias, + imageUrl, + autoAcceptConnection, + reuseConnection, + routing, + timeoutMs: config.acceptInvitationTimeoutMs, + ourDid: config.ourDid, + }) + } + + return { outOfBandRecord } + } + + /** + * Creates a connection if the out-of-band invitation message contains `handshake_protocols` + * attribute, except for the case when connection already exists and `reuseConnection` is enabled. + * + * It passes first supported message from `requests~attach` attribute to the agent, except for the + * case reuse of connection is applied when it just sends `handshake-reuse` message to existing + * connection. + * + * Agent role: receiver (invitee) + * + * @param outOfBandId + * @param config + * @returns out-of-band record and connection record if one has been created. + */ + public async acceptInvitation( + outOfBandId: string, + config: { + autoAcceptConnection?: boolean + reuseConnection?: boolean + label?: string + alias?: string + imageUrl?: string + /** + * Routing for the exchange (either connection or connection-less exchange). + * + * If a connection is reused, the routing WILL NOT be used. + */ + routing?: Routing + timeoutMs?: number + ourDid?: string + } + ) { + const outOfBandRecord = await this.outOfBandService.getById(this.agentContext, outOfBandId) + + const { outOfBandInvitation } = outOfBandRecord + const { label, alias, imageUrl, autoAcceptConnection, reuseConnection, ourDid } = config + const services = outOfBandInvitation.getServices() + const messages = outOfBandInvitation.getRequests() + const timeoutMs = config.timeoutMs ?? 20000 + + let routing = config.routing + + // recipient routing from the receiveInvitation method. + const recipientRouting = outOfBandRecord.metadata.get(OutOfBandRecordMetadataKeys.RecipientRouting) + if (!routing && recipientRouting) { + routing = { + recipientKey: Key.fromFingerprint(recipientRouting.recipientKeyFingerprint), + routingKeys: recipientRouting.routingKeyFingerprints.map((fingerprint) => Key.fromFingerprint(fingerprint)), + endpoints: recipientRouting.endpoints, + mediatorId: recipientRouting.mediatorId, + } + } + + const { handshakeProtocols } = outOfBandInvitation + + const existingConnection = await this.findExistingConnection(outOfBandInvitation) + + await this.outOfBandService.updateState(this.agentContext, outOfBandRecord, OutOfBandState.PrepareResponse) + + if (handshakeProtocols) { + this.logger.debug('Out of band message contains handshake protocols.') + + let connectionRecord + if (existingConnection && reuseConnection) { + this.logger.debug( + `Connection already exists and reuse is enabled. Reusing an existing connection with ID ${existingConnection.id}.` + ) + + if (!messages) { + this.logger.debug('Out of band message does not contain any request messages.') + const isHandshakeReuseSuccessful = await this.handleHandshakeReuse(outOfBandRecord, existingConnection) + + // Handshake reuse was successful + if (isHandshakeReuseSuccessful) { + this.logger.debug(`Handshake reuse successful. Reusing existing connection ${existingConnection.id}.`) + connectionRecord = existingConnection + } else { + // Handshake reuse failed. Not setting connection record + this.logger.debug(`Handshake reuse failed. Not using existing connection ${existingConnection.id}.`) + } + } else { + // Handshake reuse because we found a connection and we can respond directly to the message + this.logger.debug(`Reusing existing connection ${existingConnection.id}.`) + connectionRecord = existingConnection + } + } + + // If no existing connection was found, reuseConnection is false, or we didn't receive a + // handshake-reuse-accepted message we create a new connection + if (!connectionRecord) { + this.logger.debug('Connection does not exist or reuse is disabled. Creating a new connection.') + // Find first supported handshake protocol preserving the order of handshake protocols + // defined by `handshake_protocols` attribute in the invitation message + const firstSupportedProtocol = this.getFirstSupportedProtocol(handshakeProtocols) + connectionRecord = await this.connectionsApi.acceptOutOfBandInvitation(outOfBandRecord, { + label, + alias, + imageUrl, + autoAcceptConnection, + protocol: firstSupportedProtocol.handshakeProtocol, + routing, + ourDid, + }) + } + + if (messages) { + this.logger.debug('Out of band message contains request messages.') + if (connectionRecord.isReady) { + await this.emitWithConnection(outOfBandRecord, connectionRecord, messages) + } else { + // Wait until the connection is ready and then pass the messages to the agent for further processing + this.connectionsApi + .returnWhenIsConnected(connectionRecord.id, { timeoutMs }) + .then((connectionRecord) => this.emitWithConnection(outOfBandRecord, connectionRecord, messages)) + .catch((error) => { + if (error instanceof EmptyError) { + this.logger.warn( + `Agent unsubscribed before connection got into ${DidExchangeState.Completed} state`, + error + ) + } else { + this.logger.error('Promise waiting for the connection to be complete failed.', error) + } + }) + } + } + return { outOfBandRecord, connectionRecord } + } else if (messages) { + this.logger.debug('Out of band message contains only request messages.') + if (existingConnection && reuseConnection) { + this.logger.debug('Connection already exists.', { connectionId: existingConnection.id }) + await this.emitWithConnection(outOfBandRecord, existingConnection, messages) + } else { + await this.emitWithServices(outOfBandRecord, services, messages) + } + } + return { outOfBandRecord } + } + + public async findByReceivedInvitationId(receivedInvitationId: string) { + return this.outOfBandService.findByReceivedInvitationId(this.agentContext, receivedInvitationId) + } + + public async findByCreatedInvitationId(createdInvitationId: string) { + return this.outOfBandService.findByCreatedInvitationId(this.agentContext, createdInvitationId) + } + + /** + * Retrieve all out of bands records + * + * @returns List containing all out of band records + */ + public getAll() { + return this.outOfBandService.getAll(this.agentContext) + } + + /** + * Retrieve all out of bands records by specified query param + * + * @returns List containing all out of band records matching specified query params + */ + public findAllByQuery(query: Query, queryOptions?: QueryOptions) { + return this.outOfBandService.findAllByQuery(this.agentContext, query, queryOptions) + } + + /** + * Retrieve a out of band record by id + * + * @param outOfBandId The out of band record id + * @throws {RecordNotFoundError} If no record is found + * @return The out of band record + * + */ + public getById(outOfBandId: string): Promise { + return this.outOfBandService.getById(this.agentContext, outOfBandId) + } + + /** + * Find an out of band record by id + * + * @param outOfBandId the out of band record id + * @returns The out of band record or null if not found + */ + public findById(outOfBandId: string): Promise { + return this.outOfBandService.findById(this.agentContext, outOfBandId) + } + + /** + * Delete an out of band record by id + * + * @param outOfBandId the out of band record id + */ + public async deleteById(outOfBandId: string) { + const outOfBandRecord = await this.getById(outOfBandId) + + const relatedConnections = await this.connectionsApi.findAllByOutOfBandId(outOfBandId) + + // If it uses mediation and there are no related connections, AND we didn't use a did in the invitation + // (if that is the case the did is managed outside of this exchange) proceed to delete keys from mediator + // Note: if OOB Record is reusable, it is safe to delete it because every connection created from + // it will use its own recipient key + if ( + outOfBandRecord.mediatorId && + outOfBandRecord.outOfBandInvitation.getDidServices().length === 0 && + (relatedConnections.length === 0 || outOfBandRecord.reusable) + ) { + const recipientKeys = outOfBandRecord.getTags().recipientKeyFingerprints.map((item) => Key.fromFingerprint(item)) + + await this.routingService.removeRouting(this.agentContext, { + recipientKeys, + mediatorId: outOfBandRecord.mediatorId, + }) + } + + return this.outOfBandService.deleteById(this.agentContext, outOfBandId) + } + + private assertHandshakeProtocolsSupported(handshakeProtocols: HandshakeProtocol[]) { + if (!this.areHandshakeProtocolsSupported(handshakeProtocols)) { + const supportedProtocols = this.getSupportedHandshakeProtocols() + throw new CredoError( + `Handshake protocols [${handshakeProtocols}] are not supported. Supported protocols are [${supportedProtocols}]` + ) + } + } + + private areHandshakeProtocolsSupported(handshakeProtocols: HandshakeProtocol[]) { + const supportedProtocols = this.getSupportedHandshakeProtocols(handshakeProtocols) + return supportedProtocols.length === handshakeProtocols.length + } + + private getSupportedHandshakeProtocols(limitToHandshakeProtocols?: HandshakeProtocol[]) { + const allHandshakeProtocols = limitToHandshakeProtocols ?? Object.values(HandshakeProtocol) + + // Replace .x in the handshake protocol with .0 to allow it to be parsed + const parsedHandshakeProtocolUris = allHandshakeProtocols.map((h) => ({ + handshakeProtocol: h, + parsedProtocolUri: parseDidCommProtocolUri(h.replace('.x', '.0')), + })) + + // Now find all handshake protocols that start with the protocol uri without minor version '//.' + const supportedHandshakeProtocols = this.messageHandlerRegistry.filterSupportedProtocolsByProtocolUris( + parsedHandshakeProtocolUris.map((p) => p.parsedProtocolUri) + ) + + if (supportedHandshakeProtocols.length === 0) { + throw new CredoError('There is no handshake protocol supported. Agent can not create a connection.') + } + + // Order protocols according to `parsedHandshakeProtocolUris` array (order of preference) + const orderedProtocols = parsedHandshakeProtocolUris + .map((p) => { + const found = supportedHandshakeProtocols.find((s) => + supportsIncomingDidCommProtocolUri(s, p.parsedProtocolUri) + ) + // We need to override the parsedProtocolUri with the one from the supported protocols, as we used `.0` as the minor + // version before. But when we return it, we want to return the correct minor version that we actually support + return found ? { ...p, parsedProtocolUri: found } : null + }) + .filter((p): p is NonNullable => p !== null) + + return orderedProtocols + } + + /** + * Get the first supported protocol based on the handshake protocols provided in the out of band + * invitation. + * + * Returns an enum value from {@link HandshakeProtocol} or throw an error if no protocol is supported. + * Minor versions are ignored when selecting a supported protocols, so if the `outOfBandInvitationSupportedProtocolsWithMinorVersion` + * value is `https://didcomm.org/didexchange/1.0` and the agent supports `https://didcomm.org/didexchange/1.1` + * this will be fine, and the returned value will be {@link HandshakeProtocol.DidExchange}. + */ + private getFirstSupportedProtocol(protocolUris: string[]) { + const supportedProtocols = this.getSupportedHandshakeProtocols() + const parsedProtocolUris = protocolUris.map(parseDidCommProtocolUri) + + const firstSupportedProtocol = supportedProtocols.find((supportedProtocol) => + parsedProtocolUris.find((parsedProtocol) => + supportsIncomingDidCommProtocolUri(supportedProtocol.parsedProtocolUri, parsedProtocol) + ) + ) + + if (!firstSupportedProtocol) { + throw new CredoError( + `Handshake protocols [${protocolUris}] are not supported. Supported protocols are [${supportedProtocols.map( + (p) => p.handshakeProtocol + )}]` + ) + } + + return firstSupportedProtocol + } + + private async findExistingConnection(outOfBandInvitation: OutOfBandInvitation) { + this.logger.debug('Searching for an existing connection for out-of-band invitation.', { outOfBandInvitation }) + + const invitationDids = [ + ...outOfBandInvitation.invitationDids, + // Also search for legacy invitationDids based on inline services (TODO: remove in 0.6.0) + ...outOfBandInvitation.getInlineServices().map(outOfBandServiceToInlineKeysNumAlgo2Did), + ] + + for (const invitationDid of invitationDids) { + const connections = await this.connectionsApi.findByInvitationDid(invitationDid) + + this.logger.debug(`Retrieved ${connections.length} connections for invitation did ${invitationDid}`) + + if (connections.length === 1) { + const [firstConnection] = connections + return firstConnection + } else if (connections.length > 1) { + this.logger.warn( + `There is more than one connection created from invitationDid ${invitationDid}. Taking the first one.` + ) + const [firstConnection] = connections + return firstConnection + } + return null + } + } + + private async emitWithConnection( + outOfBandRecord: OutOfBandRecord, + connectionRecord: ConnectionRecord, + messages: PlaintextMessage[] + ) { + const supportedMessageTypes = this.messageHandlerRegistry.supportedMessageTypes + const plaintextMessage = messages.find((message) => { + const parsedMessageType = parseMessageType(message['@type']) + return supportedMessageTypes.find((type) => supportsIncomingMessageType(parsedMessageType, type)) + }) + + if (!plaintextMessage) { + throw new CredoError('There is no message in requests~attach supported by agent.') + } + + // Make sure message has correct parent thread id + this.ensureParentThreadId(outOfBandRecord, plaintextMessage) + + this.logger.debug(`Message with type ${plaintextMessage['@type']} can be processed.`) + + this.eventEmitter.emit(this.agentContext, { + type: AgentEventTypes.AgentMessageReceived, + payload: { + message: plaintextMessage, + connection: connectionRecord, + contextCorrelationId: this.agentContext.contextCorrelationId, + }, + }) + } + + private async emitWithServices( + outOfBandRecord: OutOfBandRecord, + services: Array, + messages: PlaintextMessage[] + ) { + if (!services || services.length === 0) { + throw new CredoError(`There are no services. We can not emit messages`) + } + + const supportedMessageTypes = this.messageHandlerRegistry.supportedMessageTypes + const plaintextMessage = messages.find((message) => { + const parsedMessageType = parseMessageType(message['@type']) + return supportedMessageTypes.find((type) => supportsIncomingMessageType(parsedMessageType, type)) + }) + + if (!plaintextMessage) { + throw new CredoError('There is no message in requests~attach supported by agent.') + } + + // Make sure message has correct parent thread id + this.ensureParentThreadId(outOfBandRecord, plaintextMessage) + + this.logger.debug(`Message with type ${plaintextMessage['@type']} can be processed.`) + + this.eventEmitter.emit(this.agentContext, { + type: AgentEventTypes.AgentMessageReceived, + payload: { + message: plaintextMessage, + contextCorrelationId: this.agentContext.contextCorrelationId, + }, + }) + } + + private ensureParentThreadId(outOfBandRecord: OutOfBandRecord, plaintextMessage: PlaintextMessage) { + const legacyInvitationMetadata = outOfBandRecord.metadata.get(OutOfBandRecordMetadataKeys.LegacyInvitation) + + // We need to set the parent thread id to the invitation id, according to RFC 0434. + // So if it already has a pthid and it is not the same as the invitation id, we throw an error + if ( + plaintextMessage['~thread']?.pthid && + plaintextMessage['~thread'].pthid !== outOfBandRecord.outOfBandInvitation.id + ) { + throw new CredoError( + `Out of band invitation requests~attach message contains parent thread id ${plaintextMessage['~thread'].pthid} that does not match the invitation id ${outOfBandRecord.outOfBandInvitation.id}` + ) + } + + // If the invitation is created from a legacy connectionless invitation, we don't need to set the pthid + // as that's not expected, and it's generated on our side only + if (legacyInvitationMetadata?.legacyInvitationType === InvitationType.Connectionless) { + return + } + + if (!plaintextMessage['~thread']) { + plaintextMessage['~thread'] = {} + } + + // The response to an out-of-band message MUST set its ~thread.pthid equal to the @id property of the out-of-band message. + // By adding the pthid to the message, we ensure that the response will take over this pthid + plaintextMessage['~thread'].pthid = outOfBandRecord.outOfBandInvitation.id + } + + private async handleHandshakeReuse(outOfBandRecord: OutOfBandRecord, connectionRecord: ConnectionRecord) { + const reuseMessage = await this.outOfBandService.createHandShakeReuse( + this.agentContext, + outOfBandRecord, + connectionRecord + ) + + const reuseAcceptedEventPromise = firstValueFrom( + this.eventEmitter.observable(OutOfBandEventTypes.HandshakeReused).pipe( + filterContextCorrelationId(this.agentContext.contextCorrelationId), + // Find the first reuse event where the handshake reuse accepted matches the reuse message thread + // TODO: Should we store the reuse state? Maybe we can keep it in memory for now + first( + (event) => + event.payload.reuseThreadId === reuseMessage.threadId && + event.payload.outOfBandRecord.id === outOfBandRecord.id && + event.payload.connectionRecord.id === connectionRecord.id + ), + // If the event is found, we return the value true + map(() => true), + timeout({ + first: 15000, + meta: 'OutOfBandApi.handleHandshakeReuse', + }), + // If timeout is reached, we return false + catchError(() => of(false)) + ) + ) + + const outboundMessageContext = new OutboundMessageContext(reuseMessage, { + agentContext: this.agentContext, + connection: connectionRecord, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return reuseAcceptedEventPromise + } + + private async resolveInvitationRecipientKeyFingerprints(outOfBandInvitation: OutOfBandInvitation) { + const recipientKeyFingerprints: string[] = [] + + for (const service of outOfBandInvitation.getServices()) { + // Resolve dids to DIDDocs to retrieve services + if (typeof service === 'string') { + this.logger.debug(`Resolving services for did ${service}.`) + const resolvedDidCommServices = await this.didCommDocumentService.resolveServicesFromDid( + this.agentContext, + service + ) + recipientKeyFingerprints.push( + ...resolvedDidCommServices + .reduce((aggr, { recipientKeys }) => [...aggr, ...recipientKeys], []) + .map((key) => key.fingerprint) + ) + } else { + recipientKeyFingerprints.push(...service.recipientKeys.map((didKey) => DidKey.fromDid(didKey).key.fingerprint)) + } + } + + return recipientKeyFingerprints + } + + // TODO: we should probably move these to the out of band module and register the handler there + private registerMessageHandlers(messageHandlerRegistry: MessageHandlerRegistry) { + messageHandlerRegistry.registerMessageHandler(new HandshakeReuseHandler(this.outOfBandService)) + messageHandlerRegistry.registerMessageHandler(new HandshakeReuseAcceptedHandler(this.outOfBandService)) + } +} diff --git a/packages/core/src/modules/oob/OutOfBandModule.ts b/packages/core/src/modules/oob/OutOfBandModule.ts new file mode 100644 index 0000000000..c13fc6a150 --- /dev/null +++ b/packages/core/src/modules/oob/OutOfBandModule.ts @@ -0,0 +1,31 @@ +import type { FeatureRegistry } from '../../agent/FeatureRegistry' +import type { DependencyManager, Module } from '../../plugins' + +import { Protocol } from '../../agent/models' + +import { OutOfBandApi } from './OutOfBandApi' +import { OutOfBandService } from './OutOfBandService' +import { OutOfBandRepository } from './repository' + +export class OutOfBandModule implements Module { + public readonly api = OutOfBandApi + + /** + * Registers the dependencies of the ot of band module on the dependency manager. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Services + dependencyManager.registerSingleton(OutOfBandService) + + // Repositories + dependencyManager.registerSingleton(OutOfBandRepository) + + // Features + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/out-of-band/1.1', + roles: ['sender', 'receiver'], + }) + ) + } +} diff --git a/packages/core/src/modules/oob/OutOfBandService.ts b/packages/core/src/modules/oob/OutOfBandService.ts new file mode 100644 index 0000000000..be14890049 --- /dev/null +++ b/packages/core/src/modules/oob/OutOfBandService.ts @@ -0,0 +1,283 @@ +import type { OutOfBandDidCommService } from './domain' +import type { HandshakeReusedEvent, OutOfBandStateChangedEvent } from './domain/OutOfBandEvents' +import type { AgentContext } from '../../agent' +import type { InboundMessageContext } from '../../agent/models/InboundMessageContext' +import type { Key } from '../../crypto' +import type { Query, QueryOptions } from '../../storage/StorageService' +import type { ConnectionRecord } from '../connections' +import type { HandshakeProtocol } from '../connections/models' + +import { EventEmitter } from '../../agent/EventEmitter' +import { CredoError } from '../../error' +import { injectable } from '../../plugins' +import { DidCommDocumentService } from '../didcomm/services/DidCommDocumentService' +import { DidsApi } from '../dids' +import { parseDid } from '../dids/domain/parse' + +import { OutOfBandEventTypes } from './domain/OutOfBandEvents' +import { OutOfBandRole } from './domain/OutOfBandRole' +import { OutOfBandState } from './domain/OutOfBandState' +import { HandshakeReuseMessage, OutOfBandInvitation } from './messages' +import { HandshakeReuseAcceptedMessage } from './messages/HandshakeReuseAcceptedMessage' +import { OutOfBandRecord, OutOfBandRepository } from './repository' + +export interface CreateFromImplicitInvitationConfig { + did: string + threadId: string + handshakeProtocols: HandshakeProtocol[] + autoAcceptConnection?: boolean + recipientKey: Key +} + +@injectable() +export class OutOfBandService { + private outOfBandRepository: OutOfBandRepository + private eventEmitter: EventEmitter + private didCommDocumentService: DidCommDocumentService + + public constructor( + outOfBandRepository: OutOfBandRepository, + eventEmitter: EventEmitter, + didCommDocumentService: DidCommDocumentService + ) { + this.outOfBandRepository = outOfBandRepository + this.eventEmitter = eventEmitter + this.didCommDocumentService = didCommDocumentService + } + + /** + * Creates an Out of Band record from a Connection/DIDExchange request started by using + * a publicly resolvable DID this agent can control + */ + public async createFromImplicitInvitation( + agentContext: AgentContext, + config: CreateFromImplicitInvitationConfig + ): Promise { + const { did, threadId, handshakeProtocols, autoAcceptConnection, recipientKey } = config + + // Verify it is a valid did and it is present in the wallet + const publicDid = parseDid(did) + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const [createdDid] = await didsApi.getCreatedDids({ did: publicDid.did }) + if (!createdDid) { + throw new CredoError(`Referenced public did ${did} not found.`) + } + + // Recreate an 'implicit invitation' matching the parameters used by the invitee when + // initiating the flow + const outOfBandInvitation = new OutOfBandInvitation({ + id: did, + services: [did], + handshakeProtocols, + }) + + outOfBandInvitation.setThread({ threadId }) + + const outOfBandRecord = new OutOfBandRecord({ + role: OutOfBandRole.Sender, + state: OutOfBandState.AwaitResponse, + reusable: true, + autoAcceptConnection: autoAcceptConnection ?? false, + outOfBandInvitation, + tags: { + recipientKeyFingerprints: [recipientKey.fingerprint], + }, + }) + + await this.save(agentContext, outOfBandRecord) + this.emitStateChangedEvent(agentContext, outOfBandRecord, null) + return outOfBandRecord + } + + public async processHandshakeReuse(messageContext: InboundMessageContext) { + const reuseMessage = messageContext.message + const parentThreadId = reuseMessage.thread?.parentThreadId + + if (!parentThreadId) { + throw new CredoError('handshake-reuse message must have a parent thread id') + } + + const outOfBandRecord = await this.findByCreatedInvitationId(messageContext.agentContext, parentThreadId) + if (!outOfBandRecord) { + throw new CredoError('No out of band record found for handshake-reuse message') + } + + // Assert + outOfBandRecord.assertRole(OutOfBandRole.Sender) + outOfBandRecord.assertState(OutOfBandState.AwaitResponse) + + const requestLength = outOfBandRecord.outOfBandInvitation.getRequests()?.length ?? 0 + if (requestLength > 0) { + throw new CredoError('Handshake reuse should only be used when no requests are present') + } + + const reusedConnection = messageContext.assertReadyConnection() + this.eventEmitter.emit(messageContext.agentContext, { + type: OutOfBandEventTypes.HandshakeReused, + payload: { + reuseThreadId: reuseMessage.threadId, + connectionRecord: reusedConnection, + outOfBandRecord, + }, + }) + + // If the out of band record is not reusable we can set the state to done + if (!outOfBandRecord.reusable) { + await this.updateState(messageContext.agentContext, outOfBandRecord, OutOfBandState.Done) + } + + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + threadId: reuseMessage.threadId, + parentThreadId, + }) + + return reuseAcceptedMessage + } + + public async processHandshakeReuseAccepted(messageContext: InboundMessageContext) { + const reuseAcceptedMessage = messageContext.message + const parentThreadId = reuseAcceptedMessage.thread?.parentThreadId + + if (!parentThreadId) { + throw new CredoError('handshake-reuse-accepted message must have a parent thread id') + } + + const outOfBandRecord = await this.findByReceivedInvitationId(messageContext.agentContext, parentThreadId) + if (!outOfBandRecord) { + throw new CredoError('No out of band record found for handshake-reuse-accepted message') + } + + // Assert + outOfBandRecord.assertRole(OutOfBandRole.Receiver) + outOfBandRecord.assertState(OutOfBandState.PrepareResponse) + + const reusedConnection = messageContext.assertReadyConnection() + + // Checks whether the connection associated with reuse accepted message matches with the connection + // associated with the reuse message. + // FIXME: not really a fan of the reuseConnectionId, but it's the only way I can think of now to get the connection + // associated with the reuse message. Maybe we can at least move it to the metadata and remove it directly afterwards? + // But this is an issue in general that has also come up for ACA-Py. How do I find the connection associated with an oob record? + // Because it doesn't work really well with connection reuse. + if (outOfBandRecord.reuseConnectionId !== reusedConnection.id) { + throw new CredoError('handshake-reuse-accepted is not in response to a handshake-reuse message.') + } + + this.eventEmitter.emit(messageContext.agentContext, { + type: OutOfBandEventTypes.HandshakeReused, + payload: { + reuseThreadId: reuseAcceptedMessage.threadId, + connectionRecord: reusedConnection, + outOfBandRecord, + }, + }) + + // receiver role is never reusable, so we can set the state to done + await this.updateState(messageContext.agentContext, outOfBandRecord, OutOfBandState.Done) + } + + public async createHandShakeReuse( + agentContext: AgentContext, + outOfBandRecord: OutOfBandRecord, + connectionRecord: ConnectionRecord + ) { + const reuseMessage = new HandshakeReuseMessage({ parentThreadId: outOfBandRecord.outOfBandInvitation.id }) + + // Store the reuse connection id + outOfBandRecord.reuseConnectionId = connectionRecord.id + await this.outOfBandRepository.update(agentContext, outOfBandRecord) + + return reuseMessage + } + + public async save(agentContext: AgentContext, outOfBandRecord: OutOfBandRecord) { + return this.outOfBandRepository.save(agentContext, outOfBandRecord) + } + + public async updateState(agentContext: AgentContext, outOfBandRecord: OutOfBandRecord, newState: OutOfBandState) { + const previousState = outOfBandRecord.state + outOfBandRecord.state = newState + await this.outOfBandRepository.update(agentContext, outOfBandRecord) + + this.emitStateChangedEvent(agentContext, outOfBandRecord, previousState) + } + + public emitStateChangedEvent( + agentContext: AgentContext, + outOfBandRecord: OutOfBandRecord, + previousState: OutOfBandState | null + ) { + this.eventEmitter.emit(agentContext, { + type: OutOfBandEventTypes.OutOfBandStateChanged, + payload: { + outOfBandRecord: outOfBandRecord.clone(), + previousState, + }, + }) + } + + public async findById(agentContext: AgentContext, outOfBandRecordId: string) { + return this.outOfBandRepository.findById(agentContext, outOfBandRecordId) + } + + public async getById(agentContext: AgentContext, outOfBandRecordId: string) { + return this.outOfBandRepository.getById(agentContext, outOfBandRecordId) + } + + public async findByReceivedInvitationId(agentContext: AgentContext, receivedInvitationId: string) { + return this.outOfBandRepository.findSingleByQuery(agentContext, { + invitationId: receivedInvitationId, + role: OutOfBandRole.Receiver, + }) + } + + public async findByCreatedInvitationId(agentContext: AgentContext, createdInvitationId: string, threadId?: string) { + return this.outOfBandRepository.findSingleByQuery(agentContext, { + invitationId: createdInvitationId, + role: OutOfBandRole.Sender, + threadId, + }) + } + + public async findCreatedByRecipientKey(agentContext: AgentContext, recipientKey: Key) { + return this.outOfBandRepository.findSingleByQuery(agentContext, { + recipientKeyFingerprints: [recipientKey.fingerprint], + role: OutOfBandRole.Sender, + }) + } + + public async getAll(agentContext: AgentContext) { + return this.outOfBandRepository.getAll(agentContext) + } + + public async findAllByQuery(agentContext: AgentContext, query: Query, queryOptions?: QueryOptions) { + return this.outOfBandRepository.findByQuery(agentContext, query, queryOptions) + } + + public async deleteById(agentContext: AgentContext, outOfBandId: string) { + const outOfBandRecord = await this.getById(agentContext, outOfBandId) + return this.outOfBandRepository.delete(agentContext, outOfBandRecord) + } + + /** + * Extract a resolved didcomm service from an out of band invitation. + * + * Currently the first service that can be resolved is returned. + */ + public async getResolvedServiceForOutOfBandServices( + agentContext: AgentContext, + services: Array + ) { + for (const service of services) { + if (typeof service === 'string') { + const [didService] = await this.didCommDocumentService.resolveServicesFromDid(agentContext, service) + + if (didService) return didService + } else { + return service.resolvedDidCommService + } + } + + throw new CredoError('Could not extract a service from the out of band invitation.') + } +} diff --git a/packages/core/src/modules/oob/__tests__/OutOfBandInvitation.test.ts b/packages/core/src/modules/oob/__tests__/OutOfBandInvitation.test.ts new file mode 100644 index 0000000000..a30fe2331e --- /dev/null +++ b/packages/core/src/modules/oob/__tests__/OutOfBandInvitation.test.ts @@ -0,0 +1,313 @@ +import type { ClassValidationError } from '../../../error/ClassValidationError' + +import { Attachment } from '../../../decorators/attachment/Attachment' +import { MessageValidator } from '../../../utils' +import { JsonEncoder } from '../../../utils/JsonEncoder' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { HandshakeProtocol } from '../../connections' +import { OutOfBandDidCommService } from '../domain' +import { OutOfBandInvitation } from '../messages/OutOfBandInvitation' + +describe('toUrl', () => { + test('encode the message into the URL containing the base64 encoded invitation as the oob query parameter', async () => { + const domain = 'https://example.com/ssi' + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + } + const invitation = JsonTransformer.fromJSON(json, OutOfBandInvitation) + const invitationUrl = invitation.toUrl({ + domain, + }) + + expect(invitationUrl).toBe(`${domain}?oob=${JsonEncoder.toBase64URL(json)}`) + }) +}) + +describe('validation', () => { + test('Out-of-Band Invitation instance with did as service', async () => { + const invitation = new OutOfBandInvitation({ + id: '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + handshakeProtocols: [HandshakeProtocol.DidExchange], + }) + + expect(() => MessageValidator.validateSync(invitation)).not.toThrow() + }) + + test('Out-of-Band Invitation instance with object as service', async () => { + const invitation = new OutOfBandInvitation({ + id: '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + services: [ + new OutOfBandDidCommService({ + id: 'didcomm', + serviceEndpoint: 'http://endpoint', + recipientKeys: ['did:key:z6MkqgkLrRyLg6bqk27djwbbaQWgaSYgFVCKq9YKxZbNkpVv'], + }), + ], + handshakeProtocols: [HandshakeProtocol.DidExchange], + }) + + expect(() => MessageValidator.validateSync(invitation)).not.toThrow() + }) + + test('Out-of-Band Invitation instance with string and object as services', async () => { + const invitation = new OutOfBandInvitation({ + id: '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + services: [ + 'did:sov:LjgpST2rjsoxYegQDRm7EL', + new OutOfBandDidCommService({ + id: 'didcomm', + serviceEndpoint: 'http://endpoint', + recipientKeys: ['did:key:z6MkqgkLrRyLg6bqk27djwbbaQWgaSYgFVCKq9YKxZbNkpVv'], + }), + ], + handshakeProtocols: [HandshakeProtocol.DidExchange], + }) + + expect(() => MessageValidator.validateSync(invitation)).not.toThrow() + }) +}) + +describe('fromUrl', () => { + test('decode the URL containing the base64 encoded invitation as the oob parameter into an `OutOfBandInvitation`', () => { + const invitationUrl = + 'http://example.com/ssi?oob=eyJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvb3V0LW9mLWJhbmQvMS4xL2ludml0YXRpb24iLCJAaWQiOiI2OTIxMmEzYS1kMDY4LTRmOWQtYTJkZC00NzQxYmNhODlhZjMiLCJsYWJlbCI6IkZhYmVyIENvbGxlZ2UiLCJnb2FsX2NvZGUiOiJpc3N1ZS12YyIsImdvYWwiOiJUbyBpc3N1ZSBhIEZhYmVyIENvbGxlZ2UgR3JhZHVhdGUgY3JlZGVudGlhbCIsImhhbmRzaGFrZV9wcm90b2NvbHMiOlsiaHR0cHM6Ly9kaWRjb21tLm9yZy9kaWRleGNoYW5nZS8xLjAiLCJodHRwczovL2RpZGNvbW0ub3JnL2Nvbm5lY3Rpb25zLzEuMCJdLCJzZXJ2aWNlcyI6WyJkaWQ6c292OkxqZ3BTVDJyanNveFllZ1FEUm03RUwiXX0K' + + const invitation = OutOfBandInvitation.fromUrl(invitationUrl) + const json = JsonTransformer.toJSON(invitation) + expect(json).toEqual({ + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + }) + }) +}) + +describe('fromJson', () => { + test('create an instance of `OutOfBandInvitation` from JSON object', () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + } + + const invitation = OutOfBandInvitation.fromJson(json) + + expect(invitation).toBeDefined() + expect(invitation).toBeInstanceOf(OutOfBandInvitation) + }) + + test('create an instance of `OutOfBandInvitation` from JSON object with inline service', () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: [ + { + id: '#inline', + recipientKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + routingKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + serviceEndpoint: 'https://example.com/ssi', + }, + ], + } + + const invitation = OutOfBandInvitation.fromJson(json) + expect(invitation).toBeDefined() + expect(invitation).toBeInstanceOf(OutOfBandInvitation) + }) + + test('create an instance of `OutOfBandInvitation` from JSON object with appended attachments', () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + '~attach': [ + { + '@id': 'view-1', + 'mime-type': 'image/png', + filename: 'IMG1092348.png', + lastmod_time: '2018-12-24 18:24:07Z', + description: 'view from doorway, facing east, with lights off', + data: { + base64: 'dmlldyBmcm9tIGRvb3J3YXksIGZhY2luZyBlYXN0LCB3aXRoIGxpZ2h0cyBvZmY=', + }, + }, + { + '@id': 'view-2', + 'mime-type': 'image/png', + filename: 'IMG1092349.png', + lastmod_time: '2018-12-24 18:25:49Z', + description: 'view with lamp in the background', + data: { + base64: 'dmlldyB3aXRoIGxhbXAgaW4gdGhlIGJhY2tncm91bmQ=', + }, + }, + ], + } + + const invitation = OutOfBandInvitation.fromJson(json) + expect(invitation).toBeDefined() + expect(invitation).toBeInstanceOf(OutOfBandInvitation) + expect(invitation.appendedAttachments).toBeDefined() + expect(invitation.appendedAttachments?.length).toEqual(2) + expect(invitation.getAppendedAttachmentById('view-1')).toEqual( + new Attachment({ + id: 'view-1', + mimeType: 'image/png', + filename: 'IMG1092348.png', + lastmodTime: new Date('2018-12-24 18:24:07Z'), + description: 'view from doorway, facing east, with lights off', + data: { + base64: 'dmlldyBmcm9tIGRvb3J3YXksIGZhY2luZyBlYXN0LCB3aXRoIGxpZ2h0cyBvZmY=', + }, + }) + ) + expect(invitation.getAppendedAttachmentById('view-2')).toEqual( + new Attachment({ + id: 'view-2', + mimeType: 'image/png', + filename: 'IMG1092349.png', + lastmodTime: new Date('2018-12-24 18:25:49Z'), + description: 'view with lamp in the background', + data: { + base64: 'dmlldyB3aXRoIGxhbXAgaW4gdGhlIGJhY2tncm91bmQ=', + }, + }) + ) + }) + + test('throw validation error when services attribute is empty', () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: [], + } + + expect.assertions(1) + try { + OutOfBandInvitation.fromJson(json) + } catch (error) { + const firstError = error as ClassValidationError + expect(firstError.validationErrors[0]).toMatchObject({ + children: [], + constraints: { arrayNotEmpty: 'services should not be empty' }, + property: 'services', + target: { + goal: 'To issue a Faber College Graduate credential', + label: 'Faber College', + services: [], + }, + value: [], + }) + } + }) + + test('transforms legacy prefix message @type and handshake_protocols to https://didcomm.org prefix', () => { + const json = { + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: [ + 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0', + 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0', + ], + services: ['did:sov:123'], + } + + const invitation = OutOfBandInvitation.fromJson(json) + + expect(invitation.type).toBe('https://didcomm.org/out-of-band/1.1/invitation') + expect(invitation.handshakeProtocols).toEqual([ + 'https://didcomm.org/didexchange/1.0', + 'https://didcomm.org/connections/1.0', + ]) + }) + + // Check if options @Transform for legacy did:sov prefix doesn't fail if handshake_protocols is not present + test('should successfully transform if no handshake_protocols is present', () => { + const json = { + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + services: ['did:sov:123'], + } + + const invitation = OutOfBandInvitation.fromJson(json) + + expect(invitation.type).toBe('https://didcomm.org/out-of-band/1.1/invitation') + expect(invitation.handshakeProtocols).toBeUndefined() + }) + + test('throw validation error when incorrect service object present in services attribute', async () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: [ + { + id: '#inline', + routingKeys: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + serviceEndpoint: 'https://example.com/ssi', + }, + ], + } + + expect.assertions(1) + try { + OutOfBandInvitation.fromJson(json) + } catch (error) { + const firstError = error as ClassValidationError + expect(firstError.validationErrors[0]).toMatchObject({ + children: [], + constraints: { + arrayNotEmpty: 'recipientKeys should not be empty', + isDidKeyString: 'each value in recipientKeys must be a did:key string', + }, + property: 'recipientKeys', + target: { + id: '#inline', + routingKeys: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + serviceEndpoint: 'https://example.com/ssi', + type: 'did-communication', + }, + value: undefined, + }) + } + }) +}) diff --git a/packages/core/src/modules/oob/__tests__/OutOfBandModule.test.ts b/packages/core/src/modules/oob/__tests__/OutOfBandModule.test.ts new file mode 100644 index 0000000000..1f012a8608 --- /dev/null +++ b/packages/core/src/modules/oob/__tests__/OutOfBandModule.test.ts @@ -0,0 +1,24 @@ +import { FeatureRegistry } from '../../../agent/FeatureRegistry' +import { DependencyManager } from '../../../plugins/DependencyManager' +import { OutOfBandModule } from '../OutOfBandModule' +import { OutOfBandService } from '../OutOfBandService' +import { OutOfBandRepository } from '../repository/OutOfBandRepository' + +jest.mock('../../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +jest.mock('../../../agent/FeatureRegistry') +const FeatureRegistryMock = FeatureRegistry as jest.Mock + +const featureRegistry = new FeatureRegistryMock() +describe('OutOfBandModule', () => { + test('registers dependencies on the dependency manager', () => { + new OutOfBandModule().register(dependencyManager, featureRegistry) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OutOfBandService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OutOfBandRepository) + }) +}) diff --git a/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts b/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts new file mode 100644 index 0000000000..0bd4facc3a --- /dev/null +++ b/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts @@ -0,0 +1,522 @@ +import type { DidCommDocumentService } from '../../didcomm' + +import { Subject } from 'rxjs' + +import { + agentDependencies, + getAgentContext, + getMockConnection, + getMockOutOfBand, + mockFunction, +} from '../../../../tests/helpers' +import { EventEmitter } from '../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import { KeyType, Key } from '../../../crypto' +import { CredoError } from '../../../error' +import { DidExchangeState } from '../../connections/models' +import { OutOfBandService } from '../OutOfBandService' +import { OutOfBandEventTypes } from '../domain/OutOfBandEvents' +import { OutOfBandRole } from '../domain/OutOfBandRole' +import { OutOfBandState } from '../domain/OutOfBandState' +import { HandshakeReuseMessage } from '../messages' +import { HandshakeReuseAcceptedMessage } from '../messages/HandshakeReuseAcceptedMessage' +import { OutOfBandRepository } from '../repository' + +jest.mock('../repository/OutOfBandRepository') +const OutOfBandRepositoryMock = OutOfBandRepository as jest.Mock + +const key = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) + +const agentContext = getAgentContext() + +describe('OutOfBandService', () => { + let outOfBandRepository: OutOfBandRepository + let outOfBandService: OutOfBandService + let didCommDocumentService: DidCommDocumentService + let eventEmitter: EventEmitter + + beforeEach(async () => { + eventEmitter = new EventEmitter(agentDependencies, new Subject()) + outOfBandRepository = new OutOfBandRepositoryMock() + didCommDocumentService = {} as DidCommDocumentService + outOfBandService = new OutOfBandService(outOfBandRepository, eventEmitter, didCommDocumentService) + }) + + describe('processHandshakeReuse', () => { + test('throw error when no parentThreadId is present', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + reuseMessage.setThread({ + parentThreadId: undefined, + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + agentContext, + senderKey: key, + recipientKey: key, + }) + + await expect(outOfBandService.processHandshakeReuse(messageContext)).rejects.toThrowError( + new CredoError('handshake-reuse message must have a parent thread id') + ) + }) + + test('throw error when no out of band record is found for parentThreadId', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + agentContext, + senderKey: key, + recipientKey: key, + }) + + await expect(outOfBandService.processHandshakeReuse(messageContext)).rejects.toThrowError( + new CredoError('No out of band record found for handshake-reuse message') + ) + }) + + test('throw error when role or state is incorrect ', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + agentContext, + senderKey: key, + recipientKey: key, + }) + + // Correct state, incorrect role + const mockOob = getMockOutOfBand({ + state: OutOfBandState.AwaitResponse, + role: OutOfBandRole.Receiver, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + await expect(outOfBandService.processHandshakeReuse(messageContext)).rejects.toThrowError( + new CredoError('Invalid out-of-band record role receiver, expected is sender.') + ) + + mockOob.state = OutOfBandState.PrepareResponse + mockOob.role = OutOfBandRole.Sender + await expect(outOfBandService.processHandshakeReuse(messageContext)).rejects.toThrowError( + new CredoError('Invalid out-of-band record state prepare-response, valid states are: await-response.') + ) + }) + + test('throw error when the out of band record has request messages ', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + agentContext, + senderKey: key, + recipientKey: key, + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.AwaitResponse, + role: OutOfBandRole.Sender, + }) + mockOob.outOfBandInvitation.addRequest(reuseMessage) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + await expect(outOfBandService.processHandshakeReuse(messageContext)).rejects.toThrowError( + new CredoError('Handshake reuse should only be used when no requests are present') + ) + }) + + test("throw error when the message context doesn't have a ready connection", async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + agentContext, + senderKey: key, + recipientKey: key, + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.AwaitResponse, + role: OutOfBandRole.Sender, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + await expect(outOfBandService.processHandshakeReuse(messageContext)).rejects.toThrowError( + new CredoError(`No connection associated with incoming message ${reuseMessage.type}`) + ) + }) + + test('emits handshake reused event ', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const reuseListener = jest.fn() + + const connection = getMockConnection({ state: DidExchangeState.Completed }) + const messageContext = new InboundMessageContext(reuseMessage, { + agentContext, + senderKey: key, + recipientKey: key, + connection, + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.AwaitResponse, + role: OutOfBandRole.Sender, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + eventEmitter.on(OutOfBandEventTypes.HandshakeReused, reuseListener) + await outOfBandService.processHandshakeReuse(messageContext) + eventEmitter.off(OutOfBandEventTypes.HandshakeReused, reuseListener) + + expect(reuseListener).toHaveBeenCalledTimes(1) + const [[reuseEvent]] = reuseListener.mock.calls + + expect(reuseEvent).toMatchObject({ + type: OutOfBandEventTypes.HandshakeReused, + payload: { + connectionRecord: connection, + outOfBandRecord: mockOob, + reuseThreadId: reuseMessage.threadId, + }, + }) + }) + + it('updates state to done if out of band record is not reusable', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + agentContext, + senderKey: key, + recipientKey: key, + connection: getMockConnection({ state: DidExchangeState.Completed }), + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.AwaitResponse, + role: OutOfBandRole.Sender, + reusable: true, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + const updateStateSpy = jest.spyOn(outOfBandService, 'updateState') + + // Reusable shouldn't update state + await outOfBandService.processHandshakeReuse(messageContext) + expect(updateStateSpy).not.toHaveBeenCalled() + + // Non-reusable should update state + mockOob.reusable = false + await outOfBandService.processHandshakeReuse(messageContext) + expect(updateStateSpy).toHaveBeenCalledWith(agentContext, mockOob, OutOfBandState.Done) + }) + + it('returns a handshake-reuse-accepted message', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + agentContext, + senderKey: key, + recipientKey: key, + connection: getMockConnection({ state: DidExchangeState.Completed }), + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.AwaitResponse, + role: OutOfBandRole.Sender, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + const reuseAcceptedMessage = await outOfBandService.processHandshakeReuse(messageContext) + + expect(reuseAcceptedMessage).toBeInstanceOf(HandshakeReuseAcceptedMessage) + expect(reuseAcceptedMessage.thread).toMatchObject({ + threadId: reuseMessage.id, + parentThreadId: reuseMessage.thread?.parentThreadId, + }) + }) + }) + + describe('processHandshakeReuseAccepted', () => { + test('throw error when no parentThreadId is present', async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + threadId: 'threadId', + parentThreadId: 'parentThreadId', + }) + + reuseAcceptedMessage.setThread({ + parentThreadId: undefined, + }) + + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + agentContext, + senderKey: key, + recipientKey: key, + }) + + await expect(outOfBandService.processHandshakeReuseAccepted(messageContext)).rejects.toThrowError( + new CredoError('handshake-reuse-accepted message must have a parent thread id') + ) + }) + + test('throw error when no out of band record is found for parentThreadId', async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + parentThreadId: 'parentThreadId', + threadId: 'threadId', + }) + + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + agentContext, + senderKey: key, + recipientKey: key, + }) + + await expect(outOfBandService.processHandshakeReuseAccepted(messageContext)).rejects.toThrowError( + new CredoError('No out of band record found for handshake-reuse-accepted message') + ) + }) + + test('throw error when role or state is incorrect ', async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + parentThreadId: 'parentThreadId', + threadId: 'threadId', + }) + + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + agentContext, + senderKey: key, + recipientKey: key, + }) + + // Correct state, incorrect role + const mockOob = getMockOutOfBand({ + state: OutOfBandState.PrepareResponse, + role: OutOfBandRole.Sender, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + await expect(outOfBandService.processHandshakeReuseAccepted(messageContext)).rejects.toThrowError( + new CredoError('Invalid out-of-band record role sender, expected is receiver.') + ) + + mockOob.state = OutOfBandState.AwaitResponse + mockOob.role = OutOfBandRole.Receiver + await expect(outOfBandService.processHandshakeReuseAccepted(messageContext)).rejects.toThrowError( + new CredoError('Invalid out-of-band record state await-response, valid states are: prepare-response.') + ) + }) + + test("throw error when the message context doesn't have a ready connection", async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + parentThreadId: 'parentThreadId', + threadId: 'threadId', + }) + + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + agentContext, + senderKey: key, + recipientKey: key, + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.PrepareResponse, + role: OutOfBandRole.Receiver, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + await expect(outOfBandService.processHandshakeReuseAccepted(messageContext)).rejects.toThrowError( + new CredoError(`No connection associated with incoming message ${reuseAcceptedMessage.type}`) + ) + }) + + test("throw error when the reuseConnectionId on the oob record doesn't match with the inbound message connection id", async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + parentThreadId: 'parentThreadId', + threadId: 'threadId', + }) + + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + agentContext, + senderKey: key, + recipientKey: key, + connection: getMockConnection({ state: DidExchangeState.Completed, id: 'connectionId' }), + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.PrepareResponse, + role: OutOfBandRole.Receiver, + reuseConnectionId: 'anotherConnectionId', + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + await expect(outOfBandService.processHandshakeReuseAccepted(messageContext)).rejects.toThrowError( + new CredoError(`handshake-reuse-accepted is not in response to a handshake-reuse message.`) + ) + }) + + test('emits handshake reused event ', async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + parentThreadId: 'parentThreadId', + threadId: 'threadId', + }) + + const reuseListener = jest.fn() + + const connection = getMockConnection({ state: DidExchangeState.Completed, id: 'connectionId' }) + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + agentContext, + senderKey: key, + recipientKey: key, + connection, + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.PrepareResponse, + role: OutOfBandRole.Receiver, + reuseConnectionId: 'connectionId', + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + eventEmitter.on(OutOfBandEventTypes.HandshakeReused, reuseListener) + await outOfBandService.processHandshakeReuseAccepted(messageContext) + eventEmitter.off(OutOfBandEventTypes.HandshakeReused, reuseListener) + + expect(reuseListener).toHaveBeenCalledTimes(1) + const [[reuseEvent]] = reuseListener.mock.calls + + expect(reuseEvent).toMatchObject({ + type: OutOfBandEventTypes.HandshakeReused, + payload: { + connectionRecord: connection, + outOfBandRecord: mockOob, + reuseThreadId: reuseAcceptedMessage.threadId, + }, + }) + }) + + it('updates state to done', async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + parentThreadId: 'parentThreadId', + threadId: 'threadId', + }) + + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + agentContext, + senderKey: key, + recipientKey: key, + connection: getMockConnection({ state: DidExchangeState.Completed, id: 'connectionId' }), + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.PrepareResponse, + role: OutOfBandRole.Receiver, + reusable: true, + reuseConnectionId: 'connectionId', + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + const updateStateSpy = jest.spyOn(outOfBandService, 'updateState') + + await outOfBandService.processHandshakeReuseAccepted(messageContext) + expect(updateStateSpy).toHaveBeenCalledWith(agentContext, mockOob, OutOfBandState.Done) + }) + }) + + describe('updateState', () => { + test('updates the state on the out of band record', async () => { + const mockOob = getMockOutOfBand({ + state: OutOfBandState.Initial, + }) + + await outOfBandService.updateState(agentContext, mockOob, OutOfBandState.Done) + + expect(mockOob.state).toEqual(OutOfBandState.Done) + }) + + test('updates the record in the out of band repository', async () => { + const mockOob = getMockOutOfBand({ + state: OutOfBandState.Initial, + }) + + await outOfBandService.updateState(agentContext, mockOob, OutOfBandState.Done) + + expect(outOfBandRepository.update).toHaveBeenCalledWith(agentContext, mockOob) + }) + + test('emits an OutOfBandStateChangedEvent', async () => { + const stateChangedListener = jest.fn() + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.Initial, + }) + + eventEmitter.on(OutOfBandEventTypes.OutOfBandStateChanged, stateChangedListener) + await outOfBandService.updateState(agentContext, mockOob, OutOfBandState.Done) + eventEmitter.off(OutOfBandEventTypes.OutOfBandStateChanged, stateChangedListener) + + expect(stateChangedListener).toHaveBeenCalledTimes(1) + const [[stateChangedEvent]] = stateChangedListener.mock.calls + + expect(stateChangedEvent).toMatchObject({ + type: OutOfBandEventTypes.OutOfBandStateChanged, + payload: { + outOfBandRecord: mockOob, + previousState: OutOfBandState.Initial, + }, + }) + }) + }) + + describe('repository methods', () => { + it('getById should return value from outOfBandRepository.getById', async () => { + const expected = getMockOutOfBand() + mockFunction(outOfBandRepository.getById).mockReturnValue(Promise.resolve(expected)) + const result = await outOfBandService.getById(agentContext, expected.id) + expect(outOfBandRepository.getById).toBeCalledWith(agentContext, expected.id) + + expect(result).toBe(expected) + }) + + it('findById should return value from outOfBandRepository.findById', async () => { + const expected = getMockOutOfBand() + mockFunction(outOfBandRepository.findById).mockReturnValue(Promise.resolve(expected)) + const result = await outOfBandService.findById(agentContext, expected.id) + expect(outOfBandRepository.findById).toBeCalledWith(agentContext, expected.id) + + expect(result).toBe(expected) + }) + + it('getAll should return value from outOfBandRepository.getAll', async () => { + const expected = [getMockOutOfBand(), getMockOutOfBand()] + + mockFunction(outOfBandRepository.getAll).mockReturnValue(Promise.resolve(expected)) + const result = await outOfBandService.getAll(agentContext) + expect(outOfBandRepository.getAll).toBeCalledWith(agentContext) + + expect(result).toEqual(expect.arrayContaining(expected)) + }) + + it('findAllByQuery should return value from outOfBandRepository.findByQuery', async () => { + const expected = [getMockOutOfBand(), getMockOutOfBand()] + + mockFunction(outOfBandRepository.findByQuery).mockReturnValue(Promise.resolve(expected)) + const result = await outOfBandService.findAllByQuery(agentContext, { state: OutOfBandState.Initial }, {}) + expect(outOfBandRepository.findByQuery).toBeCalledWith(agentContext, { state: OutOfBandState.Initial }, {}) + + expect(result).toEqual(expect.arrayContaining(expected)) + }) + }) +}) diff --git a/packages/core/src/modules/oob/__tests__/connect-to-self.test.ts b/packages/core/src/modules/oob/__tests__/connect-to-self.test.ts new file mode 100644 index 0000000000..2532e9e0af --- /dev/null +++ b/packages/core/src/modules/oob/__tests__/connect-to-self.test.ts @@ -0,0 +1,110 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../../../../tests/transport/SubjectInboundTransport' + +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' +import { getInMemoryAgentOptions } from '../../../../tests/helpers' +import { HandshakeProtocol, DidExchangeState } from '../../connections' +import { OutOfBandState } from '../domain/OutOfBandState' + +import { Agent } from '@credo-ts/core' + +const faberAgentOptions = getInMemoryAgentOptions('Faber Agent OOB Connect to Self', { + endpoints: ['rxjs:faber'], +}) + +describe('out of band', () => { + let faberAgent: Agent + + beforeEach(async () => { + const faberMessages = new Subject() + const subjectMap = { + 'rxjs:faber': faberMessages, + } + + faberAgent = new Agent(faberAgentOptions) + + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + }) + + afterEach(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + }) + + describe('connect with self', () => { + test(`make a connection with self using ${HandshakeProtocol.DidExchange} protocol`, async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation() + const { outOfBandInvitation } = outOfBandRecord + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + // eslint-disable-next-line prefer-const + let { outOfBandRecord: receivedOutOfBandRecord, connectionRecord: receiverSenderConnection } = + await faberAgent.oob.receiveInvitationFromUrl(urlMessage) + expect(receivedOutOfBandRecord.state).toBe(OutOfBandState.PrepareResponse) + + receiverSenderConnection = await faberAgent.connections.returnWhenIsConnected(receiverSenderConnection!.id) + expect(receiverSenderConnection.state).toBe(DidExchangeState.Completed) + + let [senderReceiverConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + senderReceiverConnection = await faberAgent.connections.returnWhenIsConnected(senderReceiverConnection.id) + expect(senderReceiverConnection.state).toBe(DidExchangeState.Completed) + expect(senderReceiverConnection.protocol).toBe(HandshakeProtocol.DidExchange) + + expect(receiverSenderConnection).toBeConnectedWith(senderReceiverConnection!) + expect(senderReceiverConnection).toBeConnectedWith(receiverSenderConnection) + }) + + test(`make a connection with self using https://didcomm.org/didexchange/1.1 protocol, but invitation using https://didcomm.org/didexchange/1.0`, async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation() + + const { outOfBandInvitation } = outOfBandRecord + outOfBandInvitation.handshakeProtocols = ['https://didcomm.org/didexchange/1.0'] + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + // eslint-disable-next-line prefer-const + let { outOfBandRecord: receivedOutOfBandRecord, connectionRecord: receiverSenderConnection } = + await faberAgent.oob.receiveInvitationFromUrl(urlMessage) + expect(receivedOutOfBandRecord.state).toBe(OutOfBandState.PrepareResponse) + + receiverSenderConnection = await faberAgent.connections.returnWhenIsConnected(receiverSenderConnection!.id) + expect(receiverSenderConnection.state).toBe(DidExchangeState.Completed) + + let [senderReceiverConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + senderReceiverConnection = await faberAgent.connections.returnWhenIsConnected(senderReceiverConnection.id) + expect(senderReceiverConnection.state).toBe(DidExchangeState.Completed) + expect(senderReceiverConnection.protocol).toBe(HandshakeProtocol.DidExchange) + + expect(receiverSenderConnection).toBeConnectedWith(senderReceiverConnection!) + expect(senderReceiverConnection).toBeConnectedWith(receiverSenderConnection) + }) + + test(`make a connection with self using ${HandshakeProtocol.Connections} protocol`, async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + }) + const { outOfBandInvitation } = outOfBandRecord + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + // eslint-disable-next-line prefer-const + let { outOfBandRecord: receivedOutOfBandRecord, connectionRecord: receiverSenderConnection } = + await faberAgent.oob.receiveInvitationFromUrl(urlMessage) + expect(receivedOutOfBandRecord.state).toBe(OutOfBandState.PrepareResponse) + + receiverSenderConnection = await faberAgent.connections.returnWhenIsConnected(receiverSenderConnection!.id) + expect(receiverSenderConnection.state).toBe(DidExchangeState.Completed) + + let [senderReceiverConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + senderReceiverConnection = await faberAgent.connections.returnWhenIsConnected(senderReceiverConnection.id) + expect(senderReceiverConnection.state).toBe(DidExchangeState.Completed) + expect(senderReceiverConnection.protocol).toBe(HandshakeProtocol.Connections) + + expect(receiverSenderConnection).toBeConnectedWith(senderReceiverConnection!) + expect(senderReceiverConnection).toBeConnectedWith(receiverSenderConnection) + }) + }) +}) diff --git a/packages/core/src/modules/oob/__tests__/helpers.test.ts b/packages/core/src/modules/oob/__tests__/helpers.test.ts new file mode 100644 index 0000000000..8ecba2a69a --- /dev/null +++ b/packages/core/src/modules/oob/__tests__/helpers.test.ts @@ -0,0 +1,161 @@ +import { Attachment } from '../../../decorators/attachment/Attachment' +import { JsonTransformer } from '../../../utils' +import { ConnectionInvitationMessage } from '../../connections' +import { OutOfBandDidCommService } from '../domain' +import { convertToNewInvitation, convertToOldInvitation } from '../helpers' +import { OutOfBandInvitation } from '../messages' + +describe('convertToNewInvitation', () => { + it('should convert a connection invitation with service to an out of band invitation', () => { + const connectionInvitation = new ConnectionInvitationMessage({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + recipientKeys: ['8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'], + serviceEndpoint: 'https://my-agent.com', + routingKeys: ['6fioC1zcDPyPEL19pXRS2E4iJ46zH7xP6uSgAaPdwDrx'], + appendedAttachments: [ + new Attachment({ + id: 'attachment-1', + mimeType: 'text/plain', + description: 'attachment description', + filename: 'test.jpg', + data: { + json: { + text: 'sample', + value: 1, + }, + }, + }), + ], + }) + + const oobInvitation = convertToNewInvitation(connectionInvitation) + expect(oobInvitation).toMatchObject({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + services: [ + { + id: '#inline', + recipientKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + routingKeys: ['did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL'], + serviceEndpoint: 'https://my-agent.com', + }, + ], + appendedAttachments: [ + { + id: 'attachment-1', + description: 'attachment description', + filename: 'test.jpg', + mimeType: 'text/plain', + data: { json: { text: 'sample', value: 1 } }, + }, + ], + }) + }) + + it('should convert a connection invitation with public did to an out of band invitation', () => { + const connectionInvitation = new ConnectionInvitationMessage({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + did: 'did:sov:a-did', + appendedAttachments: [ + new Attachment({ + id: 'attachment-1', + mimeType: 'text/plain', + description: 'attachment description', + filename: 'test.jpg', + data: { + json: { + text: 'sample', + value: 1, + }, + }, + }), + ], + }) + + const oobInvitation = convertToNewInvitation(connectionInvitation) + + expect(oobInvitation).toMatchObject({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + services: ['did:sov:a-did'], + appendedAttachments: [ + { + id: 'attachment-1', + description: 'attachment description', + filename: 'test.jpg', + mimeType: 'text/plain', + data: { json: { text: 'sample', value: 1 } }, + }, + ], + }) + }) + + it('throws an error when no did and serviceEndpoint/routingKeys are present in the connection invitation', () => { + const connectionInvitation = JsonTransformer.fromJSON( + { + '@id': 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + '@type': 'https://didcomm.org/connections/1.0/invitation', + label: 'a-label', + imageUrl: 'https://my-image.com', + }, + ConnectionInvitationMessage, + // Don't validate because we want this to be mal-formatted + { validate: false } + ) + + expect(() => convertToNewInvitation(connectionInvitation)).toThrowError() + }) +}) + +describe('convertToOldInvitation', () => { + it('should convert an out of band invitation with inline service to a connection invitation', () => { + const oobInvitation = new OutOfBandInvitation({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + services: [ + new OutOfBandDidCommService({ + id: '#inline', + recipientKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + routingKeys: ['did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL'], + serviceEndpoint: 'https://my-agent.com', + }), + ], + }) + + const connectionInvitation = convertToOldInvitation(oobInvitation) + + expect(connectionInvitation).toMatchObject({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + recipientKeys: ['8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'], + routingKeys: ['6fioC1zcDPyPEL19pXRS2E4iJ46zH7xP6uSgAaPdwDrx'], + serviceEndpoint: 'https://my-agent.com', + }) + }) + + it('should convert an out of band invitation with did service to a connection invitation', () => { + const oobInvitation = new OutOfBandInvitation({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + services: ['did:sov:a-did'], + }) + + const connectionInvitation = convertToOldInvitation(oobInvitation) + + expect(connectionInvitation).toMatchObject({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + did: 'did:sov:a-did', + }) + }) +}) diff --git a/packages/core/src/modules/oob/__tests__/implicit.test.ts b/packages/core/src/modules/oob/__tests__/implicit.test.ts new file mode 100644 index 0000000000..96eaab7a4c --- /dev/null +++ b/packages/core/src/modules/oob/__tests__/implicit.test.ts @@ -0,0 +1,292 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import { setupSubjectTransports } from '../../../../tests' +import { getInMemoryAgentOptions, waitForConnectionRecord } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { KeyType } from '../../../crypto' +import { DidExchangeState, HandshakeProtocol } from '../../connections' +import { InMemoryDidRegistry } from '../../connections/__tests__/InMemoryDidRegistry' +import { + DidCommV1Service, + NewDidCommV2Service, + DidDocumentService, + DidDocumentBuilder, + getEd25519VerificationKey2018, + DidsModule, + NewDidCommV2ServiceEndpoint, +} from '../../dids' + +const inMemoryDidsRegistry = new InMemoryDidRegistry() + +const faberAgentOptions = getInMemoryAgentOptions( + 'Faber Agent OOB Implicit', + { + endpoints: ['rxjs:faber'], + }, + { + dids: new DidsModule({ + resolvers: [inMemoryDidsRegistry], + registrars: [inMemoryDidsRegistry], + }), + } +) +const aliceAgentOptions = getInMemoryAgentOptions( + 'Alice Agent OOB Implicit', + { + endpoints: ['rxjs:alice'], + }, + { + dids: new DidsModule({ + resolvers: [inMemoryDidsRegistry], + registrars: [inMemoryDidsRegistry], + }), + } +) + +describe('out of band implicit', () => { + let faberAgent: Agent + let aliceAgent: Agent + + beforeAll(async () => { + faberAgent = new Agent(faberAgentOptions) + aliceAgent = new Agent(aliceAgentOptions) + + setupSubjectTransports([faberAgent, aliceAgent]) + await faberAgent.initialize() + await aliceAgent.initialize() + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + afterEach(async () => { + const connections = await faberAgent.connections.getAll() + for (const connection of connections) { + await faberAgent.connections.deleteById(connection.id) + } + + jest.resetAllMocks() + }) + + test(`make a connection with ${HandshakeProtocol.DidExchange} based on implicit OOB invitation`, async () => { + const inMemoryDid = await createInMemoryDid(faberAgent, 'rxjs:faber') + + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveImplicitInvitation({ + did: inMemoryDid, + alias: 'Faber public', + label: 'Alice', + handshakeProtocols: [HandshakeProtocol.DidExchange], + }) + + // Wait for a connection event in faber agent and accept the request + let faberAliceConnection = await waitForConnectionRecord(faberAgent, { state: DidExchangeState.RequestReceived }) + await faberAgent.connections.acceptRequest(faberAliceConnection.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + + // Alice should now be connected + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + expect(faberAliceConnection.theirLabel).toBe('Alice') + expect(aliceFaberConnection.alias).toBe('Faber public') + expect(aliceFaberConnection.invitationDid).toBe(inMemoryDid) + + // It is possible for an agent to check if it has already a connection to a certain public entity + expect(await aliceAgent.connections.findByInvitationDid(inMemoryDid)).toEqual([aliceFaberConnection]) + }) + + test(`make a connection with ${HandshakeProtocol.DidExchange} based on implicit OOB invitation pointing to specific service`, async () => { + const inMemoryDid = await createInMemoryDid(faberAgent, 'rxjs:faber') + const inMemoryDidDocument = await faberAgent.dids.resolveDidDocument(inMemoryDid) + const serviceUrl = inMemoryDidDocument.service![1].id + + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveImplicitInvitation({ + did: serviceUrl, + alias: 'Faber public', + label: 'Alice', + handshakeProtocols: [HandshakeProtocol.DidExchange], + }) + + // Wait for a connection event in faber agent and accept the request + let faberAliceConnection = await waitForConnectionRecord(faberAgent, { state: DidExchangeState.RequestReceived }) + await faberAgent.connections.acceptRequest(faberAliceConnection.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + + // Alice should now be connected + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + expect(faberAliceConnection.theirLabel).toBe('Alice') + expect(aliceFaberConnection.alias).toBe('Faber public') + expect(aliceFaberConnection.invitationDid).toBe(serviceUrl) + + // It is possible for an agent to check if it has already a connection to a certain public entity + expect(await aliceAgent.connections.findByInvitationDid(serviceUrl)).toEqual([aliceFaberConnection]) + }) + + test(`make a connection with ${HandshakeProtocol.Connections} based on implicit OOB invitation`, async () => { + const inMemoryDid = await createInMemoryDid(faberAgent, 'rxjs:faber') + + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveImplicitInvitation({ + did: inMemoryDid, + alias: 'Faber public', + label: 'Alice', + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + // Wait for a connection event in faber agent and accept the request + let faberAliceConnection = await waitForConnectionRecord(faberAgent, { state: DidExchangeState.RequestReceived }) + await faberAgent.connections.acceptRequest(faberAliceConnection.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + + // Alice should now be connected + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + expect(faberAliceConnection.theirLabel).toBe('Alice') + expect(aliceFaberConnection.alias).toBe('Faber public') + expect(aliceFaberConnection.invitationDid).toBe(inMemoryDid) + + // It is possible for an agent to check if it has already a connection to a certain public entity + expect(await aliceAgent.connections.findByInvitationDid(inMemoryDid)).toEqual([aliceFaberConnection]) + }) + + test(`receive an implicit invitation using an unresolvable did`, async () => { + await expect( + aliceAgent.oob.receiveImplicitInvitation({ + did: 'did:sov:ZSEqSci581BDZCFPa29ScB', + alias: 'Faber public', + label: 'Alice', + handshakeProtocols: [HandshakeProtocol.DidExchange], + }) + ).rejects.toThrow(/Unable to resolve did/) + }) + + test(`create two connections using the same implicit invitation`, async () => { + const inMemoryDid = await createInMemoryDid(faberAgent, 'rxjs:faber') + + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveImplicitInvitation({ + did: inMemoryDid, + alias: 'Faber public', + label: 'Alice', + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + // Wait for a connection event in faber agent and accept the request + let faberAliceConnection = await waitForConnectionRecord(faberAgent, { state: DidExchangeState.RequestReceived }) + await faberAgent.connections.acceptRequest(faberAliceConnection.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + + // Alice should now be connected + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + expect(faberAliceConnection.theirLabel).toBe('Alice') + expect(aliceFaberConnection.alias).toBe('Faber public') + expect(aliceFaberConnection.invitationDid).toBe(inMemoryDid) + + // Repeat implicit invitation procedure + let { connectionRecord: aliceFaberNewConnection } = await aliceAgent.oob.receiveImplicitInvitation({ + did: inMemoryDid, + alias: 'Faber public New', + label: 'Alice New', + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + // Wait for a connection event in faber agent + let faberAliceNewConnection = await waitForConnectionRecord(faberAgent, { state: DidExchangeState.RequestReceived }) + await faberAgent.connections.acceptRequest(faberAliceNewConnection.id) + faberAliceNewConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceNewConnection!.id) + expect(faberAliceNewConnection.state).toBe(DidExchangeState.Completed) + + // Alice should now be connected + aliceFaberNewConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberNewConnection!.id) + expect(aliceFaberNewConnection.state).toBe(DidExchangeState.Completed) + + expect(aliceFaberNewConnection).toBeConnectedWith(faberAliceNewConnection) + expect(faberAliceNewConnection).toBeConnectedWith(aliceFaberNewConnection) + expect(faberAliceNewConnection.theirLabel).toBe('Alice New') + expect(aliceFaberNewConnection.alias).toBe('Faber public New') + expect(aliceFaberNewConnection.invitationDid).toBe(inMemoryDid) + + // Both connections will be associated to the same invitation did + const connectionsFromFaberPublicDid = await aliceAgent.connections.findByInvitationDid(inMemoryDid) + expect(connectionsFromFaberPublicDid).toHaveLength(2) + expect(connectionsFromFaberPublicDid).toEqual( + expect.arrayContaining([aliceFaberConnection, aliceFaberNewConnection]) + ) + }) +}) + +async function createInMemoryDid(agent: Agent, endpoint: string) { + const ed25519Key = await agent.wallet.createKey({ + keyType: KeyType.Ed25519, + }) + + const did = `did:inmemory:${ed25519Key.fingerprint}` + const builder = new DidDocumentBuilder(did) + const ed25519VerificationMethod = getEd25519VerificationKey2018({ + key: ed25519Key, + id: `${did}#${ed25519Key.fingerprint}`, + controller: did, + }) + + builder.addService( + new DidDocumentService({ + id: `${did}#endpoint`, + serviceEndpoint: endpoint, + type: 'endpoint', + }) + ) + builder.addService( + new DidCommV1Service({ + id: `${did}#did-communication`, + priority: 0, + recipientKeys: [ed25519VerificationMethod.id], + routingKeys: [], + serviceEndpoint: endpoint, + accept: ['didcomm/aip2;env=rfc19'], + }) + ) + + builder.addService( + new NewDidCommV2Service({ + id: `${did}#didcomm-messaging-1`, + serviceEndpoint: new NewDidCommV2ServiceEndpoint({ + accept: ['didcomm/v2'], + routingKeys: [], + uri: endpoint, + }), + }) + ) + + builder.addVerificationMethod(ed25519VerificationMethod) + builder.addAuthentication(ed25519VerificationMethod.id) + builder.addAssertionMethod(ed25519VerificationMethod.id) + + // Create the did:inmemory did + const { + didState: { state }, + } = await agent.dids.create({ did, didDocument: builder.build() }) + if (state !== 'finished') { + throw new Error('Error creating DID') + } + + return did +} diff --git a/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts b/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts new file mode 100644 index 0000000000..e40759d007 --- /dev/null +++ b/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts @@ -0,0 +1,80 @@ +import type { ResolvedDidCommService } from '../../didcomm' +import type { ValidationOptions } from 'class-validator' + +import { ArrayNotEmpty, buildMessage, IsOptional, isString, IsString, ValidateBy } from 'class-validator' + +import { isDid, IsUri } from '../../../utils' +import { DidDocumentService, DidKey } from '../../dids' + +export class OutOfBandDidCommService extends DidDocumentService { + public constructor(options: { + id: string + serviceEndpoint: string + recipientKeys: string[] + routingKeys?: string[] + accept?: string[] + }) { + super({ ...options, type: OutOfBandDidCommService.type }) + + if (options) { + this.recipientKeys = options.recipientKeys + this.routingKeys = options.routingKeys + this.accept = options.accept + } + } + + public static type = 'did-communication' + + @IsString() + @IsUri() + public serviceEndpoint!: string + + @ArrayNotEmpty() + @IsDidKeyString({ each: true }) + public recipientKeys!: string[] + + @IsDidKeyString({ each: true }) + @IsOptional() + public routingKeys?: string[] + + @IsString({ each: true }) + @IsOptional() + public accept?: string[] + + public get resolvedDidCommService(): ResolvedDidCommService { + return { + id: this.id, + recipientKeys: this.recipientKeys.map((didKey) => DidKey.fromDid(didKey).key), + routingKeys: this.routingKeys?.map((didKey) => DidKey.fromDid(didKey).key) ?? [], + serviceEndpoint: this.serviceEndpoint, + } + } + + public static fromResolvedDidCommService(service: ResolvedDidCommService) { + return new OutOfBandDidCommService({ + id: service.id, + recipientKeys: service.recipientKeys.map((key) => new DidKey(key).did), + routingKeys: service.routingKeys.map((key) => new DidKey(key).did), + serviceEndpoint: service.serviceEndpoint, + }) + } +} + +/** + * Checks if a given value is a did:key did string + */ +function IsDidKeyString(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'isDidKeyString', + validator: { + validate: (value): boolean => isString(value) && isDid(value, 'key'), + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be a did:key string', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/modules/oob/domain/OutOfBandEvents.ts b/packages/core/src/modules/oob/domain/OutOfBandEvents.ts new file mode 100644 index 0000000000..15561062b5 --- /dev/null +++ b/packages/core/src/modules/oob/domain/OutOfBandEvents.ts @@ -0,0 +1,27 @@ +import type { OutOfBandState } from './OutOfBandState' +import type { BaseEvent } from '../../../agent/Events' +import type { ConnectionRecord } from '../../connections' +import type { OutOfBandRecord } from '../repository' + +export enum OutOfBandEventTypes { + OutOfBandStateChanged = 'OutOfBandStateChanged', + HandshakeReused = 'HandshakeReused', +} + +export interface OutOfBandStateChangedEvent extends BaseEvent { + type: typeof OutOfBandEventTypes.OutOfBandStateChanged + payload: { + outOfBandRecord: OutOfBandRecord + previousState: OutOfBandState | null + } +} + +export interface HandshakeReusedEvent extends BaseEvent { + type: typeof OutOfBandEventTypes.HandshakeReused + payload: { + // We need the thread id (can be multiple reuse happening at the same time) + reuseThreadId: string + outOfBandRecord: OutOfBandRecord + connectionRecord: ConnectionRecord + } +} diff --git a/packages/core/src/modules/oob/domain/OutOfBandRole.ts b/packages/core/src/modules/oob/domain/OutOfBandRole.ts new file mode 100644 index 0000000000..5cb80da351 --- /dev/null +++ b/packages/core/src/modules/oob/domain/OutOfBandRole.ts @@ -0,0 +1,4 @@ +export enum OutOfBandRole { + Sender = 'sender', + Receiver = 'receiver', +} diff --git a/packages/core/src/modules/oob/domain/OutOfBandState.ts b/packages/core/src/modules/oob/domain/OutOfBandState.ts new file mode 100644 index 0000000000..a82936517f --- /dev/null +++ b/packages/core/src/modules/oob/domain/OutOfBandState.ts @@ -0,0 +1,6 @@ +export enum OutOfBandState { + Initial = 'initial', + AwaitResponse = 'await-response', + PrepareResponse = 'prepare-response', + Done = 'done', +} diff --git a/packages/core/src/modules/oob/domain/index.ts b/packages/core/src/modules/oob/domain/index.ts new file mode 100644 index 0000000000..b49b6338b3 --- /dev/null +++ b/packages/core/src/modules/oob/domain/index.ts @@ -0,0 +1,4 @@ +export * from './OutOfBandRole' +export * from './OutOfBandState' +export * from './OutOfBandDidCommService' +export * from './OutOfBandEvents' diff --git a/packages/core/src/modules/oob/handlers/HandshakeReuseAcceptedHandler.ts b/packages/core/src/modules/oob/handlers/HandshakeReuseAcceptedHandler.ts new file mode 100644 index 0000000000..07fe48259a --- /dev/null +++ b/packages/core/src/modules/oob/handlers/HandshakeReuseAcceptedHandler.ts @@ -0,0 +1,20 @@ +import type { MessageHandler } from '../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { OutOfBandService } from '../OutOfBandService' + +import { HandshakeReuseAcceptedMessage } from '../messages/HandshakeReuseAcceptedMessage' + +export class HandshakeReuseAcceptedHandler implements MessageHandler { + public supportedMessages = [HandshakeReuseAcceptedMessage] + private outOfBandService: OutOfBandService + + public constructor(outOfBandService: OutOfBandService) { + this.outOfBandService = outOfBandService + } + + public async handle(messageContext: InboundMessageContext) { + messageContext.assertReadyConnection() + + await this.outOfBandService.processHandshakeReuseAccepted(messageContext) + } +} diff --git a/packages/core/src/modules/oob/handlers/HandshakeReuseHandler.ts b/packages/core/src/modules/oob/handlers/HandshakeReuseHandler.ts new file mode 100644 index 0000000000..c4db9cdaf4 --- /dev/null +++ b/packages/core/src/modules/oob/handlers/HandshakeReuseHandler.ts @@ -0,0 +1,25 @@ +import type { MessageHandler } from '../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { OutOfBandService } from '../OutOfBandService' + +import { OutboundMessageContext } from '../../../agent/models' +import { HandshakeReuseMessage } from '../messages/HandshakeReuseMessage' + +export class HandshakeReuseHandler implements MessageHandler { + public supportedMessages = [HandshakeReuseMessage] + private outOfBandService: OutOfBandService + + public constructor(outOfBandService: OutOfBandService) { + this.outOfBandService = outOfBandService + } + + public async handle(messageContext: InboundMessageContext) { + const connectionRecord = messageContext.assertReadyConnection() + const handshakeReuseAcceptedMessage = await this.outOfBandService.processHandshakeReuse(messageContext) + + return new OutboundMessageContext(handshakeReuseAcceptedMessage, { + agentContext: messageContext.agentContext, + connection: connectionRecord, + }) + } +} diff --git a/packages/core/src/modules/oob/handlers/index.ts b/packages/core/src/modules/oob/handlers/index.ts new file mode 100644 index 0000000000..c9edcca3d6 --- /dev/null +++ b/packages/core/src/modules/oob/handlers/index.ts @@ -0,0 +1 @@ +export * from './HandshakeReuseHandler' diff --git a/packages/core/src/modules/oob/helpers.ts b/packages/core/src/modules/oob/helpers.ts new file mode 100644 index 0000000000..110bbd904c --- /dev/null +++ b/packages/core/src/modules/oob/helpers.ts @@ -0,0 +1,71 @@ +import type { OutOfBandInvitationOptions } from './messages' + +import { ConnectionInvitationMessage } from '../connections' +import { didKeyToVerkey, verkeyToDidKey } from '../dids/helpers' + +import { OutOfBandDidCommService } from './domain/OutOfBandDidCommService' +import { InvitationType, OutOfBandInvitation } from './messages' + +export function convertToNewInvitation(oldInvitation: ConnectionInvitationMessage) { + let service + + if (oldInvitation.did) { + service = oldInvitation.did + } else if (oldInvitation.serviceEndpoint && oldInvitation.recipientKeys && oldInvitation.recipientKeys.length > 0) { + service = new OutOfBandDidCommService({ + id: '#inline', + recipientKeys: oldInvitation.recipientKeys?.map(verkeyToDidKey), + routingKeys: oldInvitation.routingKeys?.map(verkeyToDidKey), + serviceEndpoint: oldInvitation.serviceEndpoint, + }) + } else { + throw new Error('Missing required serviceEndpoint, routingKeys and/or did fields in connection invitation') + } + + const options: OutOfBandInvitationOptions = { + id: oldInvitation.id, + label: oldInvitation.label, + imageUrl: oldInvitation.imageUrl, + appendedAttachments: oldInvitation.appendedAttachments, + accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], + services: [service], + // NOTE: we hardcode it to 1.0, we won't see support for newer versions of the protocol + // and we also can process 1.0 if we support newer versions + handshakeProtocols: ['https://didcomm.org/connections/1.0'], + } + + const outOfBandInvitation = new OutOfBandInvitation(options) + outOfBandInvitation.invitationType = InvitationType.Connection + return outOfBandInvitation +} + +export function convertToOldInvitation(newInvitation: OutOfBandInvitation) { + // Taking first service, as we can only include one service in a legacy invitation. + const [service] = newInvitation.getServices() + + let options + if (typeof service === 'string') { + options = { + id: newInvitation.id, + // label is optional + label: newInvitation.label ?? '', + did: service, + imageUrl: newInvitation.imageUrl, + appendedAttachments: newInvitation.appendedAttachments, + } + } else { + options = { + id: newInvitation.id, + // label is optional + label: newInvitation.label ?? '', + recipientKeys: service.recipientKeys.map(didKeyToVerkey), + routingKeys: service.routingKeys?.map(didKeyToVerkey), + serviceEndpoint: service.serviceEndpoint, + imageUrl: newInvitation.imageUrl, + appendedAttachments: newInvitation.appendedAttachments, + } + } + + const connectionInvitationMessage = new ConnectionInvitationMessage(options) + return connectionInvitationMessage +} diff --git a/packages/core/src/modules/oob/index.ts b/packages/core/src/modules/oob/index.ts new file mode 100644 index 0000000000..b0c593951b --- /dev/null +++ b/packages/core/src/modules/oob/index.ts @@ -0,0 +1,6 @@ +export * from './messages' +export * from './repository' +export * from './OutOfBandApi' +export * from './OutOfBandService' +export * from './OutOfBandModule' +export * from './domain' diff --git a/packages/core/src/modules/oob/messages/HandshakeReuseAcceptedMessage.ts b/packages/core/src/modules/oob/messages/HandshakeReuseAcceptedMessage.ts new file mode 100644 index 0000000000..bfffcdab5b --- /dev/null +++ b/packages/core/src/modules/oob/messages/HandshakeReuseAcceptedMessage.ts @@ -0,0 +1,26 @@ +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface HandshakeReuseAcceptedMessageOptions { + id?: string + threadId: string + parentThreadId: string +} + +export class HandshakeReuseAcceptedMessage extends AgentMessage { + public constructor(options: HandshakeReuseAcceptedMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.setThread({ + threadId: options.threadId, + parentThreadId: options.parentThreadId, + }) + } + } + + @IsValidMessageType(HandshakeReuseAcceptedMessage.type) + public readonly type = HandshakeReuseAcceptedMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/out-of-band/1.1/handshake-reuse-accepted') +} diff --git a/packages/core/src/modules/oob/messages/HandshakeReuseMessage.ts b/packages/core/src/modules/oob/messages/HandshakeReuseMessage.ts new file mode 100644 index 0000000000..c70e8b2832 --- /dev/null +++ b/packages/core/src/modules/oob/messages/HandshakeReuseMessage.ts @@ -0,0 +1,24 @@ +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface HandshakeReuseMessageOptions { + id?: string + parentThreadId: string +} + +export class HandshakeReuseMessage extends AgentMessage { + public constructor(options: HandshakeReuseMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.setThread({ + parentThreadId: options.parentThreadId, + }) + } + } + + @IsValidMessageType(HandshakeReuseMessage.type) + public readonly type = HandshakeReuseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/out-of-band/1.1/handshake-reuse') +} diff --git a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts new file mode 100644 index 0000000000..6bfc021fe0 --- /dev/null +++ b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts @@ -0,0 +1,195 @@ +import type { PlaintextMessage } from '../../../types' + +import { Exclude, Expose, Transform, TransformationType, Type } from 'class-transformer' +import { ArrayNotEmpty, IsArray, IsInstance, IsOptional, IsUrl, ValidateNested } from 'class-validator' +import { parseUrl } from 'query-string' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' +import { CredoError } from '../../../error' +import { JsonEncoder } from '../../../utils/JsonEncoder' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { IsValidMessageType, parseMessageType, replaceLegacyDidSovPrefix } from '../../../utils/messageType' +import { IsStringOrInstance } from '../../../utils/validators' +import { outOfBandServiceToNumAlgo2Did } from '../../dids/methods/peer/peerDidNumAlgo2' +import { OutOfBandDidCommService } from '../domain/OutOfBandDidCommService' + +export interface OutOfBandInvitationOptions { + id?: string + label?: string + goalCode?: string + goal?: string + accept?: string[] + handshakeProtocols?: string[] + services: Array + imageUrl?: string + appendedAttachments?: Attachment[] +} + +export class OutOfBandInvitation extends AgentMessage { + public constructor(options: OutOfBandInvitationOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.label = options.label + this.goalCode = options.goalCode + this.goal = options.goal + this.accept = options.accept + this.handshakeProtocols = options.handshakeProtocols + this.services = options.services + this.imageUrl = options.imageUrl + this.appendedAttachments = options.appendedAttachments + } + } + + /** + * The original type of the invitation. This is not part of the RFC, but allows to identify + * from what the oob invitation was originally created (e.g. legacy connectionless invitation). + */ + @Exclude() + public invitationType?: InvitationType + + public addRequest(message: AgentMessage) { + if (!this.requests) this.requests = [] + const requestAttachment = new Attachment({ + id: this.generateId(), + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(message.toJSON()), + }), + }) + this.requests.push(requestAttachment) + } + + public getRequests(): PlaintextMessage[] | undefined { + return this.requests?.map((request) => request.getDataAsJson()) + } + + public toUrl({ domain }: { domain: string }) { + const invitationJson = this.toJSON() + const encodedInvitation = JsonEncoder.toBase64URL(invitationJson) + const invitationUrl = `${domain}?oob=${encodedInvitation}` + return invitationUrl + } + + public static fromUrl(invitationUrl: string) { + const parsedUrl = parseUrl(invitationUrl).query + const encodedInvitation = parsedUrl['oob'] + if (typeof encodedInvitation === 'string') { + const invitationJson = JsonEncoder.fromBase64(encodedInvitation) + const invitation = this.fromJson(invitationJson) + + return invitation + } else { + throw new CredoError( + 'InvitationUrl is invalid. It needs to contain one, and only one, of the following parameters; `oob`' + ) + } + } + + public static fromJson(json: Record) { + return JsonTransformer.fromJSON(json, OutOfBandInvitation) + } + + public get invitationDids() { + const dids = this.getServices().map((didOrService) => { + if (typeof didOrService === 'string') { + return didOrService + } + return outOfBandServiceToNumAlgo2Did(didOrService) + }) + return dids + } + + // shorthand for services without the need to deal with the String DIDs + public getServices(): Array { + return this.services.map((service) => { + if (service instanceof String) return service.toString() + return service + }) + } + public getDidServices(): Array { + return this.getServices().filter((service): service is string => typeof service === 'string') + } + public getInlineServices(): Array { + return this.getServices().filter((service): service is OutOfBandDidCommService => typeof service !== 'string') + } + + @Transform(({ value }) => replaceLegacyDidSovPrefix(value), { + toClassOnly: true, + }) + @IsValidMessageType(OutOfBandInvitation.type) + public readonly type = OutOfBandInvitation.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/out-of-band/1.1/invitation') + + public readonly label?: string + + @Expose({ name: 'goal_code' }) + public readonly goalCode?: string + + public readonly goal?: string + + public readonly accept?: string[] + @Transform(({ value }) => value?.map(replaceLegacyDidSovPrefix), { toClassOnly: true }) + @Expose({ name: 'handshake_protocols' }) + public handshakeProtocols?: string[] + + @Expose({ name: 'requests~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + @IsOptional() + private requests?: Attachment[] + + @IsArray() + @ArrayNotEmpty() + @OutOfBandServiceTransformer() + @IsStringOrInstance(OutOfBandDidCommService, { each: true }) + // eslint-disable-next-line @typescript-eslint/ban-types + private services!: Array + + /** + * Custom property. It is not part of the RFC. + */ + @IsOptional() + @IsUrl() + public readonly imageUrl?: string +} + +/** + * Decorator that transforms services json to corresponding class instances + * @note Because of ValidateNested limitation, this produces instances of String for DID services except plain js string + */ +function OutOfBandServiceTransformer() { + return Transform(({ value, type }: { value: Array; type: TransformationType }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + return value.map((service) => { + // did + if (typeof service === 'string') return new String(service) + + // inline didcomm service + return JsonTransformer.fromJSON(service, OutOfBandDidCommService) + }) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + return value.map((service) => + typeof service === 'string' || service instanceof String ? service.toString() : JsonTransformer.toJSON(service) + ) + } + + // PLAIN_TO_PLAIN + return value + }) +} + +/** + * The original invitation an out of band invitation was derived from. + */ +export enum InvitationType { + OutOfBand = 'out-of-band/1.x', + Connection = 'connections/1.x', + Connectionless = 'connectionless', +} diff --git a/packages/core/src/modules/oob/messages/index.ts b/packages/core/src/modules/oob/messages/index.ts new file mode 100644 index 0000000000..1849ee4f54 --- /dev/null +++ b/packages/core/src/modules/oob/messages/index.ts @@ -0,0 +1,2 @@ +export * from './OutOfBandInvitation' +export * from './HandshakeReuseMessage' diff --git a/packages/core/src/modules/oob/repository/OutOfBandRecord.ts b/packages/core/src/modules/oob/repository/OutOfBandRecord.ts new file mode 100644 index 0000000000..1832996478 --- /dev/null +++ b/packages/core/src/modules/oob/repository/OutOfBandRecord.ts @@ -0,0 +1,112 @@ +import type { OutOfBandRecordMetadata } from './outOfBandRecordMetadataTypes' +import type { TagsBase } from '../../../storage/BaseRecord' +import type { OutOfBandRole } from '../domain/OutOfBandRole' +import type { OutOfBandState } from '../domain/OutOfBandState' + +import { Type } from 'class-transformer' + +import { CredoError } from '../../../error' +import { BaseRecord } from '../../../storage/BaseRecord' +import { getThreadIdFromPlainTextMessage } from '../../../utils/thread' +import { uuid } from '../../../utils/uuid' +import { OutOfBandInvitation } from '../messages' + +type DefaultOutOfBandRecordTags = { + role: OutOfBandRole + state: OutOfBandState + invitationId: string + threadId?: string + /** + * The thread ids from the attached request messages from the out + * of band invitation. + */ + invitationRequestsThreadIds?: string[] +} + +interface CustomOutOfBandRecordTags extends TagsBase { + recipientKeyFingerprints: string[] +} + +export interface OutOfBandRecordProps { + id?: string + createdAt?: Date + updatedAt?: Date + tags?: CustomOutOfBandRecordTags + outOfBandInvitation: OutOfBandInvitation + role: OutOfBandRole + state: OutOfBandState + alias?: string + autoAcceptConnection?: boolean + reusable?: boolean + mediatorId?: string + reuseConnectionId?: string + threadId?: string +} + +export class OutOfBandRecord extends BaseRecord< + DefaultOutOfBandRecordTags, + CustomOutOfBandRecordTags, + OutOfBandRecordMetadata +> { + @Type(() => OutOfBandInvitation) + public outOfBandInvitation!: OutOfBandInvitation + public role!: OutOfBandRole + public state!: OutOfBandState + public alias?: string + public reusable!: boolean + public autoAcceptConnection?: boolean + public mediatorId?: string + public reuseConnectionId?: string + + public static readonly type = 'OutOfBandRecord' + public readonly type = OutOfBandRecord.type + + public constructor(props: OutOfBandRecordProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.outOfBandInvitation = props.outOfBandInvitation + this.role = props.role + this.state = props.state + this.alias = props.alias + this.autoAcceptConnection = props.autoAcceptConnection + this.reusable = props.reusable ?? false + this.mediatorId = props.mediatorId + this.reuseConnectionId = props.reuseConnectionId + this._tags = props.tags ?? { recipientKeyFingerprints: [] } + } + } + + public getTags() { + return { + ...this._tags, + role: this.role, + state: this.state, + invitationId: this.outOfBandInvitation.id, + threadId: this.outOfBandInvitation.threadId, + invitationRequestsThreadIds: this.outOfBandInvitation + .getRequests() + ?.map((r) => getThreadIdFromPlainTextMessage(r)), + } + } + + public assertRole(expectedRole: OutOfBandRole) { + if (this.role !== expectedRole) { + throw new CredoError(`Invalid out-of-band record role ${this.role}, expected is ${expectedRole}.`) + } + } + + public assertState(expectedStates: OutOfBandState | OutOfBandState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new CredoError( + `Invalid out-of-band record state ${this.state}, valid states are: ${expectedStates.join(', ')}.` + ) + } + } +} diff --git a/packages/core/src/modules/oob/repository/OutOfBandRepository.ts b/packages/core/src/modules/oob/repository/OutOfBandRepository.ts new file mode 100644 index 0000000000..afe979d75a --- /dev/null +++ b/packages/core/src/modules/oob/repository/OutOfBandRepository.ts @@ -0,0 +1,17 @@ +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { OutOfBandRecord } from './OutOfBandRecord' + +@injectable() +export class OutOfBandRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(OutOfBandRecord, storageService, eventEmitter) + } +} diff --git a/packages/core/src/modules/oob/repository/__tests__/OutOfBandRecord.test.ts b/packages/core/src/modules/oob/repository/__tests__/OutOfBandRecord.test.ts new file mode 100644 index 0000000000..ce83b30af0 --- /dev/null +++ b/packages/core/src/modules/oob/repository/__tests__/OutOfBandRecord.test.ts @@ -0,0 +1,76 @@ +import { JsonTransformer } from '../../../../utils' +import { OutOfBandDidCommService } from '../../domain/OutOfBandDidCommService' +import { OutOfBandRole } from '../../domain/OutOfBandRole' +import { OutOfBandState } from '../../domain/OutOfBandState' +import { OutOfBandInvitation } from '../../messages' +import { OutOfBandRecord } from '../OutOfBandRecord' + +describe('OutOfBandRecord', () => { + describe('getTags', () => { + it('should return default tags', () => { + const outOfBandRecord = new OutOfBandRecord({ + state: OutOfBandState.Done, + role: OutOfBandRole.Receiver, + outOfBandInvitation: new OutOfBandInvitation({ + label: 'label', + services: [ + new OutOfBandDidCommService({ + id: 'id', + recipientKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + serviceEndpoint: 'service-endpoint', + }), + ], + id: 'a-message-id', + }), + tags: { + recipientKeyFingerprints: ['z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + }, + }) + + expect(outOfBandRecord.getTags()).toEqual({ + state: OutOfBandState.Done, + role: OutOfBandRole.Receiver, + invitationId: 'a-message-id', + threadId: 'a-message-id', + recipientKeyFingerprints: ['z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + }) + }) + }) + + describe('clone', () => { + test('should correctly clone the record', () => { + const jsonRecord = { + _tags: {}, + metadata: {}, + id: 'd565b4d8-3e5d-42da-a87c-4454fdfbaff0', + createdAt: '2022-06-02T18:35:06.374Z', + outOfBandInvitation: { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '5d57ca2d-80ed-432c-8def-c40c75e8ab09', + label: 'Faber College', + goalCode: 'p2p-messaging', + goal: 'To make a connection', + accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: [ + { + id: '#inline-0', + serviceEndpoint: 'rxjs:faber', + type: 'did-communication', + recipientKeys: ['did:key:z6MkhngxtGfzTvGVbFjVVqBHvniY1f2XrTMZLM5BZvPh31Dc'], + routingKeys: [], + }, + ], + }, + role: 'sender', + state: 'await-response', + autoAcceptConnection: true, + reusable: false, + } + + const oobRecord = JsonTransformer.fromJSON(jsonRecord, OutOfBandRecord) + + expect(oobRecord.toJSON()).toMatchObject(oobRecord.clone().toJSON()) + }) + }) +}) diff --git a/packages/core/src/modules/oob/repository/index.ts b/packages/core/src/modules/oob/repository/index.ts new file mode 100644 index 0000000000..8bfa55b8dd --- /dev/null +++ b/packages/core/src/modules/oob/repository/index.ts @@ -0,0 +1,2 @@ +export * from './OutOfBandRecord' +export * from './OutOfBandRepository' diff --git a/packages/core/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts b/packages/core/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts new file mode 100644 index 0000000000..079339a9bf --- /dev/null +++ b/packages/core/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts @@ -0,0 +1,21 @@ +import type { InvitationType } from '../messages' + +export enum OutOfBandRecordMetadataKeys { + RecipientRouting = '_internal/recipientRouting', + LegacyInvitation = '_internal/legacyInvitation', +} + +export type OutOfBandRecordMetadata = { + [OutOfBandRecordMetadataKeys.RecipientRouting]: { + recipientKeyFingerprint: string + routingKeyFingerprints: string[] + endpoints: string[] + mediatorId?: string + } + [OutOfBandRecordMetadataKeys.LegacyInvitation]: { + /** + * Indicates the type of the legacy invitation that was used for this out of band exchange. + */ + legacyInvitationType?: Exclude + } +} diff --git a/packages/core/src/modules/problem-reports/errors/ProblemReportError.ts b/packages/core/src/modules/problem-reports/errors/ProblemReportError.ts new file mode 100644 index 0000000000..8fe3dd8db5 --- /dev/null +++ b/packages/core/src/modules/problem-reports/errors/ProblemReportError.ts @@ -0,0 +1,20 @@ +import { CredoError } from '../../../error/CredoError' +import { ProblemReportMessage } from '../messages/ProblemReportMessage' + +export interface ProblemReportErrorOptions { + problemCode: string +} + +export class ProblemReportError extends CredoError { + public problemReport: ProblemReportMessage + + public constructor(message: string, { problemCode }: ProblemReportErrorOptions) { + super(message) + this.problemReport = new ProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/core/src/modules/problem-reports/errors/index.ts b/packages/core/src/modules/problem-reports/errors/index.ts new file mode 100644 index 0000000000..1eb23b7c6b --- /dev/null +++ b/packages/core/src/modules/problem-reports/errors/index.ts @@ -0,0 +1 @@ +export * from './ProblemReportError' diff --git a/packages/core/src/modules/problem-reports/index.ts b/packages/core/src/modules/problem-reports/index.ts new file mode 100644 index 0000000000..479c831166 --- /dev/null +++ b/packages/core/src/modules/problem-reports/index.ts @@ -0,0 +1,3 @@ +export * from './errors' +export * from './messages' +export * from './models' diff --git a/packages/core/src/modules/problem-reports/messages/ProblemReportMessage.ts b/packages/core/src/modules/problem-reports/messages/ProblemReportMessage.ts new file mode 100644 index 0000000000..2c4a8d16fc --- /dev/null +++ b/packages/core/src/modules/problem-reports/messages/ProblemReportMessage.ts @@ -0,0 +1,122 @@ +// Create a base ProblemReportMessage message class and add it to the messages directory +import { Expose } from 'class-transformer' +import { IsEnum, IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export enum WhoRetriesStatus { + You = 'YOU', + Me = 'ME', + Both = 'BOTH', + None = 'NONE', +} + +export enum ImpactStatus { + Message = 'MESSAGE', + Thread = 'THREAD', + Connection = 'CONNECTION', +} + +export enum WhereStatus { + Cloud = 'CLOUD', + Edge = 'EDGE', + Wire = 'WIRE', + Agency = 'AGENCY', +} + +export enum OtherStatus { + You = 'YOU', + Me = 'ME', + Other = 'OTHER', +} + +export interface DescriptionOptions { + en: string + code: string +} + +export interface FixHintOptions { + en: string +} + +export interface ProblemReportMessageOptions { + id?: string + description: DescriptionOptions + problemItems?: string[] + whoRetries?: WhoRetriesStatus + fixHint?: FixHintOptions + impact?: ImpactStatus + where?: WhereStatus + noticedTime?: string + trackingUri?: string + escalationUri?: string +} + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class ProblemReportMessage extends AgentMessage { + /** + * Create new ReportProblem instance. + * @param options + */ + public constructor(options: ProblemReportMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.description = options.description + this.problemItems = options.problemItems + this.whoRetries = options.whoRetries + this.fixHint = options.fixHint + this.impact = options.impact + this.where = options.where + this.noticedTime = options.noticedTime + this.trackingUri = options.trackingUri + this.escalationUri = options.escalationUri + } + } + + @IsValidMessageType(ProblemReportMessage.type) + public readonly type: string = ProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/notification/1.0/problem-report') + + public description!: DescriptionOptions + + @IsOptional() + @Expose({ name: 'problem_items' }) + public problemItems?: string[] + + @IsOptional() + @IsEnum(WhoRetriesStatus) + @Expose({ name: 'who_retries' }) + public whoRetries?: WhoRetriesStatus + + @IsOptional() + @Expose({ name: 'fix_hint' }) + public fixHint?: FixHintOptions + + @IsOptional() + @IsEnum(WhereStatus) + public where?: WhereStatus + + @IsOptional() + @IsEnum(ImpactStatus) + public impact?: ImpactStatus + + @IsOptional() + @IsString() + @Expose({ name: 'noticed_time' }) + public noticedTime?: string + + @IsOptional() + @IsString() + @Expose({ name: 'tracking_uri' }) + public trackingUri?: string + + @IsOptional() + @IsString() + @Expose({ name: 'escalation_uri' }) + public escalationUri?: string +} diff --git a/packages/core/src/modules/problem-reports/messages/index.ts b/packages/core/src/modules/problem-reports/messages/index.ts new file mode 100644 index 0000000000..57670e5421 --- /dev/null +++ b/packages/core/src/modules/problem-reports/messages/index.ts @@ -0,0 +1 @@ +export * from './ProblemReportMessage' diff --git a/packages/core/src/modules/problem-reports/models/ProblemReportReason.ts b/packages/core/src/modules/problem-reports/models/ProblemReportReason.ts new file mode 100644 index 0000000000..6f85917c1a --- /dev/null +++ b/packages/core/src/modules/problem-reports/models/ProblemReportReason.ts @@ -0,0 +1,3 @@ +export enum ProblemReportReason { + MessageParseFailure = 'message-parse-failure', +} diff --git a/packages/core/src/modules/problem-reports/models/index.ts b/packages/core/src/modules/problem-reports/models/index.ts new file mode 100644 index 0000000000..1cbfb94d73 --- /dev/null +++ b/packages/core/src/modules/problem-reports/models/index.ts @@ -0,0 +1 @@ +export * from './ProblemReportReason' diff --git a/packages/core/src/modules/proofs/ProofEvents.ts b/packages/core/src/modules/proofs/ProofEvents.ts new file mode 100644 index 0000000000..86b0e7673c --- /dev/null +++ b/packages/core/src/modules/proofs/ProofEvents.ts @@ -0,0 +1,15 @@ +import type { ProofState } from './models/ProofState' +import type { ProofExchangeRecord } from './repository' +import type { BaseEvent } from '../../agent/Events' + +export enum ProofEventTypes { + ProofStateChanged = 'ProofStateChanged', +} + +export interface ProofStateChangedEvent extends BaseEvent { + type: typeof ProofEventTypes.ProofStateChanged + payload: { + proofRecord: ProofExchangeRecord + previousState: ProofState | null + } +} diff --git a/packages/core/src/modules/proofs/ProofsApi.ts b/packages/core/src/modules/proofs/ProofsApi.ts new file mode 100644 index 0000000000..8b48972163 --- /dev/null +++ b/packages/core/src/modules/proofs/ProofsApi.ts @@ -0,0 +1,652 @@ +import type { + AcceptProofOptions, + AcceptProofProposalOptions, + AcceptProofRequestOptions, + CreateProofRequestOptions, + DeleteProofOptions, + FindProofPresentationMessageReturn, + FindProofProposalMessageReturn, + FindProofRequestMessageReturn, + GetCredentialsForProofRequestOptions, + GetCredentialsForProofRequestReturn, + GetProofFormatDataReturn, + NegotiateProofProposalOptions, + NegotiateProofRequestOptions, + ProposeProofOptions, + RequestProofOptions, + SelectCredentialsForProofRequestOptions, + SelectCredentialsForProofRequestReturn, + SendProofProblemReportOptions, + DeclineProofRequestOptions, +} from './ProofsApiOptions' +import type { ProofProtocol } from './protocol/ProofProtocol' +import type { ProofFormatsFromProtocols } from './protocol/ProofProtocolOptions' +import type { ProofExchangeRecord } from './repository/ProofExchangeRecord' +import type { AgentMessage } from '../../agent/AgentMessage' +import type { Query, QueryOptions } from '../../storage/StorageService' + +import { injectable } from 'tsyringe' + +import { MessageSender } from '../../agent/MessageSender' +import { AgentContext } from '../../agent/context/AgentContext' +import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' +import { CredoError } from '../../error' +import { ConnectionService } from '../connections/services/ConnectionService' + +import { ProofsModuleConfig } from './ProofsModuleConfig' +import { ProofState } from './models/ProofState' +import { ProofRepository } from './repository/ProofRepository' + +export interface ProofsApi { + // Proposal methods + proposeProof(options: ProposeProofOptions): Promise + acceptProposal(options: AcceptProofProposalOptions): Promise + negotiateProposal(options: NegotiateProofProposalOptions): Promise + + // Request methods + requestProof(options: RequestProofOptions): Promise + acceptRequest(options: AcceptProofRequestOptions): Promise + declineRequest(options: DeclineProofRequestOptions): Promise + negotiateRequest(options: NegotiateProofRequestOptions): Promise + + // Present + acceptPresentation(options: AcceptProofOptions): Promise + + // out of band + createRequest(options: CreateProofRequestOptions): Promise<{ + message: AgentMessage + proofRecord: ProofExchangeRecord + }> + + // Auto Select + selectCredentialsForRequest( + options: SelectCredentialsForProofRequestOptions + ): Promise> + + // Get credentials for request + getCredentialsForRequest( + options: GetCredentialsForProofRequestOptions + ): Promise> + + sendProblemReport(options: SendProofProblemReportOptions): Promise + + // Record Methods + getAll(): Promise + findAllByQuery(query: Query, queryOptions?: QueryOptions): Promise + getById(proofRecordId: string): Promise + findById(proofRecordId: string): Promise + deleteById(proofId: string, options?: DeleteProofOptions): Promise + update(proofRecord: ProofExchangeRecord): Promise + getFormatData(proofRecordId: string): Promise>> + + // DidComm Message Records + findProposalMessage(proofRecordId: string): Promise> + findRequestMessage(proofRecordId: string): Promise> + findPresentationMessage(proofRecordId: string): Promise> +} + +@injectable() +export class ProofsApi implements ProofsApi { + /** + * Configuration for the proofs module + */ + public readonly config: ProofsModuleConfig + + private connectionService: ConnectionService + private messageSender: MessageSender + private proofRepository: ProofRepository + private agentContext: AgentContext + + public constructor( + messageSender: MessageSender, + connectionService: ConnectionService, + agentContext: AgentContext, + proofRepository: ProofRepository, + config: ProofsModuleConfig + ) { + this.messageSender = messageSender + this.connectionService = connectionService + this.proofRepository = proofRepository + this.agentContext = agentContext + this.config = config + } + + private getProtocol(protocolVersion: PVT): ProofProtocol { + const proofProtocol = this.config.proofProtocols.find((protocol) => protocol.version === protocolVersion) + + if (!proofProtocol) { + throw new CredoError(`No proof protocol registered for protocol version ${protocolVersion}`) + } + + return proofProtocol + } + + /** + * Initiate a new presentation exchange as prover by sending a presentation proposal message + * to the connection with the specified connection id. + * + * @param options configuration to use for the proposal + * @returns Proof exchange record associated with the sent proposal message + */ + public async proposeProof(options: ProposeProofOptions): Promise { + const protocol = this.getProtocol(options.protocolVersion) + + const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) + + // Assert + connectionRecord.assertReady() + + const { message, proofRecord } = await protocol.createProposal(this.agentContext, { + connectionRecord, + proofFormats: options.proofFormats, + autoAcceptProof: options.autoAcceptProof, + goalCode: options.goalCode, + goal: options.goal, + comment: options.comment, + parentThreadId: options.parentThreadId, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: proofRecord, + connectionRecord, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + return proofRecord + } + + /** + * Accept a presentation proposal as verifier (by sending a presentation request message) to the connection + * associated with the proof record. + * + * @param options config object for accepting the proposal + * @returns Proof exchange record associated with the presentation request + */ + public async acceptProposal(options: AcceptProofProposalOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) + + if (!proofRecord.connectionId) { + throw new CredoError( + `No connectionId found for proof record '${proofRecord.id}'. Connection-less verification does not support presentation proposal or negotiation.` + ) + } + + // with version we can get the protocol + const protocol = this.getProtocol(proofRecord.protocolVersion) + const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + + // Assert + connectionRecord.assertReady() + + const { message } = await protocol.acceptProposal(this.agentContext, { + proofRecord, + proofFormats: options.proofFormats, + goalCode: options.goalCode, + goal: options.goal, + willConfirm: options.willConfirm, + comment: options.comment, + autoAcceptProof: options.autoAcceptProof, + }) + + // send the message + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: proofRecord, + connectionRecord, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + return proofRecord + } + + /** + * Answer with a new presentation request in response to received presentation proposal message + * to the connection associated with the proof record. + * + * @param options multiple properties like proof record id, proof formats to accept requested credentials object + * specifying which credentials to use for the proof + * @returns Proof record associated with the sent request message + */ + public async negotiateProposal(options: NegotiateProofProposalOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) + + if (!proofRecord.connectionId) { + throw new CredoError( + `No connectionId found for proof record '${proofRecord.id}'. Connection-less verification does not support negotiation.` + ) + } + + const protocol = this.getProtocol(proofRecord.protocolVersion) + const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + + // Assert + connectionRecord.assertReady() + + const { message } = await protocol.negotiateProposal(this.agentContext, { + proofRecord, + proofFormats: options.proofFormats, + autoAcceptProof: options.autoAcceptProof, + comment: options.comment, + goalCode: options.goalCode, + goal: options.goal, + willConfirm: options.willConfirm, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: proofRecord, + connectionRecord, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return proofRecord + } + + /** + * Initiate a new presentation exchange as verifier by sending a presentation request message + * to the connection with the specified connection id + * + * @param options multiple properties like connection id, protocol version, proof Formats to build the proof request + * @returns Proof record associated with the sent request message + */ + public async requestProof(options: RequestProofOptions): Promise { + const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) + const protocol = this.getProtocol(options.protocolVersion) + + // Assert + connectionRecord.assertReady() + + const { message, proofRecord } = await protocol.createRequest(this.agentContext, { + connectionRecord, + proofFormats: options.proofFormats, + autoAcceptProof: options.autoAcceptProof, + parentThreadId: options.parentThreadId, + comment: options.comment, + goalCode: options.goalCode, + goal: options.goal, + willConfirm: options.willConfirm, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: proofRecord, + connectionRecord, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + return proofRecord + } + + /** + * Accept a presentation request as prover (by sending a presentation message) to the connection + * associated with the proof record. + * + * @param options multiple properties like proof record id, proof formats to accept requested credentials object + * specifying which credentials to use for the proof + * @returns Proof record associated with the sent presentation message + */ + public async acceptRequest(options: AcceptProofRequestOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) + + const protocol = this.getProtocol(proofRecord.protocolVersion) + + const requestMessage = await protocol.findRequestMessage(this.agentContext, proofRecord.id) + if (!requestMessage) { + throw new CredoError(`No request message found for proof record with id '${proofRecord.id}'`) + } + + // Use connection if present + const connectionRecord = proofRecord.connectionId + ? await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + : undefined + connectionRecord?.assertReady() + + const { message } = await protocol.acceptRequest(this.agentContext, { + proofFormats: options.proofFormats, + proofRecord, + comment: options.comment, + autoAcceptProof: options.autoAcceptProof, + goalCode: options.goalCode, + goal: options.goal, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: proofRecord, + lastReceivedMessage: requestMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return proofRecord + } + + public async declineRequest(options: DeclineProofRequestOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) + proofRecord.assertState(ProofState.RequestReceived) + + const protocol = this.getProtocol(proofRecord.protocolVersion) + if (options.sendProblemReport) { + await this.sendProblemReport({ + proofRecordId: options.proofRecordId, + description: options.problemReportDescription ?? 'Request declined', + }) + } + + await protocol.updateState(this.agentContext, proofRecord, ProofState.Declined) + + return proofRecord + } + + /** + * Answer with a new presentation proposal in response to received presentation request message + * to the connection associated with the proof record. + * + * @param options multiple properties like proof record id, proof format (indy/ presentation exchange) + * to include in the message + * @returns Proof record associated with the sent proposal message + */ + public async negotiateRequest(options: NegotiateProofRequestOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) + + if (!proofRecord.connectionId) { + throw new CredoError( + `No connectionId found for proof record '${proofRecord.id}'. Connection-less verification does not support presentation proposal or negotiation.` + ) + } + + const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + + // Assert + connectionRecord.assertReady() + + const protocol = this.getProtocol(proofRecord.protocolVersion) + const { message } = await protocol.negotiateRequest(this.agentContext, { + proofRecord, + proofFormats: options.proofFormats, + autoAcceptProof: options.autoAcceptProof, + goalCode: options.goalCode, + goal: options.goal, + comment: options.comment, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: proofRecord, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return proofRecord + } + + /** + * Initiate a new presentation exchange as verifier by sending an out of band presentation + * request message + * + * @param options multiple properties like protocol version, proof Formats to build the proof request + * @returns the message itself and the proof record associated with the sent request message + */ + public async createRequest(options: CreateProofRequestOptions): Promise<{ + message: AgentMessage + proofRecord: ProofExchangeRecord + }> { + const protocol = this.getProtocol(options.protocolVersion) + + return await protocol.createRequest(this.agentContext, { + proofFormats: options.proofFormats, + autoAcceptProof: options.autoAcceptProof, + comment: options.comment, + parentThreadId: options.parentThreadId, + goalCode: options.goalCode, + goal: options.goal, + willConfirm: options.willConfirm, + }) + } + + /** + * Accept a presentation as prover (by sending a presentation acknowledgement message) to the connection + * associated with the proof record. + * + * @param proofRecordId The id of the proof exchange record for which to accept the presentation + * @returns Proof record associated with the sent presentation acknowledgement message + * + */ + public async acceptPresentation(options: AcceptProofOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) + const protocol = this.getProtocol(proofRecord.protocolVersion) + + const requestMessage = await protocol.findRequestMessage(this.agentContext, proofRecord.id) + if (!requestMessage) { + throw new CredoError(`No request message found for proof record with id '${proofRecord.id}'`) + } + + const presentationMessage = await protocol.findPresentationMessage(this.agentContext, proofRecord.id) + if (!presentationMessage) { + throw new CredoError(`No presentation message found for proof record with id '${proofRecord.id}'`) + } + + // Use connection if present + const connectionRecord = proofRecord.connectionId + ? await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + : undefined + connectionRecord?.assertReady() + + const { message } = await protocol.acceptPresentation(this.agentContext, { + proofRecord, + }) + + // FIXME: returnRoute: false + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: proofRecord, + lastSentMessage: requestMessage, + lastReceivedMessage: presentationMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return proofRecord + } + + /** + * Create a {@link RetrievedCredentials} object. Given input proof request and presentation proposal, + * use credentials in the wallet to build indy requested credentials object for input to proof creation. + * If restrictions allow, self attested attributes will be used. + * + * @param options multiple properties like proof record id and optional configuration + * @returns RequestedCredentials + */ + public async selectCredentialsForRequest( + options: SelectCredentialsForProofRequestOptions + ): Promise> { + const proofRecord = await this.getById(options.proofRecordId) + + const protocol = this.getProtocol(proofRecord.protocolVersion) + + return protocol.selectCredentialsForRequest(this.agentContext, { + proofFormats: options.proofFormats, + proofRecord, + }) + } + + /** + * Get credentials in the wallet for a received proof request. + * + * @param options multiple properties like proof record id and optional configuration + */ + public async getCredentialsForRequest( + options: GetCredentialsForProofRequestOptions + ): Promise> { + const proofRecord = await this.getById(options.proofRecordId) + + const protocol = this.getProtocol(proofRecord.protocolVersion) + + return protocol.getCredentialsForRequest(this.agentContext, { + proofRecord, + proofFormats: options.proofFormats, + }) + } + + /** + * Send problem report message for a proof record + * + * @param proofRecordId The id of the proof record for which to send problem report + * @param message message to send + * @returns proof record associated with the proof problem report message + */ + public async sendProblemReport(options: SendProofProblemReportOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) + + const protocol = this.getProtocol(proofRecord.protocolVersion) + + const requestMessage = await protocol.findRequestMessage(this.agentContext, proofRecord.id) + + const { message: problemReport } = await protocol.createProblemReport(this.agentContext, { + proofRecord, + description: options.description, + }) + + // Use connection if present + const connectionRecord = proofRecord.connectionId + ? await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + : undefined + connectionRecord?.assertReady() + + // If there's no connection (so connection-less, we require the state to be request received) + if (!connectionRecord) { + proofRecord.assertState(ProofState.RequestReceived) + + if (!requestMessage) { + throw new CredoError(`No request message found for proof record with id '${proofRecord.id}'`) + } + } + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message: problemReport, + connectionRecord, + associatedRecord: proofRecord, + lastReceivedMessage: requestMessage ?? undefined, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return proofRecord + } + + public async getFormatData(proofRecordId: string): Promise>> { + const proofRecord = await this.getById(proofRecordId) + const protocol = this.getProtocol(proofRecord.protocolVersion) + + return protocol.getFormatData(this.agentContext, proofRecordId) + } + + /** + * Retrieve all proof records + * + * @returns List containing all proof records + */ + public async getAll(): Promise { + return this.proofRepository.getAll(this.agentContext) + } + + /** + * Retrieve all proof records by specified query params + * + * @returns List containing all proof records matching specified params + */ + public findAllByQuery( + query: Query, + queryOptions?: QueryOptions + ): Promise { + return this.proofRepository.findByQuery(this.agentContext, query, queryOptions) + } + + /** + * Retrieve a proof record by id + * + * @param proofRecordId The proof record id + * @throws {RecordNotFoundError} If no record is found + * @return The proof record + * + */ + public async getById(proofRecordId: string): Promise { + return await this.proofRepository.getById(this.agentContext, proofRecordId) + } + + /** + * Retrieve a proof record by id + * + * @param proofRecordId The proof record id + * @return The proof record or null if not found + * + */ + public async findById(proofRecordId: string): Promise { + return await this.proofRepository.findById(this.agentContext, proofRecordId) + } + + /** + * Delete a proof record by id + * + * @param proofId the proof record id + */ + public async deleteById(proofId: string, options?: DeleteProofOptions) { + const proofRecord = await this.getById(proofId) + const protocol = this.getProtocol(proofRecord.protocolVersion) + return protocol.delete(this.agentContext, proofRecord, options) + } + + /** + * Retrieve a proof record by connection id and thread id + * + * @param connectionId The connection id + * @param threadId The thread id + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * @returns The proof record + */ + public async getByThreadAndConnectionId(threadId: string, connectionId?: string): Promise { + return this.proofRepository.getByThreadAndConnectionId(this.agentContext, threadId, connectionId) + } + + /** + * Retrieve proof records by connection id and parent thread id + * + * @param connectionId The connection id + * @param parentThreadId The parent thread id + * @returns List containing all proof records matching the given query + */ + public async getByParentThreadAndConnectionId( + parentThreadId: string, + connectionId?: string + ): Promise { + return this.proofRepository.getByParentThreadAndConnectionId(this.agentContext, parentThreadId, connectionId) + } + + /** + * Update a proof record by + * + * @param proofRecord the proof record + */ + public async update(proofRecord: ProofExchangeRecord): Promise { + await this.proofRepository.update(this.agentContext, proofRecord) + } + + public async findProposalMessage(proofRecordId: string): Promise> { + const record = await this.getById(proofRecordId) + const protocol = this.getProtocol(record.protocolVersion) + return protocol.findProposalMessage(this.agentContext, proofRecordId) as FindProofProposalMessageReturn + } + + public async findRequestMessage(proofRecordId: string): Promise> { + const record = await this.getById(proofRecordId) + const protocol = this.getProtocol(record.protocolVersion) + return protocol.findRequestMessage(this.agentContext, proofRecordId) as FindProofRequestMessageReturn + } + + public async findPresentationMessage(proofRecordId: string): Promise> { + const record = await this.getById(proofRecordId) + const protocol = this.getProtocol(record.protocolVersion) + return protocol.findPresentationMessage(this.agentContext, proofRecordId) as FindProofPresentationMessageReturn + } +} diff --git a/packages/core/src/modules/proofs/ProofsApiOptions.ts b/packages/core/src/modules/proofs/ProofsApiOptions.ts new file mode 100644 index 0000000000..6da0cb0dd1 --- /dev/null +++ b/packages/core/src/modules/proofs/ProofsApiOptions.ts @@ -0,0 +1,195 @@ +import type { ProofFormatCredentialForRequestPayload, ProofFormatPayload } from './formats' +import type { AutoAcceptProof } from './models' +import type { ProofProtocol } from './protocol/ProofProtocol' +import type { + DeleteProofOptions, + GetProofFormatDataReturn, + ProofFormatsFromProtocols, +} from './protocol/ProofProtocolOptions' + +// re-export GetFormatDataReturn type from protocol, as it is also used in the api +export type { GetProofFormatDataReturn, DeleteProofOptions } + +export type FindProofProposalMessageReturn = ReturnType +export type FindProofRequestMessageReturn = ReturnType +export type FindProofPresentationMessageReturn = ReturnType< + PPs[number]['findPresentationMessage'] +> + +/** + * Get the supported protocol versions based on the provided proof protocols. + */ +export type ProofsProtocolVersionType = PPs[number]['version'] + +interface BaseOptions { + autoAcceptProof?: AutoAcceptProof + comment?: string + + /** + * Will be ignored for v1 protocol as it is not supported + */ + goalCode?: string + + /** + * Will be ignored for v1 protocol as it is not supported + */ + goal?: string +} + +/** + * Interface for ProofsApi.proposeProof. Will send a proposal. + */ +export interface ProposeProofOptions extends BaseOptions { + connectionId: string + protocolVersion: ProofsProtocolVersionType + proofFormats: ProofFormatPayload, 'createProposal'> + + parentThreadId?: string +} + +/** + * Interface for ProofsApi.acceptProposal. Will send a request + * + * proofFormats is optional because this is an accept method + */ +export interface AcceptProofProposalOptions extends BaseOptions { + proofRecordId: string + proofFormats?: ProofFormatPayload, 'acceptProposal'> + + /** @default true */ + willConfirm?: boolean +} + +/** + * Interface for ProofsApi.negotiateProposal. Will send a request + */ +export interface NegotiateProofProposalOptions extends BaseOptions { + proofRecordId: string + proofFormats: ProofFormatPayload, 'createRequest'> + + /** @default true */ + willConfirm?: boolean +} + +/** + * Interface for ProofsApi.createRequest. Will create an out of band request + */ +export interface CreateProofRequestOptions extends BaseOptions { + protocolVersion: ProofsProtocolVersionType + proofFormats: ProofFormatPayload, 'createRequest'> + + parentThreadId?: string + + /** @default true */ + willConfirm?: boolean +} + +/** + * Interface for ProofsApi.requestCredential. Extends CreateProofRequestOptions, will send a request + */ +export interface RequestProofOptions + extends BaseOptions, + CreateProofRequestOptions { + connectionId: string +} + +/** + * Interface for ProofsApi.acceptRequest. Will send a presentation + */ +export interface AcceptProofRequestOptions extends BaseOptions { + proofRecordId: string + + /** + * whether to enable return routing on the send presentation message. This value only + * has an effect for connectionless exchanges. + */ + useReturnRoute?: boolean + proofFormats?: ProofFormatPayload, 'acceptRequest'> + + /** @default true */ + willConfirm?: boolean +} + +/** + * Interface for ProofsApi.negotiateRequest. Will send a proposal + */ +export interface NegotiateProofRequestOptions extends BaseOptions { + proofRecordId: string + proofFormats: ProofFormatPayload, 'createProposal'> +} + +/** + * Interface for ProofsApi.acceptPresentation. Will send an ack message + */ +export interface AcceptProofOptions { + proofRecordId: string +} + +/** + * Interface for ProofsApi.getCredentialsForRequest. Will return the credentials that match the proof request + */ +export interface GetCredentialsForProofRequestOptions { + proofRecordId: string + proofFormats?: ProofFormatCredentialForRequestPayload< + ProofFormatsFromProtocols, + 'getCredentialsForRequest', + 'input' + > +} + +export interface GetCredentialsForProofRequestReturn { + proofFormats: ProofFormatCredentialForRequestPayload< + ProofFormatsFromProtocols, + 'getCredentialsForRequest', + 'output' + > +} + +/** + * Interface for ProofsApi.selectCredentialsForRequest. Will automatically select return the first/best + * credentials that match the proof request + */ +export interface SelectCredentialsForProofRequestOptions { + proofRecordId: string + proofFormats?: ProofFormatCredentialForRequestPayload< + ProofFormatsFromProtocols, + 'getCredentialsForRequest', + 'input' + > +} + +export interface SelectCredentialsForProofRequestReturn { + proofFormats: ProofFormatCredentialForRequestPayload< + ProofFormatsFromProtocols, + 'selectCredentialsForRequest', + 'output' + > +} + +/** + * Interface for ProofsApi.sendProblemReport. Will send a problem-report message + */ +export interface SendProofProblemReportOptions { + proofRecordId: string + description: string +} + +/** + * Interface for ProofsApi.declineRequest. Decline a received proof request and optionally send a problem-report message to Verifier + */ +export interface DeclineProofRequestOptions { + proofRecordId: string + + /** + * Whether to send a problem-report message to the verifier as part + * of declining the proof request + */ + sendProblemReport?: boolean + + /** + * Description to include in the problem-report message + * Only used if `sendProblemReport` is set to `true`. + * @default "Request declined" + */ + problemReportDescription?: string +} diff --git a/packages/core/src/modules/proofs/ProofsModule.ts b/packages/core/src/modules/proofs/ProofsModule.ts new file mode 100644 index 0000000000..483b0ee057 --- /dev/null +++ b/packages/core/src/modules/proofs/ProofsModule.ts @@ -0,0 +1,52 @@ +import type { ProofsModuleConfigOptions } from './ProofsModuleConfig' +import type { ProofProtocol } from './protocol/ProofProtocol' +import type { FeatureRegistry } from '../../agent/FeatureRegistry' +import type { ApiModule, DependencyManager } from '../../plugins' +import type { Optional } from '../../utils' +import type { Constructor } from '../../utils/mixins' + +import { ProofsApi } from './ProofsApi' +import { ProofsModuleConfig } from './ProofsModuleConfig' +import { V2ProofProtocol } from './protocol' +import { ProofRepository } from './repository' + +/** + * Default proofProtocols that will be registered if the `proofProtocols` property is not configured. + */ +export type DefaultProofProtocols = [V2ProofProtocol<[]>] + +// ProofsModuleOptions makes the proofProtocols property optional from the config, as it will set it when not provided. +export type ProofsModuleOptions = Optional< + ProofsModuleConfigOptions, + 'proofProtocols' +> + +export class ProofsModule implements ApiModule { + public readonly config: ProofsModuleConfig + + public readonly api: Constructor> = ProofsApi + + public constructor(config?: ProofsModuleOptions) { + this.config = new ProofsModuleConfig({ + ...config, + // NOTE: the proofProtocols defaults are set in the ProofsModule rather than the ProofsModuleConfig to + // avoid dependency cycles. + proofProtocols: config?.proofProtocols ?? [new V2ProofProtocol({ proofFormats: [] })], + }) as ProofsModuleConfig + } + + /** + * Registers the dependencies of the proofs module on the dependency manager. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Config + dependencyManager.registerInstance(ProofsModuleConfig, this.config) + + // Repositories + dependencyManager.registerSingleton(ProofRepository) + + for (const proofProtocol of this.config.proofProtocols) { + proofProtocol.register(dependencyManager, featureRegistry) + } + } +} diff --git a/packages/core/src/modules/proofs/ProofsModuleConfig.ts b/packages/core/src/modules/proofs/ProofsModuleConfig.ts new file mode 100644 index 0000000000..e87966ef27 --- /dev/null +++ b/packages/core/src/modules/proofs/ProofsModuleConfig.ts @@ -0,0 +1,47 @@ +import type { ProofProtocol } from './protocol/ProofProtocol' + +import { AutoAcceptProof } from './models/ProofAutoAcceptType' + +/** + * ProofsModuleConfigOptions defines the interface for the options of the ProofsModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface ProofsModuleConfigOptions { + /** + * Whether to automatically accept proof messages. Applies to all present proof protocol versions. + * + * @default {@link AutoAcceptProof.Never} + */ + autoAcceptProofs?: AutoAcceptProof + + /** + * Proof protocols to make available to the proofs module. Only one proof protocol should be registered for each proof + * protocol version. + * + * When not provided, the `V2ProofProtocol` is registered by default. + * + * @default + * ``` + * [V2ProofProtocol] + * ``` + */ + proofProtocols: ProofProtocols +} + +export class ProofsModuleConfig { + private options: ProofsModuleConfigOptions + + public constructor(options: ProofsModuleConfigOptions) { + this.options = options + } + + /** See {@link ProofsModuleConfigOptions.autoAcceptProofs} */ + public get autoAcceptProofs() { + return this.options.autoAcceptProofs ?? AutoAcceptProof.Never + } + + /** See {@link ProofsModuleConfigOptions.proofProtocols} */ + public get proofProtocols() { + return this.options.proofProtocols + } +} diff --git a/packages/core/src/modules/proofs/__tests__/ProofsModule.test.ts b/packages/core/src/modules/proofs/__tests__/ProofsModule.test.ts new file mode 100644 index 0000000000..6d817c07d5 --- /dev/null +++ b/packages/core/src/modules/proofs/__tests__/ProofsModule.test.ts @@ -0,0 +1,55 @@ +import type { ProofProtocol } from '../protocol/ProofProtocol' + +import { FeatureRegistry } from '../../../agent/FeatureRegistry' +import { DependencyManager } from '../../../plugins/DependencyManager' +import { ProofsModule } from '../ProofsModule' +import { ProofsModuleConfig } from '../ProofsModuleConfig' +import { V2ProofProtocol } from '../protocol/v2/V2ProofProtocol' +import { ProofRepository } from '../repository' + +jest.mock('../../../plugins/DependencyManager') +jest.mock('../../../agent/FeatureRegistry') + +const DependencyManagerMock = DependencyManager as jest.Mock +const dependencyManager = new DependencyManagerMock() +const FeatureRegistryMock = FeatureRegistry as jest.Mock +const featureRegistry = new FeatureRegistryMock() + +describe('ProofsModule', () => { + test('registers dependencies on the dependency manager', () => { + const proofsModule = new ProofsModule({ + proofProtocols: [], + }) + proofsModule.register(dependencyManager, featureRegistry) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(ProofsModuleConfig, proofsModule.config) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(ProofRepository) + }) + + test('registers V2ProofProtocol if no proofProtocols are configured', () => { + const proofsModule = new ProofsModule() + + expect(proofsModule.config.proofProtocols).toEqual([expect.any(V2ProofProtocol)]) + }) + + test('calls register on the provided ProofProtocols', () => { + const registerMock = jest.fn() + const proofProtocol = { + register: registerMock, + } as unknown as ProofProtocol + + const proofsModule = new ProofsModule({ + proofProtocols: [proofProtocol], + }) + + expect(proofsModule.config.proofProtocols).toEqual([proofProtocol]) + + proofsModule.register(dependencyManager, featureRegistry) + + expect(registerMock).toHaveBeenCalledTimes(1) + expect(registerMock).toHaveBeenCalledWith(dependencyManager, featureRegistry) + }) +}) diff --git a/packages/core/src/modules/proofs/__tests__/ProofsModuleConfig.test.ts b/packages/core/src/modules/proofs/__tests__/ProofsModuleConfig.test.ts new file mode 100644 index 0000000000..35920a4f48 --- /dev/null +++ b/packages/core/src/modules/proofs/__tests__/ProofsModuleConfig.test.ts @@ -0,0 +1,26 @@ +import type { ProofProtocol } from '../protocol/ProofProtocol' + +import { ProofsModuleConfig } from '../ProofsModuleConfig' +import { AutoAcceptProof } from '../models' + +describe('ProofsModuleConfig', () => { + test('sets default values', () => { + const config = new ProofsModuleConfig({ + proofProtocols: [], + }) + + expect(config.autoAcceptProofs).toBe(AutoAcceptProof.Never) + expect(config.proofProtocols).toEqual([]) + }) + + test('sets values', () => { + const proofProtocol = jest.fn() as unknown as ProofProtocol + const config = new ProofsModuleConfig({ + autoAcceptProofs: AutoAcceptProof.Always, + proofProtocols: [proofProtocol], + }) + + expect(config.autoAcceptProofs).toBe(AutoAcceptProof.Always) + expect(config.proofProtocols).toEqual([proofProtocol]) + }) +}) diff --git a/packages/core/src/modules/proofs/errors/PresentationProblemReportReason.ts b/packages/core/src/modules/proofs/errors/PresentationProblemReportReason.ts new file mode 100644 index 0000000000..0fc1676dcc --- /dev/null +++ b/packages/core/src/modules/proofs/errors/PresentationProblemReportReason.ts @@ -0,0 +1,8 @@ +/** + * Presentation error code in RFC 0037. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0037-present-proof/README.md + */ +export enum PresentationProblemReportReason { + Abandoned = 'abandoned', +} diff --git a/packages/core/src/modules/proofs/errors/index.ts b/packages/core/src/modules/proofs/errors/index.ts new file mode 100644 index 0000000000..b14650ff96 --- /dev/null +++ b/packages/core/src/modules/proofs/errors/index.ts @@ -0,0 +1 @@ +export * from './PresentationProblemReportReason' diff --git a/packages/core/src/modules/proofs/formats/ProofFormat.ts b/packages/core/src/modules/proofs/formats/ProofFormat.ts new file mode 100644 index 0000000000..c573c12579 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/ProofFormat.ts @@ -0,0 +1,74 @@ +/** + * Get the payload for a specific method from a list of ProofFormat interfaces and a method + * + * @example + * ``` + * + * type CreateRequestProofFormats = ProofFormatPayload<[IndyProofFormat, PresentationExchangeProofFormat], 'createRequest'> + * + * // equal to + * type CreateRequestProofFormats = { + * indy: { + * // ... params for indy create request ... + * }, + * presentationExchange: { + * // ... params for pex create request ... + * } + * } + * ``` + */ +export type ProofFormatPayload = { + [ProofFormat in PFs[number] as ProofFormat['formatKey']]?: ProofFormat['proofFormats'][M] +} + +/** + * Get the input or output for the getCredentialsForRequest and selectCredentialsForRequest method with specific format data + * + * @example + * ``` + * + * type SelectedCredentialsForRequest = ProofFormatCredentialForRequestPayload<[IndyProofFormat, PresentationExchangeProofFormat], 'selectCredentialsForRequest', 'output'> + * + * // equal to + * type SelectedCredentialsForRequest = { + * indy: { + * // ... return value for indy selected credentials ... + * }, + * presentationExchange: { + * // ... return value for presentation exchange selected credentials ... + * } + * } + * ``` + */ +export type ProofFormatCredentialForRequestPayload< + PFs extends ProofFormat[], + M extends 'selectCredentialsForRequest' | 'getCredentialsForRequest', + IO extends 'input' | 'output' +> = { + [ProofFormat in PFs[number] as ProofFormat['formatKey']]?: ProofFormat['proofFormats'][M][IO] +} + +export interface ProofFormat { + formatKey: string // e.g. 'presentationExchange', cannot be shared between different formats + + proofFormats: { + createProposal: unknown + acceptProposal: unknown + createRequest: unknown + acceptRequest: unknown + + getCredentialsForRequest: { + input: unknown + output: unknown + } + selectCredentialsForRequest: { + input: unknown + output: unknown + } + } + formatData: { + proposal: unknown + request: unknown + presentation: unknown + } +} diff --git a/packages/core/src/modules/proofs/formats/ProofFormatService.ts b/packages/core/src/modules/proofs/formats/ProofFormatService.ts new file mode 100644 index 0000000000..f48db80624 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/ProofFormatService.ts @@ -0,0 +1,70 @@ +import type { ProofFormat } from './ProofFormat' +import type { + ProofFormatAcceptProposalOptions, + ProofFormatAcceptRequestOptions, + ProofFormatCreateProposalOptions, + FormatCreateRequestOptions, + ProofFormatProcessPresentationOptions, + ProofFormatCreateReturn, + ProofFormatProcessOptions, + ProofFormatGetCredentialsForRequestOptions, + ProofFormatGetCredentialsForRequestReturn, + ProofFormatSelectCredentialsForRequestOptions, + ProofFormatSelectCredentialsForRequestReturn, + ProofFormatAutoRespondProposalOptions, + ProofFormatAutoRespondRequestOptions, + ProofFormatAutoRespondPresentationOptions, +} from './ProofFormatServiceOptions' +import type { AgentContext } from '../../../agent' + +export interface ProofFormatService { + formatKey: PF['formatKey'] + + // proposal methods + createProposal( + agentContext: AgentContext, + options: ProofFormatCreateProposalOptions + ): Promise + processProposal(agentContext: AgentContext, options: ProofFormatProcessOptions): Promise + acceptProposal( + agentContext: AgentContext, + options: ProofFormatAcceptProposalOptions + ): Promise + + // request methods + createRequest(agentContext: AgentContext, options: FormatCreateRequestOptions): Promise + processRequest(agentContext: AgentContext, options: ProofFormatProcessOptions): Promise + acceptRequest( + agentContext: AgentContext, + options: ProofFormatAcceptRequestOptions + ): Promise + + // presentation methods + processPresentation(agentContext: AgentContext, options: ProofFormatProcessPresentationOptions): Promise + + // credentials for request + getCredentialsForRequest( + agentContext: AgentContext, + options: ProofFormatGetCredentialsForRequestOptions + ): Promise> + selectCredentialsForRequest( + agentContext: AgentContext, + options: ProofFormatSelectCredentialsForRequestOptions + ): Promise> + + // auto accept methods + shouldAutoRespondToProposal( + agentContext: AgentContext, + options: ProofFormatAutoRespondProposalOptions + ): Promise + shouldAutoRespondToRequest( + agentContext: AgentContext, + options: ProofFormatAutoRespondRequestOptions + ): Promise + shouldAutoRespondToPresentation( + agentContext: AgentContext, + options: ProofFormatAutoRespondPresentationOptions + ): Promise + + supportsFormat(formatIdentifier: string): boolean +} diff --git a/packages/core/src/modules/proofs/formats/ProofFormatServiceOptions.ts b/packages/core/src/modules/proofs/formats/ProofFormatServiceOptions.ts new file mode 100644 index 0000000000..db7923bafc --- /dev/null +++ b/packages/core/src/modules/proofs/formats/ProofFormatServiceOptions.ts @@ -0,0 +1,125 @@ +import type { ProofFormat, ProofFormatCredentialForRequestPayload, ProofFormatPayload } from './ProofFormat' +import type { ProofFormatService } from './ProofFormatService' +import type { Attachment } from '../../../decorators/attachment/Attachment' +import type { ProofFormatSpec } from '../models/ProofFormatSpec' +import type { ProofExchangeRecord } from '../repository/ProofExchangeRecord' + +/** + * Infer the {@link ProofFormat} based on a {@link ProofFormatService}. + * + * It does this by extracting the `ProofFormat` generic from the `ProofFormatService`. + * + * @example + * ``` + * // TheProofFormat is now equal to IndyProofFormat + * type TheProofFormat = ExtractProofFormat + * ``` + * + * Because the `IndyProofFormatService` is defined as follows: + * ``` + * class IndyProofFormatService implements ProofFormatService { + * } + * ``` + */ +export type ExtractProofFormat = Type extends ProofFormatService ? ProofFormat : never + +/** + * Infer an array of {@link ProofFormat} types based on an array of {@link ProofFormatService} types. + * + * This is based on {@link ExtractProofFormat}, but allows to handle arrays. + */ +export type ExtractProofFormats = { + [PF in keyof PFs]: ExtractProofFormat +} + +/** + * Base return type for all methods that create an attachment format. + * + * It requires an attachment and a format to be returned. + */ +export interface ProofFormatCreateReturn { + format: ProofFormatSpec + attachment: Attachment +} + +/** + * Base type for all proof process methods. + */ +export interface ProofFormatProcessOptions { + attachment: Attachment + proofRecord: ProofExchangeRecord +} + +export interface ProofFormatProcessPresentationOptions extends ProofFormatProcessOptions { + requestAttachment: Attachment +} + +export interface ProofFormatCreateProposalOptions { + proofRecord: ProofExchangeRecord + proofFormats: ProofFormatPayload<[PF], 'createProposal'> + attachmentId?: string +} + +export interface ProofFormatAcceptProposalOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload<[PF], 'acceptProposal'> + attachmentId?: string + + proposalAttachment: Attachment +} + +export interface FormatCreateRequestOptions { + proofRecord: ProofExchangeRecord + proofFormats: ProofFormatPayload<[PF], 'createRequest'> + attachmentId?: string +} + +export interface ProofFormatAcceptRequestOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload<[PF], 'acceptRequest'> + attachmentId?: string + + requestAttachment: Attachment + proposalAttachment?: Attachment +} + +export interface ProofFormatGetCredentialsForRequestOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatCredentialForRequestPayload<[PF], 'getCredentialsForRequest', 'input'> + + requestAttachment: Attachment + proposalAttachment?: Attachment +} + +export type ProofFormatGetCredentialsForRequestReturn = + PF['proofFormats']['getCredentialsForRequest']['output'] + +export interface ProofFormatSelectCredentialsForRequestOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatCredentialForRequestPayload<[PF], 'selectCredentialsForRequest', 'input'> + + requestAttachment: Attachment + proposalAttachment?: Attachment +} + +export type ProofFormatSelectCredentialsForRequestReturn = + PF['proofFormats']['selectCredentialsForRequest']['output'] + +export interface ProofFormatAutoRespondProposalOptions { + proofRecord: ProofExchangeRecord + proposalAttachment: Attachment + requestAttachment: Attachment +} + +export interface ProofFormatAutoRespondRequestOptions { + proofRecord: ProofExchangeRecord + requestAttachment: Attachment + proposalAttachment: Attachment +} + +export interface ProofFormatAutoRespondPresentationOptions { + proofRecord: ProofExchangeRecord + proposalAttachment?: Attachment + requestAttachment: Attachment + presentationAttachment: Attachment +} diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts new file mode 100644 index 0000000000..98c5df0d4d --- /dev/null +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts @@ -0,0 +1,76 @@ +import type { + DifPexInputDescriptorToCredentials, + DifPexCredentialsForRequest, + DifPresentationExchangeDefinitionV1, +} from '../../../dif-presentation-exchange' +import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' +import type { ProofFormat } from '../ProofFormat' + +export type DifPresentationExchangeProposal = DifPresentationExchangeDefinitionV1 + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DifPexGetCredentialsForProofRequestOptions {} + +export type DifPresentationExchangeRequest = { + options?: { + challenge?: string + domain?: string + } + presentation_definition: DifPresentationExchangeDefinitionV1 +} + +export type DifPresentationExchangePresentation = + | W3cJsonPresentation + // NOTE: this is not spec compliant, as it doesn't describe how to submit + // JWT VPs but to support JWT VPs we also allow the value to be a string + | string + +export interface DifPresentationExchangeProofFormat extends ProofFormat { + formatKey: 'presentationExchange' + + proofFormats: { + createProposal: { + presentationDefinition: DifPresentationExchangeDefinitionV1 + } + + acceptProposal: { + options?: { + challenge?: string + domain?: string + } + } + + createRequest: { + presentationDefinition: DifPresentationExchangeDefinitionV1 + options?: { + challenge?: string + domain?: string + } + } + + acceptRequest: { + credentials?: DifPexInputDescriptorToCredentials + } + + getCredentialsForRequest: { + input: DifPexGetCredentialsForProofRequestOptions + // Presentation submission details which the options that are available + output: DifPexCredentialsForRequest + } + + selectCredentialsForRequest: { + input: DifPexGetCredentialsForProofRequestOptions + // Input descriptor to credentials specifically details which credentials + // should be used for which input descriptor + output: { + credentials: DifPexInputDescriptorToCredentials + } + } + } + + formatData: { + proposal: DifPresentationExchangeProposal + request: DifPresentationExchangeRequest + presentation: DifPresentationExchangePresentation + } +} diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts new file mode 100644 index 0000000000..befb69dca4 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -0,0 +1,427 @@ +import type { + DifPresentationExchangePresentation, + DifPresentationExchangeProofFormat, + DifPresentationExchangeProposal, + DifPresentationExchangeRequest, +} from './DifPresentationExchangeProofFormat' +import type { AgentContext } from '../../../../agent' +import type { JsonValue } from '../../../../types' +import type { + DifPexInputDescriptorToCredentials, + DifPresentationExchangeSubmission, +} from '../../../dif-presentation-exchange' +import type { + IAnonCredsDataIntegrityService, + W3cVerifiablePresentation, + W3cVerifyPresentationResult, +} from '../../../vc' +import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' +import type { ProofFormatService } from '../ProofFormatService' +import type { + ProofFormatCreateProposalOptions, + ProofFormatCreateReturn, + ProofFormatProcessOptions, + ProofFormatAcceptProposalOptions, + FormatCreateRequestOptions, + ProofFormatAcceptRequestOptions, + ProofFormatProcessPresentationOptions, + ProofFormatGetCredentialsForRequestOptions, + ProofFormatSelectCredentialsForRequestOptions, + ProofFormatAutoRespondProposalOptions, + ProofFormatAutoRespondRequestOptions, + ProofFormatAutoRespondPresentationOptions, +} from '../ProofFormatServiceOptions' + +import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' +import { CredoError } from '../../../../error' +import { deepEquality, JsonTransformer } from '../../../../utils' +import { + DifPresentationExchangeService, + DifPresentationExchangeSubmissionLocation, +} from '../../../dif-presentation-exchange' +import { + ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE, + AnonCredsDataIntegrityServiceSymbol, + W3cCredentialService, + ClaimFormat, + W3cJsonLdVerifiablePresentation, + W3cJwtVerifiablePresentation, +} from '../../../vc' +import { ProofFormatSpec } from '../../models' + +const PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL = 'dif/presentation-exchange/definitions@v1.0' +const PRESENTATION_EXCHANGE_PRESENTATION_REQUEST = 'dif/presentation-exchange/definitions@v1.0' +const PRESENTATION_EXCHANGE_PRESENTATION = 'dif/presentation-exchange/submission@v1.0' + +export class DifPresentationExchangeProofFormatService + implements ProofFormatService +{ + public readonly formatKey = 'presentationExchange' as const + + private presentationExchangeService(agentContext: AgentContext) { + return agentContext.dependencyManager.resolve(DifPresentationExchangeService) + } + + public supportsFormat(formatIdentifier: string): boolean { + return [ + PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL, + PRESENTATION_EXCHANGE_PRESENTATION_REQUEST, + PRESENTATION_EXCHANGE_PRESENTATION, + ].includes(formatIdentifier) + } + + public async createProposal( + agentContext: AgentContext, + { proofFormats, attachmentId }: ProofFormatCreateProposalOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + + const pexFormat = proofFormats.presentationExchange + if (!pexFormat) { + throw new CredoError('Missing Presentation Exchange format in create proposal attachment format') + } + + const { presentationDefinition } = pexFormat + + ps.validatePresentationDefinition(presentationDefinition) + + const format = new ProofFormatSpec({ format: PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL, attachmentId }) + + const attachment = this.getFormatData(presentationDefinition, format.attachmentId) + + return { format, attachment } + } + + public async processProposal(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const ps = this.presentationExchangeService(agentContext) + const proposal = attachment.getDataAsJson() + ps.validatePresentationDefinition(proposal) + } + + public async acceptProposal( + agentContext: AgentContext, + { + attachmentId, + proposalAttachment, + proofFormats, + }: ProofFormatAcceptProposalOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + + const presentationExchangeFormat = proofFormats?.presentationExchange + + const format = new ProofFormatSpec({ + format: PRESENTATION_EXCHANGE_PRESENTATION_REQUEST, + attachmentId, + }) + + const presentationDefinition = proposalAttachment.getDataAsJson() + ps.validatePresentationDefinition(presentationDefinition) + + const attachment = this.getFormatData( + { + presentation_definition: presentationDefinition, + options: { + // NOTE: we always want to include a challenge to prevent replay attacks + challenge: presentationExchangeFormat?.options?.challenge ?? (await agentContext.wallet.generateNonce()), + domain: presentationExchangeFormat?.options?.domain, + }, + } satisfies DifPresentationExchangeRequest, + format.attachmentId + ) + + return { format, attachment } + } + + public async createRequest( + agentContext: AgentContext, + { attachmentId, proofFormats }: FormatCreateRequestOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + + const presentationExchangeFormat = proofFormats.presentationExchange + if (!presentationExchangeFormat) { + throw Error('Missing presentation exchange format in create request attachment format') + } + + const { presentationDefinition, options } = presentationExchangeFormat + + ps.validatePresentationDefinition(presentationDefinition) + + const format = new ProofFormatSpec({ + format: PRESENTATION_EXCHANGE_PRESENTATION_REQUEST, + attachmentId, + }) + + const attachment = this.getFormatData( + { + presentation_definition: presentationDefinition, + options: { + // NOTE: we always want to include a challenge to prevent replay attacks + challenge: options?.challenge ?? (await agentContext.wallet.generateNonce()), + domain: options?.domain, + }, + } satisfies DifPresentationExchangeRequest, + format.attachmentId + ) + + return { attachment, format } + } + + public async processRequest(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const ps = this.presentationExchangeService(agentContext) + const { presentation_definition: presentationDefinition } = + attachment.getDataAsJson() + ps.validatePresentationDefinition(presentationDefinition) + } + + public async acceptRequest( + agentContext: AgentContext, + { + attachmentId, + requestAttachment, + proofFormats, + }: ProofFormatAcceptRequestOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + + const format = new ProofFormatSpec({ + format: PRESENTATION_EXCHANGE_PRESENTATION, + attachmentId, + }) + + const { presentation_definition: presentationDefinition, options } = + requestAttachment.getDataAsJson() + + let credentials: DifPexInputDescriptorToCredentials + if (proofFormats?.presentationExchange?.credentials) { + credentials = proofFormats.presentationExchange.credentials + } else { + const credentialsForRequest = await ps.getCredentialsForRequest(agentContext, presentationDefinition) + credentials = ps.selectCredentialsForRequest(credentialsForRequest) + } + + const presentation = await ps.createPresentation(agentContext, { + presentationDefinition, + credentialsForInputDescriptor: credentials, + challenge: options?.challenge ?? (await agentContext.wallet.generateNonce()), + domain: options?.domain, + }) + + if (presentation.verifiablePresentations.length > 1) { + throw new CredoError('Invalid amount of verifiable presentations. Only one is allowed.') + } + + if (presentation.presentationSubmissionLocation === DifPresentationExchangeSubmissionLocation.EXTERNAL) { + throw new CredoError('External presentation submission is not supported.') + } + + const firstPresentation = presentation.verifiablePresentations[0] + + // TODO: they should all have `encoded` property so it's easy to use the resulting VP + const encodedFirstPresentation = + firstPresentation instanceof W3cJwtVerifiablePresentation || + firstPresentation instanceof W3cJsonLdVerifiablePresentation + ? firstPresentation.encoded + : firstPresentation?.compact + const attachment = this.getFormatData(encodedFirstPresentation, format.attachmentId) + + return { attachment, format } + } + + private shouldVerifyUsingAnonCredsDataIntegrity( + presentation: W3cVerifiablePresentation, + presentationSubmission: DifPresentationExchangeSubmission + ) { + if (presentation.claimFormat !== ClaimFormat.LdpVp) return false + const descriptorMap = presentationSubmission.descriptor_map + + const verifyUsingDataIntegrity = descriptorMap.every((descriptor) => descriptor.format === ClaimFormat.DiVp) + if (!verifyUsingDataIntegrity) return false + + return presentation.dataIntegrityCryptosuites.includes(ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE) + } + + public async processPresentation( + agentContext: AgentContext, + { requestAttachment, attachment }: ProofFormatProcessPresentationOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + + const request = requestAttachment.getDataAsJson() + const presentation = attachment.getDataAsJson() + let parsedPresentation: W3cVerifiablePresentation + let jsonPresentation: W3cJsonPresentation + + // TODO: we should probably move this transformation logic into the VC module, so it + // can be reused in Credo when we need to go from encoded -> parsed + if (typeof presentation === 'string' && presentation.includes('~')) { + // NOTE: we need to define in the PEX RFC where to put the presentation_submission + throw new CredoError('Received SD-JWT VC in PEX proof format. This is not supported yet.') + } else if (typeof presentation === 'string') { + // If it's a string, we expect it to be a JWT VP + parsedPresentation = W3cJwtVerifiablePresentation.fromSerializedJwt(presentation) + jsonPresentation = parsedPresentation.presentation.toJSON() + } else { + // Otherwise we expect it to be a JSON-LD VP + parsedPresentation = JsonTransformer.fromJSON(presentation, W3cJsonLdVerifiablePresentation) + jsonPresentation = parsedPresentation.toJSON() + } + + if (!jsonPresentation.presentation_submission) { + agentContext.config.logger.error( + 'Received presentation in PEX proof format without presentation submission. This should not happen.' + ) + return false + } + + if (!request.options?.challenge) { + agentContext.config.logger.error( + 'Received presentation in PEX proof format without challenge. This should not happen.' + ) + return false + } + + try { + ps.validatePresentationDefinition(request.presentation_definition) + ps.validatePresentationSubmission(jsonPresentation.presentation_submission) + ps.validatePresentation(request.presentation_definition, parsedPresentation) + + let verificationResult: W3cVerifyPresentationResult + + // FIXME: for some reason it won't accept the input if it doesn't know + // whether it's a JWT or JSON-LD VP even though the input is the same. + // Not sure how to fix + if (parsedPresentation.claimFormat === ClaimFormat.JwtVp) { + verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { + presentation: parsedPresentation, + challenge: request.options.challenge, + domain: request.options.domain, + }) + } else if (parsedPresentation.claimFormat === ClaimFormat.LdpVp) { + if ( + this.shouldVerifyUsingAnonCredsDataIntegrity(parsedPresentation, jsonPresentation.presentation_submission) + ) { + const dataIntegrityService = agentContext.dependencyManager.resolve( + AnonCredsDataIntegrityServiceSymbol + ) + const proofVerificationResult = await dataIntegrityService.verifyPresentation(agentContext, { + presentation: parsedPresentation as W3cJsonLdVerifiablePresentation, + presentationDefinition: request.presentation_definition, + presentationSubmission: jsonPresentation.presentation_submission, + challenge: request.options.challenge, + }) + + verificationResult = { + isValid: proofVerificationResult, + validations: {}, + error: { + name: 'DataIntegrityError', + message: 'Verifying the Data Integrity Proof failed. An unknown error occurred.', + }, + } + } else { + verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { + presentation: parsedPresentation, + challenge: request.options.challenge, + domain: request.options.domain, + }) + } + } else { + agentContext.config.logger.error( + `Received presentation in PEX proof format with unsupported format ${parsedPresentation['claimFormat']}.` + ) + return false + } + + if (!verificationResult.isValid) { + agentContext.config.logger.error( + `Received presentation in PEX proof format that could not be verified: ${verificationResult.error}`, + { verificationResult } + ) + return false + } + + return true + } catch (e) { + agentContext.config.logger.error(`Failed to verify presentation in PEX proof format service: ${e.message}`, { + cause: e, + }) + return false + } + } + + public async getCredentialsForRequest( + agentContext: AgentContext, + { requestAttachment }: ProofFormatGetCredentialsForRequestOptions + ) { + const ps = this.presentationExchangeService(agentContext) + const { presentation_definition: presentationDefinition } = + requestAttachment.getDataAsJson() + + ps.validatePresentationDefinition(presentationDefinition) + + const presentationSubmission = await ps.getCredentialsForRequest(agentContext, presentationDefinition) + return presentationSubmission + } + + public async selectCredentialsForRequest( + agentContext: AgentContext, + { requestAttachment }: ProofFormatSelectCredentialsForRequestOptions + ) { + const ps = this.presentationExchangeService(agentContext) + const { presentation_definition: presentationDefinition } = + requestAttachment.getDataAsJson() + + const credentialsForRequest = await ps.getCredentialsForRequest(agentContext, presentationDefinition) + return { credentials: ps.selectCredentialsForRequest(credentialsForRequest) } + } + + public async shouldAutoRespondToProposal( + _agentContext: AgentContext, + { requestAttachment, proposalAttachment }: ProofFormatAutoRespondProposalOptions + ): Promise { + const proposalData = proposalAttachment.getDataAsJson() + const requestData = requestAttachment.getDataAsJson() + + return deepEquality(requestData.presentation_definition, proposalData) + } + + public async shouldAutoRespondToRequest( + _agentContext: AgentContext, + { requestAttachment, proposalAttachment }: ProofFormatAutoRespondRequestOptions + ): Promise { + const proposalData = proposalAttachment.getDataAsJson() + const requestData = requestAttachment.getDataAsJson() + + return deepEquality(requestData.presentation_definition, proposalData) + } + + /** + * + * The presentation is already verified in processPresentation, so we can just return true here. + * It's only an ack, so it's just that we received the presentation. + * + */ + public async shouldAutoRespondToPresentation( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _options: ProofFormatAutoRespondPresentationOptions + ): Promise { + return true + } + + private getFormatData(data: unknown, id: string): Attachment { + const attachment = new Attachment({ + id, + mimeType: 'application/json', + data: new AttachmentData({ + json: data as JsonValue, + }), + }) + + return attachment + } +} diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts new file mode 100644 index 0000000000..e02bc8b08e --- /dev/null +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -0,0 +1,205 @@ +import type { DifPresentationExchangeDefinitionV1 } from '../../../../dif-presentation-exchange' +import type { ProofFormatService } from '../../ProofFormatService' +import type { DifPresentationExchangeProofFormat } from '../DifPresentationExchangeProofFormat' + +import { PresentationSubmissionLocation } from '@sphereon/pex' + +import { getInMemoryAgentOptions } from '../../../../../../tests' +import { Agent } from '../../../../../agent/Agent' +import { DifPresentationExchangeModule, DifPresentationExchangeService } from '../../../../dif-presentation-exchange' +import { + W3cJsonLdVerifiableCredential, + W3cCredentialRecord, + W3cCredentialRepository, + CREDENTIALS_CONTEXT_V1_URL, + W3cJsonLdVerifiablePresentation, +} from '../../../../vc' +import { ProofsModule } from '../../../ProofsModule' +import { ProofRole, ProofState } from '../../../models' +import { V2ProofProtocol } from '../../../protocol' +import { ProofExchangeRecord } from '../../../repository' +import { DifPresentationExchangeProofFormatService } from '../DifPresentationExchangeProofFormatService' + +const mockProofRecord = () => + new ProofExchangeRecord({ + state: ProofState.ProposalSent, + threadId: 'add7e1a0-109e-4f37-9caa-cfd0fcdfe540', + protocolVersion: 'v2', + role: ProofRole.Prover, + }) + +const mockPresentationDefinition = (): DifPresentationExchangeDefinitionV1 => ({ + id: '32f54163-7166-48f1-93d8-ff217bdb0653', + input_descriptors: [ + { + schema: [{ uri: 'https://www.w3.org/2018/credentials/examples/v1' }], + id: 'wa_driver_license', + name: 'Washington State Business License', + purpose: 'We can only allow licensed Washington State business representatives into the WA Business Conference', + constraints: { + fields: [ + { + path: ['$.credentialSubject.id'], + }, + ], + }, + }, + ], +}) + +const mockCredentialRecord = new W3cCredentialRecord({ + tags: {}, + credential: new W3cJsonLdVerifiableCredential({ + id: 'did:some:id', + context: [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + }, + proof: { + type: 'Ed25519Signature2020', + created: '2021-11-13T18:19:39Z', + verificationMethod: 'https://example.edu/issuers/14#key-1', + proofPurpose: 'assertionMethod', + proofValue: 'z58DAdFfa9SkqZMVPxAQpic7ndSayn1PzZs6ZjWp1CktyGesjuTSwRdoWhAfGFCF5bppETSTojQCrfFPP2oumHKtz', + }, + }), +}) + +const presentationSubmission = { id: 'did:id', definition_id: 'my-id', descriptor_map: [] } +jest.spyOn(W3cCredentialRepository.prototype, 'findByQuery').mockResolvedValue([mockCredentialRecord]) +jest.spyOn(DifPresentationExchangeService.prototype, 'createPresentation').mockResolvedValue({ + presentationSubmission, + verifiablePresentations: [ + new W3cJsonLdVerifiablePresentation({ + verifiableCredential: [mockCredentialRecord.credential], + proof: { + type: 'Ed25519Signature2020', + created: '2021-11-13T18:19:39Z', + verificationMethod: 'https://example.edu/issuers/14#key-1', + proofPurpose: 'assertionMethod', + proofValue: 'z58DAdFfa9SkqZMVPxAQpic7ndSayn1PzZs6ZjWp1CktyGesjuTSwRdoWhAfGFCF5bppETSTojQCrfFPP2oumHKtz', + }, + }), + ], + presentationSubmissionLocation: PresentationSubmissionLocation.PRESENTATION, +}) + +describe('Presentation Exchange ProofFormatService', () => { + let pexFormatService: ProofFormatService + let agent: Agent + + beforeAll(async () => { + agent = new Agent( + getInMemoryAgentOptions( + 'PresentationExchangeProofFormatService', + {}, + { + pex: new DifPresentationExchangeModule(), + proofs: new ProofsModule({ + proofProtocols: [new V2ProofProtocol({ proofFormats: [new DifPresentationExchangeProofFormatService()] })], + }), + } + ) + ) + + await agent.initialize() + + pexFormatService = agent.dependencyManager.resolve(DifPresentationExchangeProofFormatService) + }) + + describe('Create Presentation Exchange Proof Proposal / Request', () => { + test('Creates Presentation Exchange Proposal', async () => { + const presentationDefinition = mockPresentationDefinition() + const { format, attachment } = await pexFormatService.createProposal(agent.context, { + proofRecord: mockProofRecord(), + proofFormats: { presentationExchange: { presentationDefinition } }, + }) + + expect(attachment).toMatchObject({ + id: expect.any(String), + mimeType: 'application/json', + data: { + json: presentationDefinition, + }, + }) + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }) + }) + + test('Creates Presentation Exchange Request', async () => { + const presentationDefinition = mockPresentationDefinition() + const { format, attachment } = await pexFormatService.createRequest(agent.context, { + proofRecord: mockProofRecord(), + proofFormats: { presentationExchange: { presentationDefinition } }, + }) + + expect(attachment).toMatchObject({ + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + options: { + challenge: expect.any(String), + }, + presentation_definition: presentationDefinition, + }, + }, + }) + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }) + }) + }) + + describe('Accept Proof Request', () => { + test('Accept a Presentation Exchange Proof Request', async () => { + const presentationDefinition = mockPresentationDefinition() + const { attachment: requestAttachment } = await pexFormatService.createRequest(agent.context, { + proofRecord: mockProofRecord(), + proofFormats: { presentationExchange: { presentationDefinition } }, + }) + + const { attachment, format } = await pexFormatService.acceptRequest(agent.context, { + proofRecord: mockProofRecord(), + requestAttachment, + }) + + expect(attachment).toMatchObject({ + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + '@context': expect.any(Array), + type: expect.any(Array), + verifiableCredential: [ + { + '@context': expect.any(Array), + id: expect.any(String), + type: expect.any(Array), + issuer: expect.any(String), + issuanceDate: expect.any(String), + credentialSubject: { + id: expect.any(String), + }, + proof: expect.any(Object), + }, + ], + }, + }, + }) + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/submission@v1.0', + }) + }) + }) +}) diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts new file mode 100644 index 0000000000..b8a8c35e4e --- /dev/null +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts @@ -0,0 +1,2 @@ +export * from './DifPresentationExchangeProofFormat' +export * from './DifPresentationExchangeProofFormatService' diff --git a/packages/core/src/modules/proofs/formats/index.ts b/packages/core/src/modules/proofs/formats/index.ts new file mode 100644 index 0000000000..a2cc952c57 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/index.ts @@ -0,0 +1,9 @@ +export * from './ProofFormat' +export * from './ProofFormatService' +export * from './ProofFormatServiceOptions' + +export * from './dif-presentation-exchange' + +import * as ProofFormatServiceOptions from './ProofFormatServiceOptions' + +export { ProofFormatServiceOptions } diff --git a/packages/core/src/modules/proofs/index.ts b/packages/core/src/modules/proofs/index.ts new file mode 100644 index 0000000000..30eb44ba0f --- /dev/null +++ b/packages/core/src/modules/proofs/index.ts @@ -0,0 +1,14 @@ +export * from './errors' +export * from './formats' +export * from './models' +export * from './protocol' +export * from './repository' +export * from './ProofEvents' + +// Api +export * from './ProofsApi' +export * from './ProofsApiOptions' + +// Module +export * from './ProofsModule' +export * from './ProofsModuleConfig' diff --git a/packages/core/src/modules/proofs/models/ProofAutoAcceptType.ts b/packages/core/src/modules/proofs/models/ProofAutoAcceptType.ts new file mode 100644 index 0000000000..53d80c89b9 --- /dev/null +++ b/packages/core/src/modules/proofs/models/ProofAutoAcceptType.ts @@ -0,0 +1,13 @@ +/** + * Typing of the state for auto acceptance + */ +export enum AutoAcceptProof { + // Always auto accepts the proof no matter if it changed in subsequent steps + Always = 'always', + + // Needs one acceptation and the rest will be automated if nothing changes + ContentApproved = 'contentApproved', + + // DEFAULT: Never auto accept a proof + Never = 'never', +} diff --git a/packages/core/src/modules/proofs/models/ProofFormatSpec.ts b/packages/core/src/modules/proofs/models/ProofFormatSpec.ts new file mode 100644 index 0000000000..54c0b40f73 --- /dev/null +++ b/packages/core/src/modules/proofs/models/ProofFormatSpec.ts @@ -0,0 +1,25 @@ +import { Expose } from 'class-transformer' +import { IsString } from 'class-validator' + +import { uuid } from '../../../utils/uuid' + +export interface ProofFormatSpecOptions { + attachmentId?: string + format: string +} + +export class ProofFormatSpec { + public constructor(options: ProofFormatSpecOptions) { + if (options) { + this.attachmentId = options.attachmentId ?? uuid() + this.format = options.format + } + } + + @Expose({ name: 'attach_id' }) + @IsString() + public attachmentId!: string + + @IsString() + public format!: string +} diff --git a/packages/core/src/modules/proofs/models/ProofRole.ts b/packages/core/src/modules/proofs/models/ProofRole.ts new file mode 100644 index 0000000000..8a3dffa5a5 --- /dev/null +++ b/packages/core/src/modules/proofs/models/ProofRole.ts @@ -0,0 +1,4 @@ +export enum ProofRole { + Verifier = 'verifier', + Prover = 'prover', +} diff --git a/src/lib/modules/proofs/ProofState.ts b/packages/core/src/modules/proofs/models/ProofState.ts similarity index 89% rename from src/lib/modules/proofs/ProofState.ts rename to packages/core/src/modules/proofs/models/ProofState.ts index 5446b23790..e10b5d1ff8 100644 --- a/src/lib/modules/proofs/ProofState.ts +++ b/packages/core/src/modules/proofs/models/ProofState.ts @@ -10,5 +10,7 @@ export enum ProofState { RequestReceived = 'request-received', PresentationSent = 'presentation-sent', PresentationReceived = 'presentation-received', + Declined = 'declined', + Abandoned = 'abandoned', Done = 'done', } diff --git a/packages/core/src/modules/proofs/models/__tests__/ProofState.test.ts b/packages/core/src/modules/proofs/models/__tests__/ProofState.test.ts new file mode 100644 index 0000000000..9cabafd183 --- /dev/null +++ b/packages/core/src/modules/proofs/models/__tests__/ProofState.test.ts @@ -0,0 +1,14 @@ +import { ProofState } from '../ProofState' + +describe('ProofState', () => { + test('state matches Present Proof 1.0 (RFC 0037) state value', () => { + expect(ProofState.ProposalSent).toBe('proposal-sent') + expect(ProofState.ProposalReceived).toBe('proposal-received') + expect(ProofState.RequestSent).toBe('request-sent') + expect(ProofState.RequestReceived).toBe('request-received') + expect(ProofState.PresentationSent).toBe('presentation-sent') + expect(ProofState.PresentationReceived).toBe('presentation-received') + expect(ProofState.Declined).toBe('declined') + expect(ProofState.Done).toBe('done') + }) +}) diff --git a/packages/core/src/modules/proofs/models/index.ts b/packages/core/src/modules/proofs/models/index.ts new file mode 100644 index 0000000000..ee2fa1425a --- /dev/null +++ b/packages/core/src/modules/proofs/models/index.ts @@ -0,0 +1,4 @@ +export * from './ProofAutoAcceptType' +export * from './ProofState' +export * from './ProofFormatSpec' +export * from './ProofRole' diff --git a/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts b/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts new file mode 100644 index 0000000000..0dbc8c206b --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts @@ -0,0 +1,309 @@ +import type { ProofProtocol } from './ProofProtocol' +import type { + CreateProofProposalOptions, + CreateProofRequestOptions, + DeleteProofOptions, + GetProofFormatDataReturn, + CreateProofProblemReportOptions, + ProofProtocolMsgReturnType, + AcceptPresentationOptions, + AcceptProofProposalOptions, + AcceptProofRequestOptions, + GetCredentialsForRequestOptions, + GetCredentialsForRequestReturn, + NegotiateProofProposalOptions, + NegotiateProofRequestOptions, + SelectCredentialsForRequestOptions, + SelectCredentialsForRequestReturn, +} from './ProofProtocolOptions' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { FeatureRegistry } from '../../../agent/FeatureRegistry' +import type { AgentContext } from '../../../agent/context/AgentContext' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../plugins' +import type { Query, QueryOptions } from '../../../storage/StorageService' +import type { ProblemReportMessage } from '../../problem-reports' +import type { ProofStateChangedEvent } from '../ProofEvents' +import type { ExtractProofFormats, ProofFormatService } from '../formats' +import type { ProofRole } from '../models' +import type { ProofExchangeRecord } from '../repository' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { DidCommMessageRepository } from '../../../storage/didcomm' +import { ConnectionService } from '../../connections' +import { ProofEventTypes } from '../ProofEvents' +import { ProofState } from '../models/ProofState' +import { ProofRepository } from '../repository' + +export abstract class BaseProofProtocol + implements ProofProtocol +{ + public abstract readonly version: string + + public abstract register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void + + // methods for proposal + public abstract createProposal( + agentContext: AgentContext, + options: CreateProofProposalOptions + ): Promise> + public abstract processProposal(messageContext: InboundMessageContext): Promise + public abstract acceptProposal( + agentContext: AgentContext, + options: AcceptProofProposalOptions + ): Promise> + public abstract negotiateProposal( + agentContext: AgentContext, + options: NegotiateProofProposalOptions + ): Promise> + + // methods for request + public abstract createRequest( + agentContext: AgentContext, + options: CreateProofRequestOptions + ): Promise> + public abstract processRequest(messageContext: InboundMessageContext): Promise + public abstract acceptRequest( + agentContext: AgentContext, + options: AcceptProofRequestOptions + ): Promise> + public abstract negotiateRequest( + agentContext: AgentContext, + options: NegotiateProofRequestOptions + ): Promise> + + // retrieving credentials for request + public abstract getCredentialsForRequest( + agentContext: AgentContext, + options: GetCredentialsForRequestOptions + ): Promise> + public abstract selectCredentialsForRequest( + agentContext: AgentContext, + options: SelectCredentialsForRequestOptions + ): Promise> + + // methods for presentation + public abstract processPresentation(messageContext: InboundMessageContext): Promise + public abstract acceptPresentation( + agentContext: AgentContext, + options: AcceptPresentationOptions + ): Promise> + + // methods for ack + public abstract processAck(messageContext: InboundMessageContext): Promise + // method for problem report + public abstract createProblemReport( + agentContext: AgentContext, + options: CreateProofProblemReportOptions + ): Promise> + + public abstract findProposalMessage(agentContext: AgentContext, proofExchangeId: string): Promise + public abstract findRequestMessage(agentContext: AgentContext, proofExchangeId: string): Promise + public abstract findPresentationMessage( + agentContext: AgentContext, + proofExchangeId: string + ): Promise + public abstract getFormatData( + agentContext: AgentContext, + proofExchangeId: string + ): Promise>> + + public async processProblemReport( + messageContext: InboundMessageContext + ): Promise { + const { message: proofProblemReportMessage, agentContext, connection } = messageContext + + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing problem report with message id ${proofProblemReportMessage.id}`) + + const proofRecord = await this.getByProperties(agentContext, { + threadId: proofProblemReportMessage.threadId, + }) + + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + expectedConnectionId: proofRecord.connectionId, + }) + + // This makes sure that the sender of the incoming message is authorized to do so. + if (!proofRecord?.connectionId) { + await connectionService.matchIncomingMessageToRequestMessageInOutOfBandExchange(messageContext, { + expectedConnectionId: proofRecord?.connectionId, + }) + + proofRecord.connectionId = connection?.id + } + + // Update record + proofRecord.errorMessage = `${proofProblemReportMessage.description.code}: ${proofProblemReportMessage.description.en}` + await this.updateState(agentContext, proofRecord, ProofState.Abandoned) + return proofRecord + } + + /** + * Update the record to a new state and emit an state changed event. Also updates the record + * in storage. + * + * @param proofRecord The proof record to update the state for + * @param newState The state to update to + * + */ + public async updateState(agentContext: AgentContext, proofRecord: ProofExchangeRecord, newState: ProofState) { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + agentContext.config.logger.debug( + `Updating proof record ${proofRecord.id} to state ${newState} (previous=${proofRecord.state})` + ) + + const previousState = proofRecord.state + proofRecord.state = newState + await proofRepository.update(agentContext, proofRecord) + + this.emitStateChangedEvent(agentContext, proofRecord, previousState) + } + + protected emitStateChangedEvent( + agentContext: AgentContext, + proofRecord: ProofExchangeRecord, + previousState: ProofState | null + ) { + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + + eventEmitter.emit(agentContext, { + type: ProofEventTypes.ProofStateChanged, + payload: { + proofRecord: proofRecord.clone(), + previousState: previousState, + }, + }) + } + + /** + * Retrieve a proof record by id + * + * @param proofRecordId The proof record id + * @throws {RecordNotFoundError} If no record is found + * @return The proof record + * + */ + public getById(agentContext: AgentContext, proofRecordId: string): Promise { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return proofRepository.getById(agentContext, proofRecordId) + } + + /** + * Retrieve all proof records + * + * @returns List containing all proof records + */ + public getAll(agentContext: AgentContext): Promise { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return proofRepository.getAll(agentContext) + } + + public async findAllByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ): Promise { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return proofRepository.findByQuery(agentContext, query, queryOptions) + } + + /** + * Find a proof record by id + * + * @param proofRecordId the proof record id + * @returns The proof record or null if not found + */ + public findById(agentContext: AgentContext, proofRecordId: string): Promise { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return proofRepository.findById(agentContext, proofRecordId) + } + + public async delete( + agentContext: AgentContext, + proofRecord: ProofExchangeRecord, + options?: DeleteProofOptions + ): Promise { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + await proofRepository.delete(agentContext, proofRecord) + + const deleteAssociatedDidCommMessages = options?.deleteAssociatedDidCommMessages ?? true + + if (deleteAssociatedDidCommMessages) { + const didCommMessages = await didCommMessageRepository.findByQuery(agentContext, { + associatedRecordId: proofRecord.id, + }) + for (const didCommMessage of didCommMessages) { + await didCommMessageRepository.delete(agentContext, didCommMessage) + } + } + } + + /** + * Retrieve a proof record by connection id and thread id + * + * @param properties Properties to query by + * + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * + * @returns The proof record + */ + public getByProperties( + agentContext: AgentContext, + properties: { + threadId: string + role?: ProofRole + connectionId?: string + } + ): Promise { + const { threadId, connectionId, role } = properties + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return proofRepository.getSingleByQuery(agentContext, { + connectionId, + threadId, + role, + }) + } + + /** + * Find a proof record by connection id and thread id, returns null if not found + * + * @param properties Properties to query by + * + * @returns The proof record + */ + public findByProperties( + agentContext: AgentContext, + properties: { + threadId: string + role?: ProofRole + connectionId?: string + } + ): Promise { + const { role, connectionId, threadId } = properties + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return proofRepository.findSingleByQuery(agentContext, { + connectionId, + threadId, + role, + }) + } + + public async update(agentContext: AgentContext, proofRecord: ProofExchangeRecord) { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return await proofRepository.update(agentContext, proofRecord) + } +} diff --git a/packages/core/src/modules/proofs/protocol/ProofProtocol.ts b/packages/core/src/modules/proofs/protocol/ProofProtocol.ts new file mode 100644 index 0000000000..fe00033531 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/ProofProtocol.ts @@ -0,0 +1,128 @@ +import type { + CreateProofProposalOptions, + CreateProofRequestOptions, + DeleteProofOptions, + GetProofFormatDataReturn, + CreateProofProblemReportOptions, + ProofProtocolMsgReturnType, + AcceptProofProposalOptions, + NegotiateProofProposalOptions, + AcceptProofRequestOptions, + NegotiateProofRequestOptions, + AcceptPresentationOptions, + GetCredentialsForRequestOptions, + GetCredentialsForRequestReturn, + SelectCredentialsForRequestOptions, + SelectCredentialsForRequestReturn, +} from './ProofProtocolOptions' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { FeatureRegistry } from '../../../agent/FeatureRegistry' +import type { AgentContext } from '../../../agent/context/AgentContext' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../plugins' +import type { Query, QueryOptions } from '../../../storage/StorageService' +import type { ProblemReportMessage } from '../../problem-reports' +import type { ExtractProofFormats, ProofFormatService } from '../formats' +import type { ProofRole } from '../models' +import type { ProofState } from '../models/ProofState' +import type { ProofExchangeRecord } from '../repository' + +export interface ProofProtocol { + readonly version: string + + // methods for proposal + createProposal( + agentContext: AgentContext, + options: CreateProofProposalOptions + ): Promise> + processProposal(messageContext: InboundMessageContext): Promise + acceptProposal( + agentContext: AgentContext, + options: AcceptProofProposalOptions + ): Promise> + negotiateProposal( + agentContext: AgentContext, + options: NegotiateProofProposalOptions + ): Promise> + + // methods for request + createRequest( + agentContext: AgentContext, + options: CreateProofRequestOptions + ): Promise> + processRequest(messageContext: InboundMessageContext): Promise + acceptRequest( + agentContext: AgentContext, + options: AcceptProofRequestOptions + ): Promise> + negotiateRequest( + agentContext: AgentContext, + options: NegotiateProofRequestOptions + ): Promise> + + // retrieving credentials for request + getCredentialsForRequest( + agentContext: AgentContext, + options: GetCredentialsForRequestOptions + ): Promise> + selectCredentialsForRequest( + agentContext: AgentContext, + options: SelectCredentialsForRequestOptions + ): Promise> + + // methods for presentation + processPresentation(messageContext: InboundMessageContext): Promise + acceptPresentation( + agentContext: AgentContext, + options: AcceptPresentationOptions + ): Promise> + + // methods for ack + processAck(messageContext: InboundMessageContext): Promise + + // method for problem report + createProblemReport( + agentContext: AgentContext, + options: CreateProofProblemReportOptions + ): Promise> + processProblemReport(messageContext: InboundMessageContext): Promise + + findProposalMessage(agentContext: AgentContext, proofExchangeId: string): Promise + findRequestMessage(agentContext: AgentContext, proofExchangeId: string): Promise + findPresentationMessage(agentContext: AgentContext, proofExchangeId: string): Promise + getFormatData( + agentContext: AgentContext, + proofExchangeId: string + ): Promise>> + + // repository methods + updateState(agentContext: AgentContext, proofRecord: ProofExchangeRecord, newState: ProofState): Promise + getById(agentContext: AgentContext, proofExchangeId: string): Promise + getAll(agentContext: AgentContext): Promise + findAllByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ): Promise + findById(agentContext: AgentContext, proofExchangeId: string): Promise + delete(agentContext: AgentContext, proofRecord: ProofExchangeRecord, options?: DeleteProofOptions): Promise + getByProperties( + agentContext: AgentContext, + properties: { + threadId: string + connectionId?: string + role?: ProofRole + } + ): Promise + findByProperties( + agentContext: AgentContext, + properties: { + threadId: string + connectionId?: string + role?: ProofRole + } + ): Promise + update(agentContext: AgentContext, proofRecord: ProofExchangeRecord): Promise + + register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void +} diff --git a/packages/core/src/modules/proofs/protocol/ProofProtocolOptions.ts b/packages/core/src/modules/proofs/protocol/ProofProtocolOptions.ts new file mode 100644 index 0000000000..e1860bfe07 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/ProofProtocolOptions.ts @@ -0,0 +1,174 @@ +import type { ProofProtocol } from './ProofProtocol' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { ConnectionRecord } from '../../connections' +import type { + ExtractProofFormats, + ProofFormat, + ProofFormatCredentialForRequestPayload, + ProofFormatPayload, + ProofFormatService, +} from '../formats' +import type { AutoAcceptProof } from '../models' +import type { ProofExchangeRecord } from '../repository' + +/** + * Get the format data payload for a specific message from a list of ProofFormat interfaces and a message + * + * For an indy offer, this resolves to the proof request format as defined here: + * https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0592-indy-attachments#proof-request-format + * + * @example + * ``` + * + * type RequestFormatData = ProofFormatDataMessagePayload<[IndyProofFormat, PresentationExchangeProofFormat], 'createRequest'> + * + * // equal to + * type RequestFormatData = { + * indy: { + * // ... payload for indy proof request attachment as defined in RFC 0592 ... + * }, + * presentationExchange: { + * // ... payload for presentation exchange request attachment as defined in RFC 0510 ... + * } + * } + * ``` + */ +export type ProofFormatDataMessagePayload< + CFs extends ProofFormat[] = ProofFormat[], + M extends keyof ProofFormat['formatData'] = keyof ProofFormat['formatData'] +> = { + [ProofFormat in CFs[number] as ProofFormat['formatKey']]?: ProofFormat['formatData'][M] +} + +/** + * Infer the {@link ProofFormat} types based on an array of {@link ProofProtocol} types. + * + * It does this by extracting the `ProofFormatServices` generic from the `ProofProtocol`, and + * then extracting the `ProofFormat` generic from each of the `ProofFormatService` types. + * + * @example + * ``` + * // TheProofFormatServices is now equal to [IndyProofFormatService] + * type TheProofFormatServices = ProofFormatsFromProtocols<[V1ProofProtocol]> + * ``` + * + * Because the `V1ProofProtocol` is defined as follows: + * ``` + * class V1ProofProtocol implements ProofProtocol<[IndyProofFormatService]> { + * } + * ``` + */ +export type ProofFormatsFromProtocols = Type[number] extends ProofProtocol< + infer ProofFormatServices +> + ? ProofFormatServices extends ProofFormatService[] + ? ExtractProofFormats + : never + : never + +export type GetProofFormatDataReturn = { + proposal?: ProofFormatDataMessagePayload + request?: ProofFormatDataMessagePayload + presentation?: ProofFormatDataMessagePayload +} + +interface BaseOptions { + comment?: string + autoAcceptProof?: AutoAcceptProof + + /** + * Will be ignored for v1 protocol as it is not supported + */ + goalCode?: string + + /** + * Will be ignored for v1 protocol as it is not supported + */ + goal?: string +} + +export interface CreateProofProposalOptions extends BaseOptions { + connectionRecord: ConnectionRecord + proofFormats: ProofFormatPayload, 'createProposal'> + parentThreadId?: string +} + +export interface AcceptProofProposalOptions extends BaseOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload, 'acceptProposal'> + + /** @default true */ + willConfirm?: boolean +} + +export interface NegotiateProofProposalOptions extends BaseOptions { + proofRecord: ProofExchangeRecord + proofFormats: ProofFormatPayload, 'createRequest'> + + /** @default true */ + willConfirm?: boolean +} + +export interface CreateProofRequestOptions extends BaseOptions { + // Create request can also be used for connection-less, so connection is optional + connectionRecord?: ConnectionRecord + proofFormats: ProofFormatPayload, 'createRequest'> + parentThreadId?: string + + /** @default true */ + willConfirm?: boolean +} + +export interface AcceptProofRequestOptions extends BaseOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload, 'acceptRequest'> +} + +export interface NegotiateProofRequestOptions extends BaseOptions { + proofRecord: ProofExchangeRecord + proofFormats: ProofFormatPayload, 'createProposal'> +} + +export interface GetCredentialsForRequestOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatCredentialForRequestPayload, 'getCredentialsForRequest', 'input'> +} + +export interface GetCredentialsForRequestReturn { + proofFormats: ProofFormatCredentialForRequestPayload, 'getCredentialsForRequest', 'output'> +} + +export interface SelectCredentialsForRequestOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatCredentialForRequestPayload< + ExtractProofFormats, + 'selectCredentialsForRequest', + 'input' + > +} + +export interface SelectCredentialsForRequestReturn { + proofFormats: ProofFormatCredentialForRequestPayload< + ExtractProofFormats, + 'selectCredentialsForRequest', + 'output' + > +} + +export interface AcceptPresentationOptions { + proofRecord: ProofExchangeRecord +} + +export interface CreateProofProblemReportOptions { + proofRecord: ProofExchangeRecord + description: string +} + +export interface ProofProtocolMsgReturnType { + message: MessageType + proofRecord: ProofExchangeRecord +} + +export interface DeleteProofOptions { + deleteAssociatedDidCommMessages?: boolean +} diff --git a/packages/core/src/modules/proofs/protocol/index.ts b/packages/core/src/modules/proofs/protocol/index.ts new file mode 100644 index 0000000000..71799a5c45 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/index.ts @@ -0,0 +1,10 @@ +export * from './v2' +import * as ProofProtocolOptions from './ProofProtocolOptions' + +export { ProofProtocol } from './ProofProtocol' +// NOTE: ideally we don't export the BaseProofProtocol, but as the V1ProofProtocol is defined in the +// anoncreds package, we need to export it. We should at some point look at creating a core package which can be used for +// sharing internal types, and when you want to build you own modules, and an agent package, which is the one you use when +// consuming the framework +export { BaseProofProtocol } from './BaseProofProtocol' +export { ProofProtocolOptions } diff --git a/packages/core/src/modules/proofs/protocol/v2/ProofFormatCoordinator.ts b/packages/core/src/modules/proofs/protocol/v2/ProofFormatCoordinator.ts new file mode 100644 index 0000000000..abeb1463cc --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/ProofFormatCoordinator.ts @@ -0,0 +1,566 @@ +import type { AgentContext } from '../../../../agent' +import type { Attachment } from '../../../../decorators/attachment/Attachment' +import type { + ExtractProofFormats, + ProofFormatCredentialForRequestPayload, + ProofFormatPayload, + ProofFormatService, +} from '../../formats' +import type { ProofFormatSpec } from '../../models/ProofFormatSpec' +import type { ProofExchangeRecord } from '../../repository' + +import { CredoError } from '../../../../error' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage/didcomm' + +import { V2PresentationMessage, V2ProposePresentationMessage, V2RequestPresentationMessage } from './messages' + +export class ProofFormatCoordinator { + /** + * Create a {@link V2ProposePresentationMessage}. + * + * @param options + * @returns The created {@link V2ProposePresentationMessage} + * + */ + public async createProposal( + agentContext: AgentContext, + { + proofFormats, + formatServices, + proofRecord, + comment, + goalCode, + goal, + }: { + formatServices: ProofFormatService[] + proofFormats: ProofFormatPayload, 'createProposal'> + proofRecord: ProofExchangeRecord + comment?: string + goalCode?: string + goal?: string + } + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: ProofFormatSpec[] = [] + const proposalAttachments: Attachment[] = [] + + for (const formatService of formatServices) { + const { format, attachment } = await formatService.createProposal(agentContext, { + proofFormats, + proofRecord, + }) + + proposalAttachments.push(attachment) + formats.push(format) + } + + const message = new V2ProposePresentationMessage({ + id: proofRecord.threadId, + formats, + proposalAttachments, + comment: comment, + goalCode, + goal, + }) + + message.setThread({ threadId: proofRecord.threadId, parentThreadId: proofRecord.parentThreadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: proofRecord.id, + }) + + return message + } + + public async processProposal( + agentContext: AgentContext, + { + proofRecord, + message, + formatServices, + }: { + proofRecord: ProofExchangeRecord + message: V2ProposePresentationMessage + formatServices: ProofFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.formats, message.proposalAttachments) + + await formatService.processProposal(agentContext, { + attachment, + proofRecord, + }) + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: proofRecord.id, + }) + } + + public async acceptProposal( + agentContext: AgentContext, + { + proofRecord, + proofFormats, + formatServices, + comment, + goalCode, + goal, + presentMultiple, + willConfirm, + }: { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload, 'acceptProposal'> + formatServices: ProofFormatService[] + comment?: string + goalCode?: string + goal?: string + presentMultiple?: boolean + willConfirm?: boolean + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: ProofFormatSpec[] = [] + const requestAttachments: Attachment[] = [] + + const proposalMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + for (const formatService of formatServices) { + const proposalAttachment = this.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + + const { attachment, format } = await formatService.acceptProposal(agentContext, { + proofRecord, + proofFormats, + proposalAttachment, + }) + + requestAttachments.push(attachment) + formats.push(format) + } + + const message = new V2RequestPresentationMessage({ + formats, + requestAttachments, + comment, + goalCode, + goal, + presentMultiple, + willConfirm, + }) + + message.setThread({ threadId: proofRecord.threadId, parentThreadId: proofRecord.parentThreadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + return message + } + + /** + * Create a {@link V2RequestPresentationMessage}. + * + * @param options + * @returns The created {@link V2RequestPresentationMessage} + * + */ + public async createRequest( + agentContext: AgentContext, + { + proofFormats, + formatServices, + proofRecord, + comment, + goalCode, + goal, + presentMultiple, + willConfirm, + }: { + formatServices: ProofFormatService[] + proofFormats: ProofFormatPayload, 'createRequest'> + proofRecord: ProofExchangeRecord + comment?: string + goalCode?: string + goal?: string + presentMultiple?: boolean + willConfirm?: boolean + } + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: ProofFormatSpec[] = [] + const requestAttachments: Attachment[] = [] + + for (const formatService of formatServices) { + const { format, attachment } = await formatService.createRequest(agentContext, { + proofFormats, + proofRecord, + }) + + requestAttachments.push(attachment) + formats.push(format) + } + + const message = new V2RequestPresentationMessage({ + formats, + comment, + requestAttachments, + goalCode, + goal, + presentMultiple, + willConfirm, + }) + + message.setThread({ threadId: proofRecord.threadId, parentThreadId: proofRecord.parentThreadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: proofRecord.id, + }) + + return message + } + + public async processRequest( + agentContext: AgentContext, + { + proofRecord, + message, + formatServices, + }: { + proofRecord: ProofExchangeRecord + message: V2RequestPresentationMessage + formatServices: ProofFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.formats, message.requestAttachments) + + await formatService.processRequest(agentContext, { + attachment, + proofRecord, + }) + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: proofRecord.id, + }) + } + + public async acceptRequest( + agentContext: AgentContext, + { + proofRecord, + proofFormats, + formatServices, + comment, + lastPresentation, + goalCode, + goal, + }: { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload, 'acceptRequest'> + formatServices: ProofFormatService[] + comment?: string + lastPresentation?: boolean + goalCode?: string + goal?: string + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: ProofFormatSpec[] = [] + const presentationAttachments: Attachment[] = [] + + for (const formatService of formatServices) { + const requestAttachment = this.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const proposalAttachment = proposalMessage + ? this.getAttachmentForService(formatService, proposalMessage.formats, proposalMessage.proposalAttachments) + : undefined + + const { attachment, format } = await formatService.acceptRequest(agentContext, { + requestAttachment, + proposalAttachment, + proofRecord, + proofFormats, + }) + + presentationAttachments.push(attachment) + formats.push(format) + } + + const message = new V2PresentationMessage({ + formats, + presentationAttachments, + comment, + lastPresentation, + goalCode, + goal, + }) + + message.setThread({ threadId: proofRecord.threadId, parentThreadId: proofRecord.parentThreadId }) + message.setPleaseAck() + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + return message + } + + public async getCredentialsForRequest( + agentContext: AgentContext, + { + proofRecord, + proofFormats, + formatServices, + }: { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatCredentialForRequestPayload< + ExtractProofFormats, + 'getCredentialsForRequest', + 'input' + > + formatServices: ProofFormatService[] + } + ): Promise, 'getCredentialsForRequest', 'output'>> { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + const credentialsForRequest: Record = {} + + for (const formatService of formatServices) { + const requestAttachment = this.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const proposalAttachment = proposalMessage + ? this.getAttachmentForService(formatService, proposalMessage.formats, proposalMessage.proposalAttachments) + : undefined + + const credentialsForFormat = await formatService.getCredentialsForRequest(agentContext, { + requestAttachment, + proposalAttachment, + proofRecord, + proofFormats, + }) + + credentialsForRequest[formatService.formatKey] = credentialsForFormat + } + + return credentialsForRequest as ProofFormatCredentialForRequestPayload< + ExtractProofFormats, + 'getCredentialsForRequest', + 'output' + > + } + + public async selectCredentialsForRequest( + agentContext: AgentContext, + { + proofRecord, + proofFormats, + formatServices, + }: { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatCredentialForRequestPayload< + ExtractProofFormats, + 'selectCredentialsForRequest', + 'input' + > + formatServices: ProofFormatService[] + } + ): Promise< + ProofFormatCredentialForRequestPayload, 'selectCredentialsForRequest', 'output'> + > { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + const credentialsForRequest: Record = {} + + for (const formatService of formatServices) { + const requestAttachment = this.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const proposalAttachment = proposalMessage + ? this.getAttachmentForService(formatService, proposalMessage.formats, proposalMessage.proposalAttachments) + : undefined + + const credentialsForFormat = await formatService.selectCredentialsForRequest(agentContext, { + requestAttachment, + proposalAttachment, + proofRecord, + proofFormats, + }) + + credentialsForRequest[formatService.formatKey] = credentialsForFormat + } + + return credentialsForRequest as ProofFormatCredentialForRequestPayload< + ExtractProofFormats, + 'selectCredentialsForRequest', + 'output' + > + } + + public async processPresentation( + agentContext: AgentContext, + { + proofRecord, + message, + requestMessage, + formatServices, + }: { + proofRecord: ProofExchangeRecord + message: V2PresentationMessage + requestMessage: V2RequestPresentationMessage + formatServices: ProofFormatService[] + } + ): Promise<{ isValid: true; message: undefined } | { isValid: false; message: string }> { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const formatVerificationResults: boolean[] = [] + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.formats, message.presentationAttachments) + const requestAttachment = this.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + try { + // TODO: this should return a more complex object explaining why it is invalid + const isValid = await formatService.processPresentation(agentContext, { + attachment, + requestAttachment, + proofRecord, + }) + + formatVerificationResults.push(isValid) + } catch (error) { + return { + message: error.message, + isValid: false, + } + } + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: proofRecord.id, + }) + + const isValid = formatVerificationResults.every((isValid) => isValid === true) + + if (isValid) { + return { + isValid, + message: undefined, + } + } else { + return { + isValid, + message: 'Not all presentations are valid', + } + } + } + + public getAttachmentForService( + credentialFormatService: ProofFormatService, + formats: ProofFormatSpec[], + attachments: Attachment[] + ) { + const attachmentId = this.getAttachmentIdForService(credentialFormatService, formats) + const attachment = attachments.find((attachment) => attachment.id === attachmentId) + + if (!attachment) { + throw new CredoError(`Attachment with id ${attachmentId} not found in attachments.`) + } + + return attachment + } + + private getAttachmentIdForService(credentialFormatService: ProofFormatService, formats: ProofFormatSpec[]) { + const format = formats.find((format) => credentialFormatService.supportsFormat(format.format)) + + if (!format) throw new CredoError(`No attachment found for service ${credentialFormatService.formatKey}`) + + return format.attachmentId + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts new file mode 100644 index 0000000000..bc48bd8da7 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts @@ -0,0 +1,1137 @@ +import type { AgentContext } from '../../../../agent' +import type { AgentMessage } from '../../../../agent/AgentMessage' +import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../../plugins' +import type { ProblemReportMessage } from '../../../problem-reports' +import type { + ExtractProofFormats, + ProofFormat, + ProofFormatCredentialForRequestPayload, + ProofFormatPayload, +} from '../../formats' +import type { ProofFormatService } from '../../formats/ProofFormatService' +import type { ProofFormatSpec } from '../../models/ProofFormatSpec' +import type { ProofProtocol } from '../ProofProtocol' +import type { + AcceptPresentationOptions, + AcceptProofProposalOptions, + AcceptProofRequestOptions, + CreateProofProblemReportOptions, + CreateProofProposalOptions, + CreateProofRequestOptions, + ProofFormatDataMessagePayload, + GetCredentialsForRequestOptions, + GetCredentialsForRequestReturn, + GetProofFormatDataReturn, + NegotiateProofProposalOptions, + NegotiateProofRequestOptions, + ProofProtocolMsgReturnType, + SelectCredentialsForRequestOptions, + SelectCredentialsForRequestReturn, +} from '../ProofProtocolOptions' + +import { Protocol } from '../../../../agent/models' +import { CredoError } from '../../../../error' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage/didcomm' +import { uuid } from '../../../../utils/uuid' +import { AckStatus } from '../../../common' +import { ConnectionService } from '../../../connections' +import { ProofsModuleConfig } from '../../ProofsModuleConfig' +import { PresentationProblemReportReason } from '../../errors/PresentationProblemReportReason' +import { AutoAcceptProof, ProofRole, ProofState } from '../../models' +import { ProofExchangeRecord, ProofRepository } from '../../repository' +import { composeAutoAccept } from '../../utils' +import { BaseProofProtocol } from '../BaseProofProtocol' + +import { ProofFormatCoordinator } from './ProofFormatCoordinator' +import { V2PresentationProblemReportError } from './errors' +import { V2PresentationAckHandler } from './handlers/V2PresentationAckHandler' +import { V2PresentationHandler } from './handlers/V2PresentationHandler' +import { V2PresentationProblemReportHandler } from './handlers/V2PresentationProblemReportHandler' +import { V2ProposePresentationHandler } from './handlers/V2ProposePresentationHandler' +import { V2RequestPresentationHandler } from './handlers/V2RequestPresentationHandler' +import { V2PresentationAckMessage, V2RequestPresentationMessage } from './messages' +import { V2PresentationMessage } from './messages/V2PresentationMessage' +import { V2PresentationProblemReportMessage } from './messages/V2PresentationProblemReportMessage' +import { V2ProposePresentationMessage } from './messages/V2ProposePresentationMessage' + +export interface V2ProofProtocolConfig { + proofFormats: ProofFormatServices +} + +export class V2ProofProtocol + extends BaseProofProtocol + implements ProofProtocol +{ + private proofFormatCoordinator = new ProofFormatCoordinator() + private proofFormats: PFs + + public constructor({ proofFormats }: V2ProofProtocolConfig) { + super() + + this.proofFormats = proofFormats + } + + /** + * The version of the present proof protocol this service supports + */ + public readonly version = 'v2' as const + + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Register message handlers for the Present Proof V2 Protocol + dependencyManager.registerMessageHandlers([ + new V2ProposePresentationHandler(this), + new V2RequestPresentationHandler(this), + new V2PresentationHandler(this), + new V2PresentationAckHandler(this), + new V2PresentationProblemReportHandler(this), + ]) + + // Register Present Proof V2 in feature registry, with supported roles + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/present-proof/2.0', + roles: ['prover', 'verifier'], + }) + ) + } + + public async createProposal( + agentContext: AgentContext, + { + connectionRecord, + proofFormats, + comment, + autoAcceptProof, + goalCode, + goal, + parentThreadId, + }: CreateProofProposalOptions + ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + const formatServices = this.getFormatServices(proofFormats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to create proposal. No supported formats`) + } + + const proofRecord = new ProofExchangeRecord({ + connectionId: connectionRecord.id, + threadId: uuid(), + parentThreadId, + state: ProofState.ProposalSent, + role: ProofRole.Prover, + protocolVersion: 'v2', + autoAcceptProof, + }) + + const proposalMessage = await this.proofFormatCoordinator.createProposal(agentContext, { + proofFormats, + proofRecord, + formatServices, + comment, + goalCode, + goal, + }) + + agentContext.config.logger.debug('Save record and emit state change event') + await proofRepository.save(agentContext, proofRecord) + this.emitStateChangedEvent(agentContext, proofRecord, null) + + return { + proofRecord, + message: proposalMessage, + } + } + + /** + * Method called by {@link V2ProposeCredentialHandler} on reception of a propose presentation message + * We do the necessary processing here to accept the proposal and do the state change, emit event etc. + * @param messageContext the inbound propose presentation message + * @returns proof record appropriate for this incoming message (once accepted) + */ + public async processProposal( + messageContext: InboundMessageContext + ): Promise { + const { message: proposalMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing presentation proposal with id ${proposalMessage.id}`) + + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + let proofRecord = await this.findByProperties(messageContext.agentContext, { + threadId: proposalMessage.threadId, + role: ProofRole.Verifier, + connectionId: connection?.id, + }) + + const formatServices = this.getFormatServicesFromMessage(proposalMessage.formats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to process proposal. No supported formats`) + } + + // credential record already exists + if (proofRecord) { + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + role: DidCommMessageRole.Receiver, + }) + const lastSentMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + role: DidCommMessageRole.Sender, + }) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.RequestSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, + expectedConnectionId: proofRecord.connectionId, + }) + + await this.proofFormatCoordinator.processProposal(messageContext.agentContext, { + proofRecord, + formatServices, + message: proposalMessage, + }) + + await this.updateState(messageContext.agentContext, proofRecord, ProofState.ProposalReceived) + + return proofRecord + } else { + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + + // No proof record exists with thread id + proofRecord = new ProofExchangeRecord({ + connectionId: connection?.id, + threadId: proposalMessage.threadId, + state: ProofState.ProposalReceived, + role: ProofRole.Verifier, + protocolVersion: 'v2', + parentThreadId: proposalMessage.thread?.parentThreadId, + }) + + await this.proofFormatCoordinator.processProposal(messageContext.agentContext, { + proofRecord, + formatServices, + message: proposalMessage, + }) + + // Save record and emit event + await proofRepository.save(messageContext.agentContext, proofRecord) + this.emitStateChangedEvent(messageContext.agentContext, proofRecord, null) + + return proofRecord + } + } + + public async acceptProposal( + agentContext: AgentContext, + { + proofRecord, + proofFormats, + autoAcceptProof, + comment, + goalCode, + goal, + willConfirm, + }: AcceptProofProposalOptions + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.ProposalReceived) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Use empty proofFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(proofFormats ?? {}) + + // if no format services could be extracted from the proofFormats + // take all available format services from the proposal message + if (formatServices.length === 0) { + const proposalMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + formatServices = this.getFormatServicesFromMessage(proposalMessage.formats) + } + + // If the format services list is still empty, throw an error as we don't support any + // of the formats + if (formatServices.length === 0) { + throw new CredoError(`Unable to accept proposal. No supported formats provided as input or in proposal message`) + } + + const requestMessage = await this.proofFormatCoordinator.acceptProposal(agentContext, { + proofRecord, + formatServices, + comment, + proofFormats, + goalCode, + goal, + willConfirm, + // Not supported at the moment + presentMultiple: false, + }) + + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.RequestSent) + + return { proofRecord, message: requestMessage } + } + + /** + * Negotiate a proof proposal as verifier (by sending a proof request message) to the connection + * associated with the proof record. + * + * @param options configuration for the request see {@link NegotiateProofProposalOptions} + * @returns Proof exchange record associated with the proof request + * + */ + public async negotiateProposal( + agentContext: AgentContext, + { + proofRecord, + proofFormats, + autoAcceptProof, + comment, + goalCode, + goal, + willConfirm, + }: NegotiateProofProposalOptions + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.ProposalReceived) + + if (!proofRecord.connectionId) { + throw new CredoError( + `No connectionId found for proof record '${proofRecord.id}'. Connection-less verification does not support negotiation.` + ) + } + + const formatServices = this.getFormatServices(proofFormats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to create request. No supported formats`) + } + + const requestMessage = await this.proofFormatCoordinator.createRequest(agentContext, { + formatServices, + proofFormats, + proofRecord, + comment, + goalCode, + goal, + willConfirm, + // Not supported at the moment + presentMultiple: false, + }) + + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.RequestSent) + + return { proofRecord, message: requestMessage } + } + + /** + * Create a {@link V2RequestPresentationMessage} as beginning of protocol process. + * @returns Object containing request message and associated credential record + * + */ + public async createRequest( + agentContext: AgentContext, + { + proofFormats, + autoAcceptProof, + comment, + connectionRecord, + parentThreadId, + goalCode, + goal, + willConfirm, + }: CreateProofRequestOptions + ): Promise> { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + const formatServices = this.getFormatServices(proofFormats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to create request. No supported formats`) + } + + const proofRecord = new ProofExchangeRecord({ + connectionId: connectionRecord?.id, + threadId: uuid(), + state: ProofState.RequestSent, + role: ProofRole.Verifier, + autoAcceptProof, + protocolVersion: 'v2', + parentThreadId, + }) + + const requestMessage = await this.proofFormatCoordinator.createRequest(agentContext, { + formatServices, + proofFormats, + proofRecord, + comment, + goalCode, + goal, + willConfirm, + }) + + agentContext.config.logger.debug( + `Saving record and emitting state changed for proof exchange record ${proofRecord.id}` + ) + await proofRepository.save(agentContext, proofRecord) + this.emitStateChangedEvent(agentContext, proofRecord, null) + + return { proofRecord, message: requestMessage } + } + + /** + * Process a received {@link V2RequestPresentationMessage}. This will not accept the proof request + * or send a proof. It will only update the existing proof record with + * the information from the proof request message. Use {@link createCredential} + * after calling this method to create a proof. + *z + * @param messageContext The message context containing a v2 proof request message + * @returns proof record associated with the proof request message + * + */ + public async processRequest( + messageContext: InboundMessageContext + ): Promise { + const { message: requestMessage, connection, agentContext } = messageContext + + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing proof request with id ${requestMessage.id}`) + + let proofRecord = await this.findByProperties(messageContext.agentContext, { + threadId: requestMessage.threadId, + role: ProofRole.Prover, + connectionId: connection?.id, + }) + + const formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to process request. No supported formats`) + } + + // proof record already exists + if (proofRecord) { + const lastSentMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + role: DidCommMessageRole.Sender, + }) + + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.ProposalSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, + expectedConnectionId: proofRecord.connectionId, + }) + + await this.proofFormatCoordinator.processRequest(messageContext.agentContext, { + proofRecord, + formatServices, + message: requestMessage, + }) + + await this.updateState(messageContext.agentContext, proofRecord, ProofState.RequestReceived) + return proofRecord + } else { + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + + // No proof record exists with thread id + agentContext.config.logger.debug('No proof record found for request, creating a new one') + proofRecord = new ProofExchangeRecord({ + connectionId: connection?.id, + threadId: requestMessage.threadId, + state: ProofState.RequestReceived, + role: ProofRole.Prover, + protocolVersion: 'v2', + parentThreadId: requestMessage.thread?.parentThreadId, + }) + + await this.proofFormatCoordinator.processRequest(messageContext.agentContext, { + proofRecord, + formatServices, + message: requestMessage, + }) + + // Save in repository + agentContext.config.logger.debug('Saving proof record and emit request-received event') + await proofRepository.save(messageContext.agentContext, proofRecord) + + this.emitStateChangedEvent(messageContext.agentContext, proofRecord, null) + return proofRecord + } + } + + public async acceptRequest( + agentContext: AgentContext, + { proofRecord, autoAcceptProof, comment, proofFormats, goalCode, goal }: AcceptProofRequestOptions + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.RequestReceived) + + // Use empty proofFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(proofFormats ?? {}) + + // if no format services could be extracted from the proofFormats + // take all available format services from the request message + if (formatServices.length === 0) { + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + } + + // If the format services list is still empty, throw an error as we don't support any + // of the formats + if (formatServices.length === 0) { + throw new CredoError(`Unable to accept request. No supported formats provided as input or in request message`) + } + const message = await this.proofFormatCoordinator.acceptRequest(agentContext, { + proofRecord, + formatServices, + comment, + proofFormats, + goalCode, + goal, + // Sending multiple presentation messages not supported at the moment + lastPresentation: true, + }) + + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.PresentationSent) + + return { proofRecord, message } + } + + /** + * Create a {@link V2ProposePresentationMessage} as response to a received credential request. + * To create a proposal not bound to an existing proof exchange, use {@link createProposal}. + * + * @param options configuration to use for the proposal + * @returns Object containing proposal message and associated proof record + * + */ + public async negotiateRequest( + agentContext: AgentContext, + { proofRecord, proofFormats, autoAcceptProof, comment, goalCode, goal }: NegotiateProofRequestOptions + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.RequestReceived) + + if (!proofRecord.connectionId) { + throw new CredoError( + `No connectionId found for proof record '${proofRecord.id}'. Connection-less verification does not support negotiation.` + ) + } + + const formatServices = this.getFormatServices(proofFormats) + if (formatServices.length === 0) { + throw new CredoError(`Unable to create proposal. No supported formats`) + } + + const proposalMessage = await this.proofFormatCoordinator.createProposal(agentContext, { + formatServices, + proofFormats, + proofRecord, + comment, + goalCode, + goal, + }) + + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.ProposalSent) + + return { proofRecord, message: proposalMessage } + } + + public async getCredentialsForRequest( + agentContext: AgentContext, + { proofRecord, proofFormats }: GetCredentialsForRequestOptions + ): Promise> { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.RequestReceived) + + // Use empty proofFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(proofFormats ?? {}) + + // if no format services could be extracted from the proofFormats + // take all available format services from the request message + if (formatServices.length === 0) { + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + } + + // If the format services list is still empty, throw an error as we don't support any + // of the formats + if (formatServices.length === 0) { + throw new CredoError( + `Unable to get credentials for request. No supported formats provided as input or in request message` + ) + } + + const result = await this.proofFormatCoordinator.getCredentialsForRequest(agentContext, { + formatServices, + proofFormats, + proofRecord, + }) + + return { + proofFormats: result, + } + } + + public async selectCredentialsForRequest( + agentContext: AgentContext, + { proofRecord, proofFormats }: SelectCredentialsForRequestOptions + ): Promise> { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.RequestReceived) + + // Use empty proofFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(proofFormats ?? {}) + + // if no format services could be extracted from the proofFormats + // take all available format services from the request message + if (formatServices.length === 0) { + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + } + + // If the format services list is still empty, throw an error as we don't support any + // of the formats + if (formatServices.length === 0) { + throw new CredoError( + `Unable to get credentials for request. No supported formats provided as input or in request message` + ) + } + + const result = await this.proofFormatCoordinator.selectCredentialsForRequest(agentContext, { + formatServices, + proofFormats, + proofRecord, + }) + + return { + proofFormats: result, + } + } + + public async processPresentation( + messageContext: InboundMessageContext + ): Promise { + const { message: presentationMessage, connection, agentContext } = messageContext + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing presentation with id ${presentationMessage.id}`) + + const proofRecord = await this.getByProperties(messageContext.agentContext, { + threadId: presentationMessage.threadId, + role: ProofRole.Verifier, + }) + + const lastSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + role: DidCommMessageRole.Sender, + }) + + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.RequestSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, + expectedConnectionId: proofRecord.connectionId, + }) + + // This makes sure that the sender of the incoming message is authorized to do so. + if (!proofRecord.connectionId) { + await connectionService.matchIncomingMessageToRequestMessageInOutOfBandExchange(messageContext, { + expectedConnectionId: proofRecord.connectionId, + }) + + proofRecord.connectionId = connection?.id + } + + const formatServices = this.getFormatServicesFromMessage(presentationMessage.formats) + // Abandon if no supported formats + if (formatServices.length === 0) { + proofRecord.errorMessage = `Unable to process presentation. No supported formats` + await this.updateState(messageContext.agentContext, proofRecord, ProofState.Abandoned) + throw new V2PresentationProblemReportError(proofRecord.errorMessage, { + problemCode: PresentationProblemReportReason.Abandoned, + }) + } + + const result = await this.proofFormatCoordinator.processPresentation(messageContext.agentContext, { + proofRecord, + formatServices, + requestMessage: lastSentMessage, + message: presentationMessage, + }) + + proofRecord.isVerified = result.isValid + if (result.isValid) { + await this.updateState(messageContext.agentContext, proofRecord, ProofState.PresentationReceived) + } else { + proofRecord.errorMessage = result.message + proofRecord.isVerified = false + await this.updateState(messageContext.agentContext, proofRecord, ProofState.Abandoned) + throw new V2PresentationProblemReportError(proofRecord.errorMessage, { + problemCode: PresentationProblemReportReason.Abandoned, + }) + } + + return proofRecord + } + + public async acceptPresentation( + agentContext: AgentContext, + { proofRecord }: AcceptPresentationOptions + ): Promise> { + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.PresentationReceived) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // assert we've received the final presentation + const presentation = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2PresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + if (!presentation.lastPresentation) { + throw new CredoError( + `Trying to send an ack message while presentation with id ${presentation.id} indicates this is not the last presentation (presentation.last_presentation is set to false)` + ) + } + + const message = new V2PresentationAckMessage({ + threadId: proofRecord.threadId, + status: AckStatus.OK, + }) + + message.setThread({ + threadId: proofRecord.threadId, + parentThreadId: proofRecord.parentThreadId, + }) + + await this.updateState(agentContext, proofRecord, ProofState.Done) + + return { + message, + proofRecord, + } + } + + public async processAck( + messageContext: InboundMessageContext + ): Promise { + const { message: ackMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing proof ack with id ${ackMessage.id}`) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + const proofRecord = await this.getByProperties(messageContext.agentContext, { + threadId: ackMessage.threadId, + role: ProofRole.Prover, + connectionId: connection?.id, + }) + proofRecord.connectionId = connection?.id + + const lastReceivedMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + role: DidCommMessageRole.Receiver, + }) + + const lastSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2PresentationMessage, + role: DidCommMessageRole.Sender, + }) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.PresentationSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, + expectedConnectionId: proofRecord.connectionId, + }) + + // Update record + await this.updateState(messageContext.agentContext, proofRecord, ProofState.Done) + + return proofRecord + } + + public async createProblemReport( + _agentContext: AgentContext, + { description, proofRecord }: CreateProofProblemReportOptions + ): Promise> { + const message = new V2PresentationProblemReportMessage({ + description: { + en: description, + code: PresentationProblemReportReason.Abandoned, + }, + }) + + message.setThread({ + threadId: proofRecord.threadId, + parentThreadId: proofRecord.parentThreadId, + }) + + return { + proofRecord, + message, + } + } + + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + options: { + proofRecord: ProofExchangeRecord + proposalMessage: V2ProposePresentationMessage + } + ): Promise { + const { proofRecord, proposalMessage } = options + const proofsModuleConfig = agentContext.dependencyManager.resolve(ProofsModuleConfig) + + const autoAccept = composeAutoAccept(proofRecord.autoAcceptProof, proofsModuleConfig.autoAcceptProofs) + + // Handle always / never cases + if (autoAccept === AutoAcceptProof.Always) return true + if (autoAccept === AutoAcceptProof.Never) return false + + const requestMessage = await this.findRequestMessage(agentContext, proofRecord.id) + if (!requestMessage) return false + + // NOTE: we take the formats from the requestMessage so we always check all services that we last sent + // Otherwise we'll only check the formats from the proposal, which could be different from the formats + // we use. + const formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + + for (const formatService of formatServices) { + const requestAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const proposalAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToProposal(agentContext, { + proofRecord, + requestAttachment, + proposalAttachment, + }) + // If any of the formats return false, we should not auto accept + if (!shouldAutoRespondToFormat) return false + } + + return true + } + + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + options: { + proofRecord: ProofExchangeRecord + requestMessage: V2RequestPresentationMessage + } + ): Promise { + const { proofRecord, requestMessage } = options + const proofsModuleConfig = agentContext.dependencyManager.resolve(ProofsModuleConfig) + + const autoAccept = composeAutoAccept(proofRecord.autoAcceptProof, proofsModuleConfig.autoAcceptProofs) + + // Handle always / never cases + if (autoAccept === AutoAcceptProof.Always) return true + if (autoAccept === AutoAcceptProof.Never) return false + + const proposalMessage = await this.findProposalMessage(agentContext, proofRecord.id) + if (!proposalMessage) return false + + // NOTE: we take the formats from the proposalMessage so we always check all services that we last sent + // Otherwise we'll only check the formats from the request, which could be different from the formats + // we use. + const formatServices = this.getFormatServicesFromMessage(proposalMessage.formats) + + for (const formatService of formatServices) { + const proposalAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + + const requestAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToRequest(agentContext, { + proofRecord, + requestAttachment, + proposalAttachment, + }) + + // If any of the formats return false, we should not auto accept + if (!shouldAutoRespondToFormat) return false + } + + return true + } + + public async shouldAutoRespondToPresentation( + agentContext: AgentContext, + options: { proofRecord: ProofExchangeRecord; presentationMessage: V2PresentationMessage } + ): Promise { + const { proofRecord, presentationMessage } = options + const proofsModuleConfig = agentContext.dependencyManager.resolve(ProofsModuleConfig) + + // If this isn't the last presentation yet, we should not auto accept + if (!presentationMessage.lastPresentation) return false + + const autoAccept = composeAutoAccept(proofRecord.autoAcceptProof, proofsModuleConfig.autoAcceptProofs) + + // Handle always / never cases + if (autoAccept === AutoAcceptProof.Always) return true + if (autoAccept === AutoAcceptProof.Never) return false + + const proposalMessage = await this.findProposalMessage(agentContext, proofRecord.id) + + const requestMessage = await this.findRequestMessage(agentContext, proofRecord.id) + if (!requestMessage) return false + if (!requestMessage.willConfirm) return false + + // NOTE: we take the formats from the requestMessage so we always check all services that we last sent + // Otherwise we'll only check the formats from the credential, which could be different from the formats + // we use. + const formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + + for (const formatService of formatServices) { + const proposalAttachment = proposalMessage + ? this.proofFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + : undefined + + const requestAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const presentationAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + presentationMessage.formats, + presentationMessage.presentationAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToPresentation(agentContext, { + proofRecord, + presentationAttachment, + requestAttachment, + proposalAttachment, + }) + + // If any of the formats return false, we should not auto accept + if (!shouldAutoRespondToFormat) return false + } + return true + } + + public async findRequestMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V2RequestPresentationMessage, + }) + } + + public async findPresentationMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V2PresentationMessage, + }) + } + + public async findProposalMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V2ProposePresentationMessage, + }) + } + + public async getFormatData(agentContext: AgentContext, proofRecordId: string): Promise { + // TODO: we could looking at fetching all record using a single query and then filtering based on the type of the message. + const [proposalMessage, requestMessage, presentationMessage] = await Promise.all([ + this.findProposalMessage(agentContext, proofRecordId), + this.findRequestMessage(agentContext, proofRecordId), + this.findPresentationMessage(agentContext, proofRecordId), + ]) + + // Create object with the keys and the message formats/attachments. We can then loop over this in a generic + // way so we don't have to add the same operation code four times + const messages = { + proposal: [proposalMessage?.formats, proposalMessage?.proposalAttachments], + request: [requestMessage?.formats, requestMessage?.requestAttachments], + presentation: [presentationMessage?.formats, presentationMessage?.presentationAttachments], + } as const + + const formatData: GetProofFormatDataReturn = {} + + // We loop through all of the message keys as defined above + for (const [messageKey, [formats, attachments]] of Object.entries(messages)) { + // Message can be undefined, so we continue if it is not defined + if (!formats || !attachments) continue + + // Find all format services associated with the message + const formatServices = this.getFormatServicesFromMessage(formats) + + const messageFormatData: ProofFormatDataMessagePayload = {} + + // Loop through all of the format services, for each we will extract the attachment data and assign this to the object + // using the unique format key (e.g. indy) + for (const formatService of formatServices) { + const attachment = this.proofFormatCoordinator.getAttachmentForService(formatService, formats, attachments) + messageFormatData[formatService.formatKey] = attachment.getDataAsJson() + } + + formatData[messageKey as keyof GetProofFormatDataReturn] = messageFormatData + } + + return formatData + } + + /** + * Get all the format service objects for a given proof format from an incoming message + * @param messageFormats the format objects containing the format name (eg indy) + * @return the proof format service objects in an array - derived from format object keys + */ + private getFormatServicesFromMessage(messageFormats: ProofFormatSpec[]): ProofFormatService[] { + const formatServices = new Set() + + for (const msg of messageFormats) { + const service = this.getFormatServiceForFormat(msg.format) + if (service) formatServices.add(service) + } + + return Array.from(formatServices) + } + + /** + * Get all the format service objects for a given proof format + * @param proofFormats the format object containing various optional parameters + * @return the proof format service objects in an array - derived from format object keys + */ + private getFormatServices( + proofFormats: M extends 'selectCredentialsForRequest' | 'getCredentialsForRequest' + ? ProofFormatCredentialForRequestPayload, M, 'input'> + : ProofFormatPayload, M> + ): ProofFormatService[] { + const formats = new Set() + + for (const formatKey of Object.keys(proofFormats)) { + const formatService = this.getFormatServiceForFormatKey(formatKey) + + if (formatService) formats.add(formatService) + } + + return Array.from(formats) + } + + private getFormatServiceForFormatKey(formatKey: string): ProofFormatService | null { + const formatService = this.proofFormats.find((proofFormats) => proofFormats.formatKey === formatKey) + + return formatService ?? null + } + + private getFormatServiceForFormat(format: string): ProofFormatService | null { + const formatService = this.proofFormats.find((proofFormats) => proofFormats.supportsFormat(format)) + + return formatService ?? null + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/V2ProofProtocol.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/V2ProofProtocol.test.ts new file mode 100644 index 0000000000..d0fabec0a2 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/V2ProofProtocol.test.ts @@ -0,0 +1,241 @@ +import type { ProofStateChangedEvent } from '../../../ProofEvents' +import type { ProofFormatService } from '../../../formats' +import type { CustomProofTags } from '../../../repository/ProofExchangeRecord' + +import { Subject } from 'rxjs' + +import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../../tests/helpers' +import { EventEmitter } from '../../../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' +import { DidCommMessageRepository } from '../../../../../storage' +import { uuid } from '../../../../../utils/uuid' +import { ConnectionService, DidExchangeState } from '../../../../connections' +import { ProofEventTypes } from '../../../ProofEvents' +import { PresentationProblemReportReason } from '../../../errors/PresentationProblemReportReason' +import { ProofRole } from '../../../models' +import { ProofFormatSpec } from '../../../models/ProofFormatSpec' +import { ProofState } from '../../../models/ProofState' +import { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' +import { ProofRepository } from '../../../repository/ProofRepository' +import { V2ProofProtocol } from '../V2ProofProtocol' +import { V2PresentationProblemReportMessage, V2RequestPresentationMessage } from '../messages' + +// Mock classes +jest.mock('../../../repository/ProofRepository') +jest.mock('../../../../connections/services/ConnectionService') +jest.mock('../../../../../storage/Repository') + +// Mock typed object +const ProofRepositoryMock = ProofRepository as jest.Mock +const connectionServiceMock = ConnectionService as jest.Mock +const didCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock + +const proofRepository = new ProofRepositoryMock() +const connectionService = new connectionServiceMock() +const didCommMessageRepository = new didCommMessageRepositoryMock() +const proofFormatService = { + supportsFormat: () => true, + processRequest: jest.fn(), +} as unknown as ProofFormatService + +const agentConfig = getAgentConfig('V2ProofProtocolTest') +const eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) + +const agentContext = getAgentContext({ + registerInstances: [ + [ProofRepository, proofRepository], + [DidCommMessageRepository, didCommMessageRepository], + [ConnectionService, connectionService], + [EventEmitter, eventEmitter], + ], + agentConfig, +}) + +const proofProtocol = new V2ProofProtocol({ proofFormats: [proofFormatService] }) + +const connection = getMockConnection({ + id: '123', + state: DidExchangeState.Completed, +}) + +const requestAttachment = new Attachment({ + id: 'abdc8b63-29c6-49ad-9e10-98f9d85db9a2', + mimeType: 'application/json', + data: new AttachmentData({ + base64: + 'eyJuYW1lIjogIlByb29mIHJlcXVlc3QiLCAibm9uX3Jldm9rZWQiOiB7ImZyb20iOiAxNjQwOTk1MTk5LCAidG8iOiAxNjQwOTk1MTk5fSwgIm5vbmNlIjogIjEiLCAicmVxdWVzdGVkX2F0dHJpYnV0ZXMiOiB7ImFkZGl0aW9uYWxQcm9wMSI6IHsibmFtZSI6ICJmYXZvdXJpdGVEcmluayIsICJub25fcmV2b2tlZCI6IHsiZnJvbSI6IDE2NDA5OTUxOTksICJ0byI6IDE2NDA5OTUxOTl9LCAicmVzdHJpY3Rpb25zIjogW3siY3JlZF9kZWZfaWQiOiAiV2dXeHF6dHJOb29HOTJSWHZ4U1RXdjozOkNMOjIwOnRhZyJ9XX19LCAicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOiB7fSwgInZlcnNpb24iOiAiMS4wIn0=', + }), +}) + +// A record is deserialized to JSON when it's stored into the storage. We want to simulate this behaviour for `offer` +// object to test our service would behave correctly. We use type assertion for `offer` attribute to `any`. +const mockProofExchangeRecord = ({ + state, + role, + threadId, + connectionId, + tags, + id, +}: { + state?: ProofState + role?: ProofRole + tags?: CustomProofTags + threadId?: string + connectionId?: string + id?: string +} = {}) => { + const proofRecord = new ProofExchangeRecord({ + protocolVersion: 'v2', + id, + state: state || ProofState.RequestSent, + role: role || ProofRole.Verifier, + threadId: threadId ?? uuid(), + connectionId: connectionId ?? '123', + tags, + }) + + return proofRecord +} + +describe('V2ProofProtocol', () => { + describe('processProofRequest', () => { + let presentationRequest: V2RequestPresentationMessage + let messageContext: InboundMessageContext + + beforeEach(() => { + presentationRequest = new V2RequestPresentationMessage({ + formats: [ + new ProofFormatSpec({ + attachmentId: 'abdc8b63-29c6-49ad-9e10-98f9d85db9a2', + format: 'hlindy/proof-req@v2.0', + }), + ], + requestAttachments: [requestAttachment], + comment: 'Proof Request', + }) + + messageContext = new InboundMessageContext(presentationRequest, { agentContext, connection }) + }) + + test(`creates and return proof record in ${ProofState.PresentationReceived} state with offer, without thread ID`, async () => { + const repositorySaveSpy = jest.spyOn(proofRepository, 'save') + + // when + const returnedProofExchangeRecord = await proofProtocol.processRequest(messageContext) + + // then + const expectedProofExchangeRecord = { + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: ProofState.RequestReceived, + threadId: presentationRequest.id, + connectionId: connection.id, + } + expect(repositorySaveSpy).toHaveBeenCalledTimes(1) + const [[, createdProofExchangeRecord]] = repositorySaveSpy.mock.calls + expect(createdProofExchangeRecord).toMatchObject(expectedProofExchangeRecord) + expect(returnedProofExchangeRecord).toMatchObject(expectedProofExchangeRecord) + }) + + test(`emits stateChange event with ${ProofState.RequestReceived}`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(ProofEventTypes.ProofStateChanged, eventListenerMock) + + // when + await proofProtocol.processRequest(messageContext) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'ProofStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: null, + proofRecord: expect.objectContaining({ + state: ProofState.RequestReceived, + }), + }, + }) + }) + }) + + describe('createProblemReport', () => { + const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' + let proof: ProofExchangeRecord + + beforeEach(() => { + proof = mockProofExchangeRecord({ + state: ProofState.RequestReceived, + threadId, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + }) + + test('returns problem report message base once get error', async () => { + // given + mockFunction(proofRepository.getById).mockReturnValue(Promise.resolve(proof)) + + // when + const presentationProblemReportMessage = await new V2PresentationProblemReportMessage({ + description: { + en: 'Indy error', + code: PresentationProblemReportReason.Abandoned, + }, + }) + + presentationProblemReportMessage.setThread({ threadId }) + // then + expect(presentationProblemReportMessage.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/present-proof/2.0/problem-report', + '~thread': { + thid: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + }, + }) + }) + }) + + describe('processProblemReport', () => { + let proof: ProofExchangeRecord + let messageContext: InboundMessageContext + beforeEach(() => { + proof = mockProofExchangeRecord({ + state: ProofState.RequestReceived, + }) + + const presentationProblemReportMessage = new V2PresentationProblemReportMessage({ + description: { + en: 'Indy error', + code: PresentationProblemReportReason.Abandoned, + }, + }) + presentationProblemReportMessage.setThread({ threadId: 'somethreadid' }) + messageContext = new InboundMessageContext(presentationProblemReportMessage, { agentContext, connection }) + }) + + test(`updates problem report error message and returns proof record`, async () => { + const repositoryUpdateSpy = jest.spyOn(proofRepository, 'update') + + // given + mockFunction(proofRepository.getSingleByQuery).mockReturnValue(Promise.resolve(proof)) + + // when + const returnedCredentialRecord = await proofProtocol.processProblemReport(messageContext) + + // then + const expectedCredentialRecord = { + errorMessage: 'abandoned: Indy error', + } + expect(proofRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { + threadId: 'somethreadid', + }) + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + const [[, updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls + expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord) + expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts new file mode 100644 index 0000000000..0b3d8c39b9 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts @@ -0,0 +1,13 @@ +import type { InputDescriptorV1 } from '@sphereon/pex-models' + +export const TEST_INPUT_DESCRIPTORS_CITIZENSHIP = { + constraints: { + fields: [ + { + path: ['$.credentialSubject.degree.type'], + }, + ], + }, + id: 'citizenship_input_1', + schema: [{ uri: 'https://www.w3.org/2018/credentials/examples/v1' }], +} satisfies InputDescriptorV1 diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-anoncreds-unqualified-proofs.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-anoncreds-unqualified-proofs.e2e.test.ts new file mode 100644 index 0000000000..ffa3a81421 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-anoncreds-unqualified-proofs.e2e.test.ts @@ -0,0 +1,928 @@ +import type { AnonCredsTestsAgent } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import type { EventReplaySubject } from '../../../../../../tests' + +import { isUnqualifiedCredentialDefinitionId } from '../../../../../../../anoncreds/src/utils/indyIdentifiers' +import { + issueLegacyAnonCredsCredential, + setupAnonCredsTests, +} from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import { waitForProofExchangeRecord } from '../../../../../../tests' +import testLogger from '../../../../../../tests/logger' +import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' +import { LinkedAttachment } from '../../../../../utils/LinkedAttachment' +import { ProofState } from '../../../models' +import { ProofExchangeRecord } from '../../../repository' +import { V2ProposePresentationMessage, V2RequestPresentationMessage, V2PresentationMessage } from '../messages' + +describe('Present Proof', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let credentialDefinitionId: string + let aliceConnectionId: string + let faberConnectionId: string + let faberProofExchangeRecord: ProofExchangeRecord + let aliceProofExchangeRecord: ProofExchangeRecord + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber agent anoncreds unqualified indy proofs', + holderName: 'Alice agent anoncreds unqualified indy proofs', + attributeNames: ['name', 'age', 'image_0', 'image_1'], + })) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'John', + }, + { + name: 'age', + value: '99', + }, + ], + linkedAttachments: [ + new LinkedAttachment({ + name: 'image_0', + attachment: new Attachment({ + filename: 'picture-of-a-cat.png', + data: new AttachmentData({ base64: 'cGljdHVyZSBvZiBhIGNhdA==' }), + }), + }), + new LinkedAttachment({ + name: 'image_1', + attachment: new Attachment({ + filename: 'picture-of-a-dog.png', + data: new AttachmentData({ base64: 'UGljdHVyZSBvZiBhIGRvZw==' }), + }), + }), + ], + }, + }) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with proof proposal to Faber', async () => { + // Alice sends a presentation proposal to Faber + testLogger.test('Alice sends a presentation proposal to Faber') + + let faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + state: ProofState.ProposalReceived, + }) + + aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + proofFormats: { + anoncreds: { + name: 'abc', + version: '1.0', + attributes: [ + { + name: 'name', + value: 'Alice', + credentialDefinitionId, + }, + ], + predicates: [ + { + name: 'age', + predicate: '>=', + threshold: 50, + credentialDefinitionId, + }, + ], + }, + }, + }) + + // Faber waits for a presentation proposal from Alice + testLogger.test('Faber waits for a presentation proposal from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + }) + + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber accepts the presentation proposal from Alice + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits for the presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof@v1.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation provided by Alice + testLogger.test('Faber accepts the presentation provided by Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + + const proposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id) + const requestMessage = await aliceAgent.proofs.findRequestMessage(aliceProofExchangeRecord.id) + const presentationMessage = await aliceAgent.proofs.findPresentationMessage(aliceProofExchangeRecord.id) + + expect(proposalMessage).toBeInstanceOf(V2ProposePresentationMessage) + expect(requestMessage).toBeInstanceOf(V2RequestPresentationMessage) + expect(presentationMessage).toBeInstanceOf(V2PresentationMessage) + + const formatData = await aliceAgent.proofs.getFormatData(aliceProofExchangeRecord.id) + + expect(formatData).toMatchObject({ + proposal: { + anoncreds: { + name: 'abc', + version: '1.0', + nonce: expect.any(String), + requested_attributes: { + [Object.keys(formatData.proposal?.anoncreds?.requested_attributes ?? {})[0]]: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + [Object.keys(formatData.proposal?.anoncreds?.requested_predicates ?? {})[0]]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + request: { + anoncreds: { + name: 'abc', + version: '1.0', + nonce: expect.any(String), + requested_attributes: { + [Object.keys(formatData.request?.anoncreds?.requested_attributes ?? {})[0]]: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + [Object.keys(formatData.request?.anoncreds?.requested_predicates ?? {})[0]]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + presentation: { + anoncreds: { + proof: { + proofs: [ + { + primary_proof: expect.any(Object), + non_revoc_proof: null, + }, + ], + aggregated_proof: { + c_hash: expect.any(String), + c_list: expect.any(Array), + }, + }, + requested_proof: expect.any(Object), + identifiers: expect.any(Array), + }, + }, + }) + }) + + test('Faber starts with proof request to Alice', async () => { + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + + expect(aliceProofExchangeRecord.id).not.toBeNull() + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof@v1.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation + testLogger.test('Faber accept the presentation from Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she receives a presentation acknowledgement + testLogger.test('Alice waits for acceptance by Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) + + test('Alice provides only attributes from credentials via call to getRequestedCredentials', async () => { + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const retrievedCredentials = await aliceAgent.proofs.getCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + expect(retrievedCredentials).toMatchObject({ + proofFormats: { + anoncreds: { + attributes: { + name: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + credentialId: expect.any(String), + attributes: { + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + age: 99, + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + revocationRegistryId: null, + credentialRevocationId: null, + }, + }, + ], + image_0: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + credentialId: expect.any(String), + attributes: { + age: 99, + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + revocationRegistryId: null, + credentialRevocationId: null, + }, + }, + ], + }, + }, + }, + }) + // response should be in unqualified format + const credDefId = + retrievedCredentials.proofFormats.anoncreds?.attributes?.name?.[0]?.credentialInfo?.credentialDefinitionId ?? '' + expect(isUnqualifiedCredentialDefinitionId(credDefId)).toBe(true) + }) + + test('Alice provides only predicates from credentials via call to getRequestedCredentials', async () => { + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'Proof Request', + version: '1.0.0', + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const retrievedCredentials = await aliceAgent.proofs.getCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + expect(retrievedCredentials).toMatchObject({ + proofFormats: { + anoncreds: { + predicates: { + age: [ + { + credentialId: expect.any(String), + credentialInfo: { + credentialId: expect.any(String), + attributes: { + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + name: 'John', + age: 99, + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + revocationRegistryId: null, + credentialRevocationId: null, + }, + }, + ], + }, + }, + }, + }) + + // response should be in unqualified format + const credDefId = + retrievedCredentials.proofFormats.anoncreds?.predicates?.age?.[0]?.credentialInfo?.credentialDefinitionId ?? '' + expect(isUnqualifiedCredentialDefinitionId(credDefId)).toBe(true) + }) + + test('Alice provides both attributes and predicates from credentials via call to getRequestedCredentials', async () => { + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const retrievedCredentials = await aliceAgent.proofs.getCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + expect(retrievedCredentials).toMatchObject({ + proofFormats: { + anoncreds: { + attributes: { + name: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + credentialId: expect.any(String), + attributes: { + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + age: 99, + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + revocationRegistryId: null, + credentialRevocationId: null, + }, + }, + ], + image_0: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + credentialId: expect.any(String), + attributes: { + age: 99, + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + revocationRegistryId: null, + credentialRevocationId: null, + }, + }, + ], + }, + predicates: { + age: [ + { + credentialId: expect.any(String), + credentialInfo: { + credentialId: expect.any(String), + attributes: { + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + name: 'John', + age: 99, + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + revocationRegistryId: null, + credentialRevocationId: null, + }, + }, + ], + }, + }, + }, + }) + // response should be in unqualified format + const attrCredDefId = + retrievedCredentials.proofFormats.anoncreds?.attributes?.name?.[0]?.credentialInfo?.credentialDefinitionId ?? '' + expect(isUnqualifiedCredentialDefinitionId(attrCredDefId)).toBe(true) + const predCredDefId = + retrievedCredentials.proofFormats.anoncreds?.predicates?.age?.[0]?.credentialInfo?.credentialDefinitionId ?? '' + expect(isUnqualifiedCredentialDefinitionId(predCredDefId)).toBe(true) + }) + + test('Faber starts with proof request to Alice but gets Problem Reported', async () => { + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + + expect(aliceProofExchangeRecord.id).not.toBeNull() + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + }) + + aliceProofExchangeRecord = await aliceAgent.proofs.sendProblemReport({ + description: 'Problem inside proof request', + proofRecordId: aliceProofExchangeRecord.id, + }) + + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + protocolVersion: 'v2', + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts new file mode 100644 index 0000000000..4fecad52ea --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts @@ -0,0 +1,636 @@ +import type { SubjectMessage } from '../../../../../../../../tests/transport/SubjectInboundTransport' +import type { AnonCredsTestsAgent } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' + +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../../../../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../../../../../../tests/transport/SubjectOutboundTransport' +import { V1CredentialPreview } from '../../../../../../../anoncreds/src' +import { + getAnonCredsIndyModules, + issueLegacyAnonCredsCredential, + prepareForAnonCredsIssuance, + setupAnonCredsTests, +} from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import { + waitForProofExchangeRecordSubject, + makeConnection, + testLogger, + setupEventReplaySubjects, + waitForProofExchangeRecord, + getInMemoryAgentOptions, +} from '../../../../../../tests' +import { Agent } from '../../../../../agent/Agent' +import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' +import { LinkedAttachment } from '../../../../../utils/LinkedAttachment' +import { uuid } from '../../../../../utils/uuid' +import { HandshakeProtocol } from '../../../../connections' +import { CredentialEventTypes } from '../../../../credentials' +import { MediatorModule, MediatorPickupStrategy, MediationRecipientModule } from '../../../../routing' +import { ProofEventTypes } from '../../../ProofEvents' +import { AutoAcceptProof, ProofState } from '../../../models' + +describe('V2 Connectionless Proofs - Indy', () => { + let agents: Agent[] + + afterEach(async () => { + for (const agent of agents) { + await agent.shutdown() + await agent.wallet.delete() + } + }) + + const connectionlessTest = async (returnRoute?: boolean) => { + const { + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber connection-less Proofs v2', + holderName: 'Alice connection-less Proofs v2', + autoAcceptProofs: AutoAcceptProof.Never, + attributeNames: ['name', 'age'], + }) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'Alice', + }, + { + name: 'age', + value: '99', + }, + ], + }, + }) + + agents = [aliceAgent, faberAgent] + testLogger.test('Faber sends presentation request to Alice') + + // eslint-disable-next-line prefer-const + let { proofRecord: faberProofExchangeRecord, message } = await faberAgent.proofs.createRequest({ + protocolVersion: 'v2', + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + const { message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberProofExchangeRecord.id, + message, + domain: 'https://a-domain.com', + }) + + await aliceAgent.receiveMessage(requestMessage.toJSON()) + + testLogger.test('Alice waits for presentation request from Faber') + let aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, { + state: ProofState.RequestReceived, + }) + + testLogger.test('Alice accepts presentation request from Faber') + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + aliceProofExchangeRecord = await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + useReturnRoute: returnRoute, + proofFormats: { indy: requestedCredentials.proofFormats.indy }, + }) + + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await waitForProofExchangeRecordSubject(faberReplay, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + const sentPresentationMessage = aliceAgent.proofs.findPresentationMessage(aliceProofExchangeRecord.id) + + // assert presentation is valid + expect(faberProofExchangeRecord.isVerified).toBe(true) + + // Faber accepts presentation + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until it receives presentation ack + aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + return sentPresentationMessage + } + + test('Faber starts with connection-less proof requests to Alice', async () => { + await connectionlessTest() + }) + + test('Faber starts with connection-less proof requests to Alice with auto-accept enabled', async () => { + testLogger.test('Faber sends presentation request to Alice') + + const { + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber connection-less Proofs v2 - Auto Accept', + holderName: 'Alice connection-less Proofs v2 - Auto Accept', + autoAcceptProofs: AutoAcceptProof.Always, + attributeNames: ['name', 'age'], + }) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'Alice', + }, + { + name: 'age', + value: '99', + }, + ], + }, + }) + + agents = [aliceAgent, faberAgent] + + // eslint-disable-next-line prefer-const + let { message, proofRecord: faberProofExchangeRecord } = await faberAgent.proofs.createRequest({ + protocolVersion: 'v2', + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + autoAcceptProof: AutoAcceptProof.ContentApproved, + }) + + const { message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberProofExchangeRecord.id, + message, + domain: 'https://a-domain.com', + }) + + await aliceAgent.receiveMessage(requestMessage.toJSON()) + + await waitForProofExchangeRecordSubject(aliceReplay, { + state: ProofState.Done, + threadId: requestMessage.threadId, + }) + + await waitForProofExchangeRecordSubject(faberReplay, { + state: ProofState.Done, + threadId: requestMessage.threadId, + }) + }) + + test('Faber starts with connection-less proof requests to Alice with auto-accept enabled and both agents having a mediator', async () => { + testLogger.test('Faber sends presentation request to Alice') + + const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', + }) + + const unique = uuid().substring(0, 4) + + const mediatorOptions = getInMemoryAgentOptions( + `Connectionless proofs with mediator Mediator-${unique}`, + { + endpoints: ['rxjs:mediator'], + }, + { + ...getAnonCredsIndyModules({ + autoAcceptProofs: AutoAcceptProof.Always, + }), + mediator: new MediatorModule({ + autoAcceptMediationRequests: true, + }), + } + ) + + const mediatorMessages = new Subject() + const subjectMap = { 'rxjs:mediator': mediatorMessages } + + // Initialize mediator + const mediatorAgent = new Agent(mediatorOptions) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + await mediatorAgent.initialize() + + const faberMediationOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'faber invitation', + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + const aliceMediationOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'alice invitation', + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + const faberOptions = getInMemoryAgentOptions( + `Connectionless proofs with mediator Faber-${unique}`, + {}, + { + ...getAnonCredsIndyModules({ + autoAcceptProofs: AutoAcceptProof.Always, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorInvitationUrl: faberMediationOutOfBandRecord.outOfBandInvitation.toUrl({ + domain: 'https://example.com', + }), + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + } + ) + + const aliceOptions = getInMemoryAgentOptions( + `Connectionless proofs with mediator Alice-${unique}`, + {}, + { + ...getAnonCredsIndyModules({ + autoAcceptProofs: AutoAcceptProof.Always, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorInvitationUrl: aliceMediationOutOfBandRecord.outOfBandInvitation.toUrl({ + domain: 'https://example.com', + }), + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + } + ) + + const faberAgent = new Agent(faberOptions) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + + const aliceAgent = new Agent(aliceOptions) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + const [faberReplay, aliceReplay] = setupEventReplaySubjects( + [faberAgent, aliceAgent], + [CredentialEventTypes.CredentialStateChanged, ProofEventTypes.ProofStateChanged] + ) + agents = [aliceAgent, faberAgent, mediatorAgent] + + const { credentialDefinition } = await prepareForAnonCredsIssuance(faberAgent, { + attributeNames: ['name', 'age', 'image_0', 'image_1'], + }) + + const [faberConnection] = await makeConnection(faberAgent, aliceAgent) + + // issue credential with two linked attachments + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent as AnonCredsTestsAgent, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnection.id, + holderAgent: aliceAgent as AnonCredsTestsAgent, + holderReplay: aliceReplay, + offer: { + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + attributes: credentialPreview.attributes, + linkedAttachments: [ + new LinkedAttachment({ + name: 'image_0', + attachment: new Attachment({ + filename: 'picture-of-a-cat.png', + data: new AttachmentData({ base64: 'cGljdHVyZSBvZiBhIGNhdA==' }), + }), + }), + new LinkedAttachment({ + name: 'image_1', + attachment: new Attachment({ + filename: 'picture-of-a-dog.png', + data: new AttachmentData({ base64: 'UGljdHVyZSBvZiBhIGRvZw==' }), + }), + }), + ], + }, + }) + + // eslint-disable-next-line prefer-const + let { message, proofRecord: faberProofExchangeRecord } = await faberAgent.proofs.createRequest({ + protocolVersion: 'v2', + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinition.credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinition.credentialDefinitionId, + }, + ], + }, + }, + }, + }, + autoAcceptProof: AutoAcceptProof.ContentApproved, + }) + + const { message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberProofExchangeRecord.id, + message, + domain: 'https://a-domain.com', + }) + + const mediationRecord = await faberAgent.mediationRecipient.findDefaultMediator() + if (!mediationRecord) throw new Error('Faber agent has no default mediator') + + expect(requestMessage).toMatchObject({ + service: { + recipientKeys: [expect.any(String)], + routingKeys: mediationRecord.routingKeys, + serviceEndpoint: mediationRecord.endpoint, + }, + }) + + await aliceAgent.receiveMessage(requestMessage.toJSON()) + + await waitForProofExchangeRecordSubject(aliceReplay, { + state: ProofState.Done, + threadId: requestMessage.threadId, + }) + + await waitForProofExchangeRecordSubject(faberReplay, { + state: ProofState.Done, + threadId: requestMessage.threadId, + }) + }) + + test('Faber starts with connection-less proof requests to Alice with auto-accept enabled and without an outbound transport', async () => { + testLogger.test('Faber sends presentation request to Alice') + + const { + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber connection-less Proofs v2 - Auto Accept', + holderName: 'Alice connection-less Proofs v2 - Auto Accept', + autoAcceptProofs: AutoAcceptProof.Always, + attributeNames: ['name', 'age'], + }) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'Alice', + }, + { + name: 'age', + value: '99', + }, + ], + }, + }) + + agents = [aliceAgent, faberAgent] + + // eslint-disable-next-line prefer-const + let { message, proofRecord: faberProofExchangeRecord } = await faberAgent.proofs.createRequest({ + protocolVersion: 'v2', + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + autoAcceptProof: AutoAcceptProof.ContentApproved, + }) + + const { message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberProofExchangeRecord.id, + message, + domain: 'rxjs:faber', + }) + + for (const transport of faberAgent.outboundTransports) { + await faberAgent.unregisterOutboundTransport(transport) + } + + await aliceAgent.receiveMessage(requestMessage.toJSON()) + await waitForProofExchangeRecordSubject(aliceReplay, { + state: ProofState.Done, + threadId: requestMessage.threadId, + }) + + await waitForProofExchangeRecordSubject(faberReplay, { + state: ProofState.Done, + threadId: requestMessage.threadId, + }) + }) + + test('Faber starts with connection-less proof requests to Alice but gets Problem Reported', async () => { + testLogger.test('Faber sends presentation request to Alice') + + const { + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber connection-less Proofs v2 - Reject Request', + holderName: 'Alice connection-less Proofs v2 - Reject Request', + autoAcceptProofs: AutoAcceptProof.Never, + attributeNames: ['name', 'age'], + }) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'Alice', + }, + { + name: 'age', + value: '99', + }, + ], + }, + }) + + agents = [aliceAgent, faberAgent] + + // eslint-disable-next-line prefer-const + // eslint-disable-next-line prefer-const + let { message, proofRecord: faberProofExchangeRecord } = await faberAgent.proofs.createRequest({ + protocolVersion: 'v2', + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: {}, + }, + }, + autoAcceptProof: AutoAcceptProof.ContentApproved, + }) + + const { message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberProofExchangeRecord.id, + message, + domain: 'rxjs:faber', + }) + + for (const transport of faberAgent.outboundTransports) { + await faberAgent.unregisterOutboundTransport(transport) + } + + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + await aliceAgent.receiveMessage(requestMessage.toJSON()) + const aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + await aliceAgent.proofs.declineRequest({ proofRecordId: aliceProofExchangeRecord.id, sendProblemReport: true }) + + await waitForProofExchangeRecordSubject(aliceReplay, { + state: ProofState.Declined, + threadId: requestMessage.threadId, + }) + + await waitForProofExchangeRecordSubject(faberReplay, { + state: ProofState.Abandoned, + threadId: requestMessage.threadId, + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-negotiation.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-negotiation.e2e.test.ts new file mode 100644 index 0000000000..6bed06c5ab --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-negotiation.e2e.test.ts @@ -0,0 +1,404 @@ +import type { AnonCredsProofRequest } from '../../../../../../../anoncreds/src/models/exchange' +import type { AnonCredsTestsAgent } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import type { EventReplaySubject } from '../../../../../../tests' +import type { V2ProposePresentationMessage, V2RequestPresentationMessage } from '../messages' + +import { AnonCredsProofRequest as AnonCredsProofRequestClass } from '../../../../../../../anoncreds/src/models/AnonCredsProofRequest' +import { + issueLegacyAnonCredsCredential, + setupAnonCredsTests, +} from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import { waitForProofExchangeRecordSubject, testLogger } from '../../../../../../tests' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { ProofState } from '../../../models/ProofState' + +describe('V2 Proofs Negotiation - Indy', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let faberConnectionId: string + let aliceConnectionId: string + let credentialDefinitionId: string + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + + credentialDefinitionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber agent v2', + holderName: 'Alice agent v2', + attributeNames: ['name', 'age'], + })) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'Alice', + }, + { + name: 'age', + value: '99', + }, + ], + }, + }) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test(`Proof negotiation between Alice and Faber`, async () => { + testLogger.test('Alice sends proof proposal to Faber') + + let aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + attributes: [], + predicates: [ + { + credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 50, + }, + ], + }, + }, + comment: 'V2 propose proof test 1', + }) + + testLogger.test('Faber waits for presentation from Alice') + let faberProofExchangeRecord = await waitForProofExchangeRecordSubject(faberReplay, { + state: ProofState.ProposalReceived, + threadId: aliceProofExchangeRecord.threadId, + }) + + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof-req@v2.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + comment: 'V2 propose proof test 1', + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const proposalAttach = ( + proposal as V2ProposePresentationMessage + )?.proposalAttachments?.[0].getDataAsJson() + + expect(proposalAttach).toMatchObject({ + requested_attributes: {}, + requested_predicates: { + [Object.keys(proposalAttach.requested_predicates)[0]]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }) + expect(faberProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + }) + + testLogger.test('Faber sends new proof request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.negotiateProposal({ + proofRecordId: faberProofExchangeRecord.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + testLogger.test('Alice waits for proof request from Faber') + aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, { + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + id: expect.any(String), + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(aliceProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + testLogger.test('Alice sends proof proposal to Faber') + + aliceProofExchangeRecord = await aliceAgent.proofs.negotiateRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + attributes: [], + predicates: [ + { + credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 50, + }, + ], + }, + }, + comment: 'V2 propose proof test 2', + }) + + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await waitForProofExchangeRecordSubject(faberReplay, { + state: ProofState.ProposalReceived, + threadId: aliceProofExchangeRecord.threadId, + // Negotiation so this will be the second proposal + count: 2, + }) + + const proposal2 = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal2).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof-req@v2.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + comment: 'V2 propose proof test 2', + }) + + const proposalAttach2 = ( + proposal as V2ProposePresentationMessage + )?.proposalAttachments[0].getDataAsJson() + expect(proposalAttach2).toMatchObject({ + requested_attributes: {}, + requested_predicates: { + [Object.keys(proposalAttach2.requested_predicates)[0]]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }) + expect(faberProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + }) + + // Accept Proposal + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) + + testLogger.test('Alice waits for proof request from Faber') + aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, { + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + // Negotiation so this will be the second request + count: 2, + }) + + const request2 = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request2).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof-req@v2.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(aliceProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + const proposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id) + expect(proposalMessage).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof-req@v2.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + comment: 'V2 propose proof test 2', + }) + + const proposalAttach3 = ( + proposal as V2ProposePresentationMessage + )?.proposalAttachments[0].getDataAsJson() + expect(proposalAttach3).toMatchObject({ + requested_attributes: {}, + requested_predicates: { + [Object.keys(proposalAttach3.requested_predicates ?? {})[0]]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }) + + const proofRequestMessage = (await aliceAgent.proofs.findRequestMessage( + aliceProofExchangeRecord.id + )) as V2RequestPresentationMessage + + const proofRequest = JsonTransformer.fromJSON( + proofRequestMessage.requestAttachments[0].getDataAsJson(), + AnonCredsProofRequestClass + ) + const predicateKey = proofRequest.requestedPredicates?.keys().next().value + + expect(JsonTransformer.toJSON(proofRequest)).toMatchObject({ + name: 'proof-request', + nonce: expect.any(String), + version: '1.0', + requested_attributes: {}, + requested_predicates: { + [predicateKey]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-presentation.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-presentation.e2e.test.ts new file mode 100644 index 0000000000..ee4d3cd44b --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-presentation.e2e.test.ts @@ -0,0 +1,255 @@ +import type { AnonCredsTestsAgent } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import type { EventReplaySubject } from '../../../../../../tests' + +import { + setupAnonCredsTests, + issueLegacyAnonCredsCredential, +} from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import { waitForProofExchangeRecordSubject, testLogger } from '../../../../../../tests' +import { ProofState } from '../../../models/ProofState' +import { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' + +describe('V2 Proofs - Indy', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let faberConnectionId: string + let aliceConnectionId: string + let credentialDefinitionId: string + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + + credentialDefinitionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber agent v2', + holderName: 'Alice agent v2', + attributeNames: ['name', 'age'], + })) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'Alice', + }, + { + name: 'age', + value: '99', + }, + ], + }, + }) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test(`Alice Creates and sends Proof Proposal to Faber`, async () => { + testLogger.test('Alice sends proof proposal to Faber') + + let aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + proofFormats: { + indy: { + name: 'ProofRequest', + version: '1.0', + attributes: [ + { + name: 'name', + value: 'Alice', + credentialDefinitionId, + }, + ], + predicates: [ + { + credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 18, + }, + ], + }, + }, + comment: 'V2 propose proof test', + }) + + testLogger.test('Faber waits for presentation from Alice') + let faberProofExchangeRecord = await waitForProofExchangeRecordSubject(faberReplay, { + state: ProofState.ProposalReceived, + }) + + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof-req@v2.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + comment: 'V2 propose proof test', + }) + expect(faberProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + }) + + // Accept Proposal + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) + + testLogger.test('Alice waits for proof request from Faber') + aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, { + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof-req@v2.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(aliceProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { indy: requestedCredentials.proofFormats.indy }, + }) + + faberProofExchangeRecord = await waitForProofExchangeRecordSubject(faberReplay, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + // Faber waits for the presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof@v2.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + + const aliceProofExchangeRecordPromise = waitForProofExchangeRecordSubject(aliceReplay, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation provided by Alice + testLogger.test('Faber accepts the presentation provided by Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-request.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-request.e2e.test.ts new file mode 100644 index 0000000000..afe41e9417 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-request.e2e.test.ts @@ -0,0 +1,173 @@ +import type { AnonCredsTestsAgent } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import type { EventReplaySubject } from '../../../../../../tests' + +import { + issueLegacyAnonCredsCredential, + setupAnonCredsTests, +} from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import { waitForProofExchangeRecordSubject } from '../../../../../../tests' +import testLogger from '../../../../../../tests/logger' +import { ProofState } from '../../../models/ProofState' + +describe('V2 Proofs - Indy', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let faberConnectionId: string + let aliceConnectionId: string + let credentialDefinitionId: string + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + + credentialDefinitionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber agent v2', + holderName: 'Alice agent v2', + attributeNames: ['name', 'age'], + })) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'Alice', + }, + { + name: 'age', + value: '99', + }, + ], + }, + }) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test(`Alice Creates and sends Proof Proposal to Faber`, async () => { + testLogger.test('Alice sends proof proposal to Faber') + + let aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + proofFormats: { + indy: { + name: 'ProofRequest', + version: '1.0', + attributes: [ + { + name: 'name', + value: 'Alice', + credentialDefinitionId, + }, + ], + predicates: [ + { + credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 18, + }, + ], + }, + }, + comment: 'V2 propose proof test', + }) + + testLogger.test('Faber waits for presentation from Alice') + let faberProofExchangeRecord = await waitForProofExchangeRecordSubject(faberReplay, { + state: ProofState.ProposalReceived, + }) + + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof-req@v2.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + comment: 'V2 propose proof test', + }) + expect(faberProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + }) + + // Accept Proposal + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) + + testLogger.test('Alice waits for proof request from Faber') + aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, { + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof-req@v2.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(aliceProofExchangeRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs-auto-accept.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs-auto-accept.e2e.test.ts new file mode 100644 index 0000000000..06ddf5cc34 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs-auto-accept.e2e.test.ts @@ -0,0 +1,291 @@ +import type { AnonCredsTestsAgent } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import type { EventReplaySubject } from '../../../../../../tests' + +import { + issueLegacyAnonCredsCredential, + setupAnonCredsTests, +} from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import { waitForProofExchangeRecord, testLogger } from '../../../../../../tests' +import { AutoAcceptProof, ProofState } from '../../../models' + +describe('Auto accept present proof', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let credentialDefinitionId: string + let faberConnectionId: string + let aliceConnectionId: string + + describe("Auto accept on 'always'", () => { + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber Auto Accept Always Proofs', + holderName: 'Alice Auto Accept Always Proofs', + attributeNames: ['name', 'age'], + autoAcceptProofs: AutoAcceptProof.Always, + })) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'Alice', + }, + { + name: 'age', + value: '99', + }, + ], + }, + }) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test("Alice starts with proof proposal to Faber, both with autoAcceptProof on 'always'", async () => { + testLogger.test('Alice sends presentation proposal to Faber') + + await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + proofFormats: { + indy: { + name: 'abc', + version: '1.0', + attributes: [ + { + credentialDefinitionId, + name: 'name', + value: 'Alice', + }, + ], + predicates: [ + { + credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 50, + }, + ], + }, + }, + }) + + testLogger.test('Faber waits for presentation from Alice') + testLogger.test('Alice waits till it receives presentation ack') + await Promise.all([ + waitForProofExchangeRecord(faberAgent, { state: ProofState.Done }), + waitForProofExchangeRecord(aliceAgent, { state: ProofState.Done }), + ]) + }) + + test("Faber starts with proof requests to Alice, both with autoAcceptProof on 'always'", async () => { + testLogger.test('Faber sends presentation request to Alice') + + await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + testLogger.test('Alice waits for presentation from Faber') + testLogger.test('Faber waits till it receives presentation ack') + await Promise.all([ + waitForProofExchangeRecord(faberAgent, { state: ProofState.Done }), + waitForProofExchangeRecord(aliceAgent, { state: ProofState.Done }), + ]) + }) + }) + + describe("Auto accept on 'contentApproved'", () => { + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber Auto Accept ContentApproved Proofs', + holderName: 'Alice Auto Accept ContentApproved Proofs', + attributeNames: ['name', 'age'], + autoAcceptProofs: AutoAcceptProof.ContentApproved, + })) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'Alice', + }, + { + name: 'age', + value: '99', + }, + ], + }, + }) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test("Alice starts with proof proposal to Faber, both with autoAcceptProof on 'contentApproved'", async () => { + testLogger.test('Alice sends presentation proposal to Faber') + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + state: ProofState.ProposalReceived, + }) + + await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + proofFormats: { + indy: { + name: 'abc', + version: '1.0', + attributes: [ + { + credentialDefinitionId, + name: 'name', + value: 'Alice', + }, + ], + predicates: [ + { + credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 50, + }, + ], + }, + }, + }) + + const faberProofExchangeRecord = await faberProofExchangeRecordPromise + await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) + + await Promise.all([ + waitForProofExchangeRecord(aliceAgent, { state: ProofState.Done }), + waitForProofExchangeRecord(faberAgent, { state: ProofState.Done }), + ]) + }) + + test("Faber starts with proof requests to Alice, both with autoAcceptProof on 'contentApproved'", async () => { + testLogger.test('Faber sends presentation request to Alice') + + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + const aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + await Promise.all([ + waitForProofExchangeRecord(faberAgent, { state: ProofState.Done }), + waitForProofExchangeRecord(aliceAgent, { state: ProofState.Done }), + ]) + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts new file mode 100644 index 0000000000..f2eada74a6 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts @@ -0,0 +1,753 @@ +import type { AnonCredsTestsAgent } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import type { EventReplaySubject } from '../../../../../../tests' + +import { + issueLegacyAnonCredsCredential, + setupAnonCredsTests, +} from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import { waitForProofExchangeRecord } from '../../../../../../tests' +import testLogger from '../../../../../../tests/logger' +import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' +import { LinkedAttachment } from '../../../../../utils/LinkedAttachment' +import { ProofState } from '../../../models' +import { ProofExchangeRecord } from '../../../repository' +import { V2ProposePresentationMessage, V2RequestPresentationMessage, V2PresentationMessage } from '../messages' + +describe('Present Proof', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let credentialDefinitionId: string + let aliceConnectionId: string + let faberConnectionId: string + let faberProofExchangeRecord: ProofExchangeRecord + let aliceProofExchangeRecord: ProofExchangeRecord + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber agent indy proofs', + holderName: 'Alice agent indy proofs', + attributeNames: ['name', 'age', 'image_0', 'image_1'], + })) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'John', + }, + { + name: 'age', + value: '99', + }, + ], + linkedAttachments: [ + new LinkedAttachment({ + name: 'image_0', + attachment: new Attachment({ + filename: 'picture-of-a-cat.png', + data: new AttachmentData({ base64: 'cGljdHVyZSBvZiBhIGNhdA==' }), + }), + }), + new LinkedAttachment({ + name: 'image_1', + attachment: new Attachment({ + filename: 'picture-of-a-dog.png', + data: new AttachmentData({ base64: 'UGljdHVyZSBvZiBhIGRvZw==' }), + }), + }), + ], + }, + }) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with proof proposal to Faber', async () => { + // Alice sends a presentation proposal to Faber + testLogger.test('Alice sends a presentation proposal to Faber') + + let faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + state: ProofState.ProposalReceived, + }) + + aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + proofFormats: { + indy: { + name: 'abc', + version: '1.0', + attributes: [ + { + name: 'name', + value: 'Alice', + credentialDefinitionId, + }, + ], + predicates: [ + { + name: 'age', + predicate: '>=', + threshold: 50, + credentialDefinitionId, + }, + ], + }, + }, + }) + + // Faber waits for a presentation proposal from Alice + testLogger.test('Faber waits for a presentation proposal from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof-req@v2.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + }) + + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber accepts the presentation proposal from Alice + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof-req@v2.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { indy: requestedCredentials.proofFormats.indy }, + }) + + // Faber waits for the presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof@v2.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation provided by Alice + testLogger.test('Faber accepts the presentation provided by Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + + const proposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id) + const requestMessage = await aliceAgent.proofs.findRequestMessage(aliceProofExchangeRecord.id) + const presentationMessage = await aliceAgent.proofs.findPresentationMessage(aliceProofExchangeRecord.id) + + expect(proposalMessage).toBeInstanceOf(V2ProposePresentationMessage) + expect(requestMessage).toBeInstanceOf(V2RequestPresentationMessage) + expect(presentationMessage).toBeInstanceOf(V2PresentationMessage) + + const formatData = await aliceAgent.proofs.getFormatData(aliceProofExchangeRecord.id) + + expect(formatData).toMatchObject({ + proposal: { + indy: { + name: 'abc', + version: '1.0', + nonce: expect.any(String), + requested_attributes: { + [Object.keys(formatData.proposal?.indy?.requested_attributes ?? {})[0]]: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + [Object.keys(formatData.proposal?.indy?.requested_predicates ?? {})[0]]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + request: { + indy: { + name: 'abc', + version: '1.0', + nonce: expect.any(String), + requested_attributes: { + [Object.keys(formatData.request?.indy?.requested_attributes ?? {})[0]]: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + [Object.keys(formatData.request?.indy?.requested_predicates ?? {})[0]]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + presentation: { + indy: { + proof: { + proofs: [ + { + primary_proof: expect.any(Object), + non_revoc_proof: null, + }, + ], + aggregated_proof: { + c_hash: expect.any(String), + c_list: expect.any(Array), + }, + }, + requested_proof: expect.any(Object), + identifiers: expect.any(Array), + }, + }, + }) + }) + + test('Faber starts with proof request to Alice', async () => { + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + indy: { + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof-req@v2.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + + expect(aliceProofExchangeRecord.id).not.toBeNull() + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { indy: requestedCredentials.proofFormats.indy }, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof@v2.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation + testLogger.test('Faber accept the presentation from Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she receives a presentation acknowledgement + testLogger.test('Alice waits for acceptance by Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) + + test('Alice provides credentials via call to getRequestedCredentials', async () => { + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + indy: { + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const retrievedCredentials = await aliceAgent.proofs.getCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + expect(retrievedCredentials).toMatchObject({ + proofFormats: { + indy: { + attributes: { + name: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + credentialId: expect.any(String), + attributes: { + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + age: 99, + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + revocationRegistryId: null, + credentialRevocationId: null, + }, + }, + ], + image_0: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + credentialId: expect.any(String), + attributes: { + age: 99, + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + revocationRegistryId: null, + credentialRevocationId: null, + }, + }, + ], + }, + predicates: { + age: [ + { + credentialId: expect.any(String), + credentialInfo: { + credentialId: expect.any(String), + attributes: { + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + name: 'John', + age: 99, + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + revocationRegistryId: null, + credentialRevocationId: null, + }, + }, + ], + }, + }, + }, + }) + }) + + test('Faber starts with proof request to Alice but gets Problem Reported', async () => { + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'hlindy/proof-req@v2.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + + expect(aliceProofExchangeRecord.id).not.toBeNull() + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + }) + + aliceProofExchangeRecord = await aliceAgent.proofs.sendProblemReport({ + description: 'Problem inside proof request', + proofRecordId: aliceProofExchangeRecord.id, + }) + + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + protocolVersion: 'v2', + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.test.ts new file mode 100644 index 0000000000..fdea6b6698 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.test.ts @@ -0,0 +1,451 @@ +import type { getJsonLdModules } from '../../../../../../tests' +import type { Agent } from '../../../../../agent/Agent' + +import { waitForCredentialRecord, setupJsonLdTests, waitForProofExchangeRecord } from '../../../../../../tests' +import testLogger from '../../../../../../tests/logger' +import { KeyType } from '../../../../../crypto' +import { DidCommMessageRepository } from '../../../../../storage' +import { TypedArrayEncoder } from '../../../../../utils' +import { AutoAcceptCredential, CredentialState } from '../../../../credentials' +import { CREDENTIALS_CONTEXT_V1_URL } from '../../../../vc' +import { ProofState } from '../../../models/ProofState' +import { V2PresentationMessage, V2RequestPresentationMessage } from '../messages' +import { V2ProposePresentationMessage } from '../messages/V2ProposePresentationMessage' + +import { TEST_INPUT_DESCRIPTORS_CITIZENSHIP } from './fixtures' + +const jsonld = { + credential: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + }, + options: { + proofType: 'Ed25519Signature2018', + proofPurpose: 'assertionMethod', + }, +} + +describe('Present Proof', () => { + let proverAgent: Agent> + let issuerAgent: Agent> + let verifierAgent: Agent> + + let issuerProverConnectionId: string + let proverVerifierConnectionId: string + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + holderAgent: proverAgent, + issuerAgent, + verifierAgent, + issuerHolderConnectionId: issuerProverConnectionId, + holderVerifierConnectionId: proverVerifierConnectionId, + } = await setupJsonLdTests({ + holderName: 'presentation exchange prover agent', + issuerName: 'presentation exchange issuer agent', + verifierName: 'presentation exchange verifier agent', + createConnections: true, + autoAcceptCredentials: AutoAcceptCredential.Always, + })) + + await issuerAgent.wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + keyType: KeyType.Ed25519, + }) + + await proverAgent.wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + keyType: KeyType.Ed25519, + }) + + await issuerAgent.credentials.offerCredential({ + connectionId: issuerProverConnectionId, + protocolVersion: 'v2', + credentialFormats: { jsonld }, + }) + + await waitForCredentialRecord(proverAgent, { state: CredentialState.Done }) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await proverAgent.shutdown() + await proverAgent.wallet.delete() + await verifierAgent.shutdown() + await verifierAgent.wallet.delete() + }) + + test(`Prover Creates and sends Proof Proposal to a Verifier`, async () => { + testLogger.test('Prover sends proof proposal to a Verifier') + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + state: ProofState.ProposalReceived, + }) + + await proverAgent.proofs.proposeProof({ + connectionId: proverVerifierConnectionId, + protocolVersion: 'v2', + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + input_descriptors: [TEST_INPUT_DESCRIPTORS_CITIZENSHIP], + }, + }, + }, + comment: 'V2 Presentation Exchange propose proof test', + }) + + testLogger.test('Verifier waits for presentation from the Prover') + const verifierProofExchangeRecord = await verifierPresentationRecordPromise + + const didCommMessageRepository = + verifierAgent.dependencyManager.resolve(DidCommMessageRepository) + + const proposal = await didCommMessageRepository.findAgentMessage(verifierAgent.context, { + associatedRecordId: verifierProofExchangeRecord.id, + messageClass: V2ProposePresentationMessage, + }) + + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + input_descriptors: expect.any(Array), + }, + }, + }, + ], + id: expect.any(String), + comment: 'V2 Presentation Exchange propose proof test', + }) + expect(verifierProofExchangeRecord.id).not.toBeNull() + expect(verifierProofExchangeRecord).toMatchObject({ + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + }) + }) + + test(`Verifier accepts the Proposal send by the Prover`, async () => { + testLogger.test('Prover sends proof proposal to a Verifier') + + let proverProofExchangeRecord = await proverAgent.proofs.proposeProof({ + connectionId: proverVerifierConnectionId, + protocolVersion: 'v2', + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + input_descriptors: [TEST_INPUT_DESCRIPTORS_CITIZENSHIP], + }, + }, + }, + comment: 'V2 Presentation Exchange propose proof test', + }) + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + state: ProofState.ProposalReceived, + }) + + const proverPresentationRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: proverProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Verifier accepts presentation proposal from the Prover') + let verifierProofExchangeRecord = await verifierPresentationRecordPromise + verifierProofExchangeRecord = await verifierAgent.proofs.acceptProposal({ + proofRecordId: verifierProofExchangeRecord.id, + }) + + testLogger.test('Prover waits for proof request from the Verifier') + proverProofExchangeRecord = await proverPresentationRecordPromise + + const didCommMessageRepository = + proverAgent.dependencyManager.resolve(DidCommMessageRepository) + + const request = await didCommMessageRepository.findAgentMessage(proverAgent.context, { + associatedRecordId: proverProofExchangeRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + id: expect.any(String), + formats: [ + { + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + presentation_definition: { + id: expect.any(String), + input_descriptors: [ + { + id: TEST_INPUT_DESCRIPTORS_CITIZENSHIP.id, + constraints: { + fields: TEST_INPUT_DESCRIPTORS_CITIZENSHIP.constraints.fields, + }, + }, + ], + }, + }, + }, + }, + ], + }) + + expect(proverProofExchangeRecord).toMatchObject({ + id: expect.any(String), + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + }) + + test(`Prover accepts presentation request from the Verifier`, async () => { + testLogger.test('Prover sends proof proposal to a Verifier') + + let proverProofExchangeRecord = await proverAgent.proofs.proposeProof({ + connectionId: proverVerifierConnectionId, + protocolVersion: 'v2', + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + input_descriptors: [TEST_INPUT_DESCRIPTORS_CITIZENSHIP], + }, + }, + }, + comment: 'V2 Presentation Exchange propose proof test', + }) + + const verifierProposalReceivedPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + state: ProofState.ProposalReceived, + }) + + const proverPresentationRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: proverProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Verifier accepts presentation proposal from the Prover') + let verifierProofExchangeRecord = await verifierProposalReceivedPresentationRecordPromise + verifierProofExchangeRecord = await verifierAgent.proofs.acceptProposal({ + proofRecordId: verifierProofExchangeRecord.id, + }) + + testLogger.test('Prover waits for proof request from the Verifier') + proverProofExchangeRecord = await proverPresentationRecordPromise + + // Prover retrieves the requested credentials and accepts the presentation request + testLogger.test('Prover accepts presentation request from Verifier') + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await proverAgent.proofs.acceptRequest({ + proofRecordId: proverProofExchangeRecord.id, + }) + + // Verifier waits for the presentation from the Prover + testLogger.test('Verifier waits for presentation from the Prover') + verifierProofExchangeRecord = await verifierPresentationRecordPromise + + const didCommMessageRepository = + verifierAgent.dependencyManager.resolve(DidCommMessageRepository) + + const presentation = await didCommMessageRepository.findAgentMessage(verifierAgent.context, { + associatedRecordId: verifierProofExchangeRecord.id, + messageClass: V2PresentationMessage, + }) + + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/submission@v1.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + '@context': expect.any(Array), + type: expect.any(Array), + presentation_submission: { + id: expect.any(String), + definition_id: expect.any(String), + descriptor_map: [ + { + id: 'citizenship_input_1', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + ], + }, + verifiableCredential: [ + { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://www.w3.org/2018/credentials/examples/v1', + ], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: expect.any(String), + issuanceDate: expect.any(String), + credentialSubject: { + id: expect.any(String), + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + proof: { + verificationMethod: expect.any(String), + type: 'Ed25519Signature2018', + created: expect.any(String), + proofPurpose: 'assertionMethod', + jws: expect.any(String), + }, + }, + ], + proof: { + verificationMethod: expect.any(String), + type: 'Ed25519Signature2018', + created: expect.any(String), + proofPurpose: 'authentication', + challenge: expect.any(String), + jws: expect.any(String), + }, + }, + }, + }, + ], + id: expect.any(String), + thread: { + threadId: verifierProofExchangeRecord.threadId, + }, + }) + + expect(verifierProofExchangeRecord.id).not.toBeNull() + expect(verifierProofExchangeRecord).toMatchObject({ + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + }) + + test(`Verifier accepts the presentation provided by the Prover`, async () => { + testLogger.test('Prover sends proof proposal to a Verifier') + + let proverProofExchangeRecord = await proverAgent.proofs.proposeProof({ + connectionId: proverVerifierConnectionId, + protocolVersion: 'v2', + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + input_descriptors: [TEST_INPUT_DESCRIPTORS_CITIZENSHIP], + }, + }, + }, + comment: 'V2 Presentation Exchange propose proof test', + }) + + const verifierProposalReceivedPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + state: ProofState.ProposalReceived, + }) + + const proverPresentationRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: proverProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Verifier accepts presentation proposal from the Prover') + let verifierProofExchangeRecord = await verifierProposalReceivedPresentationRecordPromise + verifierProofExchangeRecord = await verifierAgent.proofs.acceptProposal({ + proofRecordId: verifierProofExchangeRecord.id, + }) + + testLogger.test('Prover waits for proof request from the Verifier') + proverProofExchangeRecord = await proverPresentationRecordPromise + + // Prover retrieves the requested credentials and accepts the presentation request + testLogger.test('Prover accepts presentation request from Verifier') + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await proverAgent.proofs.acceptRequest({ + proofRecordId: proverProofExchangeRecord.id, + }) + + // Verifier waits for the presentation from the Prover + testLogger.test('Verifier waits for presentation from the Prover') + verifierProofExchangeRecord = await verifierPresentationRecordPromise + + const proverProofExchangeRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: proverProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Verifier accepts the presentation provided by by the Prover + testLogger.test('Verifier accepts the presentation provided by the Prover') + await verifierAgent.proofs.acceptPresentation({ proofRecordId: verifierProofExchangeRecord.id }) + + // Prover waits until she received a presentation acknowledgement + testLogger.test('Prover waits until she receives a presentation acknowledgement') + proverProofExchangeRecord = await proverProofExchangeRecordPromise + + expect(verifierProofExchangeRecord).toMatchObject({ + id: expect.any(String), + createdAt: expect.any(Date), + threadId: proverProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(proverProofExchangeRecord).toMatchObject({ + id: expect.any(String), + createdAt: expect.any(Date), + threadId: verifierProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v2/errors/V2PresentationProblemReportError.ts b/packages/core/src/modules/proofs/protocol/v2/errors/V2PresentationProblemReportError.ts new file mode 100644 index 0000000000..76fa789a6e --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/errors/V2PresentationProblemReportError.ts @@ -0,0 +1,23 @@ +import type { ProblemReportErrorOptions } from '../../../../problem-reports' +import type { PresentationProblemReportReason } from '../../../errors/PresentationProblemReportReason' + +import { ProblemReportError } from '../../../../problem-reports/errors/ProblemReportError' +import { V2PresentationProblemReportMessage } from '../messages' + +interface V2PresentationProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: PresentationProblemReportReason +} + +export class V2PresentationProblemReportError extends ProblemReportError { + public problemReport: V2PresentationProblemReportMessage + + public constructor(public message: string, { problemCode }: V2PresentationProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new V2PresentationProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/errors/index.ts b/packages/core/src/modules/proofs/protocol/v2/errors/index.ts new file mode 100644 index 0000000000..7064b070aa --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/errors/index.ts @@ -0,0 +1 @@ +export * from './V2PresentationProblemReportError' diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationAckHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationAckHandler.ts new file mode 100644 index 0000000000..43b9e15a69 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationAckHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { ProofProtocol } from '../../ProofProtocol' + +import { V2PresentationAckMessage } from '../messages' + +export class V2PresentationAckHandler implements MessageHandler { + private proofProtocol: ProofProtocol + public supportedMessages = [V2PresentationAckMessage] + + public constructor(proofProtocol: ProofProtocol) { + this.proofProtocol = proofProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.proofProtocol.processAck(messageContext) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts new file mode 100644 index 0000000000..b57fc5733e --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts @@ -0,0 +1,55 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { ProofExchangeRecord } from '../../../repository' +import type { V2ProofProtocol } from '../V2ProofProtocol' + +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../../../storage/didcomm' +import { V2PresentationMessage, V2RequestPresentationMessage } from '../messages' + +export class V2PresentationHandler implements MessageHandler { + private proofProtocol: V2ProofProtocol + public supportedMessages = [V2PresentationMessage] + + public constructor(proofProtocol: V2ProofProtocol) { + this.proofProtocol = proofProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const proofRecord = await this.proofProtocol.processPresentation(messageContext) + + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToPresentation(messageContext.agentContext, { + proofRecord, + presentationMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return await this.acceptPresentation(proofRecord, messageContext) + } + } + + private async acceptPresentation( + proofRecord: ProofExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending acknowledgement with autoAccept`) + + const { message } = await this.proofProtocol.acceptPresentation(messageContext.agentContext, { + proofRecord, + }) + + const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) + const requestMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + role: DidCommMessageRole.Sender, + }) + + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: proofRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: requestMessage, + }) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationProblemReportHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationProblemReportHandler.ts new file mode 100644 index 0000000000..5d9512d824 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V2ProofProtocol } from '../V2ProofProtocol' + +import { V2PresentationProblemReportMessage } from '../messages' + +export class V2PresentationProblemReportHandler implements MessageHandler { + private proofService: V2ProofProtocol + public supportedMessages = [V2PresentationProblemReportMessage] + + public constructor(proofService: V2ProofProtocol) { + this.proofService = proofService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.proofService.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2ProposePresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2ProposePresentationHandler.ts new file mode 100644 index 0000000000..589ff6db3e --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2ProposePresentationHandler.ts @@ -0,0 +1,47 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' +import type { V2ProofProtocol } from '../V2ProofProtocol' + +import { OutboundMessageContext } from '../../../../../agent/models' +import { V2ProposePresentationMessage } from '../messages/V2ProposePresentationMessage' + +export class V2ProposePresentationHandler implements MessageHandler { + private proofProtocol: V2ProofProtocol + public supportedMessages = [V2ProposePresentationMessage] + + public constructor(proofProtocol: V2ProofProtocol) { + this.proofProtocol = proofProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const proofRecord = await this.proofProtocol.processProposal(messageContext) + + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToProposal(messageContext.agentContext, { + proofRecord, + proposalMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return this.acceptProposal(proofRecord, messageContext) + } + } + private async acceptProposal( + proofRecord: ProofExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) + + if (!messageContext.connection) { + messageContext.agentContext.config.logger.error('No connection on the messageContext, aborting auto accept') + return + } + + const { message } = await this.proofProtocol.acceptProposal(messageContext.agentContext, { proofRecord }) + + return new OutboundMessageContext(message, { + agentContext: messageContext.agentContext, + connection: messageContext.connection, + associatedRecord: proofRecord, + }) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts new file mode 100644 index 0000000000..394a4ff2a9 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts @@ -0,0 +1,48 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' +import type { V2ProofProtocol } from '../V2ProofProtocol' + +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' +import { V2RequestPresentationMessage } from '../messages/V2RequestPresentationMessage' + +export class V2RequestPresentationHandler implements MessageHandler { + private proofProtocol: V2ProofProtocol + public supportedMessages = [V2RequestPresentationMessage] + + public constructor(proofProtocol: V2ProofProtocol) { + this.proofProtocol = proofProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const proofRecord = await this.proofProtocol.processRequest(messageContext) + + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToRequest(messageContext.agentContext, { + proofRecord, + requestMessage: messageContext.message, + }) + + messageContext.agentContext.config.logger.debug(`Should auto respond to request: ${shouldAutoRespond}`) + + if (shouldAutoRespond) { + return await this.acceptRequest(proofRecord, messageContext) + } + } + + private async acceptRequest( + proofRecord: ProofExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending presentation with autoAccept`) + + const { message } = await this.proofProtocol.acceptRequest(messageContext.agentContext, { + proofRecord, + }) + + return getOutboundMessageContext(messageContext.agentContext, { + message, + lastReceivedMessage: messageContext.message, + associatedRecord: proofRecord, + connectionRecord: messageContext.connection, + }) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/index.ts b/packages/core/src/modules/proofs/protocol/v2/index.ts new file mode 100644 index 0000000000..33918dd3bc --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/index.ts @@ -0,0 +1,3 @@ +export * from './errors' +export * from './messages' +export * from './V2ProofProtocol' diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationAckMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationAckMessage.ts new file mode 100644 index 0000000000..19e602b71d --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationAckMessage.ts @@ -0,0 +1,8 @@ +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { AckMessage } from '../../../../common/messages/AckMessage' + +export class V2PresentationAckMessage extends AckMessage { + @IsValidMessageType(V2PresentationAckMessage.type) + public readonly type = V2PresentationAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/2.0/ack') +} diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationMessage.ts new file mode 100644 index 0000000000..fee3467dfc --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationMessage.ts @@ -0,0 +1,76 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsBoolean, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { uuid } from '../../../../../utils/uuid' +import { ProofFormatSpec } from '../../../models/ProofFormatSpec' + +export interface V2PresentationMessageOptions { + id?: string + comment?: string + goalCode?: string + goal?: string + lastPresentation?: boolean + presentationAttachments: Attachment[] + formats: ProofFormatSpec[] +} + +export class V2PresentationMessage extends AgentMessage { + public constructor(options: V2PresentationMessageOptions) { + super() + + if (options) { + this.formats = [] + this.presentationAttachments = [] + this.id = options.id ?? uuid() + this.comment = options.comment + this.goalCode = options.goalCode + this.goal = options.goal + this.lastPresentation = options.lastPresentation ?? true + + this.formats = options.formats + this.presentationAttachments = options.presentationAttachments + } + } + + @IsValidMessageType(V2PresentationMessage.type) + public readonly type = V2PresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/2.0/presentation') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + + @IsString() + @IsOptional() + public goal?: string + + @Expose({ name: 'last_presentation' }) + @IsBoolean() + public lastPresentation = true + + @Expose({ name: 'formats' }) + @Type(() => ProofFormatSpec) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(ProofFormatSpec, { each: true }) + public formats!: ProofFormatSpec[] + + @Expose({ name: 'presentations~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(Attachment, { each: true }) + public presentationAttachments!: Attachment[] + + public getPresentationAttachmentById(id: string): Attachment | undefined { + return this.presentationAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationProblemReportMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationProblemReportMessage.ts new file mode 100644 index 0000000000..ed97f72319 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationProblemReportMessage.ts @@ -0,0 +1,11 @@ +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { ProblemReportMessage } from '../../../../problem-reports/messages/ProblemReportMessage' + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class V2PresentationProblemReportMessage extends ProblemReportMessage { + @IsValidMessageType(V2PresentationProblemReportMessage.type) + public readonly type = V2PresentationProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/2.0/problem-report') +} diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2ProposePresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2ProposePresentationMessage.ts new file mode 100644 index 0000000000..1683b0ef90 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/messages/V2ProposePresentationMessage.ts @@ -0,0 +1,68 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { uuid } from '../../../../../utils/uuid' +import { ProofFormatSpec } from '../../../models/ProofFormatSpec' + +export interface V2ProposePresentationMessageOptions { + id?: string + comment?: string + goalCode?: string + goal?: string + proposalAttachments: Attachment[] + formats: ProofFormatSpec[] +} + +export class V2ProposePresentationMessage extends AgentMessage { + public constructor(options: V2ProposePresentationMessageOptions) { + super() + + if (options) { + this.formats = [] + this.proposalAttachments = [] + this.id = options.id ?? uuid() + this.comment = options.comment + this.goalCode = options.goalCode + this.goal = options.goal + this.formats = options.formats + this.proposalAttachments = options.proposalAttachments + } + } + + @IsValidMessageType(V2ProposePresentationMessage.type) + public readonly type = V2ProposePresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/2.0/propose-presentation') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + + @IsString() + @IsOptional() + public goal?: string + + @Type(() => ProofFormatSpec) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(ProofFormatSpec, { each: true }) + public formats!: ProofFormatSpec[] + + @Expose({ name: 'proposals~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(Attachment, { each: true }) + public proposalAttachments!: Attachment[] + + public getProposalAttachmentById(id: string): Attachment | undefined { + return this.proposalAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2RequestPresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2RequestPresentationMessage.ts new file mode 100644 index 0000000000..10f6806c38 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/messages/V2RequestPresentationMessage.ts @@ -0,0 +1,81 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsBoolean, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { uuid } from '../../../../../utils/uuid' +import { ProofFormatSpec } from '../../../models/ProofFormatSpec' + +export interface V2RequestPresentationMessageOptions { + id?: string + comment?: string + goalCode?: string + goal?: string + presentMultiple?: boolean + willConfirm?: boolean + formats: ProofFormatSpec[] + requestAttachments: Attachment[] +} + +export class V2RequestPresentationMessage extends AgentMessage { + public constructor(options: V2RequestPresentationMessageOptions) { + super() + + if (options) { + this.formats = [] + this.requestAttachments = [] + this.id = options.id ?? uuid() + this.comment = options.comment + this.goal = options.goal + this.goalCode = options.goalCode + this.willConfirm = options.willConfirm ?? true + this.presentMultiple = options.presentMultiple ?? false + this.requestAttachments = options.requestAttachments + this.formats = options.formats + } + } + + @IsValidMessageType(V2RequestPresentationMessage.type) + public readonly type = V2RequestPresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/2.0/request-presentation') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + + @IsString() + @IsOptional() + public goal?: string + + @Expose({ name: 'will_confirm' }) + @IsBoolean() + public willConfirm = false + + @Expose({ name: 'present_multiple' }) + @IsBoolean() + public presentMultiple = false + + @Expose({ name: 'formats' }) + @Type(() => ProofFormatSpec) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(ProofFormatSpec, { each: true }) + public formats!: ProofFormatSpec[] + + @Expose({ name: 'request_presentations~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(Attachment, { each: true }) + public requestAttachments!: Attachment[] + + public getRequestAttachmentById(id: string): Attachment | undefined { + return this.requestAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/index.ts b/packages/core/src/modules/proofs/protocol/v2/messages/index.ts new file mode 100644 index 0000000000..515b0afb9c --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/messages/index.ts @@ -0,0 +1,5 @@ +export * from './V2PresentationAckMessage' +export * from './V2PresentationMessage' +export * from './V2PresentationProblemReportMessage' +export * from './V2ProposePresentationMessage' +export * from './V2RequestPresentationMessage' diff --git a/packages/core/src/modules/proofs/repository/ProofExchangeRecord.ts b/packages/core/src/modules/proofs/repository/ProofExchangeRecord.ts new file mode 100644 index 0000000000..67487b6b1f --- /dev/null +++ b/packages/core/src/modules/proofs/repository/ProofExchangeRecord.ts @@ -0,0 +1,109 @@ +import type { TagsBase } from '../../../storage/BaseRecord' +import type { ProofRole, ProofState } from '../models' +import type { AutoAcceptProof } from '../models/ProofAutoAcceptType' + +import { CredoError } from '../../../error' +import { BaseRecord } from '../../../storage/BaseRecord' +import { uuid } from '../../../utils/uuid' + +export interface ProofExchangeRecordProps { + id?: string + createdAt?: Date + protocolVersion: string + isVerified?: boolean + state: ProofState + role: ProofRole + connectionId?: string + threadId: string + parentThreadId?: string + tags?: CustomProofTags + autoAcceptProof?: AutoAcceptProof + errorMessage?: string +} + +export type CustomProofTags = TagsBase +export type DefaultProofTags = { + threadId: string + parentThreadId?: string + connectionId?: string + state: ProofState + role: ProofRole +} + +export class ProofExchangeRecord extends BaseRecord { + public connectionId?: string + public threadId!: string + public protocolVersion!: string + public parentThreadId?: string + public isVerified?: boolean + public state!: ProofState + public role!: ProofRole + public autoAcceptProof?: AutoAcceptProof + public errorMessage?: string + + public static readonly type = 'ProofRecord' + public readonly type = ProofExchangeRecord.type + + public constructor(props: ProofExchangeRecordProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.protocolVersion = props.protocolVersion + + this.isVerified = props.isVerified + this.state = props.state + this.role = props.role + this.connectionId = props.connectionId + this.threadId = props.threadId + this.parentThreadId = props.parentThreadId + this.autoAcceptProof = props.autoAcceptProof + this._tags = props.tags ?? {} + this.errorMessage = props.errorMessage + } + } + + public getTags() { + return { + ...this._tags, + threadId: this.threadId, + parentThreadId: this.parentThreadId, + connectionId: this.connectionId, + state: this.state, + role: this.role, + } + } + + public assertState(expectedStates: ProofState | ProofState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new CredoError( + `Proof record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.` + ) + } + } + + public assertProtocolVersion(version: string) { + if (this.protocolVersion !== version) { + throw new CredoError( + `Proof record has invalid protocol version ${this.protocolVersion}. Expected version ${version}` + ) + } + } + + public assertConnection(currentConnectionId: string) { + if (!this.connectionId) { + throw new CredoError( + `Proof record is not associated with any connection. This is often the case with connection-less presentation exchange` + ) + } else if (this.connectionId !== currentConnectionId) { + throw new CredoError( + `Proof record is associated with connection '${this.connectionId}'. Current connection is '${currentConnectionId}'` + ) + } + } +} diff --git a/packages/core/src/modules/proofs/repository/ProofRepository.ts b/packages/core/src/modules/proofs/repository/ProofRepository.ts new file mode 100644 index 0000000000..48c6453d76 --- /dev/null +++ b/packages/core/src/modules/proofs/repository/ProofRepository.ts @@ -0,0 +1,51 @@ +import type { AgentContext } from '../../../agent/context' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { ProofExchangeRecord } from './ProofExchangeRecord' + +@injectable() +export class ProofRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(ProofExchangeRecord, storageService, eventEmitter) + } + + /** + * Retrieve a proof record by connection id and thread id + * + * @param connectionId The connection id + * @param threadId The thread id + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * @returns The proof record + */ + public async getByThreadAndConnectionId( + agentContext: AgentContext, + threadId: string, + connectionId?: string + ): Promise { + return this.getSingleByQuery(agentContext, { threadId, connectionId }) + } + + /** + * Retrieve proof records by connection id and parent thread id + * + * @param connectionId The connection id + * @param parentThreadId The parent thread id + * @returns List containing all proof records matching the given query + */ + public async getByParentThreadAndConnectionId( + agentContext: AgentContext, + parentThreadId: string, + connectionId?: string + ): Promise { + return this.findByQuery(agentContext, { parentThreadId, connectionId }) + } +} diff --git a/packages/core/src/modules/proofs/repository/index.ts b/packages/core/src/modules/proofs/repository/index.ts new file mode 100644 index 0000000000..9937a507b5 --- /dev/null +++ b/packages/core/src/modules/proofs/repository/index.ts @@ -0,0 +1,2 @@ +export * from './ProofExchangeRecord' +export * from './ProofRepository' diff --git a/packages/core/src/modules/proofs/utils/composeAutoAccept.ts b/packages/core/src/modules/proofs/utils/composeAutoAccept.ts new file mode 100644 index 0000000000..9e58b287ef --- /dev/null +++ b/packages/core/src/modules/proofs/utils/composeAutoAccept.ts @@ -0,0 +1,11 @@ +import { AutoAcceptProof } from '../models' + +/** + * Returns the proof auto accept config based on priority: + * - The record config takes first priority + * - Otherwise the agent config + * - Otherwise {@link AutoAcceptProof.Never} is returned + */ +export function composeAutoAccept(recordConfig?: AutoAcceptProof, agentConfig?: AutoAcceptProof) { + return recordConfig ?? agentConfig ?? AutoAcceptProof.Never +} diff --git a/packages/core/src/modules/proofs/utils/index.ts b/packages/core/src/modules/proofs/utils/index.ts new file mode 100644 index 0000000000..e9685c62fe --- /dev/null +++ b/packages/core/src/modules/proofs/utils/index.ts @@ -0,0 +1 @@ +export * from './composeAutoAccept' diff --git a/packages/core/src/modules/routing/MediationRecipientApi.ts b/packages/core/src/modules/routing/MediationRecipientApi.ts new file mode 100644 index 0000000000..aa01f7b862 --- /dev/null +++ b/packages/core/src/modules/routing/MediationRecipientApi.ts @@ -0,0 +1,500 @@ +import type { MediationStateChangedEvent } from './RoutingEvents' +import type { MediationRecord } from './repository' +import type { GetRoutingOptions } from './services/RoutingService' +import type { OutboundWebSocketClosedEvent, OutboundWebSocketOpenedEvent } from '../../transport' +import type { ConnectionRecord } from '../connections' + +import { firstValueFrom, interval, merge, ReplaySubject, Subject, timer } from 'rxjs' +import { delayWhen, filter, first, takeUntil, tap, throttleTime, timeout } from 'rxjs/operators' + +import { AgentContext } from '../../agent' +import { EventEmitter } from '../../agent/EventEmitter' +import { filterContextCorrelationId } from '../../agent/Events' +import { MessageHandlerRegistry } from '../../agent/MessageHandlerRegistry' +import { MessageSender } from '../../agent/MessageSender' +import { OutboundMessageContext } from '../../agent/models' +import { InjectionSymbols } from '../../constants' +import { CredoError } from '../../error' +import { Logger } from '../../logger' +import { inject, injectable } from '../../plugins' +import { TransportEventTypes } from '../../transport' +import { ConnectionMetadataKeys } from '../connections/repository/ConnectionMetadataTypes' +import { ConnectionService } from '../connections/services' +import { DidsApi } from '../dids' +import { verkeyToDidKey } from '../dids/helpers' +import { DiscoverFeaturesApi } from '../discover-features' +import { MessagePickupApi } from '../message-pickup/MessagePickupApi' +import { V1BatchPickupMessage } from '../message-pickup/protocol/v1' +import { V2StatusMessage } from '../message-pickup/protocol/v2' + +import { MediationRecipientModuleConfig } from './MediationRecipientModuleConfig' +import { MediatorPickupStrategy } from './MediatorPickupStrategy' +import { RoutingEventTypes } from './RoutingEvents' +import { KeylistUpdateResponseHandler } from './handlers/KeylistUpdateResponseHandler' +import { MediationDenyHandler } from './handlers/MediationDenyHandler' +import { MediationGrantHandler } from './handlers/MediationGrantHandler' +import { KeylistUpdate, KeylistUpdateAction, KeylistUpdateMessage } from './messages' +import { MediationState } from './models/MediationState' +import { MediationRepository } from './repository' +import { MediationRecipientService } from './services/MediationRecipientService' +import { RoutingService } from './services/RoutingService' + +@injectable() +export class MediationRecipientApi { + public config: MediationRecipientModuleConfig + + private mediationRecipientService: MediationRecipientService + private connectionService: ConnectionService + private dids: DidsApi + private messageSender: MessageSender + private eventEmitter: EventEmitter + private logger: Logger + private discoverFeaturesApi: DiscoverFeaturesApi + private messagePickupApi: MessagePickupApi + private mediationRepository: MediationRepository + private routingService: RoutingService + private agentContext: AgentContext + private stop$: Subject + + // stopMessagePickup$ is used for stop message pickup signal + private readonly stopMessagePickup$ = new Subject() + + public constructor( + messageHandlerRegistry: MessageHandlerRegistry, + mediationRecipientService: MediationRecipientService, + connectionService: ConnectionService, + dids: DidsApi, + messageSender: MessageSender, + eventEmitter: EventEmitter, + discoverFeaturesApi: DiscoverFeaturesApi, + messagePickupApi: MessagePickupApi, + mediationRepository: MediationRepository, + routingService: RoutingService, + @inject(InjectionSymbols.Logger) logger: Logger, + agentContext: AgentContext, + @inject(InjectionSymbols.Stop$) stop$: Subject, + mediationRecipientModuleConfig: MediationRecipientModuleConfig + ) { + this.connectionService = connectionService + this.dids = dids + this.mediationRecipientService = mediationRecipientService + this.messageSender = messageSender + this.eventEmitter = eventEmitter + this.logger = logger + this.discoverFeaturesApi = discoverFeaturesApi + this.messagePickupApi = messagePickupApi + this.mediationRepository = mediationRepository + this.routingService = routingService + this.agentContext = agentContext + this.stop$ = stop$ + this.config = mediationRecipientModuleConfig + this.registerMessageHandlers(messageHandlerRegistry) + } + + public async initialize() { + // Poll for messages from mediator + const defaultMediator = await this.findDefaultMediator() + if (defaultMediator) { + this.initiateMessagePickup(defaultMediator).catch((error) => { + this.logger.warn(`Error initiating message pickup with mediator ${defaultMediator.id}`, { error }) + }) + } + } + + private async sendMessage(outboundMessageContext: OutboundMessageContext, pickupStrategy?: MediatorPickupStrategy) { + const mediatorPickupStrategy = pickupStrategy ?? this.config.mediatorPickupStrategy + const transportPriority = + mediatorPickupStrategy === MediatorPickupStrategy.Implicit + ? { schemes: ['wss', 'ws'], restrictive: true } + : undefined + + await this.messageSender.sendMessage(outboundMessageContext, { + transportPriority, + // TODO: add keepAlive: true to enforce through the public api + // we need to keep the socket alive. It already works this way, but would + // be good to make more explicit from the public facing API. + // This would also make it easier to change the internal API later on. + // keepAlive: true, + }) + } + + /** + * Implicit mode consists simply on initiating a long-lived session to a mediator and wait for the + * messages to arrive automatically. + * + * In order to do initiate this session, we open a suitable connection (using WebSocket transport) and + * send a Trust Ping message. + * + * @param mediator + */ + private async initiateImplicitMode(mediator: MediationRecord) { + const connection = await this.connectionService.getById(this.agentContext, mediator.connectionId) + const { message, connectionRecord } = await this.connectionService.createTrustPing(this.agentContext, connection, { + responseRequested: false, + }) + + const websocketSchemes = ['ws', 'wss'] + const didDocument = connectionRecord.theirDid && (await this.dids.resolveDidDocument(connectionRecord.theirDid)) + const services = didDocument && didDocument?.didCommServices + const hasWebSocketTransport = services && services.some((s) => websocketSchemes.includes(s.protocolScheme)) + + if (!hasWebSocketTransport) { + throw new CredoError('Cannot open websocket to connection without websocket service endpoint') + } + + await this.messageSender.sendMessage( + new OutboundMessageContext(message, { agentContext: this.agentContext, connection: connectionRecord }), + { + transportPriority: { + schemes: websocketSchemes, + restrictive: true, + // TODO: add keepAlive: true to enforce through the public api + // we need to keep the socket alive. It already works this way, but would + // be good to make more explicit from the public facing API. + // This would also make it easier to change the internal API later on. + // keepAlive: true, + }, + } + ) + } + + /** + * Keep track of a persistent transport session with a mediator, trying to reconnect to it as + * soon as it is disconnected, using a recursive back-off strategy + * + * @param mediator mediation record + * @param pickupStrategy chosen pick up strategy (should be Implicit or PickUp in Live Mode) + */ + private async monitorMediatorWebSocketEvents(mediator: MediationRecord, pickupStrategy: MediatorPickupStrategy) { + const { baseMediatorReconnectionIntervalMs, maximumMediatorReconnectionIntervalMs } = this.config + let interval = baseMediatorReconnectionIntervalMs + + const stopConditions$ = merge(this.stop$, this.stopMessagePickup$).pipe() + + // Reset back off interval when the websocket is successfully opened again + this.eventEmitter + .observable(TransportEventTypes.OutboundWebSocketOpenedEvent) + .pipe( + // Stop when the agent shuts down or stop message pickup signal is received + takeUntil(stopConditions$), + filter((e) => e.payload.connectionId === mediator.connectionId) + ) + .subscribe(() => { + interval = baseMediatorReconnectionIntervalMs + }) + + // FIXME: this won't work for tenant agents created by the tenants module as the agent context session + // could be closed. I'm not sure we want to support this as you probably don't want different tenants opening + // various websocket connections to mediators. However we should look at throwing an error or making sure + // it is not possible to use the mediation module with tenant agents. + + // Listens to Outbound websocket closed events and will reopen the websocket connection + // in a recursive back off strategy if it matches the following criteria: + // - Agent is not shutdown + // - Socket was for current mediator connection id + this.eventEmitter + .observable(TransportEventTypes.OutboundWebSocketClosedEvent) + .pipe( + // Stop when the agent shuts down or stop message pickup signal is received + takeUntil(stopConditions$), + filter((e) => e.payload.connectionId === mediator.connectionId), + // Make sure we're not reconnecting multiple times + throttleTime(interval), + // Wait for interval time before reconnecting + delayWhen(() => timer(interval)), + // Increase the interval (recursive back-off) + tap(() => { + interval = Math.min(interval * 2, maximumMediatorReconnectionIntervalMs) + }) + ) + .subscribe({ + next: async () => { + this.logger.debug( + `Websocket connection to mediator with connectionId '${mediator.connectionId}' is closed, attempting to reconnect...` + ) + try { + if (pickupStrategy === MediatorPickupStrategy.PickUpV2LiveMode) { + // Start Pickup v2 protocol in live mode (retrieve any queued message before) + await this.messagePickupApi.pickupMessages({ + connectionId: mediator.connectionId, + protocolVersion: 'v2', + awaitCompletion: true, + }) + + await this.messagePickupApi.setLiveDeliveryMode({ + connectionId: mediator.connectionId, + liveDelivery: true, + protocolVersion: 'v2', + }) + } else { + await this.initiateImplicitMode(mediator) + } + } catch (error) { + this.logger.warn('Unable to re-open websocket connection to mediator', { error }) + } + }, + complete: () => this.logger.info(`Stopping pickup of messages from mediator '${mediator.id}'`), + }) + } + + /** + * Start a Message Pickup flow with a registered Mediator. + * + * @param mediator optional {MediationRecord} corresponding to the mediator to pick messages from. It will use + * default mediator otherwise + * @param pickupStrategy optional {MediatorPickupStrategy} to use in the loop. It will use Agent's default + * strategy or attempt to find it by Discover Features otherwise + * @returns + */ + public async initiateMessagePickup(mediator?: MediationRecord, pickupStrategy?: MediatorPickupStrategy) { + const { mediatorPollingInterval } = this.config + const mediatorRecord = mediator ?? (await this.findDefaultMediator()) + if (!mediatorRecord) { + throw new CredoError('There is no mediator to pickup messages from') + } + + const mediatorPickupStrategy = pickupStrategy ?? (await this.getPickupStrategyForMediator(mediatorRecord)) + const mediatorConnection = await this.connectionService.getById(this.agentContext, mediatorRecord.connectionId) + + switch (mediatorPickupStrategy) { + case MediatorPickupStrategy.PickUpV1: + case MediatorPickupStrategy.PickUpV2: { + const stopConditions$ = merge(this.stop$, this.stopMessagePickup$).pipe() + // PickUpV1/PickUpV2 means polling every X seconds with batch message + const protocolVersion = mediatorPickupStrategy === MediatorPickupStrategy.PickUpV2 ? 'v2' : 'v1' + + this.logger.info( + `Starting explicit pickup of messages from mediator '${mediatorRecord.id}' using ${protocolVersion}` + ) + const subscription = interval(mediatorPollingInterval) + .pipe(takeUntil(stopConditions$)) + .subscribe({ + next: async () => { + await this.messagePickupApi.pickupMessages({ + connectionId: mediatorConnection.id, + batchSize: this.config.maximumMessagePickup, + protocolVersion, + }) + }, + complete: () => this.logger.info(`Stopping pickup of messages from mediator '${mediatorRecord.id}'`), + }) + return subscription + } + case MediatorPickupStrategy.PickUpV2LiveMode: + // PickUp V2 in Live Mode will retrieve queued messages and then set up live delivery mode + this.logger.info(`Starting Live Mode pickup of messages from mediator '${mediatorRecord.id}'`) + await this.monitorMediatorWebSocketEvents(mediatorRecord, mediatorPickupStrategy) + + await this.messagePickupApi.pickupMessages({ + connectionId: mediatorConnection.id, + protocolVersion: 'v2', + awaitCompletion: true, + }) + + await this.messagePickupApi.setLiveDeliveryMode({ + connectionId: mediatorConnection.id, + liveDelivery: true, + protocolVersion: 'v2', + }) + + break + case MediatorPickupStrategy.Implicit: + // Implicit means sending ping once and keeping connection open. This requires a long-lived transport + // such as WebSockets to work + this.logger.info(`Starting implicit pickup of messages from mediator '${mediatorRecord.id}'`) + await this.monitorMediatorWebSocketEvents(mediatorRecord, mediatorPickupStrategy) + await this.initiateImplicitMode(mediatorRecord) + break + default: + this.logger.info(`Skipping pickup of messages from mediator '${mediatorRecord.id}' due to pickup strategy none`) + } + } + + /** + * Terminate all ongoing Message Pickup loops + */ + public async stopMessagePickup() { + this.stopMessagePickup$.next(true) + } + + private async getPickupStrategyForMediator(mediator: MediationRecord) { + let mediatorPickupStrategy = mediator.pickupStrategy ?? this.config.mediatorPickupStrategy + + // If mediator pickup strategy is not configured we try to query if batch pickup + // is supported through the discover features protocol + if (!mediatorPickupStrategy) { + const discloseForPickupV2 = await this.discoverFeaturesApi.queryFeatures({ + connectionId: mediator.connectionId, + protocolVersion: 'v1', + queries: [{ featureType: 'protocol', match: V2StatusMessage.type.protocolUri }], + awaitDisclosures: true, + }) + + if (discloseForPickupV2.features?.find((item) => item.id === V2StatusMessage.type.protocolUri)) { + mediatorPickupStrategy = MediatorPickupStrategy.PickUpV2 + } else { + const discloseForPickupV1 = await this.discoverFeaturesApi.queryFeatures({ + connectionId: mediator.connectionId, + protocolVersion: 'v1', + queries: [{ featureType: 'protocol', match: V1BatchPickupMessage.type.protocolUri }], + awaitDisclosures: true, + }) + // Use explicit pickup strategy + mediatorPickupStrategy = discloseForPickupV1.features?.find( + (item) => item.id === V1BatchPickupMessage.type.protocolUri + ) + ? MediatorPickupStrategy.PickUpV1 + : MediatorPickupStrategy.Implicit + } + + // Store the result so it can be reused next time + mediator.pickupStrategy = mediatorPickupStrategy + await this.mediationRepository.update(this.agentContext, mediator) + } + + return mediatorPickupStrategy + } + + public async discoverMediation() { + return this.mediationRecipientService.discoverMediation(this.agentContext) + } + + public async setDefaultMediator(mediatorRecord: MediationRecord) { + return this.mediationRecipientService.setDefaultMediator(this.agentContext, mediatorRecord) + } + + public async requestMediation(connection: ConnectionRecord): Promise { + const { mediationRecord, message } = await this.mediationRecipientService.createRequest( + this.agentContext, + connection + ) + const outboundMessage = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connection, + }) + + await this.sendMessage(outboundMessage) + return mediationRecord + } + + public async notifyKeylistUpdate(connection: ConnectionRecord, verkey: string, action?: KeylistUpdateAction) { + // Use our useDidKey configuration unless we know the key formatting other party is using + let useDidKey = this.agentContext.config.useDidKeyInProtocols + + const useDidKeysConnectionMetadata = connection.metadata.get(ConnectionMetadataKeys.UseDidKeysForProtocol) + if (useDidKeysConnectionMetadata) { + useDidKey = useDidKeysConnectionMetadata[KeylistUpdateMessage.type.protocolUri] ?? useDidKey + } + + const message = this.mediationRecipientService.createKeylistUpdateMessage([ + new KeylistUpdate({ + action: action ?? KeylistUpdateAction.add, + recipientKey: useDidKey ? verkeyToDidKey(verkey) : verkey, + }), + ]) + + const outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection, + }) + await this.sendMessage(outboundMessageContext) + } + + public async findByConnectionId(connectionId: string) { + return await this.mediationRecipientService.findByConnectionId(this.agentContext, connectionId) + } + + public async getMediators() { + return await this.mediationRecipientService.getMediators(this.agentContext) + } + + public async findDefaultMediator(): Promise { + return this.mediationRecipientService.findDefaultMediator(this.agentContext) + } + + public async findDefaultMediatorConnection(): Promise { + const mediatorRecord = await this.findDefaultMediator() + + if (mediatorRecord) { + return this.connectionService.getById(this.agentContext, mediatorRecord.connectionId) + } + + return null + } + + public async requestAndAwaitGrant(connection: ConnectionRecord, timeoutMs = 10000): Promise { + const { mediationRecord, message } = await this.mediationRecipientService.createRequest( + this.agentContext, + connection + ) + + // Create observable for event + const observable = this.eventEmitter.observable(RoutingEventTypes.MediationStateChanged) + const subject = new ReplaySubject(1) + + // Apply required filters to observable stream subscribe to replay subject + observable + .pipe( + filterContextCorrelationId(this.agentContext.contextCorrelationId), + // Only take event for current mediation record + filter((event) => event.payload.mediationRecord.id === mediationRecord.id), + // Only take event for previous state requested, current state granted + filter((event) => event.payload.previousState === MediationState.Requested), + filter((event) => event.payload.mediationRecord.state === MediationState.Granted), + // Only wait for first event that matches the criteria + first(), + // Do not wait for longer than specified timeout + timeout({ + first: timeoutMs, + meta: 'MediationRecipientApi.requestAndAwaitGrant', + }) + ) + .subscribe(subject) + + // Send mediation request message + const outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connection, + associatedRecord: mediationRecord, + }) + await this.sendMessage(outboundMessageContext) + + const event = await firstValueFrom(subject) + return event.payload.mediationRecord + } + + /** + * Requests mediation for a given connection and sets that as default mediator. + * + * @param connection connection record which will be used for mediation + * @returns mediation record + */ + public async provision(connection: ConnectionRecord) { + this.logger.debug('Connection completed, requesting mediation') + + let mediation = await this.findByConnectionId(connection.id) + if (!mediation) { + this.logger.info(`Requesting mediation for connection ${connection.id}`) + mediation = await this.requestAndAwaitGrant(connection, 60000) // TODO: put timeout as a config parameter + this.logger.debug('Mediation granted, setting as default mediator') + await this.setDefaultMediator(mediation) + this.logger.debug('Default mediator set') + } else { + this.logger.debug(`Mediator invitation has already been ${mediation.isReady ? 'granted' : 'requested'}`) + } + + return mediation + } + + public async getRouting(options: GetRoutingOptions) { + return this.routingService.getRouting(this.agentContext, options) + } + + // Register handlers for the several messages for the mediator. + private registerMessageHandlers(messageHandlerRegistry: MessageHandlerRegistry) { + messageHandlerRegistry.registerMessageHandler(new KeylistUpdateResponseHandler(this.mediationRecipientService)) + messageHandlerRegistry.registerMessageHandler(new MediationGrantHandler(this.mediationRecipientService)) + messageHandlerRegistry.registerMessageHandler(new MediationDenyHandler(this.mediationRecipientService)) + //messageHandlerRegistry.registerMessageHandler(new KeylistListHandler(this.mediationRecipientService)) // TODO: write this + } +} diff --git a/packages/core/src/modules/routing/MediationRecipientModule.ts b/packages/core/src/modules/routing/MediationRecipientModule.ts new file mode 100644 index 0000000000..e54fc294ef --- /dev/null +++ b/packages/core/src/modules/routing/MediationRecipientModule.ts @@ -0,0 +1,43 @@ +import type { MediationRecipientModuleConfigOptions } from './MediationRecipientModuleConfig' +import type { FeatureRegistry } from '../../agent/FeatureRegistry' +import type { DependencyManager, Module } from '../../plugins' + +import { Protocol } from '../../agent/models' + +import { MediationRecipientApi } from './MediationRecipientApi' +import { MediationRecipientModuleConfig } from './MediationRecipientModuleConfig' +import { MediationRole } from './models' +import { MediationRepository } from './repository' +import { MediationRecipientService, RoutingService } from './services' + +export class MediationRecipientModule implements Module { + public readonly config: MediationRecipientModuleConfig + public readonly api = MediationRecipientApi + + public constructor(config?: MediationRecipientModuleConfigOptions) { + this.config = new MediationRecipientModuleConfig(config) + } + + /** + * Registers the dependencies of the mediator recipient module on the dependency manager. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Config + dependencyManager.registerInstance(MediationRecipientModuleConfig, this.config) + + // Services + dependencyManager.registerSingleton(MediationRecipientService) + dependencyManager.registerSingleton(RoutingService) + + // Repositories + dependencyManager.registerSingleton(MediationRepository) + + // Features + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/coordinate-mediation/1.0', + roles: [MediationRole.Recipient], + }) + ) + } +} diff --git a/packages/core/src/modules/routing/MediationRecipientModuleConfig.ts b/packages/core/src/modules/routing/MediationRecipientModuleConfig.ts new file mode 100644 index 0000000000..cab467c18e --- /dev/null +++ b/packages/core/src/modules/routing/MediationRecipientModuleConfig.ts @@ -0,0 +1,107 @@ +import type { MediatorPickupStrategy } from './MediatorPickupStrategy' + +/** + * MediationRecipientModuleConfigOptions defines the interface for the options of the MediationRecipientModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface MediationRecipientModuleConfigOptions { + /** + * Strategy to use for picking up messages from the mediator. If no strategy is provided, the agent will use the discover + * features protocol to determine the best strategy. + * + * + * - `MediatorPickupStrategy.PickUpV1` - explicitly pick up messages from the mediator in periodic loops according to [RFC 0212 Pickup Protocol](https://github.com/hyperledger/aries-rfcs/blob/main/features/0212-pickup/README.md) + * - `MediatorPickupStrategy.PickUpV2` - pick up messages from the mediator in periodic loops according to [RFC 0685 Pickup V2 Protocol](https://github.com/hyperledger/aries-rfcs/tree/main/features/0685-pickup-v2/README.md). + * - `MediatorPickupStrategy.PickUpV2LiveMode` - pick up messages from the mediator using Live Mode as specified in [RFC 0685 Pickup V2 Protocol](https://github.com/hyperledger/aries-rfcs/tree/main/features/0685-pickup-v2/README.md). + * - `MediatorPickupStrategy.Implicit` - Open a WebSocket with the mediator to implicitly receive messages. (currently used by Aries Cloud Agent Python) + * - `MediatorPickupStrategy.None` - Do not retrieve messages from the mediator automatically. You can launch manual pickup flows afterwards. + * + * @default undefined + */ + mediatorPickupStrategy?: MediatorPickupStrategy + + /** + * Interval in milliseconds between picking up message from the mediator. This is only applicable when the pickup protocol v1 or v2 in polling mode + * are used. + * + * @default 5000 + */ + mediatorPollingInterval?: number + + /** + * Maximum number of messages to retrieve from the mediator in a single batch. This is applicable for both pickup protocol v1 and v2 + * is used. + * + * @default 10 + */ + maximumMessagePickup?: number + + /** + * Initial interval in milliseconds between reconnection attempts when losing connection with the mediator. This value is doubled after + * each retry, resulting in an exponential backoff strategy. + * + * For instance, if maximumMediatorReconnectionIntervalMs is b, the agent will attempt to reconnect after b, 2*b, 4*b, 8*b, 16*b, ... ms. + * + * This is only applicable when pickup protocol v2 or implicit pickup is used. + * + * @default 100 + */ + baseMediatorReconnectionIntervalMs?: number + + /** + * Maximum interval in milliseconds between reconnection attempts when losing connection with the mediator. + * + * For instance, if maximumMediatorReconnectionIntervalMs is set to 1000 and maximumMediatorReconnectionIntervalMs is set to 10000, + * the agent will attempt to reconnect after 1000, 2000, 4000, 8000, 10000, ..., 10000 ms. + * + * This is only applicable when pickup protocol v2 or implicit pickup is used. + * @default Number.POSITIVE_INFINITY + */ + maximumMediatorReconnectionIntervalMs?: number + + /** + * Invitation url for connection to a mediator. If provided, a connection to the mediator will be made, and the mediator will be set as default. + * This is meant as the simplest form of connecting to a mediator, if more control is desired the api should be used. + * + * Supports both RFC 0434 Out Of Band v1 and RFC 0160 Connections v1 invitations. + */ + mediatorInvitationUrl?: string +} + +export class MediationRecipientModuleConfig { + private options: MediationRecipientModuleConfigOptions + + public constructor(options?: MediationRecipientModuleConfigOptions) { + this.options = options ?? {} + } + + /** See {@link MediationRecipientModuleConfigOptions.mediatorPollingInterval} */ + public get mediatorPollingInterval() { + return this.options.mediatorPollingInterval ?? 5000 + } + + /** See {@link MediationRecipientModuleConfigOptions.mediatorPickupStrategy} */ + public get mediatorPickupStrategy() { + return this.options.mediatorPickupStrategy + } + + /** See {@link MediationRecipientModuleConfigOptions.maximumMessagePickup} */ + public get maximumMessagePickup() { + return this.options.maximumMessagePickup ?? 10 + } + + /** See {@link MediationRecipientModuleConfigOptions.baseMediatorReconnectionIntervalMs} */ + public get baseMediatorReconnectionIntervalMs() { + return this.options.baseMediatorReconnectionIntervalMs ?? 100 + } + + /** See {@link MediationRecipientModuleConfigOptions.maximumMediatorReconnectionIntervalMs} */ + public get maximumMediatorReconnectionIntervalMs() { + return this.options.maximumMediatorReconnectionIntervalMs ?? Number.POSITIVE_INFINITY + } + + /** See {@link MediationRecipientModuleConfigOptions.mediatorInvitationUrl} */ + public get mediatorInvitationUrl() { + return this.options.mediatorInvitationUrl + } +} diff --git a/packages/core/src/modules/routing/MediatorApi.ts b/packages/core/src/modules/routing/MediatorApi.ts new file mode 100644 index 0000000000..62c456b31c --- /dev/null +++ b/packages/core/src/modules/routing/MediatorApi.ts @@ -0,0 +1,77 @@ +import type { MediationRecord } from './repository' + +import { AgentContext } from '../../agent' +import { MessageHandlerRegistry } from '../../agent/MessageHandlerRegistry' +import { MessageSender } from '../../agent/MessageSender' +import { OutboundMessageContext } from '../../agent/models' +import { injectable } from '../../plugins' +import { ConnectionService } from '../connections/services' + +import { MediatorModuleConfig } from './MediatorModuleConfig' +import { ForwardHandler, KeylistUpdateHandler } from './handlers' +import { MediationRequestHandler } from './handlers/MediationRequestHandler' +import { MediatorService } from './services/MediatorService' + +@injectable() +export class MediatorApi { + public config: MediatorModuleConfig + + private mediatorService: MediatorService + private messageSender: MessageSender + private agentContext: AgentContext + private connectionService: ConnectionService + + public constructor( + messageHandlerRegistry: MessageHandlerRegistry, + mediationService: MediatorService, + messageSender: MessageSender, + agentContext: AgentContext, + connectionService: ConnectionService, + config: MediatorModuleConfig + ) { + this.mediatorService = mediationService + this.messageSender = messageSender + this.connectionService = connectionService + this.agentContext = agentContext + this.config = config + this.registerMessageHandlers(messageHandlerRegistry) + } + + public async initialize() { + this.agentContext.config.logger.debug('Mediator routing record not loaded yet, retrieving from storage') + const routingRecord = await this.mediatorService.findMediatorRoutingRecord(this.agentContext) + + // If we don't have a routing record yet for this tenant, create it + if (!routingRecord) { + this.agentContext.config.logger.debug( + 'Mediator routing record does not exist yet, creating routing keys and record' + ) + await this.mediatorService.createMediatorRoutingRecord(this.agentContext) + } + } + + public async grantRequestedMediation(mediationRecordId: string): Promise { + const record = await this.mediatorService.getById(this.agentContext, mediationRecordId) + const connectionRecord = await this.connectionService.getById(this.agentContext, record.connectionId) + + const { message, mediationRecord } = await this.mediatorService.createGrantMediationMessage( + this.agentContext, + record + ) + const outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connectionRecord, + associatedRecord: mediationRecord, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + + return mediationRecord + } + + private registerMessageHandlers(messageHandlerRegistry: MessageHandlerRegistry) { + messageHandlerRegistry.registerMessageHandler(new KeylistUpdateHandler(this.mediatorService)) + messageHandlerRegistry.registerMessageHandler(new ForwardHandler(this.mediatorService)) + messageHandlerRegistry.registerMessageHandler(new MediationRequestHandler(this.mediatorService, this.config)) + } +} diff --git a/packages/core/src/modules/routing/MediatorModule.ts b/packages/core/src/modules/routing/MediatorModule.ts new file mode 100644 index 0000000000..afcfdd05b3 --- /dev/null +++ b/packages/core/src/modules/routing/MediatorModule.ts @@ -0,0 +1,43 @@ +import type { MediatorModuleConfigOptions } from './MediatorModuleConfig' +import type { FeatureRegistry } from '../../agent/FeatureRegistry' +import type { DependencyManager, Module } from '../../plugins' + +import { Protocol } from '../../agent/models' + +import { MediatorApi } from './MediatorApi' +import { MediatorModuleConfig } from './MediatorModuleConfig' +import { MediationRole } from './models' +import { MediationRepository, MediatorRoutingRepository } from './repository' +import { MediatorService } from './services' + +export class MediatorModule implements Module { + public readonly config: MediatorModuleConfig + public readonly api = MediatorApi + + public constructor(config?: MediatorModuleConfigOptions) { + this.config = new MediatorModuleConfig(config) + } + + /** + * Registers the dependencies of the question answer module on the dependency manager. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Config + dependencyManager.registerInstance(MediatorModuleConfig, this.config) + + // Services + dependencyManager.registerSingleton(MediatorService) + + // Repositories + dependencyManager.registerSingleton(MediationRepository) + dependencyManager.registerSingleton(MediatorRoutingRepository) + + // Features + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/coordinate-mediation/1.0', + roles: [MediationRole.Mediator], + }) + ) + } +} diff --git a/packages/core/src/modules/routing/MediatorModuleConfig.ts b/packages/core/src/modules/routing/MediatorModuleConfig.ts new file mode 100644 index 0000000000..e20fc8422c --- /dev/null +++ b/packages/core/src/modules/routing/MediatorModuleConfig.ts @@ -0,0 +1,45 @@ +import { MessageForwardingStrategy } from './MessageForwardingStrategy' + +/** + * MediatorModuleConfigOptions defines the interface for the options of the MediatorModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface MediatorModuleConfigOptions { + /** + * Whether to automatically accept and grant incoming mediation requests. + * + * @default false + */ + autoAcceptMediationRequests?: boolean + + /** + * Strategy to use when a Forward message is received. + * + * + * - `MessageForwardingStrategy.QueueOnly` - simply queue encrypted message into MessagePickupRepository. It will be in charge of manually trigering MessagePickupApi.deliver() afterwards. + * - `MessageForwardingStrategy.QueueAndLiveModeDelivery` - Queue message into MessagePickupRepository and deliver it (along any other queued message). + * - `MessageForwardingStrategy.DirectDelivery` - Deliver message directly. Do not add into queue (it might be manually added after, e.g. in case of failure) + * + * @default MessageForwardingStrategy.DirectDelivery + * @todo Update default to QueueAndLiveModeDelivery + */ + messageForwardingStrategy?: MessageForwardingStrategy +} + +export class MediatorModuleConfig { + private options: MediatorModuleConfigOptions + + public constructor(options?: MediatorModuleConfigOptions) { + this.options = options ?? {} + } + + /** See {@link MediatorModuleConfigOptions.autoAcceptMediationRequests} */ + public get autoAcceptMediationRequests() { + return this.options.autoAcceptMediationRequests ?? false + } + + /** See {@link MediatorModuleConfigOptions.messageForwardingStrategy} */ + public get messageForwardingStrategy() { + return this.options.messageForwardingStrategy ?? MessageForwardingStrategy.DirectDelivery + } +} diff --git a/packages/core/src/modules/routing/MediatorPickupStrategy.ts b/packages/core/src/modules/routing/MediatorPickupStrategy.ts new file mode 100644 index 0000000000..1104abf7cb --- /dev/null +++ b/packages/core/src/modules/routing/MediatorPickupStrategy.ts @@ -0,0 +1,18 @@ +export enum MediatorPickupStrategy { + // Use PickUp v1 protocol to periodically retrieve messages + PickUpV1 = 'PickUpV1', + + // Use PickUp v2 protocol to periodically retrieve messages + PickUpV2 = 'PickUpV2', + + // Use PickUp v2 protocol in Live Mode to get incoming messages as soon as they arrive + // to mediator + PickUpV2LiveMode = 'PickUpV2LiveMode', + + // Implicit pickup strategy means picking up messages only using return route + // decorator. This is what ACA-Py currently uses + Implicit = 'Implicit', + + // Do not pick up messages + None = 'None', +} diff --git a/packages/core/src/modules/routing/MessageForwardingStrategy.ts b/packages/core/src/modules/routing/MessageForwardingStrategy.ts new file mode 100644 index 0000000000..06ce1e05c9 --- /dev/null +++ b/packages/core/src/modules/routing/MessageForwardingStrategy.ts @@ -0,0 +1,13 @@ +export enum MessageForwardingStrategy { + // When a forward is received, simply queue encrypted message. MessagePickupRepository + // will be in charge of manually triggering MessagePickupApi.deliverMessages() + QueueOnly = 'QueueOnly', + + // Queue message into MessagePickupRepository and, if a Message Pickup Live mode session is active, + // deliver it along any other queued message + QueueAndLiveModeDelivery = 'QueueAndLiveModeDelivery', + + // Attempt to deliver message directly if a transport session is available. It will eventually added + // into pickup queue in case of failure on the delivery + DirectDelivery = 'DirectDelivery', +} diff --git a/packages/core/src/modules/routing/RoutingEvents.ts b/packages/core/src/modules/routing/RoutingEvents.ts new file mode 100644 index 0000000000..86a151abff --- /dev/null +++ b/packages/core/src/modules/routing/RoutingEvents.ts @@ -0,0 +1,34 @@ +import type { KeylistUpdate } from './messages/KeylistUpdateMessage' +import type { MediationState } from './models/MediationState' +import type { MediationRecord } from './repository/MediationRecord' +import type { BaseEvent } from '../../agent/Events' +import type { Routing } from '../connections' + +export enum RoutingEventTypes { + MediationStateChanged = 'MediationStateChanged', + RecipientKeylistUpdated = 'RecipientKeylistUpdated', + RoutingCreatedEvent = 'RoutingCreatedEvent', +} + +export interface RoutingCreatedEvent extends BaseEvent { + type: typeof RoutingEventTypes.RoutingCreatedEvent + payload: { + routing: Routing + } +} + +export interface MediationStateChangedEvent extends BaseEvent { + type: typeof RoutingEventTypes.MediationStateChanged + payload: { + mediationRecord: MediationRecord + previousState: MediationState | null + } +} + +export interface KeylistUpdatedEvent extends BaseEvent { + type: typeof RoutingEventTypes.RecipientKeylistUpdated + payload: { + mediationRecord: MediationRecord + keylist: KeylistUpdate[] + } +} diff --git a/packages/core/src/modules/routing/__tests__/MediationRecipientModule.test.ts b/packages/core/src/modules/routing/__tests__/MediationRecipientModule.test.ts new file mode 100644 index 0000000000..4dcba55a5f --- /dev/null +++ b/packages/core/src/modules/routing/__tests__/MediationRecipientModule.test.ts @@ -0,0 +1,26 @@ +import { FeatureRegistry } from '../../../agent/FeatureRegistry' +import { DependencyManager } from '../../../plugins/DependencyManager' +import { MediationRecipientModule } from '../MediationRecipientModule' +import { MediationRepository } from '../repository' +import { MediationRecipientService, RoutingService } from '../services' + +jest.mock('../../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +jest.mock('../../../agent/FeatureRegistry') +const FeatureRegistryMock = FeatureRegistry as jest.Mock + +const featureRegistry = new FeatureRegistryMock() + +describe('MediationRecipientModule', () => { + test('registers dependencies on the dependency manager', () => { + new MediationRecipientModule().register(dependencyManager, featureRegistry) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(MediationRecipientService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(RoutingService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(MediationRepository) + }) +}) diff --git a/packages/core/src/modules/routing/__tests__/MediatorModule.test.ts b/packages/core/src/modules/routing/__tests__/MediatorModule.test.ts new file mode 100644 index 0000000000..6161857475 --- /dev/null +++ b/packages/core/src/modules/routing/__tests__/MediatorModule.test.ts @@ -0,0 +1,25 @@ +import { FeatureRegistry } from '../../../agent/FeatureRegistry' +import { DependencyManager } from '../../../plugins/DependencyManager' +import { MediatorModule } from '../MediatorModule' +import { MediationRepository, MediatorRoutingRepository } from '../repository' +import { MediatorService } from '../services' + +jest.mock('../../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +jest.mock('../../../agent/FeatureRegistry') +const FeatureRegistryMock = FeatureRegistry as jest.Mock + +const featureRegistry = new FeatureRegistryMock() +describe('MediatorModule', () => { + test('registers dependencies on the dependency manager', () => { + new MediatorModule().register(dependencyManager, featureRegistry) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(MediatorService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(MediationRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(MediatorRoutingRepository) + }) +}) diff --git a/packages/core/src/modules/routing/__tests__/mediation.test.ts b/packages/core/src/modules/routing/__tests__/mediation.test.ts new file mode 100644 index 0000000000..cd5b284cdf --- /dev/null +++ b/packages/core/src/modules/routing/__tests__/mediation.test.ts @@ -0,0 +1,286 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../../../../tests/transport/SubjectInboundTransport' +import type { AgentDependencies } from '../../../agent/AgentDependencies' +import type { AgentModulesInput } from '../../../agent/AgentModules' +import type { InitConfig } from '../../../types' + +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' +import { getInMemoryAgentOptions, waitForBasicMessage } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { sleep } from '../../../utils/sleep' +import { ConnectionRecord, HandshakeProtocol } from '../../connections' +import { MediationRecipientModule } from '../MediationRecipientModule' +import { MediatorModule } from '../MediatorModule' +import { MediatorPickupStrategy } from '../MediatorPickupStrategy' +import { MediationState } from '../models/MediationState' + +const recipientAgentOptions = getInMemoryAgentOptions('Mediation: Recipient') +const mediatorAgentOptions = getInMemoryAgentOptions( + 'Mediation: Mediator', + { + endpoints: ['rxjs:mediator'], + }, + { + mediator: new MediatorModule({ + autoAcceptMediationRequests: true, + }), + } +) + +const senderAgentOptions = getInMemoryAgentOptions('Mediation: Sender', { + endpoints: ['rxjs:sender'], +}) + +describe('mediator establishment', () => { + let recipientAgent: Agent + let mediatorAgent: Agent + let senderAgent: Agent + + afterEach(async () => { + await recipientAgent?.shutdown() + await recipientAgent?.wallet.delete() + await mediatorAgent?.shutdown() + await mediatorAgent?.wallet.delete() + await senderAgent?.shutdown() + await senderAgent?.wallet.delete() + }) + + const e2eMediationTest = async ( + mediatorAgentOptions: { + readonly config: InitConfig + readonly dependencies: AgentDependencies + modules: AgentModulesInput + }, + recipientAgentOptions: { + config: InitConfig + dependencies: AgentDependencies + modules: AgentModulesInput + } + ) => { + const mediatorMessages = new Subject() + const recipientMessages = new Subject() + const senderMessages = new Subject() + + const subjectMap = { + 'rxjs:mediator': mediatorMessages, + 'rxjs:sender': senderMessages, + } + + // Initialize mediatorReceived message + mediatorAgent = new Agent(mediatorAgentOptions) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + await mediatorAgent.initialize() + + // Create connection to use for recipient + const mediatorOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'mediator invitation', + handshake: true, + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + // Initialize recipient with mediation connections invitation + recipientAgent = new Agent({ + ...recipientAgentOptions, + modules: { + ...recipientAgentOptions.modules, + mediationRecipient: new MediationRecipientModule({ + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + mediatorInvitationUrl: mediatorOutOfBandRecord.outOfBandInvitation.toUrl({ + domain: 'https://example.com/ssi', + }), + }), + }, + }) + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + recipientAgent.registerInboundTransport(new SubjectInboundTransport(recipientMessages)) + await recipientAgent.initialize() + + const recipientMediator = await recipientAgent.mediationRecipient.findDefaultMediator() + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain, @typescript-eslint/no-non-null-assertion + const recipientMediatorConnection = await recipientAgent.connections.getById(recipientMediator!.connectionId) + + expect(recipientMediatorConnection).toBeInstanceOf(ConnectionRecord) + expect(recipientMediatorConnection?.isReady).toBe(true) + + const [mediatorRecipientConnection] = await mediatorAgent.connections.findAllByOutOfBandId( + mediatorOutOfBandRecord.id + ) + expect(mediatorRecipientConnection!.isReady).toBe(true) + + expect(mediatorRecipientConnection).toBeConnectedWith(recipientMediatorConnection) + expect(recipientMediatorConnection).toBeConnectedWith(mediatorRecipientConnection!) + + expect(recipientMediator?.state).toBe(MediationState.Granted) + + // Initialize sender agent + senderAgent = new Agent(senderAgentOptions) + senderAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + senderAgent.registerInboundTransport(new SubjectInboundTransport(senderMessages)) + await senderAgent.initialize() + + const recipientOutOfBandRecord = await recipientAgent.oob.createInvitation({ + label: 'mediator invitation', + handshake: true, + handshakeProtocols: [HandshakeProtocol.Connections], + }) + const recipientInvitation = recipientOutOfBandRecord.outOfBandInvitation + + let { connectionRecord: senderRecipientConnection } = await senderAgent.oob.receiveInvitationFromUrl( + recipientInvitation.toUrl({ domain: 'https://example.com/ssi' }) + ) + + senderRecipientConnection = await senderAgent.connections.returnWhenIsConnected(senderRecipientConnection!.id) + + let [recipientSenderConnection] = await recipientAgent.connections.findAllByOutOfBandId(recipientOutOfBandRecord.id) + expect(recipientSenderConnection).toBeConnectedWith(senderRecipientConnection) + expect(senderRecipientConnection).toBeConnectedWith(recipientSenderConnection!) + expect(recipientSenderConnection!.isReady).toBe(true) + expect(senderRecipientConnection.isReady).toBe(true) + + recipientSenderConnection = await recipientAgent.connections.returnWhenIsConnected(recipientSenderConnection!.id) + + const message = 'hello, world' + await senderAgent.basicMessages.sendMessage(senderRecipientConnection.id, message) + + const basicMessage = await waitForBasicMessage(recipientAgent, { + content: message, + }) + + // polling interval is 100ms, so 500ms should be enough to make sure no messages are sent + await recipientAgent.mediationRecipient.stopMessagePickup() + await sleep(500) + + expect(basicMessage.content).toBe(message) + } + + test(`Mediation end-to-end flow + 1. Start mediator agent and create invitation + 2. Start recipient agent with mediatorConnectionsInvite from mediator + 3. Assert mediator and recipient are connected and mediation state is Granted + 4. Start sender agent and create connection with recipient + 5. Assert endpoint in recipient invitation for sender is mediator endpoint + 6. Send basic message from sender to recipient and assert it is received on the recipient side +`, async () => { + await e2eMediationTest(mediatorAgentOptions, recipientAgentOptions) + }) + + test('Mediation end-to-end flow (not using did:key)', async () => { + await e2eMediationTest( + { + ...mediatorAgentOptions, + config: { ...mediatorAgentOptions.config, useDidKeyInProtocols: false }, + }, + { + ...recipientAgentOptions, + config: { + ...recipientAgentOptions.config, + useDidKeyInProtocols: false, + }, + } + ) + }) + + test('restart recipient agent and create connection through mediator after recipient agent is restarted', async () => { + const mediatorMessages = new Subject() + const recipientMessages = new Subject() + const senderMessages = new Subject() + + const subjectMap = { + 'rxjs:mediator': mediatorMessages, + 'rxjs:sender': senderMessages, + } + + // Initialize mediator + mediatorAgent = new Agent(mediatorAgentOptions) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + await mediatorAgent.initialize() + + // Create connection to use for recipient + const mediatorOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'mediator invitation', + handshake: true, + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + // Initialize recipient with mediation connections invitation + recipientAgent = new Agent({ + ...recipientAgentOptions, + modules: { + ...recipientAgentOptions.modules, + mediationRecipient: new MediationRecipientModule({ + mediatorInvitationUrl: mediatorOutOfBandRecord.outOfBandInvitation.toUrl({ + domain: 'https://example.com/ssi', + }), + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + }, + }) + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + recipientAgent.registerInboundTransport(new SubjectInboundTransport(recipientMessages)) + await recipientAgent.initialize() + + const recipientMediator = await recipientAgent.mediationRecipient.findDefaultMediator() + const recipientMediatorConnection = await recipientAgent.connections.getById(recipientMediator!.connectionId) + expect(recipientMediatorConnection?.isReady).toBe(true) + + const [mediatorRecipientConnection] = await mediatorAgent.connections.findAllByOutOfBandId( + mediatorOutOfBandRecord.id + ) + expect(mediatorRecipientConnection!.isReady).toBe(true) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(mediatorRecipientConnection).toBeConnectedWith(recipientMediatorConnection!) + expect(recipientMediatorConnection).toBeConnectedWith(mediatorRecipientConnection!) + + expect(recipientMediator?.state).toBe(MediationState.Granted) + + await recipientAgent.mediationRecipient.stopMessagePickup() + + // Restart recipient agent + await recipientAgent.shutdown() + await recipientAgent.initialize() + + // Initialize sender agent + senderAgent = new Agent(senderAgentOptions) + senderAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + senderAgent.registerInboundTransport(new SubjectInboundTransport(senderMessages)) + await senderAgent.initialize() + + const recipientOutOfBandRecord = await recipientAgent.oob.createInvitation({ + label: 'mediator invitation', + handshake: true, + handshakeProtocols: [HandshakeProtocol.Connections], + }) + const recipientInvitation = recipientOutOfBandRecord.outOfBandInvitation + + let { connectionRecord: senderRecipientConnection } = await senderAgent.oob.receiveInvitationFromUrl( + recipientInvitation.toUrl({ domain: 'https://example.com/ssi' }) + ) + + senderRecipientConnection = await senderAgent.connections.returnWhenIsConnected(senderRecipientConnection!.id) + const [recipientSenderConnection] = await recipientAgent.connections.findAllByOutOfBandId( + recipientOutOfBandRecord.id + ) + expect(recipientSenderConnection).toBeConnectedWith(senderRecipientConnection) + expect(senderRecipientConnection).toBeConnectedWith(recipientSenderConnection!) + + expect(recipientSenderConnection!.isReady).toBe(true) + expect(senderRecipientConnection.isReady).toBe(true) + + const message = 'hello, world' + await senderAgent.basicMessages.sendMessage(senderRecipientConnection.id, message) + + const basicMessage = await waitForBasicMessage(recipientAgent, { + content: message, + }) + + expect(basicMessage.content).toBe(message) + + await recipientAgent.mediationRecipient.stopMessagePickup() + }) +}) diff --git a/packages/core/src/modules/routing/error/RoutingProblemReportReason.ts b/packages/core/src/modules/routing/error/RoutingProblemReportReason.ts new file mode 100644 index 0000000000..be5b373257 --- /dev/null +++ b/packages/core/src/modules/routing/error/RoutingProblemReportReason.ts @@ -0,0 +1,3 @@ +export enum RoutingProblemReportReason { + ErrorProcessingAttachments = 'error-processing-attachments', +} diff --git a/packages/core/src/modules/routing/error/index.ts b/packages/core/src/modules/routing/error/index.ts new file mode 100644 index 0000000000..d117e1d699 --- /dev/null +++ b/packages/core/src/modules/routing/error/index.ts @@ -0,0 +1 @@ +export * from './RoutingProblemReportReason' diff --git a/packages/core/src/modules/routing/handlers/ForwardHandler.ts b/packages/core/src/modules/routing/handlers/ForwardHandler.ts new file mode 100644 index 0000000000..2ff27a0dae --- /dev/null +++ b/packages/core/src/modules/routing/handlers/ForwardHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { MediatorService } from '../services' + +import { ForwardMessage } from '../messages' + +export class ForwardHandler implements MessageHandler { + private mediatorService: MediatorService + public supportedMessages = [ForwardMessage] + + public constructor(mediatorService: MediatorService) { + this.mediatorService = mediatorService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.mediatorService.processForwardMessage(messageContext) + } +} diff --git a/packages/core/src/modules/routing/handlers/KeylistUpdateHandler.ts b/packages/core/src/modules/routing/handlers/KeylistUpdateHandler.ts new file mode 100644 index 0000000000..fcc8609512 --- /dev/null +++ b/packages/core/src/modules/routing/handlers/KeylistUpdateHandler.ts @@ -0,0 +1,24 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { MediatorService } from '../services/MediatorService' + +import { OutboundMessageContext } from '../../../agent/models' +import { KeylistUpdateMessage } from '../messages' + +export class KeylistUpdateHandler implements MessageHandler { + private mediatorService: MediatorService + public supportedMessages = [KeylistUpdateMessage] + + public constructor(mediatorService: MediatorService) { + this.mediatorService = mediatorService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const connection = messageContext.assertReadyConnection() + + const response = await this.mediatorService.processKeylistUpdateRequest(messageContext) + return new OutboundMessageContext(response, { + agentContext: messageContext.agentContext, + connection, + }) + } +} diff --git a/packages/core/src/modules/routing/handlers/KeylistUpdateResponseHandler.ts b/packages/core/src/modules/routing/handlers/KeylistUpdateResponseHandler.ts new file mode 100644 index 0000000000..58fdf301d8 --- /dev/null +++ b/packages/core/src/modules/routing/handlers/KeylistUpdateResponseHandler.ts @@ -0,0 +1,19 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { MediationRecipientService } from '../services' + +import { KeylistUpdateResponseMessage } from '../messages' + +export class KeylistUpdateResponseHandler implements MessageHandler { + public mediationRecipientService: MediationRecipientService + public supportedMessages = [KeylistUpdateResponseMessage] + + public constructor(mediationRecipientService: MediationRecipientService) { + this.mediationRecipientService = mediationRecipientService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + messageContext.assertReadyConnection() + + return await this.mediationRecipientService.processKeylistUpdateResults(messageContext) + } +} diff --git a/packages/core/src/modules/routing/handlers/MediationDenyHandler.ts b/packages/core/src/modules/routing/handlers/MediationDenyHandler.ts new file mode 100644 index 0000000000..83b90da990 --- /dev/null +++ b/packages/core/src/modules/routing/handlers/MediationDenyHandler.ts @@ -0,0 +1,19 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { MediationRecipientService } from '../services' + +import { MediationDenyMessage } from '../messages' + +export class MediationDenyHandler implements MessageHandler { + private mediationRecipientService: MediationRecipientService + public supportedMessages = [MediationDenyMessage] + + public constructor(mediationRecipientService: MediationRecipientService) { + this.mediationRecipientService = mediationRecipientService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + messageContext.assertReadyConnection() + + await this.mediationRecipientService.processMediationDeny(messageContext) + } +} diff --git a/packages/core/src/modules/routing/handlers/MediationGrantHandler.ts b/packages/core/src/modules/routing/handlers/MediationGrantHandler.ts new file mode 100644 index 0000000000..f9b290c934 --- /dev/null +++ b/packages/core/src/modules/routing/handlers/MediationGrantHandler.ts @@ -0,0 +1,19 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { MediationRecipientService } from '../services/MediationRecipientService' + +import { MediationGrantMessage } from '../messages' + +export class MediationGrantHandler implements MessageHandler { + private mediationRecipientService: MediationRecipientService + public supportedMessages = [MediationGrantMessage] + + public constructor(mediationRecipientService: MediationRecipientService) { + this.mediationRecipientService = mediationRecipientService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + messageContext.assertReadyConnection() + + await this.mediationRecipientService.processMediationGrant(messageContext) + } +} diff --git a/packages/core/src/modules/routing/handlers/MediationRequestHandler.ts b/packages/core/src/modules/routing/handlers/MediationRequestHandler.ts new file mode 100644 index 0000000000..f020c6f9bd --- /dev/null +++ b/packages/core/src/modules/routing/handlers/MediationRequestHandler.ts @@ -0,0 +1,35 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { MediatorModuleConfig } from '../MediatorModuleConfig' +import type { MediatorService } from '../services/MediatorService' + +import { OutboundMessageContext } from '../../../agent/models' +import { MediationRequestMessage } from '../messages/MediationRequestMessage' + +export class MediationRequestHandler implements MessageHandler { + private mediatorService: MediatorService + private mediatorModuleConfig: MediatorModuleConfig + public supportedMessages = [MediationRequestMessage] + + public constructor(mediatorService: MediatorService, mediatorModuleConfig: MediatorModuleConfig) { + this.mediatorService = mediatorService + this.mediatorModuleConfig = mediatorModuleConfig + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const connection = messageContext.assertReadyConnection() + + const mediationRecord = await this.mediatorService.processMediationRequest(messageContext) + + if (this.mediatorModuleConfig.autoAcceptMediationRequests) { + const { message } = await this.mediatorService.createGrantMediationMessage( + messageContext.agentContext, + mediationRecord + ) + return new OutboundMessageContext(message, { + agentContext: messageContext.agentContext, + connection, + associatedRecord: mediationRecord, + }) + } + } +} diff --git a/packages/core/src/modules/routing/handlers/index.ts b/packages/core/src/modules/routing/handlers/index.ts new file mode 100644 index 0000000000..6ac04444f5 --- /dev/null +++ b/packages/core/src/modules/routing/handlers/index.ts @@ -0,0 +1,6 @@ +export * from './ForwardHandler' +export * from './KeylistUpdateHandler' +export * from './KeylistUpdateResponseHandler' +export * from './MediationDenyHandler' +export * from './MediationGrantHandler' +export * from './MediationRequestHandler' diff --git a/packages/core/src/modules/routing/index.ts b/packages/core/src/modules/routing/index.ts new file mode 100644 index 0000000000..981dbe5207 --- /dev/null +++ b/packages/core/src/modules/routing/index.ts @@ -0,0 +1,10 @@ +export * from './messages' +export * from './services' +export * from './repository' +export * from './models' +export * from './RoutingEvents' +export * from './MediatorApi' +export * from './MediationRecipientApi' +export * from './MediatorPickupStrategy' +export * from './MediatorModule' +export * from './MediationRecipientModule' diff --git a/packages/core/src/modules/routing/messages/ForwardMessage.ts b/packages/core/src/modules/routing/messages/ForwardMessage.ts new file mode 100644 index 0000000000..ea1e45a50c --- /dev/null +++ b/packages/core/src/modules/routing/messages/ForwardMessage.ts @@ -0,0 +1,45 @@ +import { Expose } from 'class-transformer' +import { IsObject, IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { EncryptedMessage } from '../../../types' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface ForwardMessageOptions { + id?: string + to: string + message: EncryptedMessage +} + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/master/concepts/0094-cross-domain-messaging/README.md#corerouting10forward + */ +export class ForwardMessage extends AgentMessage { + public readonly allowDidSovPrefix = true + + /** + * Create new ForwardMessage instance. + * + * @param options + */ + public constructor(options: ForwardMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.to = options.to + this.message = options.message + } + } + + @IsValidMessageType(ForwardMessage.type) + public readonly type = ForwardMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/routing/1.0/forward') + + @IsString() + public to!: string + + @Expose({ name: 'msg' }) + @IsObject() + public message!: EncryptedMessage +} diff --git a/packages/core/src/modules/routing/messages/KeylistMessage.ts b/packages/core/src/modules/routing/messages/KeylistMessage.ts new file mode 100644 index 0000000000..7e58af9ac4 --- /dev/null +++ b/packages/core/src/modules/routing/messages/KeylistMessage.ts @@ -0,0 +1,39 @@ +import { Type } from 'class-transformer' +import { IsArray, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface KeylistMessageOptions { + id?: string +} + +/** + * Used to notify the recipient of keys in use by the mediator. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0211-route-coordination/README.md#keylist + */ +export class KeylistMessage extends AgentMessage { + public constructor(options: KeylistMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + } + } + + @IsValidMessageType(KeylistMessage.type) + public readonly type = KeylistMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/coordinate-mediation/1.0/keylist') + + @Type(() => Keylist) + @IsArray() + @ValidateNested() + public updates!: Keylist[] +} + +export class Keylist { + public constructor(options: { paginateOffset: number }) { + return options + } +} diff --git a/packages/core/src/modules/routing/messages/KeylistUpdateMessage.ts b/packages/core/src/modules/routing/messages/KeylistUpdateMessage.ts new file mode 100644 index 0000000000..be83d5b021 --- /dev/null +++ b/packages/core/src/modules/routing/messages/KeylistUpdateMessage.ts @@ -0,0 +1,62 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, ValidateNested, IsString, IsEnum, IsInstance } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export enum KeylistUpdateAction { + add = 'add', + remove = 'remove', +} + +export interface KeylistUpdateOptions { + recipientKey: string + action: KeylistUpdateAction +} + +export class KeylistUpdate { + public constructor(options: KeylistUpdateOptions) { + if (options) { + this.recipientKey = options.recipientKey + this.action = options.action + } + } + + @IsString() + @Expose({ name: 'recipient_key' }) + public recipientKey!: string + + @IsEnum(KeylistUpdateAction) + public action!: KeylistUpdateAction +} + +export interface KeylistUpdateMessageOptions { + id?: string + updates: KeylistUpdate[] +} + +/** + * Used to notify the mediator of keys in use by the recipient. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0211-route-coordination/README.md#keylist-update + */ +export class KeylistUpdateMessage extends AgentMessage { + public constructor(options: KeylistUpdateMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.updates = options.updates + } + } + + @IsValidMessageType(KeylistUpdateMessage.type) + public readonly type = KeylistUpdateMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/coordinate-mediation/1.0/keylist-update') + + @Type(() => KeylistUpdate) + @IsArray() + @ValidateNested() + @IsInstance(KeylistUpdate, { each: true }) + public updates!: KeylistUpdate[] +} diff --git a/packages/core/src/modules/routing/messages/KeylistUpdateResponseMessage.ts b/packages/core/src/modules/routing/messages/KeylistUpdateResponseMessage.ts new file mode 100644 index 0000000000..88b75c694c --- /dev/null +++ b/packages/core/src/modules/routing/messages/KeylistUpdateResponseMessage.ts @@ -0,0 +1,69 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsEnum, IsInstance, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +import { KeylistUpdateAction } from './KeylistUpdateMessage' + +export enum KeylistUpdateResult { + ClientError = 'client_error', + ServerError = 'server_error', + NoChange = 'no_change', + Success = 'success', +} + +export class KeylistUpdated { + public constructor(options: { recipientKey: string; action: KeylistUpdateAction; result: KeylistUpdateResult }) { + if (options) { + this.recipientKey = options.recipientKey + this.action = options.action + this.result = options.result + } + } + + @IsString() + @Expose({ name: 'recipient_key' }) + public recipientKey!: string + + @IsEnum(KeylistUpdateAction) + public action!: KeylistUpdateAction + + @IsEnum(KeylistUpdateResult) + public result!: KeylistUpdateResult +} + +export interface KeylistUpdateResponseMessageOptions { + id?: string + keylist: KeylistUpdated[] + threadId: string +} + +/** + * Used to notify an edge agent with the result of updating the routing keys in the mediator. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0211-route-coordination/README.md#keylist-update-response + */ +export class KeylistUpdateResponseMessage extends AgentMessage { + public constructor(options: KeylistUpdateResponseMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.updated = options.keylist + this.setThread({ + threadId: options.threadId, + }) + } + } + + @IsValidMessageType(KeylistUpdateResponseMessage.type) + public readonly type = KeylistUpdateResponseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/coordinate-mediation/1.0/keylist-update-response') + + @Type(() => KeylistUpdated) + @IsArray() + @ValidateNested() + @IsInstance(KeylistUpdated, { each: true }) + public updated!: KeylistUpdated[] +} diff --git a/packages/core/src/modules/routing/messages/MediationDenyMessage.ts b/packages/core/src/modules/routing/messages/MediationDenyMessage.ts new file mode 100644 index 0000000000..30f0868ff7 --- /dev/null +++ b/packages/core/src/modules/routing/messages/MediationDenyMessage.ts @@ -0,0 +1,25 @@ +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface MediationDenyMessageOptions { + id: string +} + +/** + * This message serves as notification of the mediator denying the recipient's request for mediation. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0211-route-coordination/README.md#mediation-deny + */ +export class MediationDenyMessage extends AgentMessage { + public constructor(options: MediationDenyMessageOptions) { + super() + + if (options) { + this.id = options.id + } + } + + @IsValidMessageType(MediationDenyMessage.type) + public readonly type = MediationDenyMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/coordinate-mediation/1.0/mediate-deny') +} diff --git a/packages/core/src/modules/routing/messages/MediationGrantMessage.ts b/packages/core/src/modules/routing/messages/MediationGrantMessage.ts new file mode 100644 index 0000000000..baebf09913 --- /dev/null +++ b/packages/core/src/modules/routing/messages/MediationGrantMessage.ts @@ -0,0 +1,46 @@ +import { Expose } from 'class-transformer' +import { IsArray, IsNotEmpty, IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface MediationGrantMessageOptions { + id?: string + endpoint: string + routingKeys: string[] + threadId: string +} + +/** + * A route grant message is a signal from the mediator to the recipient that permission is given to distribute the + * included information as an inbound route. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0211-route-coordination/README.md#mediation-grant + */ +export class MediationGrantMessage extends AgentMessage { + public constructor(options: MediationGrantMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.endpoint = options.endpoint + this.routingKeys = options.routingKeys + this.setThread({ + threadId: options.threadId, + }) + } + } + + @IsValidMessageType(MediationGrantMessage.type) + public readonly type = MediationGrantMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/coordinate-mediation/1.0/mediate-grant') + + @IsNotEmpty() + @IsArray() + @Expose({ name: 'routing_keys' }) + public routingKeys!: string[] + + @IsNotEmpty() + @IsString() + public endpoint!: string +} diff --git a/packages/core/src/modules/routing/messages/MediationRequestMessage.ts b/packages/core/src/modules/routing/messages/MediationRequestMessage.ts new file mode 100644 index 0000000000..788ebd3044 --- /dev/null +++ b/packages/core/src/modules/routing/messages/MediationRequestMessage.ts @@ -0,0 +1,34 @@ +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface MediationRequestMessageOptions { + sentTime?: Date + id?: string + locale?: string +} + +/** + * This message serves as a request from the recipient to the mediator, asking for the permission (and routing information) + * to publish the endpoint as a mediator. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0211-route-coordination/README.md#mediation-request + */ +export class MediationRequestMessage extends AgentMessage { + /** + * Create new BasicMessage instance. + * sentTime will be assigned to new Date if not passed, id will be assigned to uuid/v4 if not passed + * @param options + */ + public constructor(options: MediationRequestMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.addLocale(options.locale || 'en') + } + } + + @IsValidMessageType(MediationRequestMessage.type) + public readonly type = MediationRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/coordinate-mediation/1.0/mediate-request') +} diff --git a/packages/core/src/modules/routing/messages/index.ts b/packages/core/src/modules/routing/messages/index.ts new file mode 100644 index 0000000000..06af8aeb93 --- /dev/null +++ b/packages/core/src/modules/routing/messages/index.ts @@ -0,0 +1,6 @@ +export * from './ForwardMessage' +export * from './KeylistUpdateMessage' +export * from './KeylistUpdateResponseMessage' +export * from './MediationGrantMessage' +export * from './MediationDenyMessage' +export * from './MediationRequestMessage' diff --git a/packages/core/src/modules/routing/models/MediationRole.ts b/packages/core/src/modules/routing/models/MediationRole.ts new file mode 100644 index 0000000000..558602ec0a --- /dev/null +++ b/packages/core/src/modules/routing/models/MediationRole.ts @@ -0,0 +1,9 @@ +/** + * Mediation roles based on the flow defined in RFC 0211. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/master/features/0211-route-coordination/README.md + */ +export enum MediationRole { + Mediator = 'MEDIATOR', + Recipient = 'RECIPIENT', +} diff --git a/packages/core/src/modules/routing/models/MediationState.ts b/packages/core/src/modules/routing/models/MediationState.ts new file mode 100644 index 0000000000..0bade8e9a9 --- /dev/null +++ b/packages/core/src/modules/routing/models/MediationState.ts @@ -0,0 +1,10 @@ +/** + * Mediation states based on the flow defined in RFC 0211. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/master/features/0211-route-coordination/README.md + */ +export enum MediationState { + Requested = 'requested', + Granted = 'granted', + Denied = 'denied', +} diff --git a/packages/core/src/modules/routing/models/index.ts b/packages/core/src/modules/routing/models/index.ts new file mode 100644 index 0000000000..81a149f982 --- /dev/null +++ b/packages/core/src/modules/routing/models/index.ts @@ -0,0 +1,2 @@ +export * from './MediationRole' +export * from './MediationState' diff --git a/packages/core/src/modules/routing/repository/MediationRecord.ts b/packages/core/src/modules/routing/repository/MediationRecord.ts new file mode 100644 index 0000000000..a672e25087 --- /dev/null +++ b/packages/core/src/modules/routing/repository/MediationRecord.ts @@ -0,0 +1,132 @@ +import type { MediationRole } from '../models/MediationRole' + +import { Transform } from 'class-transformer' + +import { CredoError } from '../../../error' +import { BaseRecord } from '../../../storage/BaseRecord' +import { uuid } from '../../../utils/uuid' +import { MediatorPickupStrategy } from '../MediatorPickupStrategy' +import { MediationState } from '../models/MediationState' + +export interface MediationRecordProps { + id?: string + state: MediationState + role: MediationRole + createdAt?: Date + connectionId: string + threadId: string + endpoint?: string + recipientKeys?: string[] + routingKeys?: string[] + pickupStrategy?: MediatorPickupStrategy + tags?: CustomMediationTags +} + +export type CustomMediationTags = { + default?: boolean +} + +export type DefaultMediationTags = { + role: MediationRole + connectionId: string + state: MediationState + threadId: string +} + +export class MediationRecord + extends BaseRecord + implements MediationRecordProps +{ + public state!: MediationState + public role!: MediationRole + public connectionId!: string + public threadId!: string + public endpoint?: string + public recipientKeys!: string[] + public routingKeys!: string[] + + @Transform(({ value }) => { + if (value === 'Explicit') { + return MediatorPickupStrategy.PickUpV1 + } else { + return value + } + }) + public pickupStrategy?: MediatorPickupStrategy + + public static readonly type = 'MediationRecord' + public readonly type = MediationRecord.type + + public constructor(props: MediationRecordProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.connectionId = props.connectionId + this.threadId = props.threadId + this.recipientKeys = props.recipientKeys || [] + this.routingKeys = props.routingKeys || [] + this.state = props.state + this.role = props.role + this.endpoint = props.endpoint ?? undefined + this.pickupStrategy = props.pickupStrategy + this._tags = props.tags ?? {} + } + } + + public getTags() { + return { + ...this._tags, + state: this.state, + role: this.role, + connectionId: this.connectionId, + threadId: this.threadId, + recipientKeys: this.recipientKeys, + } + } + + public addRecipientKey(recipientKey: string) { + this.recipientKeys.push(recipientKey) + } + + public removeRecipientKey(recipientKey: string): boolean { + const index = this.recipientKeys.indexOf(recipientKey, 0) + if (index > -1) { + this.recipientKeys.splice(index, 1) + return true + } + + return false + } + + public get isReady() { + return this.state === MediationState.Granted + } + + public assertReady() { + if (!this.isReady) { + throw new CredoError( + `Mediation record is not ready to be used. Expected ${MediationState.Granted}, found invalid state ${this.state}` + ) + } + } + + public assertState(expectedStates: MediationState | MediationState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new CredoError( + `Mediation record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.` + ) + } + } + + public assertRole(expectedRole: MediationRole) { + if (this.role !== expectedRole) { + throw new CredoError(`Mediation record has invalid role ${this.role}. Expected role ${expectedRole}.`) + } + } +} diff --git a/packages/core/src/modules/routing/repository/MediationRepository.ts b/packages/core/src/modules/routing/repository/MediationRepository.ts new file mode 100644 index 0000000000..e89c04aa11 --- /dev/null +++ b/packages/core/src/modules/routing/repository/MediationRepository.ts @@ -0,0 +1,29 @@ +import type { AgentContext } from '../../../agent' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { MediationRecord } from './MediationRecord' + +@injectable() +export class MediationRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(MediationRecord, storageService, eventEmitter) + } + + public getSingleByRecipientKey(agentContext: AgentContext, recipientKey: string) { + return this.getSingleByQuery(agentContext, { + recipientKeys: [recipientKey], + }) + } + + public async getByConnectionId(agentContext: AgentContext, connectionId: string): Promise { + return this.getSingleByQuery(agentContext, { connectionId }) + } +} diff --git a/packages/core/src/modules/routing/repository/MediatorRoutingRecord.ts b/packages/core/src/modules/routing/repository/MediatorRoutingRecord.ts new file mode 100644 index 0000000000..8031f0b3f5 --- /dev/null +++ b/packages/core/src/modules/routing/repository/MediatorRoutingRecord.ts @@ -0,0 +1,32 @@ +import type { TagsBase } from '../../../storage/BaseRecord' + +import { BaseRecord } from '../../../storage/BaseRecord' +import { uuid } from '../../../utils/uuid' + +export interface MediatorRoutingRecordProps { + id?: string + createdAt?: Date + routingKeys?: string[] + tags?: TagsBase +} + +export class MediatorRoutingRecord extends BaseRecord implements MediatorRoutingRecordProps { + public routingKeys!: string[] + + public static readonly type = 'MediatorRoutingRecord' + public readonly type = MediatorRoutingRecord.type + + public constructor(props: MediatorRoutingRecordProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.routingKeys = props.routingKeys || [] + } + } + + public getTags() { + return this._tags + } +} diff --git a/packages/core/src/modules/routing/repository/MediatorRoutingRepository.ts b/packages/core/src/modules/routing/repository/MediatorRoutingRepository.ts new file mode 100644 index 0000000000..b4197d3d9a --- /dev/null +++ b/packages/core/src/modules/routing/repository/MediatorRoutingRepository.ts @@ -0,0 +1,19 @@ +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { MediatorRoutingRecord } from './MediatorRoutingRecord' + +@injectable() +export class MediatorRoutingRepository extends Repository { + public readonly MEDIATOR_ROUTING_RECORD_ID = 'MEDIATOR_ROUTING_RECORD' + + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(MediatorRoutingRecord, storageService, eventEmitter) + } +} diff --git a/packages/core/src/modules/routing/repository/index.ts b/packages/core/src/modules/routing/repository/index.ts new file mode 100644 index 0000000000..20d2b03a1c --- /dev/null +++ b/packages/core/src/modules/routing/repository/index.ts @@ -0,0 +1,4 @@ +export * from './MediationRepository' +export * from './MediatorRoutingRepository' +export * from './MediationRecord' +export * from './MediatorRoutingRecord' diff --git a/packages/core/src/modules/routing/services/MediationRecipientService.ts b/packages/core/src/modules/routing/services/MediationRecipientService.ts new file mode 100644 index 0000000000..0b73449c4c --- /dev/null +++ b/packages/core/src/modules/routing/services/MediationRecipientService.ts @@ -0,0 +1,400 @@ +import type { GetRoutingOptions, RemoveRoutingOptions } from './RoutingService' +import type { AgentContext } from '../../../agent' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { Query, QueryOptions } from '../../../storage/StorageService' +import type { ConnectionRecord } from '../../connections' +import type { Routing } from '../../connections/services/ConnectionService' +import type { MediationStateChangedEvent, KeylistUpdatedEvent } from '../RoutingEvents' +import type { MediationDenyMessage } from '../messages' + +import { firstValueFrom, ReplaySubject } from 'rxjs' +import { filter, first, timeout } from 'rxjs/operators' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { filterContextCorrelationId } from '../../../agent/Events' +import { MessageSender } from '../../../agent/MessageSender' +import { OutboundMessageContext } from '../../../agent/models' +import { Key, KeyType } from '../../../crypto' +import { CredoError } from '../../../error' +import { injectable } from '../../../plugins' +import { ConnectionType } from '../../connections/models/ConnectionType' +import { ConnectionMetadataKeys } from '../../connections/repository/ConnectionMetadataTypes' +import { ConnectionService } from '../../connections/services/ConnectionService' +import { DidKey } from '../../dids' +import { didKeyToVerkey, isDidKey } from '../../dids/helpers' +import { RoutingEventTypes } from '../RoutingEvents' +import { + KeylistUpdateAction, + KeylistUpdateResponseMessage, + MediationRequestMessage, + MediationGrantMessage, +} from '../messages' +import { KeylistUpdate, KeylistUpdateMessage } from '../messages/KeylistUpdateMessage' +import { MediationRole, MediationState } from '../models' +import { MediationRecord } from '../repository/MediationRecord' +import { MediationRepository } from '../repository/MediationRepository' + +@injectable() +export class MediationRecipientService { + private mediationRepository: MediationRepository + private eventEmitter: EventEmitter + private connectionService: ConnectionService + private messageSender: MessageSender + + public constructor( + connectionService: ConnectionService, + messageSender: MessageSender, + mediatorRepository: MediationRepository, + eventEmitter: EventEmitter + ) { + this.mediationRepository = mediatorRepository + this.eventEmitter = eventEmitter + this.connectionService = connectionService + this.messageSender = messageSender + } + + public async createRequest( + agentContext: AgentContext, + connection: ConnectionRecord + ): Promise> { + const message = new MediationRequestMessage({}) + + const mediationRecord = new MediationRecord({ + threadId: message.threadId, + state: MediationState.Requested, + role: MediationRole.Recipient, + connectionId: connection.id, + }) + + await this.connectionService.addConnectionType(agentContext, connection, ConnectionType.Mediator) + + await this.mediationRepository.save(agentContext, mediationRecord) + this.emitStateChangedEvent(agentContext, mediationRecord, null) + + return { mediationRecord, message } + } + + public async processMediationGrant(messageContext: InboundMessageContext) { + // Assert ready connection + const connection = messageContext.assertReadyConnection() + + // Mediation record must already exists to be updated to granted status + const mediationRecord = await this.mediationRepository.getByConnectionId(messageContext.agentContext, connection.id) + + // Assert + mediationRecord.assertState(MediationState.Requested) + mediationRecord.assertRole(MediationRole.Recipient) + + // Update record + mediationRecord.endpoint = messageContext.message.endpoint + + // Update connection metadata to use their key format in further protocol messages + const connectionUsesDidKey = messageContext.message.routingKeys.some(isDidKey) + await this.updateUseDidKeysFlag( + messageContext.agentContext, + connection, + MediationGrantMessage.type.protocolUri, + connectionUsesDidKey + ) + + // According to RFC 0211 keys should be a did key, but base58 encoded verkey was used before + // RFC was accepted. This converts the key to a public key base58 if it is a did key. + mediationRecord.routingKeys = messageContext.message.routingKeys.map(didKeyToVerkey) + return await this.updateState(messageContext.agentContext, mediationRecord, MediationState.Granted) + } + + public async processKeylistUpdateResults(messageContext: InboundMessageContext) { + // Assert ready connection + const connection = messageContext.assertReadyConnection() + + const mediationRecord = await this.mediationRepository.getByConnectionId(messageContext.agentContext, connection.id) + + // Assert + mediationRecord.assertReady() + mediationRecord.assertRole(MediationRole.Recipient) + + const keylist = messageContext.message.updated + + // Update connection metadata to use their key format in further protocol messages + const connectionUsesDidKey = keylist.some((key) => isDidKey(key.recipientKey)) + await this.updateUseDidKeysFlag( + messageContext.agentContext, + connection, + KeylistUpdateResponseMessage.type.protocolUri, + connectionUsesDidKey + ) + + // update keylist in mediationRecord + for (const update of keylist) { + if (update.action === KeylistUpdateAction.add) { + mediationRecord.addRecipientKey(didKeyToVerkey(update.recipientKey)) + } else if (update.action === KeylistUpdateAction.remove) { + mediationRecord.removeRecipientKey(didKeyToVerkey(update.recipientKey)) + } + } + + await this.mediationRepository.update(messageContext.agentContext, mediationRecord) + this.eventEmitter.emit(messageContext.agentContext, { + type: RoutingEventTypes.RecipientKeylistUpdated, + payload: { + mediationRecord, + keylist, + }, + }) + } + + public async keylistUpdateAndAwait( + agentContext: AgentContext, + mediationRecord: MediationRecord, + updates: { recipientKey: Key; action: KeylistUpdateAction }[], + timeoutMs = 15000 // TODO: this should be a configurable value in agent config + ): Promise { + const connection = await this.connectionService.getById(agentContext, mediationRecord.connectionId) + + // Use our useDidKey configuration unless we know the key formatting other party is using + let useDidKey = agentContext.config.useDidKeyInProtocols + + const useDidKeysConnectionMetadata = connection.metadata.get(ConnectionMetadataKeys.UseDidKeysForProtocol) + if (useDidKeysConnectionMetadata) { + useDidKey = useDidKeysConnectionMetadata[KeylistUpdateMessage.type.protocolUri] ?? useDidKey + } + + const message = this.createKeylistUpdateMessage( + updates.map( + (item) => + new KeylistUpdate({ + action: item.action, + recipientKey: useDidKey ? new DidKey(item.recipientKey).did : item.recipientKey.publicKeyBase58, + }) + ) + ) + + mediationRecord.assertReady() + mediationRecord.assertRole(MediationRole.Recipient) + + // Create observable for event + const observable = this.eventEmitter.observable(RoutingEventTypes.RecipientKeylistUpdated) + const subject = new ReplaySubject(1) + + // Apply required filters to observable stream and create promise to subscribe to observable + observable + .pipe( + filterContextCorrelationId(agentContext.contextCorrelationId), + // Only take event for current mediation record + filter((event) => mediationRecord.id === event.payload.mediationRecord.id), + // Only wait for first event that matches the criteria + first(), + // Do not wait for longer than specified timeout + timeout({ + first: timeoutMs, + meta: 'MediationRecipientService.keylistUpdateAndAwait', + }) + ) + .subscribe(subject) + + const outboundMessageContext = new OutboundMessageContext(message, { agentContext, connection }) + await this.messageSender.sendMessage(outboundMessageContext) + + const keylistUpdate = await firstValueFrom(subject) + return keylistUpdate.payload.mediationRecord + } + + public createKeylistUpdateMessage(updates: KeylistUpdate[]): KeylistUpdateMessage { + const keylistUpdateMessage = new KeylistUpdateMessage({ + updates, + }) + return keylistUpdateMessage + } + + public async addMediationRouting( + agentContext: AgentContext, + routing: Routing, + { mediatorId, useDefaultMediator = true }: GetRoutingOptions = {} + ): Promise { + let mediationRecord: MediationRecord | null = null + + if (mediatorId) { + mediationRecord = await this.getById(agentContext, mediatorId) + } else if (useDefaultMediator) { + // If no mediatorId is provided, and useDefaultMediator is true (default) + // We use the default mediator if available + mediationRecord = await this.findDefaultMediator(agentContext) + } + + // Return early if no mediation record + if (!mediationRecord) return routing + + // new did has been created and mediator needs to be updated with the public key. + mediationRecord = await this.keylistUpdateAndAwait(agentContext, mediationRecord, [ + { + recipientKey: routing.recipientKey, + action: KeylistUpdateAction.add, + }, + ]) + + return { + ...routing, + mediatorId: mediationRecord.id, + endpoints: mediationRecord.endpoint ? [mediationRecord.endpoint] : routing.endpoints, + routingKeys: mediationRecord.routingKeys.map((key) => Key.fromPublicKeyBase58(key, KeyType.Ed25519)), + } + } + + public async removeMediationRouting( + agentContext: AgentContext, + { recipientKeys, mediatorId }: RemoveRoutingOptions + ): Promise { + const mediationRecord = await this.getById(agentContext, mediatorId) + + if (!mediationRecord) { + throw new CredoError('No mediation record to remove routing from has been found') + } + + await this.keylistUpdateAndAwait( + agentContext, + mediationRecord, + recipientKeys.map((item) => { + return { + recipientKey: item, + action: KeylistUpdateAction.remove, + } + }) + ) + } + + public async processMediationDeny(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + + // Mediation record already exists + const mediationRecord = await this.findByConnectionId(messageContext.agentContext, connection.id) + + if (!mediationRecord) { + throw new Error(`No mediation has been requested for this connection id: ${connection.id}`) + } + + // Assert + mediationRecord.assertRole(MediationRole.Recipient) + mediationRecord.assertState(MediationState.Requested) + + // Update record + await this.updateState(messageContext.agentContext, mediationRecord, MediationState.Denied) + + return mediationRecord + } + + /** + * Update the record to a new state and emit an state changed event. Also updates the record + * in storage. + * + * @param MediationRecord The proof record to update the state for + * @param newState The state to update to + * + */ + private async updateState(agentContext: AgentContext, mediationRecord: MediationRecord, newState: MediationState) { + const previousState = mediationRecord.state + mediationRecord.state = newState + await this.mediationRepository.update(agentContext, mediationRecord) + + this.emitStateChangedEvent(agentContext, mediationRecord, previousState) + return mediationRecord + } + + private emitStateChangedEvent( + agentContext: AgentContext, + mediationRecord: MediationRecord, + previousState: MediationState | null + ) { + this.eventEmitter.emit(agentContext, { + type: RoutingEventTypes.MediationStateChanged, + payload: { + mediationRecord: mediationRecord.clone(), + previousState, + }, + }) + } + + public async getById(agentContext: AgentContext, id: string): Promise { + return this.mediationRepository.getById(agentContext, id) + } + + public async findByConnectionId(agentContext: AgentContext, connectionId: string): Promise { + return this.mediationRepository.findSingleByQuery(agentContext, { connectionId }) + } + + public async getMediators(agentContext: AgentContext): Promise { + return this.mediationRepository.getAll(agentContext) + } + + public async findAllMediatorsByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ): Promise { + return await this.mediationRepository.findByQuery(agentContext, query, queryOptions) + } + + public async findDefaultMediator(agentContext: AgentContext): Promise { + return this.mediationRepository.findSingleByQuery(agentContext, { default: true }) + } + + public async discoverMediation( + agentContext: AgentContext, + mediatorId?: string + ): Promise { + // If mediatorId is passed, always use it (and error if it is not found) + if (mediatorId) { + return this.mediationRepository.getById(agentContext, mediatorId) + } + + const defaultMediator = await this.findDefaultMediator(agentContext) + if (defaultMediator) { + if (defaultMediator.state !== MediationState.Granted) { + throw new CredoError( + `Mediation State for ${defaultMediator.id} is not granted, but is set as default mediator!` + ) + } + + return defaultMediator + } + } + + public async setDefaultMediator(agentContext: AgentContext, mediator: MediationRecord) { + const mediationRecords = await this.mediationRepository.findByQuery(agentContext, { default: true }) + + for (const record of mediationRecords) { + record.setTag('default', false) + await this.mediationRepository.update(agentContext, record) + } + + // Set record coming in tag to true and then update. + mediator.setTag('default', true) + await this.mediationRepository.update(agentContext, mediator) + } + + public async clearDefaultMediator(agentContext: AgentContext) { + const mediationRecord = await this.findDefaultMediator(agentContext) + + if (mediationRecord) { + mediationRecord.setTag('default', false) + await this.mediationRepository.update(agentContext, mediationRecord) + } + } + + private async updateUseDidKeysFlag( + agentContext: AgentContext, + connection: ConnectionRecord, + protocolUri: string, + connectionUsesDidKey: boolean + ) { + const useDidKeysForProtocol = connection.metadata.get(ConnectionMetadataKeys.UseDidKeysForProtocol) ?? {} + useDidKeysForProtocol[protocolUri] = connectionUsesDidKey + connection.metadata.set(ConnectionMetadataKeys.UseDidKeysForProtocol, useDidKeysForProtocol) + await this.connectionService.update(agentContext, connection) + } +} + +export interface MediationProtocolMsgReturnType { + message: MessageType + mediationRecord: MediationRecord +} diff --git a/packages/core/src/modules/routing/services/MediatorService.ts b/packages/core/src/modules/routing/services/MediatorService.ts new file mode 100644 index 0000000000..0a585ad8a5 --- /dev/null +++ b/packages/core/src/modules/routing/services/MediatorService.ts @@ -0,0 +1,325 @@ +import type { AgentContext } from '../../../agent' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { Query, QueryOptions } from '../../../storage/StorageService' +import type { ConnectionRecord } from '../../connections' +import type { MediationStateChangedEvent } from '../RoutingEvents' +import type { ForwardMessage, MediationRequestMessage } from '../messages' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { MessageSender } from '../../../agent/MessageSender' +import { InjectionSymbols } from '../../../constants' +import { KeyType } from '../../../crypto' +import { CredoError, RecordDuplicateError } from '../../../error' +import { Logger } from '../../../logger' +import { injectable, inject } from '../../../plugins' +import { ConnectionService } from '../../connections' +import { ConnectionMetadataKeys } from '../../connections/repository/ConnectionMetadataTypes' +import { didKeyToVerkey, isDidKey, verkeyToDidKey } from '../../dids/helpers' +import { MessagePickupApi } from '../../message-pickup' +import { MessagePickupSessionRole } from '../../message-pickup/MessagePickupSession' +import { MediatorModuleConfig } from '../MediatorModuleConfig' +import { MessageForwardingStrategy } from '../MessageForwardingStrategy' +import { RoutingEventTypes } from '../RoutingEvents' +import { + KeylistUpdateMessage, + KeylistUpdateAction, + KeylistUpdated, + KeylistUpdateResponseMessage, + KeylistUpdateResult, + MediationGrantMessage, +} from '../messages' +import { MediationRole } from '../models/MediationRole' +import { MediationState } from '../models/MediationState' +import { MediatorRoutingRecord } from '../repository' +import { MediationRecord } from '../repository/MediationRecord' +import { MediationRepository } from '../repository/MediationRepository' +import { MediatorRoutingRepository } from '../repository/MediatorRoutingRepository' + +@injectable() +export class MediatorService { + private logger: Logger + private mediationRepository: MediationRepository + private mediatorRoutingRepository: MediatorRoutingRepository + private messagePickupApi: MessagePickupApi + private eventEmitter: EventEmitter + private connectionService: ConnectionService + + public constructor( + mediationRepository: MediationRepository, + mediatorRoutingRepository: MediatorRoutingRepository, + messagePickupApi: MessagePickupApi, + eventEmitter: EventEmitter, + @inject(InjectionSymbols.Logger) logger: Logger, + connectionService: ConnectionService + ) { + this.mediationRepository = mediationRepository + this.mediatorRoutingRepository = mediatorRoutingRepository + this.messagePickupApi = messagePickupApi + this.eventEmitter = eventEmitter + this.logger = logger + this.connectionService = connectionService + } + + private async getRoutingKeys(agentContext: AgentContext) { + const mediatorRoutingRecord = await this.findMediatorRoutingRecord(agentContext) + + if (mediatorRoutingRecord) { + // Return the routing keys + this.logger.debug(`Returning mediator routing keys ${mediatorRoutingRecord.routingKeys}`) + return mediatorRoutingRecord.routingKeys + } + throw new CredoError(`Mediator has not been initialized yet.`) + } + + public async processForwardMessage(messageContext: InboundMessageContext): Promise { + const { message, agentContext } = messageContext + + // TODO: update to class-validator validation + if (!message.to) { + throw new CredoError('Invalid Message: Missing required attribute "to"') + } + + const mediationRecord = await this.mediationRepository.getSingleByRecipientKey(agentContext, message.to) + + // Assert mediation record is ready to be used + mediationRecord.assertReady() + mediationRecord.assertRole(MediationRole.Mediator) + + const connection = await this.connectionService.getById(agentContext, mediationRecord.connectionId) + connection.assertReady() + + const messageForwardingStrategy = + agentContext.dependencyManager.resolve(MediatorModuleConfig).messageForwardingStrategy + const messageSender = agentContext.dependencyManager.resolve(MessageSender) + + switch (messageForwardingStrategy) { + case MessageForwardingStrategy.QueueOnly: + await this.messagePickupApi.queueMessage({ + connectionId: mediationRecord.connectionId, + recipientDids: [verkeyToDidKey(message.to)], + message: message.message, + }) + break + case MessageForwardingStrategy.QueueAndLiveModeDelivery: { + await this.messagePickupApi.queueMessage({ + connectionId: mediationRecord.connectionId, + recipientDids: [verkeyToDidKey(message.to)], + message: message.message, + }) + const session = await this.messagePickupApi.getLiveModeSession({ + connectionId: mediationRecord.connectionId, + role: MessagePickupSessionRole.MessageHolder, + }) + if (session) { + await this.messagePickupApi.deliverMessagesFromQueue({ + pickupSessionId: session.id, + recipientDid: verkeyToDidKey(message.to), + }) + } + break + } + case MessageForwardingStrategy.DirectDelivery: + // The message inside the forward message is packed so we just send the packed + // message to the connection associated with it + await messageSender.sendPackage(agentContext, { + connection, + recipientKey: verkeyToDidKey(message.to), + encryptedMessage: message.message, + }) + } + } + + public async processKeylistUpdateRequest(messageContext: InboundMessageContext) { + // Assert Ready connection + const connection = messageContext.assertReadyConnection() + + const { message } = messageContext + const keylist: KeylistUpdated[] = [] + + const mediationRecord = await this.mediationRepository.getByConnectionId(messageContext.agentContext, connection.id) + + mediationRecord.assertReady() + mediationRecord.assertRole(MediationRole.Mediator) + + // Update connection metadata to use their key format in further protocol messages + const connectionUsesDidKey = message.updates.some((update) => isDidKey(update.recipientKey)) + await this.updateUseDidKeysFlag( + messageContext.agentContext, + connection, + KeylistUpdateMessage.type.protocolUri, + connectionUsesDidKey + ) + + for (const update of message.updates) { + const updated = new KeylistUpdated({ + action: update.action, + recipientKey: update.recipientKey, + result: KeylistUpdateResult.NoChange, + }) + + // According to RFC 0211 key should be a did key, but base58 encoded verkey was used before + // RFC was accepted. This converts the key to a public key base58 if it is a did key. + const publicKeyBase58 = didKeyToVerkey(update.recipientKey) + + if (update.action === KeylistUpdateAction.add) { + mediationRecord.addRecipientKey(publicKeyBase58) + updated.result = KeylistUpdateResult.Success + + keylist.push(updated) + } else if (update.action === KeylistUpdateAction.remove) { + const success = mediationRecord.removeRecipientKey(publicKeyBase58) + updated.result = success ? KeylistUpdateResult.Success : KeylistUpdateResult.NoChange + keylist.push(updated) + } + } + + await this.mediationRepository.update(messageContext.agentContext, mediationRecord) + + return new KeylistUpdateResponseMessage({ keylist, threadId: message.threadId }) + } + + public async createGrantMediationMessage(agentContext: AgentContext, mediationRecord: MediationRecord) { + // Assert + mediationRecord.assertState(MediationState.Requested) + mediationRecord.assertRole(MediationRole.Mediator) + + await this.updateState(agentContext, mediationRecord, MediationState.Granted) + + // Use our useDidKey configuration, as this is the first interaction for this protocol + const useDidKey = agentContext.config.useDidKeyInProtocols + + const message = new MediationGrantMessage({ + endpoint: agentContext.config.endpoints[0], + routingKeys: useDidKey + ? (await this.getRoutingKeys(agentContext)).map(verkeyToDidKey) + : await this.getRoutingKeys(agentContext), + threadId: mediationRecord.threadId, + }) + + return { mediationRecord, message } + } + + public async processMediationRequest(messageContext: InboundMessageContext) { + // Assert ready connection + const connection = messageContext.assertReadyConnection() + + const mediationRecord = new MediationRecord({ + connectionId: connection.id, + role: MediationRole.Mediator, + state: MediationState.Requested, + threadId: messageContext.message.threadId, + }) + + await this.mediationRepository.save(messageContext.agentContext, mediationRecord) + this.emitStateChangedEvent(messageContext.agentContext, mediationRecord, null) + + return mediationRecord + } + + public async findById(agentContext: AgentContext, mediatorRecordId: string): Promise { + return this.mediationRepository.findById(agentContext, mediatorRecordId) + } + + public async getById(agentContext: AgentContext, mediatorRecordId: string): Promise { + return this.mediationRepository.getById(agentContext, mediatorRecordId) + } + + public async getAll(agentContext: AgentContext): Promise { + return await this.mediationRepository.getAll(agentContext) + } + + public async findMediatorRoutingRecord(agentContext: AgentContext): Promise { + const routingRecord = await this.mediatorRoutingRepository.findById( + agentContext, + this.mediatorRoutingRepository.MEDIATOR_ROUTING_RECORD_ID + ) + + return routingRecord + } + + public async createMediatorRoutingRecord(agentContext: AgentContext): Promise { + const routingKey = await agentContext.wallet.createKey({ + keyType: KeyType.Ed25519, + }) + + const routingRecord = new MediatorRoutingRecord({ + id: this.mediatorRoutingRepository.MEDIATOR_ROUTING_RECORD_ID, + // FIXME: update to fingerprint to include the key type + routingKeys: [routingKey.publicKeyBase58], + }) + + try { + await this.mediatorRoutingRepository.save(agentContext, routingRecord) + this.eventEmitter.emit(agentContext, { + type: RoutingEventTypes.RoutingCreatedEvent, + payload: { + routing: { + endpoints: agentContext.config.endpoints, + routingKeys: [], + recipientKey: routingKey, + }, + }, + }) + } catch (error) { + // This addresses some race conditions issues where we first check if the record exists + // then we create one if it doesn't, but another process has created one in the meantime + // Although not the most elegant solution, it addresses the issues + if (error instanceof RecordDuplicateError) { + // the record already exists, which is our intended end state + // we can ignore this error and fetch the existing record + return this.mediatorRoutingRepository.getById( + agentContext, + this.mediatorRoutingRepository.MEDIATOR_ROUTING_RECORD_ID + ) + } else { + throw error + } + } + + return routingRecord + } + + public async findAllByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ): Promise { + return await this.mediationRepository.findByQuery(agentContext, query, queryOptions) + } + + private async updateState(agentContext: AgentContext, mediationRecord: MediationRecord, newState: MediationState) { + const previousState = mediationRecord.state + + mediationRecord.state = newState + + await this.mediationRepository.update(agentContext, mediationRecord) + + this.emitStateChangedEvent(agentContext, mediationRecord, previousState) + } + + private emitStateChangedEvent( + agentContext: AgentContext, + mediationRecord: MediationRecord, + previousState: MediationState | null + ) { + this.eventEmitter.emit(agentContext, { + type: RoutingEventTypes.MediationStateChanged, + payload: { + mediationRecord: mediationRecord.clone(), + previousState, + }, + }) + } + + private async updateUseDidKeysFlag( + agentContext: AgentContext, + connection: ConnectionRecord, + protocolUri: string, + connectionUsesDidKey: boolean + ) { + const useDidKeysForProtocol = connection.metadata.get(ConnectionMetadataKeys.UseDidKeysForProtocol) ?? {} + useDidKeysForProtocol[protocolUri] = connectionUsesDidKey + connection.metadata.set(ConnectionMetadataKeys.UseDidKeysForProtocol, useDidKeysForProtocol) + await this.connectionService.update(agentContext, connection) + } +} diff --git a/packages/core/src/modules/routing/services/RoutingService.ts b/packages/core/src/modules/routing/services/RoutingService.ts new file mode 100644 index 0000000000..94224c58a5 --- /dev/null +++ b/packages/core/src/modules/routing/services/RoutingService.ts @@ -0,0 +1,83 @@ +import type { AgentContext } from '../../../agent' +import type { Key } from '../../../crypto' +import type { Routing } from '../../connections' +import type { RoutingCreatedEvent } from '../RoutingEvents' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { KeyType } from '../../../crypto' +import { injectable } from '../../../plugins' +import { RoutingEventTypes } from '../RoutingEvents' + +import { MediationRecipientService } from './MediationRecipientService' + +@injectable() +export class RoutingService { + private mediationRecipientService: MediationRecipientService + + private eventEmitter: EventEmitter + + public constructor(mediationRecipientService: MediationRecipientService, eventEmitter: EventEmitter) { + this.mediationRecipientService = mediationRecipientService + + this.eventEmitter = eventEmitter + } + + public async getRouting( + agentContext: AgentContext, + { mediatorId, useDefaultMediator = true }: GetRoutingOptions = {} + ): Promise { + // Create and store new key + const recipientKey = await agentContext.wallet.createKey({ keyType: KeyType.Ed25519 }) + + let routing: Routing = { + endpoints: agentContext.config.endpoints, + routingKeys: [], + recipientKey, + } + + // Extend routing with mediator keys (if applicable) + routing = await this.mediationRecipientService.addMediationRouting(agentContext, routing, { + mediatorId, + useDefaultMediator, + }) + + // Emit event so other parts of the framework can react on keys created + this.eventEmitter.emit(agentContext, { + type: RoutingEventTypes.RoutingCreatedEvent, + payload: { + routing, + }, + }) + + return routing + } + + public async removeRouting(agentContext: AgentContext, options: RemoveRoutingOptions) { + await this.mediationRecipientService.removeMediationRouting(agentContext, options) + } +} + +export interface GetRoutingOptions { + /** + * Identifier of the mediator to use when setting up routing + */ + mediatorId?: string + + /** + * Whether to use the default mediator if available and `mediatorId` has not been provided + * @default true + */ + useDefaultMediator?: boolean +} + +export interface RemoveRoutingOptions { + /** + * Keys to remove routing from + */ + recipientKeys: Key[] + + /** + * Identifier of the mediator used when routing has been set up + */ + mediatorId: string +} diff --git a/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts b/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts new file mode 100644 index 0000000000..15831a8a54 --- /dev/null +++ b/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts @@ -0,0 +1,244 @@ +import type { AgentContext } from '../../../../agent' +import type { Routing } from '../../../connections/services/ConnectionService' + +import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../tests/helpers' +import { EventEmitter } from '../../../../agent/EventEmitter' +import { MessageSender } from '../../../../agent/MessageSender' +import { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import { Key } from '../../../../crypto' +import { uuid } from '../../../../utils/uuid' +import { DidExchangeState } from '../../../connections' +import { ConnectionMetadataKeys } from '../../../connections/repository/ConnectionMetadataTypes' +import { ConnectionRepository } from '../../../connections/repository/ConnectionRepository' +import { ConnectionService } from '../../../connections/services/ConnectionService' +import { DidRepository } from '../../../dids/repository/DidRepository' +import { RoutingEventTypes } from '../../RoutingEvents' +import { + KeylistUpdateAction, + KeylistUpdateResponseMessage, + KeylistUpdateResult, + MediationGrantMessage, +} from '../../messages' +import { MediationRole, MediationState } from '../../models' +import { MediationRecord } from '../../repository/MediationRecord' +import { MediationRepository } from '../../repository/MediationRepository' +import { MediationRecipientService } from '../MediationRecipientService' + +jest.mock('../../repository/MediationRepository') +const MediationRepositoryMock = MediationRepository as jest.Mock + +jest.mock('../../../connections/repository/ConnectionRepository') +const ConnectionRepositoryMock = ConnectionRepository as jest.Mock + +jest.mock('../../../dids/repository/DidRepository') +const DidRepositoryMock = DidRepository as jest.Mock + +jest.mock('../../../../agent/EventEmitter') +const EventEmitterMock = EventEmitter as jest.Mock + +jest.mock('../../../../agent/MessageSender') +const MessageSenderMock = MessageSender as jest.Mock + +const connectionImageUrl = 'https://example.com/image.png' + +describe('MediationRecipientService', () => { + const config = getAgentConfig('MediationRecipientServiceTest', { + endpoints: ['http://agent.com:8080'], + connectionImageUrl, + }) + + let mediationRepository: MediationRepository + let didRepository: DidRepository + let eventEmitter: EventEmitter + let connectionService: ConnectionService + let connectionRepository: ConnectionRepository + let messageSender: MessageSender + let mediationRecipientService: MediationRecipientService + let mediationRecord: MediationRecord + let agentContext: AgentContext + + beforeAll(async () => { + agentContext = getAgentContext({ + agentConfig: config, + }) + }) + + beforeEach(async () => { + eventEmitter = new EventEmitterMock() + connectionRepository = new ConnectionRepositoryMock() + didRepository = new DidRepositoryMock() + connectionService = new ConnectionService(config.logger, connectionRepository, didRepository, eventEmitter) + mediationRepository = new MediationRepositoryMock() + messageSender = new MessageSenderMock() + + // Mock default return value + mediationRecord = new MediationRecord({ + connectionId: 'connectionId', + role: MediationRole.Recipient, + state: MediationState.Granted, + threadId: 'threadId', + }) + mockFunction(mediationRepository.getByConnectionId).mockResolvedValue(mediationRecord) + + mediationRecipientService = new MediationRecipientService( + connectionService, + messageSender, + mediationRepository, + eventEmitter + ) + }) + + describe('processMediationGrant', () => { + test('should process base58 encoded routing keys', async () => { + mediationRecord.state = MediationState.Requested + const mediationGrant = new MediationGrantMessage({ + endpoint: 'http://agent.com:8080', + routingKeys: ['79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ'], + threadId: 'threadId', + }) + + const connection = getMockConnection({ + state: DidExchangeState.Completed, + }) + + const messageContext = new InboundMessageContext(mediationGrant, { connection, agentContext }) + + await mediationRecipientService.processMediationGrant(messageContext) + + expect(connection.metadata.get(ConnectionMetadataKeys.UseDidKeysForProtocol)).toEqual({ + 'https://didcomm.org/coordinate-mediation/1.0': false, + }) + expect(mediationRecord.routingKeys).toEqual(['79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ']) + }) + + test('should process did:key encoded routing keys', async () => { + mediationRecord.state = MediationState.Requested + const mediationGrant = new MediationGrantMessage({ + endpoint: 'http://agent.com:8080', + routingKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + threadId: 'threadId', + }) + + const connection = getMockConnection({ + state: DidExchangeState.Completed, + }) + + const messageContext = new InboundMessageContext(mediationGrant, { connection, agentContext }) + + await mediationRecipientService.processMediationGrant(messageContext) + + expect(connection.metadata.get(ConnectionMetadataKeys.UseDidKeysForProtocol)).toEqual({ + 'https://didcomm.org/coordinate-mediation/1.0': true, + }) + expect(mediationRecord.routingKeys).toEqual(['8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K']) + }) + }) + + describe('processKeylistUpdateResults', () => { + it('it stores did:key-encoded keys in base58 format', async () => { + const spyAddRecipientKey = jest.spyOn(mediationRecord, 'addRecipientKey') + + const connection = getMockConnection({ + state: DidExchangeState.Completed, + }) + + const keylist = [ + { + result: KeylistUpdateResult.Success, + recipientKey: 'did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th', + action: KeylistUpdateAction.add, + }, + ] + + const keyListUpdateResponse = new KeylistUpdateResponseMessage({ + threadId: uuid(), + keylist, + }) + + const messageContext = new InboundMessageContext(keyListUpdateResponse, { connection, agentContext }) + + expect(connection.metadata.get(ConnectionMetadataKeys.UseDidKeysForProtocol)).toBeNull() + + await mediationRecipientService.processKeylistUpdateResults(messageContext) + + expect(connection.metadata.get(ConnectionMetadataKeys.UseDidKeysForProtocol)).toEqual({ + 'https://didcomm.org/coordinate-mediation/1.0': true, + }) + + expect(eventEmitter.emit).toHaveBeenCalledWith(agentContext, { + type: RoutingEventTypes.RecipientKeylistUpdated, + payload: { + mediationRecord, + keylist, + }, + }) + expect(spyAddRecipientKey).toHaveBeenCalledWith('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K') + spyAddRecipientKey.mockClear() + }) + }) + + describe('addMediationRouting', () => { + const routingKey = Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + const recipientKey = Key.fromFingerprint('z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th') + const routing: Routing = { + routingKeys: [routingKey], + recipientKey, + endpoints: [], + } + + const mediationRecord = new MediationRecord({ + connectionId: 'connection-id', + role: MediationRole.Recipient, + state: MediationState.Granted, + threadId: 'thread-id', + endpoint: 'https://a-mediator-endpoint.com', + routingKeys: [routingKey.publicKeyBase58], + }) + + beforeEach(() => { + jest.spyOn(mediationRecipientService, 'keylistUpdateAndAwait').mockResolvedValue(mediationRecord) + }) + + test('adds mediation routing id mediator id is passed', async () => { + mockFunction(mediationRepository.getById).mockResolvedValue(mediationRecord) + + const extendedRouting = await mediationRecipientService.addMediationRouting(agentContext, routing, { + mediatorId: 'mediator-id', + }) + + expect(extendedRouting).toMatchObject({ + endpoints: ['https://a-mediator-endpoint.com'], + routingKeys: [routingKey], + }) + expect(mediationRepository.getById).toHaveBeenCalledWith(agentContext, 'mediator-id') + }) + + test('adds mediation routing if useDefaultMediator is true and default mediation is found', async () => { + mockFunction(mediationRepository.findSingleByQuery).mockResolvedValue(mediationRecord) + + jest.spyOn(mediationRecipientService, 'keylistUpdateAndAwait').mockResolvedValue(mediationRecord) + const extendedRouting = await mediationRecipientService.addMediationRouting(agentContext, routing, { + useDefaultMediator: true, + }) + + expect(extendedRouting).toMatchObject({ + endpoints: ['https://a-mediator-endpoint.com'], + routingKeys: [routingKey], + }) + expect(mediationRepository.findSingleByQuery).toHaveBeenCalledWith(agentContext, { default: true }) + }) + + test('does not add mediation routing if no mediation is found', async () => { + mockFunction(mediationRepository.findSingleByQuery).mockResolvedValue(mediationRecord) + + jest.spyOn(mediationRecipientService, 'keylistUpdateAndAwait').mockResolvedValue(mediationRecord) + const extendedRouting = await mediationRecipientService.addMediationRouting(agentContext, routing, { + useDefaultMediator: false, + }) + + expect(extendedRouting).toMatchObject(routing) + expect(mediationRepository.findSingleByQuery).not.toHaveBeenCalled() + expect(mediationRepository.getById).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/core/src/modules/routing/services/__tests__/MediatorService.test.ts b/packages/core/src/modules/routing/services/__tests__/MediatorService.test.ts new file mode 100644 index 0000000000..a2741fc29a --- /dev/null +++ b/packages/core/src/modules/routing/services/__tests__/MediatorService.test.ts @@ -0,0 +1,203 @@ +import { Subject } from 'rxjs' + +import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../tests/helpers' +import { EventEmitter } from '../../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import { ConnectionService, DidExchangeState } from '../../../connections' +import { isDidKey } from '../../../dids/helpers' +import { MessagePickupApi } from '../../../message-pickup' +import { KeylistUpdateAction, KeylistUpdateMessage, KeylistUpdateResult } from '../../messages' +import { MediationRole, MediationState } from '../../models' +import { MediationRecord, MediatorRoutingRecord } from '../../repository' +import { MediationRepository } from '../../repository/MediationRepository' +import { MediatorRoutingRepository } from '../../repository/MediatorRoutingRepository' +import { MediatorService } from '../MediatorService' + +jest.mock('../../repository/MediationRepository') +const MediationRepositoryMock = MediationRepository as jest.Mock + +jest.mock('../../repository/MediatorRoutingRepository') +const MediatorRoutingRepositoryMock = MediatorRoutingRepository as jest.Mock + +jest.mock('../../../connections/services/ConnectionService') +const ConnectionServiceMock = ConnectionService as jest.Mock + +jest.mock('../../../connections/services/ConnectionService') +const MessagePickupApiMock = MessagePickupApi as jest.Mock + +const mediationRepository = new MediationRepositoryMock() +const mediatorRoutingRepository = new MediatorRoutingRepositoryMock() +const connectionService = new ConnectionServiceMock() +const mediationPickupApi = new MessagePickupApiMock() + +const mockConnection = getMockConnection({ + state: DidExchangeState.Completed, +}) + +describe('MediatorService - default config', () => { + const agentConfig = getAgentConfig('MediatorService') + + const agentContext = getAgentContext({ + agentConfig, + }) + + const mediatorService = new MediatorService( + mediationRepository, + mediatorRoutingRepository, + mediationPickupApi, + new EventEmitter(agentConfig.agentDependencies, new Subject()), + agentConfig.logger, + connectionService + ) + + describe('createGrantMediationMessage', () => { + test('sends did:key encoded recipient keys by default', async () => { + const mediationRecord = new MediationRecord({ + connectionId: 'connectionId', + role: MediationRole.Mediator, + state: MediationState.Requested, + threadId: 'threadId', + }) + + mockFunction(mediationRepository.getByConnectionId).mockResolvedValue(mediationRecord) + + mockFunction(mediatorRoutingRepository.findById).mockResolvedValue( + new MediatorRoutingRecord({ + routingKeys: ['8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'], + }) + ) + + const { message } = await mediatorService.createGrantMediationMessage(agentContext, mediationRecord) + + expect(message.routingKeys.length).toBe(1) + expect(isDidKey(message.routingKeys[0])).toBeTruthy() + }) + }) + + describe('processKeylistUpdateRequest', () => { + test('processes base58 encoded recipient keys', async () => { + const mediationRecord = new MediationRecord({ + connectionId: 'connectionId', + role: MediationRole.Mediator, + state: MediationState.Granted, + threadId: 'threadId', + recipientKeys: ['8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'], + }) + + mockFunction(mediationRepository.getByConnectionId).mockResolvedValue(mediationRecord) + + const keyListUpdate = new KeylistUpdateMessage({ + updates: [ + { + action: KeylistUpdateAction.add, + recipientKey: '79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', + }, + { + action: KeylistUpdateAction.remove, + recipientKey: '8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', + }, + ], + }) + + const messageContext = new InboundMessageContext(keyListUpdate, { connection: mockConnection, agentContext }) + const response = await mediatorService.processKeylistUpdateRequest(messageContext) + + expect(mediationRecord.recipientKeys).toEqual(['79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ']) + expect(response.updated).toEqual([ + { + action: KeylistUpdateAction.add, + recipientKey: '79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', + result: KeylistUpdateResult.Success, + }, + { + action: KeylistUpdateAction.remove, + recipientKey: '8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', + result: KeylistUpdateResult.Success, + }, + ]) + }) + }) + + test('processes did:key encoded recipient keys', async () => { + const mediationRecord = new MediationRecord({ + connectionId: 'connectionId', + role: MediationRole.Mediator, + state: MediationState.Granted, + threadId: 'threadId', + recipientKeys: ['8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'], + }) + + mockFunction(mediationRepository.getByConnectionId).mockResolvedValue(mediationRecord) + + const keyListUpdate = new KeylistUpdateMessage({ + updates: [ + { + action: KeylistUpdateAction.add, + recipientKey: 'did:key:z6MkkbTaLstV4fwr1rNf5CSxdS2rGbwxi3V5y6NnVFTZ2V1w', + }, + { + action: KeylistUpdateAction.remove, + recipientKey: 'did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th', + }, + ], + }) + + const messageContext = new InboundMessageContext(keyListUpdate, { connection: mockConnection, agentContext }) + const response = await mediatorService.processKeylistUpdateRequest(messageContext) + + expect(mediationRecord.recipientKeys).toEqual(['79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ']) + expect(response.updated).toEqual([ + { + action: KeylistUpdateAction.add, + recipientKey: 'did:key:z6MkkbTaLstV4fwr1rNf5CSxdS2rGbwxi3V5y6NnVFTZ2V1w', + result: KeylistUpdateResult.Success, + }, + { + action: KeylistUpdateAction.remove, + recipientKey: 'did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th', + result: KeylistUpdateResult.Success, + }, + ]) + }) +}) + +describe('MediatorService - useDidKeyInProtocols set to false', () => { + const agentConfig = getAgentConfig('MediatorService', { useDidKeyInProtocols: false }) + + const agentContext = getAgentContext({ + agentConfig, + }) + + const mediatorService = new MediatorService( + mediationRepository, + mediatorRoutingRepository, + mediationPickupApi, + new EventEmitter(agentConfig.agentDependencies, new Subject()), + agentConfig.logger, + connectionService + ) + + describe('createGrantMediationMessage', () => { + test('sends base58 encoded recipient keys when config is set', async () => { + const mediationRecord = new MediationRecord({ + connectionId: 'connectionId', + role: MediationRole.Mediator, + state: MediationState.Requested, + threadId: 'threadId', + }) + + mockFunction(mediationRepository.getByConnectionId).mockResolvedValue(mediationRecord) + + const routingRecord = new MediatorRoutingRecord({ + routingKeys: ['8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'], + }) + + mockFunction(mediatorRoutingRepository.findById).mockResolvedValue(routingRecord) + + const { message } = await mediatorService.createGrantMediationMessage(agentContext, mediationRecord) + + expect(message.routingKeys.length).toBe(1) + expect(isDidKey(message.routingKeys[0])).toBeFalsy() + }) + }) +}) diff --git a/packages/core/src/modules/routing/services/__tests__/RoutingService.test.ts b/packages/core/src/modules/routing/services/__tests__/RoutingService.test.ts new file mode 100644 index 0000000000..14ec7c22b8 --- /dev/null +++ b/packages/core/src/modules/routing/services/__tests__/RoutingService.test.ts @@ -0,0 +1,74 @@ +import type { Wallet } from '../../../../wallet' + +import { Subject } from 'rxjs' + +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../tests/helpers' +import { EventEmitter } from '../../../../agent/EventEmitter' +import { Key } from '../../../../crypto' +import { RoutingEventTypes } from '../../RoutingEvents' +import { MediationRecipientService } from '../MediationRecipientService' +import { RoutingService } from '../RoutingService' + +jest.mock('../MediationRecipientService') +const MediationRecipientServiceMock = MediationRecipientService as jest.Mock + +const recipientKey = Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') +const agentConfig = getAgentConfig('RoutingService', { + endpoints: ['http://endpoint.com'], +}) +const eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) +const wallet = { + createKey: jest.fn().mockResolvedValue(recipientKey), + // with satisfies Partial we still get type errors when the interface changes +} satisfies Partial +const agentContext = getAgentContext({ + wallet: wallet as unknown as Wallet, + agentConfig, +}) +const mediationRecipientService = new MediationRecipientServiceMock() +const routingService = new RoutingService(mediationRecipientService, eventEmitter) + +const routing = { + endpoints: ['http://endpoint.com'], + recipientKey, + routingKeys: [], +} +mockFunction(mediationRecipientService.addMediationRouting).mockResolvedValue(routing) + +describe('RoutingService', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('getRouting', () => { + test('calls mediation recipient service', async () => { + const routing = await routingService.getRouting(agentContext, { + mediatorId: 'mediator-id', + useDefaultMediator: true, + }) + + expect(mediationRecipientService.addMediationRouting).toHaveBeenCalledWith(agentContext, routing, { + mediatorId: 'mediator-id', + useDefaultMediator: true, + }) + }) + + test('emits RoutingCreatedEvent', async () => { + const routingListener = jest.fn() + eventEmitter.on(RoutingEventTypes.RoutingCreatedEvent, routingListener) + + const routing = await routingService.getRouting(agentContext) + + expect(routing).toEqual(routing) + expect(routingListener).toHaveBeenCalledWith({ + type: RoutingEventTypes.RoutingCreatedEvent, + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + routing, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/routing/services/helpers.ts b/packages/core/src/modules/routing/services/helpers.ts new file mode 100644 index 0000000000..0a3bb4fe42 --- /dev/null +++ b/packages/core/src/modules/routing/services/helpers.ts @@ -0,0 +1,13 @@ +import type { AgentContext } from '../../../agent' +import type { DidDocument } from '../../dids' + +import { MediationRecipientService } from './MediationRecipientService' + +export async function getMediationRecordForDidDocument(agentContext: AgentContext, didDocument: DidDocument) { + const [mediatorRecord] = await agentContext.dependencyManager + .resolve(MediationRecipientService) + .findAllMediatorsByQuery(agentContext, { + recipientKeys: didDocument.recipientKeys.map((key) => key.publicKeyBase58), + }) + return mediatorRecord +} diff --git a/packages/core/src/modules/routing/services/index.ts b/packages/core/src/modules/routing/services/index.ts new file mode 100644 index 0000000000..804d2eb3f9 --- /dev/null +++ b/packages/core/src/modules/routing/services/index.ts @@ -0,0 +1,3 @@ +export * from './MediationRecipientService' +export * from './MediatorService' +export * from './RoutingService' diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcApi.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcApi.ts new file mode 100644 index 0000000000..c51be097fe --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcApi.ts @@ -0,0 +1,87 @@ +import type { + SdJwtVcSignOptions, + SdJwtVcHeader, + SdJwtVcPayload, + SdJwtVcPresentOptions, + SdJwtVcVerifyOptions, +} from './SdJwtVcOptions' +import type { SdJwtVcRecord } from './repository' +import type { Query, QueryOptions } from '../../storage/StorageService' + +import { AgentContext } from '../../agent' +import { injectable } from '../../plugins' + +import { SdJwtVcService } from './SdJwtVcService' + +/** + * @public + */ +@injectable() +export class SdJwtVcApi { + private agentContext: AgentContext + private sdJwtVcService: SdJwtVcService + + public constructor(agentContext: AgentContext, sdJwtVcService: SdJwtVcService) { + this.agentContext = agentContext + this.sdJwtVcService = sdJwtVcService + } + + public async sign(options: SdJwtVcSignOptions) { + return await this.sdJwtVcService.sign(this.agentContext, options) + } + + /** + * + * Create a compact presentation of the sd-jwt. + * This presentation can be send in- or out-of-band to the verifier. + * + * Also, whether to include the holder key binding. + */ + public async present( + options: SdJwtVcPresentOptions + ): Promise { + return await this.sdJwtVcService.present(this.agentContext, options) + } + + /** + * + * Verify an incoming sd-jwt. It will check whether everything is valid, but also returns parts of the validation. + * + * For example, you might still want to continue with a flow if not all the claims are included, but the signature is valid. + * + */ + public async verify
(options: SdJwtVcVerifyOptions) { + return await this.sdJwtVcService.verify(this.agentContext, options) + } + + /** + * Get and validate a sd-jwt-vc from a serialized JWT. + */ + public fromCompact
(sdJwtVcCompact: string) { + return this.sdJwtVcService.fromCompact(sdJwtVcCompact) + } + + public async store(compactSdJwtVc: string) { + return await this.sdJwtVcService.store(this.agentContext, compactSdJwtVc) + } + + public async getById(id: string): Promise { + return await this.sdJwtVcService.getById(this.agentContext, id) + } + + public async getAll(): Promise> { + return await this.sdJwtVcService.getAll(this.agentContext) + } + + public async findAllByQuery(query: Query, queryOptions?: QueryOptions): Promise> { + return await this.sdJwtVcService.findByQuery(this.agentContext, query, queryOptions) + } + + public async deleteById(id: string) { + return await this.sdJwtVcService.deleteById(this.agentContext, id) + } + + public async update(sdJwtVcRecord: SdJwtVcRecord) { + return await this.sdJwtVcService.update(this.agentContext, sdJwtVcRecord) + } +} diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcError.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcError.ts new file mode 100644 index 0000000000..f904811686 --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcError.ts @@ -0,0 +1,3 @@ +import { CredoError } from '../../error' + +export class SdJwtVcError extends CredoError {} diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcModule.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcModule.ts new file mode 100644 index 0000000000..f25d959d97 --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcModule.ts @@ -0,0 +1,32 @@ +import type { Module, DependencyManager } from '../../plugins' + +import { AgentConfig } from '../../agent/AgentConfig' + +import { SdJwtVcApi } from './SdJwtVcApi' +import { SdJwtVcService } from './SdJwtVcService' +import { SdJwtVcRepository } from './repository' + +/** + * @public + */ +export class SdJwtVcModule implements Module { + public readonly api = SdJwtVcApi + + /** + * Registers the dependencies of the sd-jwt-vc module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The 'SdJwtVc' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." + ) + + // Services + dependencyManager.registerSingleton(SdJwtVcService) + + // Repositories + dependencyManager.registerSingleton(SdJwtVcRepository) + } +} diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcOptions.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcOptions.ts new file mode 100644 index 0000000000..27d29efcf5 --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcOptions.ts @@ -0,0 +1,103 @@ +import type { JwkJson, Jwk } from '../../crypto' +import type { HashName } from '../../utils' + +// TODO: extend with required claim names for input (e.g. vct) +export type SdJwtVcPayload = Record +export type SdJwtVcHeader = Record + +export interface IDisclosureFrame { + _sd?: string[] + _sd_decoy?: number + [x: string]: string[] | number | IDisclosureFrame | undefined +} + +export interface IPresentationFrame { + [x: string]: boolean | IPresentationFrame +} + +export interface SdJwtVcHolderDidBinding { + method: 'did' + didUrl: string +} + +export interface SdJwtVcHolderJwkBinding { + method: 'jwk' + jwk: JwkJson | Jwk +} + +export interface SdJwtVcIssuerDid { + method: 'did' + // didUrl referencing a specific key in a did document. + didUrl: string +} + +// We support jwk and did based binding for the holder at the moment +export type SdJwtVcHolderBinding = SdJwtVcHolderDidBinding | SdJwtVcHolderJwkBinding + +// We only support did based issuance currently, but we might want to add support +// for x509 or issuer metadata (as defined in SD-JWT VC) in the future +export type SdJwtVcIssuer = SdJwtVcIssuerDid + +export interface SdJwtVcSignOptions { + payload: Payload + + /** + * If holder is not provided, we don't bind the SD-JWT VC to a key (so bearer VC) + */ + holder?: SdJwtVcHolderBinding + issuer: SdJwtVcIssuer + disclosureFrame?: IDisclosureFrame + + /** + * Default of sha-256 will be used if not provided + */ + hashingAlgorithm?: HashName +} + +// TODO: use the payload type once types are fixed +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export type SdJwtVcPresentOptions = { + compactSdJwtVc: string + + /** + * Use true to disclose everything + */ + presentationFrame?: IPresentationFrame + + /** + * This information is received out-of-band from the verifier. + * The claims will be used to create a normal JWT, used for key binding. + * + * If not defined, a KB-JWT will not be created + */ + verifierMetadata?: { + audience: string + nonce: string + issuedAt: number + } +} + +export type SdJwtVcVerifyOptions = { + compactSdJwtVc: string + + /** + * If the key binding object is present, the sd-jwt is required to have a key binding jwt attached + * and will be validated against the provided key binding options. + */ + keyBinding?: { + /** + * The expected `aud` value in the payload of the KB-JWT. The value of this is dependant on the + * exchange protocol used. + */ + audience: string + + /** + * The expected `nonce` value in the payload of the KB-JWT. The value of this is dependant on the + * exchange protocol used. + */ + nonce: string + } + + // TODO: update to requiredClaimFrame + requiredClaimKeys?: Array +} diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts new file mode 100644 index 0000000000..13c2ea0336 --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts @@ -0,0 +1,547 @@ +import type { + SdJwtVcSignOptions, + SdJwtVcPresentOptions, + SdJwtVcVerifyOptions, + SdJwtVcPayload, + SdJwtVcHeader, + SdJwtVcHolderBinding, + SdJwtVcIssuer, +} from './SdJwtVcOptions' +import type { AgentContext } from '../../agent' +import type { JwkJson, Key } from '../../crypto' +import type { Query, QueryOptions } from '../../storage/StorageService' +import type { SDJwt } from '@sd-jwt/core' +import type { Signer, Verifier, HasherSync, PresentationFrame, DisclosureFrame } from '@sd-jwt/types' + +import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode' +import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc' +import { uint8ArrayToBase64Url } from '@sd-jwt/utils' +import { injectable } from 'tsyringe' + +import { JwtPayload, Jwk, getJwkFromJson, getJwkFromKey } from '../../crypto' +import { CredoError } from '../../error' +import { TypedArrayEncoder, Hasher, nowInSeconds } from '../../utils' +import { fetchWithTimeout } from '../../utils/fetch' +import { DidResolverService, parseDid, getKeyFromVerificationMethod } from '../dids' + +import { SdJwtVcError } from './SdJwtVcError' +import { SdJwtVcRecord, SdJwtVcRepository } from './repository' + +type SdJwtVcConfig = SDJwtVcInstance['userConfig'] + +export interface SdJwtVc< + Header extends SdJwtVcHeader = SdJwtVcHeader, + Payload extends SdJwtVcPayload = SdJwtVcPayload +> { + compact: string + header: Header + + // TODO: payload type here is a lie, as it is the signed payload (so fields replaced with _sd) + payload: Payload + prettyClaims: Payload +} + +export interface CnfPayload { + jwk?: JwkJson + kid?: string +} + +export interface VerificationResult { + isValid: boolean + isValidJwtPayload?: boolean + isSignatureValid?: boolean + isStatusValid?: boolean + isNotBeforeValid?: boolean + isExpiryTimeValid?: boolean + areRequiredClaimsIncluded?: boolean + isKeyBindingValid?: boolean + containsExpectedKeyBinding?: boolean + containsRequiredVcProperties?: boolean +} + +/** + * @internal + */ +@injectable() +export class SdJwtVcService { + private sdJwtVcRepository: SdJwtVcRepository + + public constructor(sdJwtVcRepository: SdJwtVcRepository) { + this.sdJwtVcRepository = sdJwtVcRepository + } + + public async sign(agentContext: AgentContext, options: SdJwtVcSignOptions) { + const { payload, disclosureFrame, hashingAlgorithm } = options + + // default is sha-256 + if (hashingAlgorithm && hashingAlgorithm !== 'sha-256') { + throw new SdJwtVcError(`Unsupported hashing algorithm used: ${hashingAlgorithm}`) + } + + const issuer = await this.extractKeyFromIssuer(agentContext, options.issuer) + + // holer binding is optional + const holderBinding = options.holder + ? await this.extractKeyFromHolderBinding(agentContext, options.holder) + : undefined + + const header = { + alg: issuer.alg, + typ: 'vc+sd-jwt', + kid: issuer.kid, + } as const + + const sdjwt = new SDJwtVcInstance({ + ...this.getBaseSdJwtConfig(agentContext), + signer: this.signer(agentContext, issuer.key), + hashAlg: 'sha-256', + signAlg: issuer.alg, + }) + + if (!payload.vct || typeof payload.vct !== 'string') { + throw new SdJwtVcError("Missing required parameter 'vct'") + } + + const compact = await sdjwt.issue( + { + ...payload, + cnf: holderBinding?.cnf, + iss: issuer.iss, + iat: nowInSeconds(), + vct: payload.vct, + }, + disclosureFrame as DisclosureFrame, + { header } + ) + + const prettyClaims = (await sdjwt.getClaims(compact)) as Payload + const a = await sdjwt.decode(compact) + const sdjwtPayload = a.jwt?.payload as Payload | undefined + if (!sdjwtPayload) { + throw new SdJwtVcError('Invalid sd-jwt-vc state.') + } + + return { + compact, + prettyClaims, + header: header, + payload: sdjwtPayload, + } satisfies SdJwtVc + } + + public fromCompact
( + compactSdJwtVc: string + ): SdJwtVc { + // NOTE: we use decodeSdJwtSync so we can make this method sync + const { jwt, disclosures } = decodeSdJwtSync(compactSdJwtVc, this.hasher) + const prettyClaims = getClaimsSync(jwt.payload, disclosures, this.hasher) + + return { + compact: compactSdJwtVc, + header: jwt.header as Header, + payload: jwt.payload as Payload, + prettyClaims: prettyClaims as Payload, + } + } + + public async present( + agentContext: AgentContext, + { compactSdJwtVc, presentationFrame, verifierMetadata }: SdJwtVcPresentOptions + ): Promise { + const sdjwt = new SDJwtVcInstance(this.getBaseSdJwtConfig(agentContext)) + + const sdJwtVc = await sdjwt.decode(compactSdJwtVc) + + const holderBinding = this.parseHolderBindingFromCredential(sdJwtVc) + if (!holderBinding && verifierMetadata) { + throw new SdJwtVcError("Verifier metadata provided, but credential has no 'cnf' claim to create a KB-JWT from") + } + + const holder = holderBinding ? await this.extractKeyFromHolderBinding(agentContext, holderBinding) : undefined + sdjwt.config({ + kbSigner: holder ? this.signer(agentContext, holder.key) : undefined, + kbSignAlg: holder?.alg, + }) + + const compactDerivedSdJwtVc = await sdjwt.present(compactSdJwtVc, presentationFrame as PresentationFrame, { + kb: verifierMetadata + ? { + payload: { + iat: verifierMetadata.issuedAt, + nonce: verifierMetadata.nonce, + aud: verifierMetadata.audience, + }, + } + : undefined, + }) + + return compactDerivedSdJwtVc + } + + public async verify
( + agentContext: AgentContext, + { compactSdJwtVc, keyBinding, requiredClaimKeys }: SdJwtVcVerifyOptions + ): Promise< + | { isValid: true; verification: VerificationResult; sdJwtVc: SdJwtVc } + | { isValid: false; verification: VerificationResult; sdJwtVc?: SdJwtVc; error: Error } + > { + const sdjwt = new SDJwtVcInstance(this.getBaseSdJwtConfig(agentContext)) + + const verificationResult: VerificationResult = { + isValid: false, + } + + let sdJwtVc: SDJwt + + try { + sdJwtVc = await sdjwt.decode(compactSdJwtVc) + if (!sdJwtVc.jwt) throw new CredoError('Invalid sd-jwt-vc') + } catch (error) { + return { + isValid: false, + verification: verificationResult, + error, + } + } + + const returnSdJwtVc: SdJwtVc = { + payload: sdJwtVc.jwt.payload as Payload, + header: sdJwtVc.jwt.header as Header, + compact: compactSdJwtVc, + prettyClaims: await sdJwtVc.getClaims(this.hasher), + } satisfies SdJwtVc + + try { + const issuer = await this.extractKeyFromIssuer(agentContext, this.parseIssuerFromCredential(sdJwtVc)) + const holderBinding = this.parseHolderBindingFromCredential(sdJwtVc) + const holder = holderBinding ? await this.extractKeyFromHolderBinding(agentContext, holderBinding) : undefined + + sdjwt.config({ + verifier: this.verifier(agentContext, issuer.key), + kbVerifier: holder ? this.verifier(agentContext, holder.key) : undefined, + }) + + const requiredKeys = requiredClaimKeys ? [...requiredClaimKeys, 'vct'] : ['vct'] + + try { + await sdjwt.verify(compactSdJwtVc, requiredKeys, keyBinding !== undefined) + + verificationResult.isSignatureValid = true + verificationResult.areRequiredClaimsIncluded = true + verificationResult.isStatusValid = true + } catch (error) { + return { + verification: verificationResult, + error, + isValid: false, + sdJwtVc: returnSdJwtVc, + } + } + + try { + JwtPayload.fromJson(returnSdJwtVc.payload).validate() + verificationResult.isValidJwtPayload = true + } catch (error) { + verificationResult.isValidJwtPayload = false + + return { + isValid: false, + error, + verification: verificationResult, + sdJwtVc: returnSdJwtVc, + } + } + + // If keyBinding is present, verify the key binding + try { + if (keyBinding) { + if (!sdJwtVc.kbJwt || !sdJwtVc.kbJwt.payload) { + throw new SdJwtVcError('Keybinding is required for verification of the sd-jwt-vc') + } + + // Assert `aud` and `nonce` claims + if (sdJwtVc.kbJwt.payload.aud !== keyBinding.audience) { + throw new SdJwtVcError('The key binding JWT does not contain the expected audience') + } + + if (sdJwtVc.kbJwt.payload.nonce !== keyBinding.nonce) { + throw new SdJwtVcError('The key binding JWT does not contain the expected nonce') + } + + verificationResult.isKeyBindingValid = true + verificationResult.containsExpectedKeyBinding = true + verificationResult.containsRequiredVcProperties = true + } + } catch (error) { + verificationResult.isKeyBindingValid = false + verificationResult.containsExpectedKeyBinding = false + verificationResult.isValid = false + + return { + isValid: false, + error, + verification: verificationResult, + sdJwtVc: returnSdJwtVc, + } + } + } catch (error) { + verificationResult.isValid = false + return { + isValid: false, + error, + verification: verificationResult, + sdJwtVc: returnSdJwtVc, + } + } + + verificationResult.isValid = true + return { + isValid: true, + verification: verificationResult, + sdJwtVc: returnSdJwtVc, + } + } + + public async store(agentContext: AgentContext, compactSdJwtVc: string) { + const sdJwtVcRecord = new SdJwtVcRecord({ + compactSdJwtVc, + }) + await this.sdJwtVcRepository.save(agentContext, sdJwtVcRecord) + + return sdJwtVcRecord + } + + public async getById(agentContext: AgentContext, id: string): Promise { + return await this.sdJwtVcRepository.getById(agentContext, id) + } + + public async getAll(agentContext: AgentContext): Promise> { + return await this.sdJwtVcRepository.getAll(agentContext) + } + + public async findByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ): Promise> { + return await this.sdJwtVcRepository.findByQuery(agentContext, query, queryOptions) + } + + public async deleteById(agentContext: AgentContext, id: string) { + await this.sdJwtVcRepository.deleteById(agentContext, id) + } + + public async update(agentContext: AgentContext, sdJwtVcRecord: SdJwtVcRecord) { + await this.sdJwtVcRepository.update(agentContext, sdJwtVcRecord) + } + + private async resolveDidUrl(agentContext: AgentContext, didUrl: string) { + const didResolver = agentContext.dependencyManager.resolve(DidResolverService) + const didDocument = await didResolver.resolveDidDocument(agentContext, didUrl) + + return { + verificationMethod: didDocument.dereferenceKey(didUrl, ['assertionMethod']), + didDocument, + } + } + + /** + * @todo validate the JWT header (alg) + */ + private signer(agentContext: AgentContext, key: Key): Signer { + return async (input: string) => { + const signedBuffer = await agentContext.wallet.sign({ key, data: TypedArrayEncoder.fromString(input) }) + return uint8ArrayToBase64Url(signedBuffer) + } + } + + /** + * @todo validate the JWT header (alg) + */ + private verifier(agentContext: AgentContext, key: Key): Verifier { + return async (message: string, signatureBase64Url: string) => { + if (!key) { + throw new SdJwtVcError('The public key used to verify the signature is missing') + } + + return await agentContext.wallet.verify({ + signature: TypedArrayEncoder.fromBase64(signatureBase64Url), + key, + data: TypedArrayEncoder.fromString(message), + }) + } + } + + private async extractKeyFromIssuer(agentContext: AgentContext, issuer: SdJwtVcIssuer) { + if (issuer.method === 'did') { + const parsedDid = parseDid(issuer.didUrl) + if (!parsedDid.fragment) { + throw new SdJwtVcError( + `didUrl '${issuer.didUrl}' does not contain a '#'. Unable to derive key from did document` + ) + } + + const { verificationMethod } = await this.resolveDidUrl(agentContext, issuer.didUrl) + const key = getKeyFromVerificationMethod(verificationMethod) + const alg = getJwkFromKey(key).supportedSignatureAlgorithms[0] + + return { + alg, + key, + iss: parsedDid.did, + kid: `#${parsedDid.fragment}`, + } + } + + throw new SdJwtVcError("Unsupported credential issuer. Only 'did' is supported at the moment.") + } + + private parseIssuerFromCredential
( + sdJwtVc: SDJwt + ): SdJwtVcIssuer { + if (!sdJwtVc.jwt?.payload) { + throw new SdJwtVcError('Credential not exist') + } + + if (!sdJwtVc.jwt?.payload['iss']) { + throw new SdJwtVcError('Credential does not contain an issuer') + } + + const iss = sdJwtVc.jwt.payload['iss'] as string + + if (iss.startsWith('did:')) { + // If `did` is used, we require a relative KID to be present to identify + // the key used by issuer to sign the sd-jwt-vc + + if (!sdJwtVc.jwt?.header) { + throw new SdJwtVcError('Credential does not contain a header') + } + + if (!sdJwtVc.jwt.header['kid']) { + throw new SdJwtVcError('Credential does not contain a kid in the header') + } + + const issuerKid = sdJwtVc.jwt.header['kid'] as string + + let didUrl: string + if (issuerKid.startsWith('#')) { + didUrl = `${iss}${issuerKid}` + } else if (issuerKid.startsWith('did:')) { + const didFromKid = parseDid(issuerKid) + if (didFromKid.did !== iss) { + throw new SdJwtVcError( + `kid in header is an absolute DID URL, but the did (${didFromKid.did}) does not match with the 'iss' did (${iss})` + ) + } + + didUrl = issuerKid + } else { + throw new SdJwtVcError( + 'Invalid issuer kid for did. Only absolute or relative (starting with #) did urls are supported.' + ) + } + + return { + method: 'did', + didUrl, + } + } + throw new SdJwtVcError("Unsupported 'iss' value. Only did is supported at the moment.") + } + + private parseHolderBindingFromCredential
( + sdJwtVc: SDJwt + ): SdJwtVcHolderBinding | null { + if (!sdJwtVc.jwt?.payload) { + throw new SdJwtVcError('Credential not exist') + } + + if (!sdJwtVc.jwt?.payload['cnf']) { + return null + } + const cnf: CnfPayload = sdJwtVc.jwt.payload['cnf'] + + if (cnf.jwk) { + return { + method: 'jwk', + jwk: cnf.jwk, + } + } else if (cnf.kid) { + if (!cnf.kid.startsWith('did:') || !cnf.kid.includes('#')) { + throw new SdJwtVcError('Invalid holder kid for did. Only absolute KIDs for cnf are supported') + } + return { + method: 'did', + didUrl: cnf.kid, + } + } + + throw new SdJwtVcError("Unsupported credential holder binding. Only 'did' and 'jwk' are supported at the moment.") + } + + private async extractKeyFromHolderBinding(agentContext: AgentContext, holder: SdJwtVcHolderBinding) { + if (holder.method === 'did') { + const parsedDid = parseDid(holder.didUrl) + if (!parsedDid.fragment) { + throw new SdJwtVcError( + `didUrl '${holder.didUrl}' does not contain a '#'. Unable to derive key from did document` + ) + } + + const { verificationMethod } = await this.resolveDidUrl(agentContext, holder.didUrl) + const key = getKeyFromVerificationMethod(verificationMethod) + const alg = getJwkFromKey(key).supportedSignatureAlgorithms[0] + + return { + alg, + key, + cnf: { + // We need to include the whole didUrl here, otherwise the verifier + // won't know which did it is associated with + kid: holder.didUrl, + }, + } + } else if (holder.method === 'jwk') { + const jwk = holder.jwk instanceof Jwk ? holder.jwk : getJwkFromJson(holder.jwk) + const key = jwk.key + const alg = jwk.supportedSignatureAlgorithms[0] + + return { + alg, + key, + cnf: { + jwk: jwk.toJson(), + }, + } + } + + throw new SdJwtVcError("Unsupported credential holder binding. Only 'did' and 'jwk' are supported at the moment.") + } + + private getBaseSdJwtConfig(agentContext: AgentContext): SdJwtVcConfig { + return { + hasher: this.hasher, + statusListFetcher: this.getStatusListFetcher(agentContext), + saltGenerator: agentContext.wallet.generateNonce, + } + } + + private get hasher(): HasherSync { + return Hasher.hash + } + + private getStatusListFetcher(agentContext: AgentContext) { + return async (uri: string) => { + const response = await fetchWithTimeout(agentContext.config.agentDependencies.fetch, uri) + if (!response.ok) { + throw new CredoError( + `Received invalid response with status ${ + response.status + } when fetching status list from ${uri}. ${await response.text()}` + ) + } + + return await response.text() + } + } +} diff --git a/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcModule.test.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcModule.test.ts new file mode 100644 index 0000000000..7ceb1a2873 --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcModule.test.ts @@ -0,0 +1,23 @@ +import type { DependencyManager } from '@credo-ts/core' + +import { SdJwtVcModule } from '../SdJwtVcModule' +import { SdJwtVcService } from '../SdJwtVcService' +import { SdJwtVcRepository } from '../repository' + +const dependencyManager = { + registerInstance: jest.fn(), + registerSingleton: jest.fn(), + registerContextScoped: jest.fn(), + resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), +} as unknown as DependencyManager + +describe('SdJwtVcModule', () => { + test('registers dependencies on the dependency manager', () => { + const sdJwtVcModule = new SdJwtVcModule() + sdJwtVcModule.register(dependencyManager) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(SdJwtVcService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(SdJwtVcRepository) + }) +}) diff --git a/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts new file mode 100644 index 0000000000..08b392123f --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts @@ -0,0 +1,1050 @@ +import type { SdJwtVcHeader } from '../SdJwtVcOptions' +import type { AgentContext, Jwk, Key } from '@credo-ts/core' + +import { createHeaderAndPayload, StatusList } from '@sd-jwt/jwt-status-list' +import { SDJWTException } from '@sd-jwt/utils' +import { randomUUID } from 'crypto' + +import { agentDependencies, getInMemoryAgentOptions } from '../../../../tests' +import * as fetchUtils from '../../../utils/fetch' +import { SdJwtVcService } from '../SdJwtVcService' +import { SdJwtVcRepository } from '../repository' + +import { + complexSdJwtVc, + complexSdJwtVcPresentation, + contentChangedSdJwtVc, + expiredSdJwtVc, + notBeforeInFutureSdJwtVc, + sdJwtVcWithSingleDisclosure, + sdJwtVcWithSingleDisclosurePresentation, + signatureInvalidSdJwtVc, + simpleJwtVc, + simpleJwtVcPresentation, + simpleJwtVcWithoutHolderBinding, + simpleSdJwtVcWithStatus, +} from './sdjwtvc.fixtures' + +import { + CredoError, + Agent, + DidKey, + DidsModule, + getJwkFromKey, + JwsService, + JwtPayload, + KeyDidRegistrar, + KeyDidResolver, + KeyType, + parseDid, + TypedArrayEncoder, +} from '@credo-ts/core' + +const jwkJsonWithoutUse = (jwk: Jwk) => { + const jwkJson = jwk.toJson() + delete jwkJson.use + return jwkJson +} + +const agent = new Agent( + getInMemoryAgentOptions( + 'sdjwtvcserviceagent', + {}, + { + dids: new DidsModule({ + resolvers: [new KeyDidResolver()], + registrars: [new KeyDidRegistrar()], + }), + } + ) +) + +agent.context.wallet.generateNonce = jest.fn(() => Promise.resolve('salt')) +Date.prototype.getTime = jest.fn(() => 1698151532000) + +jest.mock('../repository/SdJwtVcRepository') +const SdJwtVcRepositoryMock = SdJwtVcRepository as jest.Mock + +const generateStatusList = async ( + agentContext: AgentContext, + key: Key, + issuerDidUrl: string, + length: number, + revokedIndexes: number[] +): Promise => { + const statusList = new StatusList( + Array.from({ length }, (_, i) => (revokedIndexes.includes(i) ? 1 : 0)), + 1 + ) + + const [did, keyId] = issuerDidUrl.split('#') + const { header, payload } = createHeaderAndPayload( + statusList, + { + iss: did, + sub: 'https://example.com/status/1', + iat: new Date().getTime() / 1000, + }, + { + alg: 'EdDSA', + typ: 'statuslist+jwt', + kid: `#${keyId}`, + } + ) + + const jwsService = agentContext.dependencyManager.resolve(JwsService) + return jwsService.createJwsCompact(agentContext, { + key, + payload: JwtPayload.fromJson(payload), + protectedHeaderOptions: header, + }) +} + +describe('SdJwtVcService', () => { + const verifierDid = 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y' + let issuerDidUrl: string + let issuerKey: Key + let holderKey: Key + let sdJwtVcService: SdJwtVcService + + beforeAll(async () => { + await agent.initialize() + + issuerKey = await agent.context.wallet.createKey({ + keyType: KeyType.Ed25519, + seed: TypedArrayEncoder.fromString('00000000000000000000000000000000'), + }) + + const issuerDidKey = new DidKey(issuerKey) + const issuerDidDocument = issuerDidKey.didDocument + issuerDidUrl = (issuerDidDocument.verificationMethod ?? [])[0].id + await agent.dids.import({ didDocument: issuerDidDocument, did: issuerDidDocument.id }) + + holderKey = await agent.context.wallet.createKey({ + keyType: KeyType.Ed25519, + seed: TypedArrayEncoder.fromString('00000000000000000000000000000001'), + }) + + const holderDidKey = new DidKey(holderKey) + const holderDidDocument = holderDidKey.didDocument + await agent.dids.import({ didDocument: holderDidDocument, did: holderDidDocument.id }) + + const sdJwtVcRepositoryMock = new SdJwtVcRepositoryMock() + sdJwtVcService = new SdJwtVcService(sdJwtVcRepositoryMock) + }) + + describe('SdJwtVcService.sign', () => { + test('Sign sd-jwt-vc from a basic payload without disclosures', async () => { + const { compact } = await sdJwtVcService.sign(agent.context, { + payload: { + claim: 'some-claim', + vct: 'IdentityCredential', + }, + holder: { + // FIXME: is it nicer API to just pass either didUrl or JWK? + // Or none if you don't want to bind it? + method: 'jwk', + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + issuer: { + method: 'did', + didUrl: issuerDidUrl, + }, + }) + + expect(compact).toStrictEqual(simpleJwtVc) + + const sdJwtVc = sdJwtVcService.fromCompact(compact) + + expect(sdJwtVc.header).toEqual({ + alg: 'EdDSA', + typ: 'vc+sd-jwt', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + }) + + expect(sdJwtVc.prettyClaims).toEqual({ + claim: 'some-claim', + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + iss: parseDid(issuerDidUrl).did, + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + }) + }) + + test('Sign sd-jwt-vc from a basic payload without holder binding', async () => { + const { compact } = await sdJwtVcService.sign(agent.context, { + payload: { + claim: 'some-claim', + vct: 'IdentityCredential', + }, + issuer: { + method: 'did', + didUrl: issuerDidUrl, + }, + }) + + expect(compact).toStrictEqual(simpleJwtVcWithoutHolderBinding) + + const sdJwtVc = sdJwtVcService.fromCompact(compact) + + expect(sdJwtVc.header).toEqual({ + alg: 'EdDSA', + typ: 'vc+sd-jwt', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + }) + + expect(sdJwtVc.prettyClaims).toEqual({ + claim: 'some-claim', + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + iss: parseDid(issuerDidUrl).did, + }) + }) + + test('Sign sd-jwt-vc from a basic payload including false boolean values', async () => { + const { compact } = await sdJwtVcService.sign(agent.context, { + payload: { + claim: 'some-claim', + vct: 'IdentityCredential', + value: false, + discloseableValue: false, + }, + holder: { + // FIXME: is it nicer API to just pass either didUrl or JWK? + // Or none if you don't want to bind it? + method: 'jwk', + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + issuer: { + method: 'did', + didUrl: issuerDidUrl, + }, + }) + + const sdJwtVc = sdJwtVcService.fromCompact(compact) + + expect(sdJwtVc.header).toEqual({ + alg: 'EdDSA', + typ: 'vc+sd-jwt', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + }) + + expect(sdJwtVc.prettyClaims).toEqual({ + claim: 'some-claim', + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + iss: parseDid(issuerDidUrl).did, + value: false, + discloseableValue: false, + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + }) + }) + + test('Create sd-jwt-vc from a basic payload with a disclosure', async () => { + const { compact, header, prettyClaims, payload } = await sdJwtVcService.sign(agent.context, { + payload: { claim: 'some-claim', vct: 'IdentityCredential' }, + disclosureFrame: { _sd: ['claim'] }, + holder: { + method: 'jwk', + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + issuer: { + method: 'did', + didUrl: issuerDidUrl, + }, + }) + + expect(compact).toStrictEqual(sdJwtVcWithSingleDisclosure) + + expect(header).toEqual({ + alg: 'EdDSA', + typ: 'vc+sd-jwt', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + }) + + expect(payload).toEqual({ + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + iss: issuerDidUrl.split('#')[0], + _sd: ['vcvFU4DsFKTqQ1vl4nelJWXTb_-0dNoBks6iqNFptyg'], + _sd_alg: 'sha-256', + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + }) + + expect(prettyClaims).toEqual({ + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + iss: issuerDidUrl.split('#')[0], + claim: 'some-claim', + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + }) + }) + + test('Create sd-jwt-vc from a basic payload with multiple (nested) disclosure', async () => { + const { compact, header, payload, prettyClaims } = await sdJwtVcService.sign(agent.context, { + disclosureFrame: { + _sd: ['is_over_65', 'is_over_21', 'is_over_18', 'birthdate', 'email', 'given_name'], + address: { + _sd: ['region', 'country'], + }, + }, + payload: { + vct: 'IdentityCredential', + given_name: 'John', + family_name: 'Doe', + email: 'johndoe@example.com', + phone_number: '+1-202-555-0101', + address: { + street_address: '123 Main St', + locality: 'Anytown', + region: 'Anystate', + country: 'US', + }, + birthdate: '1940-01-01', + is_over_18: true, + is_over_21: true, + is_over_65: true, + }, + holder: { + method: 'jwk', + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + issuer: { + method: 'did', + didUrl: issuerDidUrl, + }, + }) + + expect(compact).toStrictEqual(complexSdJwtVc) + + expect(header).toEqual({ + alg: 'EdDSA', + typ: 'vc+sd-jwt', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + }) + + expect(payload).toEqual({ + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + address: { + _sd: ['NJnmct0BqBME1JfBlC6jRQVRuevpEONiYw7A7MHuJyQ', 'om5ZztZHB-Gd00LG21CV_xM4FaENSoiaOXnTAJNczB4'], + locality: 'Anytown', + street_address: '123 Main St', + }, + phone_number: '+1-202-555-0101', + family_name: 'Doe', + iss: issuerDidUrl.split('#')[0], + _sd: [ + '1Cur2k2A2oIB5CshSIf_A_Kg-l26u_qKuWQ79P0Vdas', + 'R1zTUvOYHgcepj0jHypGHz9EHttVKft0yswbc9ETPbU', + 'eDqQpdTXJXbWhf-EsI7zw5X6OvYmFN-UZQQMesXwKPw', + 'pdDk2_XAKHo7gOAfwF1b7OdCUVTit2kJHaxSECQ9xfc', + 'psauKUNWEi09nu3Cl89xKXgmpWENZl5uy1N1nyn_jMk', + 'sN_ge0pHXF6qmsYnX1A9SdwJ8ch8aENkxbODsT74YwI', + ], + _sd_alg: 'sha-256', + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + }) + + expect(prettyClaims).toEqual({ + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + address: { + region: 'Anystate', + country: 'US', + locality: 'Anytown', + street_address: '123 Main St', + }, + email: 'johndoe@example.com', + given_name: 'John', + phone_number: '+1-202-555-0101', + family_name: 'Doe', + iss: issuerDidUrl.split('#')[0], + birthdate: '1940-01-01', + is_over_18: true, + is_over_21: true, + is_over_65: true, + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + }) + }) + + test('Create sd-jwt-vc from a basic payload with multiple (nested) disclosure where a disclosure contains other disclosures', async () => { + const { header, payload, prettyClaims } = await sdJwtVcService.sign(agent.context, { + disclosureFrame: { + _sd: ['is_over_65', 'is_over_21', 'is_over_18', 'birthdate', 'email', 'given_name', 'address'], + address: { + _sd: ['region', 'country'], + }, + }, + payload: { + vct: 'IdentityCredential', + given_name: 'John', + family_name: 'Doe', + email: 'johndoe@example.com', + phone_number: '+1-202-555-0101', + address: { + street_address: '123 Main St', + locality: 'Anytown', + region: 'Anystate', + country: 'US', + }, + birthdate: '1940-01-01', + is_over_18: true, + is_over_21: true, + is_over_65: true, + }, + holder: { + method: 'jwk', + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + issuer: { + method: 'did', + didUrl: issuerDidUrl, + }, + }) + + expect(header).toEqual({ + alg: 'EdDSA', + typ: 'vc+sd-jwt', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + }) + + expect(payload).toEqual({ + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + phone_number: '+1-202-555-0101', + family_name: 'Doe', + iss: issuerDidUrl.split('#')[0], + _sd: [ + '1Cur2k2A2oIB5CshSIf_A_Kg-l26u_qKuWQ79P0Vdas', + 'R1zTUvOYHgcepj0jHypGHz9EHttVKft0yswbc9ETPbU', + 'eDqQpdTXJXbWhf-EsI7zw5X6OvYmFN-UZQQMesXwKPw', + 'pdDk2_XAKHo7gOAfwF1b7OdCUVTit2kJHaxSECQ9xfc', + 'psauKUNWEi09nu3Cl89xKXgmpWENZl5uy1N1nyn_jMk', + 'sN_ge0pHXF6qmsYnX1A9SdwJ8ch8aENkxbODsT74YwI', + 'yPhxDEM7k7p7eQ9eHHC-Ca6VEA8bzebZpYu7vYmwG6c', + ], + _sd_alg: 'sha-256', + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + }) + + expect(prettyClaims).toEqual({ + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + address: { + region: 'Anystate', + country: 'US', + locality: 'Anytown', + street_address: '123 Main St', + }, + email: 'johndoe@example.com', + given_name: 'John', + phone_number: '+1-202-555-0101', + family_name: 'Doe', + iss: issuerDidUrl.split('#')[0], + birthdate: '1940-01-01', + is_over_18: true, + is_over_21: true, + is_over_65: true, + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + }) + }) + }) + + describe('SdJwtVcService.receive', () => { + test('Receive sd-jwt-vc from a basic payload without disclosures', async () => { + const sdJwtVc = sdJwtVcService.fromCompact(simpleJwtVc) + const sdJwtVcRecord = await sdJwtVcService.store(agent.context, sdJwtVc.compact) + expect(sdJwtVcRecord.compactSdJwtVc).toEqual(simpleJwtVc) + + expect(sdJwtVc.header).toEqual({ + alg: 'EdDSA', + typ: 'vc+sd-jwt', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + }) + + expect(sdJwtVc.payload).toEqual({ + claim: 'some-claim', + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + iss: issuerDidUrl.split('#')[0], + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + }) + }) + + test('Receive sd-jwt-vc without holder binding', async () => { + const sdJwtVc = sdJwtVcService.fromCompact(simpleJwtVcWithoutHolderBinding) + const sdJwtVcRecord = await sdJwtVcService.store(agent.context, simpleJwtVcWithoutHolderBinding) + expect(sdJwtVcRecord.compactSdJwtVc).toEqual(simpleJwtVcWithoutHolderBinding) + + expect(sdJwtVc.header).toEqual({ + alg: 'EdDSA', + typ: 'vc+sd-jwt', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + }) + + expect(sdJwtVc.payload).toEqual({ + claim: 'some-claim', + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + iss: issuerDidUrl.split('#')[0], + }) + }) + + test('Receive sd-jwt-vc from a basic payload with a disclosure', async () => { + const sdJwtVc = sdJwtVcService.fromCompact(sdJwtVcWithSingleDisclosure) + + expect(sdJwtVc.header).toEqual({ + alg: 'EdDSA', + typ: 'vc+sd-jwt', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + }) + + expect(sdJwtVc.payload).toEqual({ + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + iss: issuerDidUrl.split('#')[0], + _sd: ['vcvFU4DsFKTqQ1vl4nelJWXTb_-0dNoBks6iqNFptyg'], + _sd_alg: 'sha-256', + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + }) + + expect(sdJwtVc.payload).not.toContain({ + claim: 'some-claim', + }) + }) + + test('Receive sd-jwt-vc from a basic payload with multiple (nested) disclosure', async () => { + const sdJwtVc = sdJwtVcService.fromCompact(complexSdJwtVc) + + expect(sdJwtVc.header).toEqual({ + alg: 'EdDSA', + typ: 'vc+sd-jwt', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + }) + + expect(sdJwtVc.payload).toEqual({ + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + family_name: 'Doe', + iss: issuerDidUrl.split('#')[0], + address: { + _sd: ['NJnmct0BqBME1JfBlC6jRQVRuevpEONiYw7A7MHuJyQ', 'om5ZztZHB-Gd00LG21CV_xM4FaENSoiaOXnTAJNczB4'], + locality: 'Anytown', + street_address: '123 Main St', + }, + _sd_alg: 'sha-256', + phone_number: '+1-202-555-0101', + _sd: [ + '1Cur2k2A2oIB5CshSIf_A_Kg-l26u_qKuWQ79P0Vdas', + 'R1zTUvOYHgcepj0jHypGHz9EHttVKft0yswbc9ETPbU', + 'eDqQpdTXJXbWhf-EsI7zw5X6OvYmFN-UZQQMesXwKPw', + 'pdDk2_XAKHo7gOAfwF1b7OdCUVTit2kJHaxSECQ9xfc', + 'psauKUNWEi09nu3Cl89xKXgmpWENZl5uy1N1nyn_jMk', + 'sN_ge0pHXF6qmsYnX1A9SdwJ8ch8aENkxbODsT74YwI', + ], + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + }) + + expect(sdJwtVc.payload).not.toContain({ + address: { + region: 'Anystate', + country: 'US', + }, + family_name: 'Doe', + phone_number: '+1-202-555-0101', + email: 'johndoe@example.com', + given_name: 'John', + birthdate: '1940-01-01', + is_over_18: true, + is_over_21: true, + is_over_65: true, + }) + + expect(sdJwtVc.prettyClaims).toEqual({ + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + family_name: 'Doe', + iss: issuerDidUrl.split('#')[0], + phone_number: '+1-202-555-0101', + email: 'johndoe@example.com', + given_name: 'John', + birthdate: '1940-01-01', + is_over_18: true, + is_over_21: true, + is_over_65: true, + address: { + region: 'Anystate', + country: 'US', + locality: 'Anytown', + street_address: '123 Main St', + }, + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + }) + }) + }) + + describe('SdJwtVcService.present', () => { + test('Present sd-jwt-vc from a basic payload without disclosures', async () => { + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: simpleJwtVc, + presentationFrame: {}, + verifierMetadata: { + issuedAt: new Date().getTime() / 1000, + audience: verifierDid, + nonce: await agent.context.wallet.generateNonce(), + }, + }) + + expect(presentation).toStrictEqual(simpleJwtVcPresentation) + }) + + test('Present sd-jwt-vc without holder binding', async () => { + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: simpleJwtVcWithoutHolderBinding, + presentationFrame: {}, + }) + + // Input should be the same as output + expect(presentation).toStrictEqual(simpleJwtVcWithoutHolderBinding) + }) + + test('Errors when providing verifier metadata but SD-JWT VC has no cnf claim', async () => { + await expect( + sdJwtVcService.present(agent.context, { + compactSdJwtVc: simpleJwtVcWithoutHolderBinding, + presentationFrame: {}, + verifierMetadata: { + audience: 'verifier', + issuedAt: Date.now() / 1000, + nonce: randomUUID(), + }, + }) + ).rejects.toThrow("Verifier metadata provided, but credential has no 'cnf' claim to create a KB-JWT from") + }) + + test('Present sd-jwt-vc from a basic payload with a disclosure', async () => { + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: sdJwtVcWithSingleDisclosure, + presentationFrame: { claim: true }, + verifierMetadata: { + issuedAt: new Date().getTime() / 1000, + audience: verifierDid, + nonce: await agent.context.wallet.generateNonce(), + }, + }) + + expect(presentation).toStrictEqual(sdJwtVcWithSingleDisclosurePresentation) + }) + + test('Present sd-jwt-vc from a basic payload with multiple (nested) disclosure', async () => { + const presentation = await sdJwtVcService.present<{ + is_over_65: boolean + is_over_21: boolean + email: boolean + address: { country: string } + given_name: boolean + }>(agent.context, { + compactSdJwtVc: complexSdJwtVc, + verifierMetadata: { + issuedAt: new Date().getTime() / 1000, + audience: verifierDid, + nonce: await agent.context.wallet.generateNonce(), + }, + presentationFrame: { + is_over_65: true, + is_over_21: true, + email: true, + address: { + country: true, + }, + given_name: true, + }, + }) + + expect(presentation).toStrictEqual(complexSdJwtVcPresentation) + }) + }) + + describe('SdJwtVcService.verify', () => { + test('Verify sd-jwt-vc without disclosures', async () => { + const nonce = await agent.context.wallet.generateNonce() + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: simpleJwtVc, + // no disclosures + presentationFrame: {}, + verifierMetadata: { + issuedAt: new Date().getTime() / 1000, + audience: verifierDid, + nonce, + }, + }) + + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: presentation, + keyBinding: { audience: verifierDid, nonce }, + requiredClaimKeys: ['claim'], + }) + + expect(verificationResult).toEqual({ + isValid: true, + sdJwtVc: expect.any(Object), + verification: { + isSignatureValid: true, + containsRequiredVcProperties: true, + containsExpectedKeyBinding: true, + areRequiredClaimsIncluded: true, + isValid: true, + isValidJwtPayload: true, + isStatusValid: true, + isKeyBindingValid: true, + }, + }) + }) + + test('Verify sd-jwt-vc without holder binding', async () => { + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: simpleJwtVcWithoutHolderBinding, + // no disclosures + presentationFrame: {}, + }) + + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: presentation, + requiredClaimKeys: ['claim'], + }) + + expect(verificationResult).toEqual({ + isValid: true, + sdJwtVc: expect.any(Object), + verification: { + isSignatureValid: true, + areRequiredClaimsIncluded: true, + isValid: true, + isValidJwtPayload: true, + isStatusValid: true, + }, + }) + }) + + test('Verify sd-jwt-vc with status where credential is not revoked', async () => { + const sdJwtVcService = agent.dependencyManager.resolve(SdJwtVcService) + + // Mock call to status list + const fetchSpy = jest.spyOn(fetchUtils, 'fetchWithTimeout') + + // First time not revoked + fetchSpy.mockResolvedValueOnce({ + ok: true, + status: 200, + text: () => generateStatusList(agent.context, issuerKey, issuerDidUrl, 24, []), + } satisfies Partial as Response) + + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: simpleSdJwtVcWithStatus, + presentationFrame: {}, + }) + + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: presentation, + }) + expect(fetchUtils.fetchWithTimeout).toHaveBeenCalledWith( + agentDependencies.fetch, + 'https://example.com/status-list' + ) + + expect(verificationResult).toEqual({ + isValid: true, + sdJwtVc: expect.any(Object), + verification: { + isSignatureValid: true, + isValid: true, + isValidJwtPayload: true, + isStatusValid: true, + areRequiredClaimsIncluded: true, + }, + }) + }) + + test('Verify sd-jwt-vc with status where credential is revoked and fails', async () => { + const sdJwtVcService = agent.dependencyManager.resolve(SdJwtVcService) + + // Mock call to status list + const fetchSpy = jest.spyOn(fetchUtils, 'fetchWithTimeout') + + // First time not revoked + fetchSpy.mockResolvedValueOnce({ + ok: true, + status: 200, + text: () => generateStatusList(agent.context, issuerKey, issuerDidUrl, 24, [12]), + } satisfies Partial as Response) + + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: simpleSdJwtVcWithStatus, + presentationFrame: {}, + }) + + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: presentation, + }) + expect(fetchUtils.fetchWithTimeout).toHaveBeenCalledWith( + agentDependencies.fetch, + 'https://example.com/status-list' + ) + + expect(verificationResult).toEqual({ + isValid: false, + sdJwtVc: expect.any(Object), + verification: { + isValid: false, + }, + error: new SDJWTException('Status is not valid'), + }) + }) + + test('Verify sd-jwt-vc with status where status list is not valid and fails', async () => { + const sdJwtVcService = agent.dependencyManager.resolve(SdJwtVcService) + + // Mock call to status list + const fetchSpy = jest.spyOn(fetchUtils, 'fetchWithTimeout') + + // First time not revoked + fetchSpy.mockResolvedValueOnce({ + ok: true, + status: 200, + text: () => generateStatusList(agent.context, issuerKey, issuerDidUrl, 8, []), + } satisfies Partial as Response) + + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: simpleSdJwtVcWithStatus, + presentationFrame: {}, + }) + + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: presentation, + }) + expect(fetchUtils.fetchWithTimeout).toHaveBeenCalledWith( + agentDependencies.fetch, + 'https://example.com/status-list' + ) + + expect(verificationResult).toEqual({ + isValid: false, + sdJwtVc: expect.any(Object), + verification: { + isValid: false, + }, + error: new Error('Index out of bounds'), + }) + }) + + test('Verify sd-jwt-vc with a disclosure', async () => { + const nonce = await agent.context.wallet.generateNonce() + + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: sdJwtVcWithSingleDisclosure, + verifierMetadata: { + issuedAt: new Date().getTime() / 1000, + audience: verifierDid, + nonce, + }, + presentationFrame: { claim: true }, + }) + + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: presentation, + keyBinding: { audience: verifierDid, nonce }, + requiredClaimKeys: ['vct', 'cnf', 'claim', 'iat'], + }) + + expect(verificationResult).toEqual({ + isValid: true, + sdJwtVc: expect.any(Object), + verification: { + isSignatureValid: true, + containsRequiredVcProperties: true, + areRequiredClaimsIncluded: true, + isValid: true, + isValidJwtPayload: true, + isStatusValid: true, + isKeyBindingValid: true, + containsExpectedKeyBinding: true, + }, + }) + }) + + test('Verify sd-jwt-vc with multiple (nested) disclosure', async () => { + const nonce = await agent.context.wallet.generateNonce() + + const presentation = await sdJwtVcService.present<{ + is_over_65: boolean + is_over_21: boolean + email: boolean + address: { country: string } + given_name: boolean + }>(agent.context, { + compactSdJwtVc: complexSdJwtVc, + verifierMetadata: { + issuedAt: new Date().getTime() / 1000, + audience: verifierDid, + nonce, + }, + presentationFrame: { + is_over_65: true, + is_over_21: true, + email: true, + address: { + country: true, + }, + given_name: true, + }, + }) + + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: presentation, + keyBinding: { audience: verifierDid, nonce }, + // FIXME: this should be a requiredFrame to be consistent with the other methods + // using frames + requiredClaimKeys: [ + 'vct', + 'family_name', + 'phone_number', + 'address', + 'cnf', + 'iss', + 'iat', + 'is_over_65', + 'is_over_21', + 'email', + 'given_name', + 'address.street_address', + 'address.locality', + 'address.country', + ], + }) + + expect(verificationResult).toEqual({ + isValid: true, + sdJwtVc: expect.any(Object), + verification: { + isSignatureValid: true, + areRequiredClaimsIncluded: true, + containsExpectedKeyBinding: true, + containsRequiredVcProperties: true, + isValid: true, + isValidJwtPayload: true, + isStatusValid: true, + isKeyBindingValid: true, + }, + }) + }) + + test('Verify did holder-bound sd-jwt-vc with disclosures and kb-jwt', async () => { + const verificationResult = await sdJwtVcService.verify( + agent.context, + { + compactSdJwtVc: + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJPcGVuQmFkZ2VDcmVkZW50aWFsIiwiZGVncmVlIjoiYmFjaGVsb3IiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZCI6WyJLbE5PM0VfYjRmdUwyOUd2QXdwTGczTGZHZTlxdDdhakUxMzlfU1pIbWk4Il0sIl9zZF9hbGciOiJzaGEtMjU2In0.TBWIECIMmNKNqVtjwHARSnR0Ii9Fefy871sXEK-zfThbTBALdvXBTBQ6iKvvI-CxsniSH1hJMEJTu1vK7esTDg~WyJzYWx0IiwidW5pdmVyc2l0eSIsImlubnNicnVjayJd~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJzZF9oYXNoIjoiODlyX3JrSjdvb3RuSGJ3TXdjMW9sNzZncU03WU1zNVUzVnpkMHN6N3VkbyJ9.VkrxL06aP8t-G_lVtlAZNgJC2gouqR__rXDgJQPParq5OGxna3ZoQQbjv7e3I2TUaVaMV6xUpJY1KufZlPDwAg', + keyBinding: { + audience: 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y', + nonce: 'salt', + }, + } + ) + + expect(verificationResult.verification.isValid).toBe(true) + }) + + test('verify expired sd-jwt-vc and fails', async () => { + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: expiredSdJwtVc, + }) + + expect(verificationResult).toEqual({ + isValid: false, + verification: { + areRequiredClaimsIncluded: true, + isSignatureValid: true, + isStatusValid: true, + isValid: false, + isValidJwtPayload: false, + }, + error: new CredoError('JWT expired at 1716111919'), + sdJwtVc: expect.any(Object), + }) + }) + + test('verify sd-jwt-vc with nbf in future and fails', async () => { + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: notBeforeInFutureSdJwtVc, + }) + + expect(verificationResult).toEqual({ + isValid: false, + verification: { + areRequiredClaimsIncluded: true, + isSignatureValid: true, + isStatusValid: true, + isValid: false, + isValidJwtPayload: false, + }, + error: new CredoError('JWT not valid before 4078944000'), + sdJwtVc: expect.any(Object), + }) + }) + + test('verify sd-jwt-vc with content changed and fails', async () => { + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: contentChangedSdJwtVc, + }) + + expect(verificationResult).toEqual({ + isValid: false, + verification: { + isValid: false, + }, + error: new SDJWTException('Verify Error: Invalid JWT Signature'), + sdJwtVc: expect.any(Object), + }) + }) + + test('verify sd-jwt-vc with invalid signature and fails', async () => { + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: signatureInvalidSdJwtVc, + }) + + expect(verificationResult).toEqual({ + isValid: false, + verification: { + isValid: false, + }, + error: new SDJWTException('Verify Error: Invalid JWT Signature'), + sdJwtVc: expect.any(Object), + }) + }) + }) +}) diff --git a/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.test.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.test.ts new file mode 100644 index 0000000000..1df450b847 --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.test.ts @@ -0,0 +1,221 @@ +import type { Key } from '@credo-ts/core' + +import { getInMemoryAgentOptions } from '../../../../tests' + +import { Agent, DidKey, getJwkFromKey, KeyType, TypedArrayEncoder } from '@credo-ts/core' + +describe('sd-jwt-vc end to end test', () => { + const issuer = new Agent(getInMemoryAgentOptions('sd-jwt-vc-issuer-agent')) + let issuerKey: Key + let issuerDidUrl: string + + const holder = new Agent(getInMemoryAgentOptions('sd-jwt-vc-holder-agent')) + let holderKey: Key + + const verifier = new Agent(getInMemoryAgentOptions('sd-jwt-vc-verifier-agent')) + const verifierDid = 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y' + + beforeAll(async () => { + await issuer.initialize() + issuerKey = await issuer.context.wallet.createKey({ + keyType: KeyType.Ed25519, + seed: TypedArrayEncoder.fromString('00000000000000000000000000000000'), + }) + + const issuerDidKey = new DidKey(issuerKey) + const issuerDidDocument = issuerDidKey.didDocument + issuerDidUrl = (issuerDidDocument.verificationMethod ?? [])[0].id + await issuer.dids.import({ didDocument: issuerDidDocument, did: issuerDidDocument.id }) + + await holder.initialize() + holderKey = await holder.context.wallet.createKey({ + keyType: KeyType.Ed25519, + seed: TypedArrayEncoder.fromString('00000000000000000000000000000001'), + }) + + await verifier.initialize() + }) + + test('end to end flow', async () => { + const credential = { + vct: 'IdentityCredential', + given_name: 'John', + family_name: 'Doe', + email: 'johndoe@example.com', + phone_number: '+1-202-555-0101', + address: { + street_address: '123 Main St', + locality: 'Anytown', + region: 'Anystate', + country: 'US', + }, + birthdate: '1940-01-01', + is_over_18: true, + is_over_21: true, + is_over_65: true, + } as const + + const { compact, header, payload } = await issuer.sdJwtVc.sign({ + payload: credential, + holder: { + method: 'jwk', + jwk: getJwkFromKey(holderKey), + }, + issuer: { + didUrl: issuerDidUrl, + method: 'did', + }, + disclosureFrame: { + _sd: [ + 'is_over_65', + 'is_over_21', + 'is_over_18', + 'birthdate', + 'email', + 'given_name', + 'family_name', + 'phone_number', + ], + _sd_decoy: 2, + address: { + _sd: ['country', 'region', 'locality', 'street_address'], + _sd_decoy: 2, + }, + }, + }) + + type Payload = typeof payload + type Header = typeof header + + // parse SD-JWT + const sdJwtVc = holder.sdJwtVc.fromCompact(compact) + expect(sdJwtVc).toEqual({ + compact: expect.any(String), + header: { + alg: 'EdDSA', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + typ: 'vc+sd-jwt', + }, + payload: { + _sd: [ + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + ], + _sd_alg: 'sha-256', + address: { + _sd: [ + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + ], + }, + cnf: { + jwk: { + crv: 'Ed25519', + kty: 'OKP', + x: 'oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo', + }, + }, + iat: expect.any(Number), + iss: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + vct: 'IdentityCredential', + }, + prettyClaims: { + address: { + country: 'US', + locality: 'Anytown', + region: 'Anystate', + street_address: '123 Main St', + }, + birthdate: '1940-01-01', + cnf: { + jwk: { + crv: 'Ed25519', + kty: 'OKP', + x: 'oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo', + }, + }, + email: 'johndoe@example.com', + family_name: 'Doe', + given_name: 'John', + iat: expect.any(Number), + is_over_18: true, + is_over_21: true, + is_over_65: true, + iss: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + phone_number: '+1-202-555-0101', + vct: 'IdentityCredential', + }, + }) + + // Verify SD-JWT (does not require key binding) + const { verification } = await holder.sdJwtVc.verify({ + compactSdJwtVc: compact, + }) + expect(verification.isValid).toBe(true) + + // Store credential + await holder.sdJwtVc.store(compact) + + // Metadata created by the verifier and send out of band by the verifier to the holder + const verifierMetadata = { + audience: verifierDid, + issuedAt: new Date().getTime() / 1000, + nonce: await verifier.wallet.generateNonce(), + } + + const presentation = await holder.sdJwtVc.present({ + compactSdJwtVc: compact, + verifierMetadata, + presentationFrame: { + given_name: true, + family_name: true, + email: true, + phone_number: true, + address: { + street_address: true, + locality: true, + region: true, + country: true, + }, + birthdate: true, + is_over_18: true, + is_over_21: true, + is_over_65: true, + }, + }) + + const { verification: presentationVerification } = await verifier.sdJwtVc.verify({ + compactSdJwtVc: presentation, + keyBinding: { audience: verifierDid, nonce: verifierMetadata.nonce }, + requiredClaimKeys: [ + 'is_over_65', + 'is_over_21', + 'is_over_18', + 'birthdate', + 'email', + 'address.country', + 'address.region', + 'address.locality', + 'address', + 'address.street_address', + 'given_name', + 'family_name', + 'phone_number', + ], + }) + + expect(presentationVerification.isValid).toBeTruthy() + }) +}) diff --git a/packages/core/src/modules/sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts new file mode 100644 index 0000000000..56cf7e1902 --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts @@ -0,0 +1,503 @@ +/**simpleJwtVc + { + "jwt": { + "header": { + "typ": "vc+sd-jwt", + "alg": "EdDSA", + "kid": "#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW" + }, + "payload": { + "claim": "some-claim", + "vct": "IdentityCredential", + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo" + } + }, + "iss": "did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW", + "iat": 1698151532 + }, + "signature": "vLkigrBr1IIVRJeYE5DQx0rKUVzO3KT9T0XBATWJE89pWCyvB3Rzs8VD7qfi0vDk_QVCPIiHq1U1PsmSe4ZqCg" + }, + "disclosures": [] + } + */ +export const simpleJwtVc = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.vLkigrBr1IIVRJeYE5DQx0rKUVzO3KT9T0XBATWJE89pWCyvB3Rzs8VD7qfi0vDk_QVCPIiHq1U1PsmSe4ZqCg~' + +export const expiredSdJwtVc = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJleHAiOjE3MTYxMTE5MTksImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyfQ.hOQ-CnT-iaL2_Dlui0NgVhBk2Lej4_AqDrEK-7bQNT2b6mJkaikvUXdNtg-z7GnCUNrjq35vm5ProqiyYQz_AA~' + +export const notBeforeInFutureSdJwtVc = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJuYmYiOjQwNzg5NDQwMDAsImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyfQ.u0GPVCt7gPTrvT3sAwXxwkKW_Zy6YRRTaVRkrcSWt9VPonxQHUua2ggOERAu5cgtLeSdXzyqvS8nE9xFJg7xCw~' + +export const contentChangedSdJwtVc = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwyIiwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.TsFJUFKwdw5kVL4eY5vHOPGHqXBCFJ-n9c9KwPHkXAVfZ1TZkGA8m0_sNuTDy5n_pCutS6uzKJDAM0dfeGPyDg~' + +export const signatureInvalidSdJwtVc = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJpc3MiOiJkaWQ6a2V5Ono2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyIsImlhdCI6MTY5ODE1MTUzMn0.TsFJUFKwdw5kVL4eY5vHOPGHqXBCFJ-n9c9KwPHkXAVfZ1TZkGA8m0_sNuTDy5n_pCutd6uzKJDAM0dfeGPyDg~' + +/**simpleSdJwtVcWithStatus + { + "jwt": { + "header": { + "typ": "vc+sd-jwt", + "alg": "EdDSA", + "kid": "#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW" + }, + "payload": { + "claim": "some-claim", + "vct": "IdentityCredential", + "iss": "did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW", + "iat": 1698151532, + "status": { + "status_list": { + "idx": 12, + "uri": "https://example.com/status-list" + } + } + }, + "signature": "vLkigrBr1IIVRJeYE5DQx0rKUVzO3KT9T0XBATWJE89pWCyvB3Rzs8VD7qfi0vDk_QVCPIiHq1U1PsmSe4ZqCg" + }, + "disclosures": [] + } + */ +export const simpleSdJwtVcWithStatus = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJzdGF0dXMiOnsic3RhdHVzX2xpc3QiOnsiaWR4IjoxMiwidXJpIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zdGF0dXMtbGlzdCJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.JWE6RRGt032UsQ9EoyJnvxq7dAQX2DjW6mLYuvDkCuq0fzse5V_7RO6R0RBCPHXWWIfnCNAA8oEI3QM6A3avDg~' + +/**simpleJwtVcWithoutHolderBinding + { + "jwt": { + "header": { + "typ": "vc+sd-jwt", + "alg": "EdDSA", + "kid": "#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW" + }, + "payload": { + "claim": "some-claim", + "vct": "IdentityCredential", + "iss": "did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW", + "iat": 1698151532, + }, + "signature": "vLkigrBr1IIVRJeYE5DQx0rKUVzO3KT9T0XBATWJE89pWCyvB3Rzs8VD7qfi0vDk_QVCPIiHq1U1PsmSe4ZqCg" + }, + "disclosures": [] + } + */ +export const simpleJwtVcWithoutHolderBinding = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJpc3MiOiJkaWQ6a2V5Ono2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyIsImlhdCI6MTY5ODE1MTUzMn0.TsFJUFKwdw5kVL4eY5vHOPGHqXBCFJ-n9c9KwPHkXAVfZ1TZkGA8m0_sNuTDy5n_pCutS6uzKJDAM0dfeGPyDg~' + +/**simpleJwtVcPresentation + * { + "jwt": { + "header": { + "typ": "vc+sd-jwt", + "alg": "EdDSA", + "kid": "#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW" + }, + "payload": { + "claim": "some-claim", + "vct": "IdentityCredential", + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo" + } + }, + "iss": "did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW", + "iat": 1698151532 + }, + "signature": "vLkigrBr1IIVRJeYE5DQx0rKUVzO3KT9T0XBATWJE89pWCyvB3Rzs8VD7qfi0vDk_QVCPIiHq1U1PsmSe4ZqCg" + }, + "disclosures": [], + "kbJwt": { + "header": { + "typ": "kb+jwt", + "alg": "EdDSA" + }, + "payload": { + "iat": 1698151532, + "nonce": "salt", + "aud": "did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y", + "sd_hash": "f48YBevUG5JVuAHMryWQ4i2OF7XJoI-dL-jjYx-HqxQ" + }, + "signature": "skMqC7ej50kOeGEJZ_8J5eK1YqKN7vkqS_t8DQ4Y3i6DdN20eAXbaGMU4G4AOGk_hAYctTZwxaeQQEBX8pu5Cg" + } + } + */ +export const simpleJwtVcPresentation = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.vLkigrBr1IIVRJeYE5DQx0rKUVzO3KT9T0XBATWJE89pWCyvB3Rzs8VD7qfi0vDk_QVCPIiHq1U1PsmSe4ZqCg~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJzZF9oYXNoIjoiZjQ4WUJldlVHNUpWdUFITXJ5V1E0aTJPRjdYSm9JLWRMLWpqWXgtSHF4USJ9.skMqC7ej50kOeGEJZ_8J5eK1YqKN7vkqS_t8DQ4Y3i6DdN20eAXbaGMU4G4AOGk_hAYctTZwxaeQQEBX8pu5Cg' + +/**sdJwtVcWithSingleDisclosure + * { + "jwt": { + "header": { + "typ": "vc+sd-jwt", + "alg": "EdDSA", + "kid": "#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW" + }, + "payload": { + "vct": "IdentityCredential", + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo" + } + }, + "iss": "did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW", + "iat": 1698151532, + "_sd": [ + "vcvFU4DsFKTqQ1vl4nelJWXTb_-0dNoBks6iqNFptyg" + ], + "_sd_alg": "sha-256" + }, + "signature": "wX-7AyTsGMFDpgaw-TMjFK2zyywB94lKAwXlc4DtNoYjhnvKEe6eln1YhKTD_IIPNyTDOCT-TgtzA-8tCg9NCQ" + }, + "disclosures": [ + { + "_digest": "vcvFU4DsFKTqQ1vl4nelJWXTb_-0dNoBks6iqNFptyg", + "_encoded": "WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0", + "salt": "salt", + "key": "claim", + "value": "some-claim" + } + ] +} + * + * claim: +{ + vct: 'IdentityCredential', + cnf: { + jwk: { + kty: 'OKP', + crv: 'Ed25519', + x: 'oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo' + } + }, + iss: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + iat: 1698151532, + claim: 'some-claim' +} + */ +export const sdJwtVcWithSingleDisclosure = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZCI6WyJ2Y3ZGVTREc0ZLVHFRMXZsNG5lbEpXWFRiXy0wZE5vQmtzNmlxTkZwdHlnIl0sIl9zZF9hbGciOiJzaGEtMjU2In0.wX-7AyTsGMFDpgaw-TMjFK2zyywB94lKAwXlc4DtNoYjhnvKEe6eln1YhKTD_IIPNyTDOCT-TgtzA-8tCg9NCQ~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~' + +/**sdJwtVcWithSingleDisclosurePresentation + * { + "jwt": { + "header": { + "typ": "vc+sd-jwt", + "alg": "EdDSA", + "kid": "#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW" + }, + "payload": { + "vct": "IdentityCredential", + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo" + } + }, + "iss": "did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW", + "iat": 1698151532, + "_sd": [ + "vcvFU4DsFKTqQ1vl4nelJWXTb_-0dNoBks6iqNFptyg" + ], + "_sd_alg": "sha-256" + }, + "signature": "wX-7AyTsGMFDpgaw-TMjFK2zyywB94lKAwXlc4DtNoYjhnvKEe6eln1YhKTD_IIPNyTDOCT-TgtzA-8tCg9NCQ" + }, + "disclosures": [ + { + "_digest": "vcvFU4DsFKTqQ1vl4nelJWXTb_-0dNoBks6iqNFptyg", + "_encoded": "WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0", + "salt": "salt", + "key": "claim", + "value": "some-claim" + } + ], + "kbJwt": { + "header": { + "typ": "kb+jwt", + "alg": "EdDSA" + }, + "payload": { + "iat": 1698151532, + "nonce": "salt", + "aud": "did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y", + "sd_hash": "9F5VQwSVO7ZAwIgyh1jrwnJWgy7fTId1mj1MRp41nM8" + }, + "signature": "9TcpFkSLYMbsQzkPMyqrT5kMk8sobEvTzfkwym5HvbTfEMa_J23LB-UFhY0FsBhe-1rYqnAykGuimQNaWIwODw" + } +} + + * claims +{ + vct: 'IdentityCredential', + cnf: { + jwk: { + kty: 'OKP', + crv: 'Ed25519', + x: 'oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo' + } + }, + iss: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + iat: 1698151532, + claim: 'some-claim' +} + */ +export const sdJwtVcWithSingleDisclosurePresentation = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZCI6WyJ2Y3ZGVTREc0ZLVHFRMXZsNG5lbEpXWFRiXy0wZE5vQmtzNmlxTkZwdHlnIl0sIl9zZF9hbGciOiJzaGEtMjU2In0.wX-7AyTsGMFDpgaw-TMjFK2zyywB94lKAwXlc4DtNoYjhnvKEe6eln1YhKTD_IIPNyTDOCT-TgtzA-8tCg9NCQ~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJzZF9oYXNoIjoiOUY1VlF3U1ZPN1pBd0lneWgxanJ3bkpXZ3k3ZlRJZDFtajFNUnA0MW5NOCJ9.9TcpFkSLYMbsQzkPMyqrT5kMk8sobEvTzfkwym5HvbTfEMa_J23LB-UFhY0FsBhe-1rYqnAykGuimQNaWIwODw' + +/**complexSdJwtVc + * { + "jwt": { + "header": { + "typ": "vc+sd-jwt", + "alg": "EdDSA", + "kid": "#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW" + }, + "payload": { + "vct": "IdentityCredential", + "family_name": "Doe", + "phone_number": "+1-202-555-0101", + "address": { + "street_address": "123 Main St", + "locality": "Anytown", + "_sd": [ + "NJnmct0BqBME1JfBlC6jRQVRuevpEONiYw7A7MHuJyQ", + "om5ZztZHB-Gd00LG21CV_xM4FaENSoiaOXnTAJNczB4" + ] + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo" + } + }, + "iss": "did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW", + "iat": 1698151532, + "_sd": [ + "1Cur2k2A2oIB5CshSIf_A_Kg-l26u_qKuWQ79P0Vdas", + "R1zTUvOYHgcepj0jHypGHz9EHttVKft0yswbc9ETPbU", + "eDqQpdTXJXbWhf-EsI7zw5X6OvYmFN-UZQQMesXwKPw", + "pdDk2_XAKHo7gOAfwF1b7OdCUVTit2kJHaxSECQ9xfc", + "psauKUNWEi09nu3Cl89xKXgmpWENZl5uy1N1nyn_jMk", + "sN_ge0pHXF6qmsYnX1A9SdwJ8ch8aENkxbODsT74YwI" + ], + "_sd_alg": "sha-256" + }, + "signature": "Kkhrxy2acd52JTl4g_0x25D5d1QNCTbqHrD9Qu9HzXMxPMu_5T4z-cSiutDYb5cIdi9NzMXPe4MXax-fUymEDg" + }, + "disclosures": [ + { + "_digest": "NJnmct0BqBME1JfBlC6jRQVRuevpEONiYw7A7MHuJyQ", + "_encoded": "WyJzYWx0IiwicmVnaW9uIiwiQW55c3RhdGUiXQ", + "salt": "salt", + "key": "region", + "value": "Anystate" + }, + { + "_digest": "om5ZztZHB-Gd00LG21CV_xM4FaENSoiaOXnTAJNczB4", + "_encoded": "WyJzYWx0IiwiY291bnRyeSIsIlVTIl0", + "salt": "salt", + "key": "country", + "value": "US" + }, + { + "_digest": "eDqQpdTXJXbWhf-EsI7zw5X6OvYmFN-UZQQMesXwKPw", + "_encoded": "WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ", + "salt": "salt", + "key": "given_name", + "value": "John" + }, + { + "_digest": "psauKUNWEi09nu3Cl89xKXgmpWENZl5uy1N1nyn_jMk", + "_encoded": "WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0", + "salt": "salt", + "key": "email", + "value": "johndoe@example.com" + }, + { + "_digest": "pdDk2_XAKHo7gOAfwF1b7OdCUVTit2kJHaxSECQ9xfc", + "_encoded": "WyJzYWx0IiwiYmlydGhkYXRlIiwiMTk0MC0wMS0wMSJd", + "salt": "salt", + "key": "birthdate", + "value": "1940-01-01" + }, + { + "_digest": "1Cur2k2A2oIB5CshSIf_A_Kg-l26u_qKuWQ79P0Vdas", + "_encoded": "WyJzYWx0IiwiaXNfb3Zlcl8xOCIsdHJ1ZV0", + "salt": "salt", + "key": "is_over_18", + "value": true + }, + { + "_digest": "R1zTUvOYHgcepj0jHypGHz9EHttVKft0yswbc9ETPbU", + "_encoded": "WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0", + "salt": "salt", + "key": "is_over_21", + "value": true + }, + { + "_digest": "sN_ge0pHXF6qmsYnX1A9SdwJ8ch8aENkxbODsT74YwI", + "_encoded": "WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0", + "salt": "salt", + "key": "is_over_65", + "value": true + } + ] +} + + * claims +{ + vct: 'IdentityCredential', + family_name: 'Doe', + phone_number: '+1-202-555-0101', + address: { + street_address: '123 Main St', + locality: 'Anytown', + region: 'Anystate', + country: 'US' + }, + cnf: { + jwk: { + kty: 'OKP', + crv: 'Ed25519', + x: 'oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo' + } + }, + iss: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + iat: 1698151532, + is_over_18: true, + is_over_21: true, + given_name: 'John', + birthdate: '1940-01-01', + email: 'johndoe@example.com', + is_over_65: true +} + */ +export const complexSdJwtVc = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2QiOlsiMUN1cjJrMkEyb0lCNUNzaFNJZl9BX0tnLWwyNnVfcUt1V1E3OVAwVmRhcyIsIlIxelRVdk9ZSGdjZXBqMGpIeXBHSHo5RUh0dFZLZnQweXN3YmM5RVRQYlUiLCJlRHFRcGRUWEpYYldoZi1Fc0k3enc1WDZPdlltRk4tVVpRUU1lc1h3S1B3IiwicGREazJfWEFLSG83Z09BZndGMWI3T2RDVVZUaXQya0pIYXhTRUNROXhmYyIsInBzYXVLVU5XRWkwOW51M0NsODl4S1hnbXBXRU5abDV1eTFOMW55bl9qTWsiLCJzTl9nZTBwSFhGNnFtc1luWDFBOVNkd0o4Y2g4YUVOa3hiT0RzVDc0WXdJIl0sIl9zZF9hbGciOiJzaGEtMjU2In0.Kkhrxy2acd52JTl4g_0x25D5d1QNCTbqHrD9Qu9HzXMxPMu_5T4z-cSiutDYb5cIdi9NzMXPe4MXax-fUymEDg~WyJzYWx0IiwicmVnaW9uIiwiQW55c3RhdGUiXQ~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwiYmlydGhkYXRlIiwiMTk0MC0wMS0wMSJd~WyJzYWx0IiwiaXNfb3Zlcl8xOCIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~' + +/**complexSdJwtVcPresentation + * { + "jwt": { + "header": { + "typ": "vc+sd-jwt", + "alg": "EdDSA", + "kid": "#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW" + }, + "payload": { + "vct": "IdentityCredential", + "family_name": "Doe", + "phone_number": "+1-202-555-0101", + "address": { + "street_address": "123 Main St", + "locality": "Anytown", + "_sd": [ + "NJnmct0BqBME1JfBlC6jRQVRuevpEONiYw7A7MHuJyQ", + "om5ZztZHB-Gd00LG21CV_xM4FaENSoiaOXnTAJNczB4" + ] + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo" + } + }, + "iss": "did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW", + "iat": 1698151532, + "_sd": [ + "1Cur2k2A2oIB5CshSIf_A_Kg-l26u_qKuWQ79P0Vdas", + "R1zTUvOYHgcepj0jHypGHz9EHttVKft0yswbc9ETPbU", + "eDqQpdTXJXbWhf-EsI7zw5X6OvYmFN-UZQQMesXwKPw", + "pdDk2_XAKHo7gOAfwF1b7OdCUVTit2kJHaxSECQ9xfc", + "psauKUNWEi09nu3Cl89xKXgmpWENZl5uy1N1nyn_jMk", + "sN_ge0pHXF6qmsYnX1A9SdwJ8ch8aENkxbODsT74YwI" + ], + "_sd_alg": "sha-256" + }, + "signature": "Kkhrxy2acd52JTl4g_0x25D5d1QNCTbqHrD9Qu9HzXMxPMu_5T4z-cSiutDYb5cIdi9NzMXPe4MXax-fUymEDg" + }, + "disclosures": [ + { + "_digest": "om5ZztZHB-Gd00LG21CV_xM4FaENSoiaOXnTAJNczB4", + "_encoded": "WyJzYWx0IiwiY291bnRyeSIsIlVTIl0", + "salt": "salt", + "key": "country", + "value": "US" + }, + { + "_digest": "psauKUNWEi09nu3Cl89xKXgmpWENZl5uy1N1nyn_jMk", + "_encoded": "WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0", + "salt": "salt", + "key": "email", + "value": "johndoe@example.com" + }, + { + "_digest": "eDqQpdTXJXbWhf-EsI7zw5X6OvYmFN-UZQQMesXwKPw", + "_encoded": "WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ", + "salt": "salt", + "key": "given_name", + "value": "John" + }, + { + "_digest": "R1zTUvOYHgcepj0jHypGHz9EHttVKft0yswbc9ETPbU", + "_encoded": "WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0", + "salt": "salt", + "key": "is_over_21", + "value": true + }, + { + "_digest": "sN_ge0pHXF6qmsYnX1A9SdwJ8ch8aENkxbODsT74YwI", + "_encoded": "WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0", + "salt": "salt", + "key": "is_over_65", + "value": true + } + ], + "kbJwt": { + "header": { + "typ": "kb+jwt", + "alg": "EdDSA" + }, + "payload": { + "iat": 1698151532, + "nonce": "salt", + "aud": "did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y", + "sd_hash": "8qgm3cypUxDaa_grER613U9UNETnbLragU6UVwJ4HlM" + }, + "signature": "62HzMUsjlMq3BWyEBZwCuQnR5LzouSZKWh6es5CtC9HphOrh0ps1Lj_2iiZHfMv_lVF5Np_ZOiZNqsHfPL3GAA" + } +} + * claims +{ + vct: 'IdentityCredential', + family_name: 'Doe', + phone_number: '+1-202-555-0101', + address: { street_address: '123 Main St', locality: 'Anytown', country: 'US' }, + cnf: { + jwk: { + kty: 'OKP', + crv: 'Ed25519', + x: 'oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo' + } + }, + iss: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + iat: 1698151532, + is_over_21: true, + given_name: 'John', + email: 'johndoe@example.com', + is_over_65: true +} + */ +export const complexSdJwtVcPresentation = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2QiOlsiMUN1cjJrMkEyb0lCNUNzaFNJZl9BX0tnLWwyNnVfcUt1V1E3OVAwVmRhcyIsIlIxelRVdk9ZSGdjZXBqMGpIeXBHSHo5RUh0dFZLZnQweXN3YmM5RVRQYlUiLCJlRHFRcGRUWEpYYldoZi1Fc0k3enc1WDZPdlltRk4tVVpRUU1lc1h3S1B3IiwicGREazJfWEFLSG83Z09BZndGMWI3T2RDVVZUaXQya0pIYXhTRUNROXhmYyIsInBzYXVLVU5XRWkwOW51M0NsODl4S1hnbXBXRU5abDV1eTFOMW55bl9qTWsiLCJzTl9nZTBwSFhGNnFtc1luWDFBOVNkd0o4Y2g4YUVOa3hiT0RzVDc0WXdJIl0sIl9zZF9hbGciOiJzaGEtMjU2In0.Kkhrxy2acd52JTl4g_0x25D5d1QNCTbqHrD9Qu9HzXMxPMu_5T4z-cSiutDYb5cIdi9NzMXPe4MXax-fUymEDg~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJzZF9oYXNoIjoiaFRtUklwNFQ1Y2ZqQlUxbTVvcXNNWDZuUlFObGpEdXZSSThTWnlTeWhsZyJ9.D0G1__PslfgjkwTC1082x3r8Wp5mf13977y7Ef2xhvDrOO7V3zio5BZzqrDwzXIi3Y5GA1Vv3ptqpUKMn14EBA' diff --git a/packages/core/src/modules/sd-jwt-vc/index.ts b/packages/core/src/modules/sd-jwt-vc/index.ts new file mode 100644 index 0000000000..0d1891ea62 --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/index.ts @@ -0,0 +1,6 @@ +export * from './SdJwtVcApi' +export * from './SdJwtVcModule' +export * from './SdJwtVcService' +export * from './SdJwtVcError' +export * from './SdJwtVcOptions' +export * from './repository' diff --git a/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRecord.ts b/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRecord.ts new file mode 100644 index 0000000000..2892ae124f --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRecord.ts @@ -0,0 +1,69 @@ +import type { JwaSignatureAlgorithm } from '../../../crypto' +import type { TagsBase } from '../../../storage/BaseRecord' +import type { Constructable } from '../../../utils/mixins' + +import { decodeSdJwtSync } from '@sd-jwt/decode' + +import { BaseRecord } from '../../../storage/BaseRecord' +import { Hasher, JsonTransformer } from '../../../utils' +import { uuid } from '../../../utils/uuid' + +export type DefaultSdJwtVcRecordTags = { + vct: string + + /** + * The sdAlg is the alg used for creating digests for selective disclosures + */ + sdAlg: string + + /** + * The alg is the alg used to sign the SD-JWT + */ + alg: JwaSignatureAlgorithm +} + +export type SdJwtVcRecordStorageProps = { + id?: string + createdAt?: Date + tags?: TagsBase + compactSdJwtVc: string +} + +export class SdJwtVcRecord extends BaseRecord { + public static readonly type = 'SdJwtVcRecord' + public readonly type = SdJwtVcRecord.type + + // We store the sdJwtVc in compact format. + public compactSdJwtVc!: string + + // TODO: should we also store the pretty claims so it's not needed to + // re-calculate the hashes each time? I think for now it's fine to re-calculate + public constructor(props: SdJwtVcRecordStorageProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.compactSdJwtVc = props.compactSdJwtVc + this._tags = props.tags ?? {} + } + } + + public getTags() { + const sdjwt = decodeSdJwtSync(this.compactSdJwtVc, Hasher.hash) + const vct = sdjwt.jwt.payload['vct'] as string + const sdAlg = sdjwt.jwt.payload['_sd_alg'] as string | undefined + const alg = sdjwt.jwt.header['alg'] as JwaSignatureAlgorithm + + return { + ...this._tags, + vct, + sdAlg: sdAlg ?? 'sha-256', + alg, + } + } + + public clone(): this { + return JsonTransformer.fromJSON(JsonTransformer.toJSON(this), this.constructor as Constructable) + } +} diff --git a/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRepository.ts b/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRepository.ts new file mode 100644 index 0000000000..0aa8bbce3d --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRepository.ts @@ -0,0 +1,17 @@ +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { SdJwtVcRecord } from './SdJwtVcRecord' + +@injectable() +export class SdJwtVcRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(SdJwtVcRecord, storageService, eventEmitter) + } +} diff --git a/packages/core/src/modules/sd-jwt-vc/repository/__tests__/SdJwtVcRecord.test.ts b/packages/core/src/modules/sd-jwt-vc/repository/__tests__/SdJwtVcRecord.test.ts new file mode 100644 index 0000000000..294ad39c08 --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/repository/__tests__/SdJwtVcRecord.test.ts @@ -0,0 +1,68 @@ +import { SdJwtVcRecord } from '../SdJwtVcRecord' + +import { JsonTransformer } from '@credo-ts/core' + +describe('SdJwtVcRecord', () => { + test('sets the values passed in the constructor on the record', () => { + const compactSdJwtVc = + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyIxQ3VyMmsyQTJvSUI1Q3NoU0lmX0FfS2ctbDI2dV9xS3VXUTc5UDBWZGFzIiwiUjF6VFV2T1lIZ2NlcGowakh5cEdIejlFSHR0VktmdDB5c3diYzlFVFBiVSIsImVEcVFwZFRYSlhiV2hmLUVzSTd6dzVYNk92WW1GTi1VWlFRTWVzWHdLUHciLCJwZERrMl9YQUtIbzdnT0Fmd0YxYjdPZENVVlRpdDJrSkhheFNFQ1E5eGZjIiwicHNhdUtVTldFaTA5bnUzQ2w4OXhLWGdtcFdFTlpsNXV5MU4xbnluX2pNayIsInNOX2dlMHBIWEY2cW1zWW5YMUE5U2R3SjhjaDhhRU5reGJPRHNUNzRZd0kiXX0.Yz5U__nC0Nccza-NNfqhp-GueKXqeFNjm_NNtC1AJ2KdmERhCHdO6KNjM7bOiruHlo4oAlj-xObuB9LRiKXeCw~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8xOCIsdHJ1ZV0~WyJzYWx0IiwiYmlydGhkYXRlIiwiMTk0MC0wMS0wMSJd~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwicmVnaW9uIiwiQW55c3RhdGUiXQ~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~' + const createdAt = new Date() + const sdJwtVcRecord = new SdJwtVcRecord({ + id: 'sdjwt-id', + createdAt, + tags: { + some: 'tag', + }, + compactSdJwtVc, + }) + + expect(sdJwtVcRecord.type).toBe('SdJwtVcRecord') + expect(sdJwtVcRecord.id).toBe('sdjwt-id') + expect(sdJwtVcRecord.createdAt).toBe(createdAt) + expect(sdJwtVcRecord.getTags()).toEqual({ + some: 'tag', + alg: 'EdDSA', + sdAlg: 'sha-256', + vct: 'IdentityCredential', + }) + expect(sdJwtVcRecord.compactSdJwtVc).toEqual(compactSdJwtVc) + }) + + test('serializes and deserializes', () => { + const compactSdJwtVc = + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyIxQ3VyMmsyQTJvSUI1Q3NoU0lmX0FfS2ctbDI2dV9xS3VXUTc5UDBWZGFzIiwiUjF6VFV2T1lIZ2NlcGowakh5cEdIejlFSHR0VktmdDB5c3diYzlFVFBiVSIsImVEcVFwZFRYSlhiV2hmLUVzSTd6dzVYNk92WW1GTi1VWlFRTWVzWHdLUHciLCJwZERrMl9YQUtIbzdnT0Fmd0YxYjdPZENVVlRpdDJrSkhheFNFQ1E5eGZjIiwicHNhdUtVTldFaTA5bnUzQ2w4OXhLWGdtcFdFTlpsNXV5MU4xbnluX2pNayIsInNOX2dlMHBIWEY2cW1zWW5YMUE5U2R3SjhjaDhhRU5reGJPRHNUNzRZd0kiXX0.Yz5U__nC0Nccza-NNfqhp-GueKXqeFNjm_NNtC1AJ2KdmERhCHdO6KNjM7bOiruHlo4oAlj-xObuB9LRiKXeCw~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8xOCIsdHJ1ZV0~WyJzYWx0IiwiYmlydGhkYXRlIiwiMTk0MC0wMS0wMSJd~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwicmVnaW9uIiwiQW55c3RhdGUiXQ~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~' + const createdAt = new Date('2022-02-02') + const sdJwtVcRecord = new SdJwtVcRecord({ + id: 'sdjwt-id', + createdAt, + tags: { + some: 'tag', + }, + compactSdJwtVc, + }) + + const json = sdJwtVcRecord.toJSON() + expect(json).toMatchObject({ + id: 'sdjwt-id', + createdAt: '2022-02-02T00:00:00.000Z', + metadata: {}, + _tags: { + some: 'tag', + }, + compactSdJwtVc, + }) + + const instance = JsonTransformer.deserialize(JSON.stringify(json), SdJwtVcRecord) + + expect(instance.type).toBe('SdJwtVcRecord') + expect(instance.id).toBe('sdjwt-id') + expect(instance.createdAt.getTime()).toBe(createdAt.getTime()) + expect(instance.getTags()).toEqual({ + some: 'tag', + alg: 'EdDSA', + sdAlg: 'sha-256', + vct: 'IdentityCredential', + }) + expect(instance.compactSdJwtVc).toBe(compactSdJwtVc) + }) +}) diff --git a/packages/core/src/modules/sd-jwt-vc/repository/index.ts b/packages/core/src/modules/sd-jwt-vc/repository/index.ts new file mode 100644 index 0000000000..ce10b08020 --- /dev/null +++ b/packages/core/src/modules/sd-jwt-vc/repository/index.ts @@ -0,0 +1,2 @@ +export * from './SdJwtVcRecord' +export * from './SdJwtVcRepository' diff --git a/packages/core/src/modules/vc/W3cCredentialService.ts b/packages/core/src/modules/vc/W3cCredentialService.ts new file mode 100644 index 0000000000..726d5eb707 --- /dev/null +++ b/packages/core/src/modules/vc/W3cCredentialService.ts @@ -0,0 +1,217 @@ +import type { + StoreCredentialOptions, + W3cCreatePresentationOptions, + W3cJsonLdVerifyCredentialOptions, + W3cJsonLdVerifyPresentationOptions, + W3cJwtVerifyCredentialOptions, + W3cJwtVerifyPresentationOptions, + W3cSignCredentialOptions, + W3cSignPresentationOptions, + W3cVerifyCredentialOptions, + W3cVerifyPresentationOptions, +} from './W3cCredentialServiceOptions' +import type { + W3cVerifiableCredential, + W3cVerifiablePresentation, + W3cVerifyCredentialResult, + W3cVerifyPresentationResult, +} from './models' +import type { AgentContext } from '../../agent/context' +import type { Query, QueryOptions } from '../../storage/StorageService' + +import { CredoError } from '../../error' +import { injectable } from '../../plugins' + +import { CREDENTIALS_CONTEXT_V1_URL } from './constants' +import { W3cJsonLdVerifiableCredential } from './data-integrity' +import { W3cJsonLdCredentialService } from './data-integrity/W3cJsonLdCredentialService' +import { W3cJsonLdVerifiablePresentation } from './data-integrity/models/W3cJsonLdVerifiablePresentation' +import { W3cJwtVerifiableCredential, W3cJwtVerifiablePresentation } from './jwt-vc' +import { W3cJwtCredentialService } from './jwt-vc/W3cJwtCredentialService' +import { ClaimFormat } from './models' +import { W3cPresentation } from './models/presentation/W3cPresentation' +import { W3cCredentialRecord, W3cCredentialRepository } from './repository' + +@injectable() +export class W3cCredentialService { + private w3cCredentialRepository: W3cCredentialRepository + private w3cJsonLdCredentialService: W3cJsonLdCredentialService + private w3cJwtCredentialService: W3cJwtCredentialService + + public constructor( + w3cCredentialRepository: W3cCredentialRepository, + w3cJsonLdCredentialService: W3cJsonLdCredentialService, + w3cJwtCredentialService: W3cJwtCredentialService + ) { + this.w3cCredentialRepository = w3cCredentialRepository + this.w3cJsonLdCredentialService = w3cJsonLdCredentialService + this.w3cJwtCredentialService = w3cJwtCredentialService + } + + /** + * Signs a credential + * + * @param credential the credential to be signed + * @returns the signed credential + */ + public async signCredential( + agentContext: AgentContext, + options: W3cSignCredentialOptions + ): Promise> { + if (options.format === ClaimFormat.JwtVc) { + const signed = await this.w3cJwtCredentialService.signCredential(agentContext, options) + return signed as W3cVerifiableCredential + } else if (options.format === ClaimFormat.LdpVc) { + const signed = await this.w3cJsonLdCredentialService.signCredential(agentContext, options) + return signed as W3cVerifiableCredential + } else { + throw new CredoError(`Unsupported format in options. Format must be either 'jwt_vc' or 'ldp_vc'`) + } + } + + /** + * Verifies the signature(s) of a credential + */ + public async verifyCredential( + agentContext: AgentContext, + options: W3cVerifyCredentialOptions + ): Promise { + if (options.credential instanceof W3cJsonLdVerifiableCredential) { + return this.w3cJsonLdCredentialService.verifyCredential(agentContext, options as W3cJsonLdVerifyCredentialOptions) + } else if (options.credential instanceof W3cJwtVerifiableCredential || typeof options.credential === 'string') { + return this.w3cJwtCredentialService.verifyCredential(agentContext, options as W3cJwtVerifyCredentialOptions) + } else { + throw new CredoError( + `Unsupported credential type in options. Credential must be either a W3cJsonLdVerifiableCredential or a W3cJwtVerifiableCredential` + ) + } + } + + /** + * Utility method that creates a {@link W3cPresentation} from one or more {@link W3cJsonLdVerifiableCredential}s. + * + * **NOTE: the presentation that is returned is unsigned.** + * + * @returns An instance of {@link W3cPresentation} + */ + public async createPresentation(options: W3cCreatePresentationOptions): Promise { + const presentation = new W3cPresentation({ + context: [CREDENTIALS_CONTEXT_V1_URL], + type: ['VerifiablePresentation'], + verifiableCredential: options.credentials, + holder: options.holder, + id: options.id, + }) + + return presentation + } + + /** + * Signs a presentation including the credentials it includes + * + * @param presentation the presentation to be signed + * @returns the signed presentation + */ + public async signPresentation( + agentContext: AgentContext, + options: W3cSignPresentationOptions + ): Promise> { + if (options.format === ClaimFormat.JwtVp) { + const signed = await this.w3cJwtCredentialService.signPresentation(agentContext, options) + return signed as W3cVerifiablePresentation + } else if (options.format === ClaimFormat.LdpVp) { + const signed = await this.w3cJsonLdCredentialService.signPresentation(agentContext, options) + return signed as W3cVerifiablePresentation + } else { + throw new CredoError(`Unsupported format in options. Format must be either 'jwt_vp' or 'ldp_vp'`) + } + } + + /** + * Verifies a presentation including the credentials it includes + * + * @param presentation the presentation to be verified + * @returns the verification result + */ + public async verifyPresentation( + agentContext: AgentContext, + options: W3cVerifyPresentationOptions + ): Promise { + if (options.presentation instanceof W3cJsonLdVerifiablePresentation) { + return this.w3cJsonLdCredentialService.verifyPresentation( + agentContext, + options as W3cJsonLdVerifyPresentationOptions + ) + } else if ( + options.presentation instanceof W3cJwtVerifiablePresentation || + typeof options.presentation === 'string' + ) { + return this.w3cJwtCredentialService.verifyPresentation(agentContext, options as W3cJwtVerifyPresentationOptions) + } else { + throw new CredoError( + 'Unsupported credential type in options. Presentation must be either a W3cJsonLdVerifiablePresentation or a W3cJwtVerifiablePresentation' + ) + } + } + + /** + * Writes a credential to storage + * + * @param record the credential to be stored + * @returns the credential record that was written to storage + */ + public async storeCredential( + agentContext: AgentContext, + options: StoreCredentialOptions + ): Promise { + let expandedTypes: string[] = [] + + // JsonLd credentials need expanded types to be stored. + if (options.credential instanceof W3cJsonLdVerifiableCredential) { + expandedTypes = await this.w3cJsonLdCredentialService.getExpandedTypesForCredential( + agentContext, + options.credential + ) + } + + // Create an instance of the w3cCredentialRecord + const w3cCredentialRecord = new W3cCredentialRecord({ + tags: { expandedTypes }, + credential: options.credential, + }) + + // Store the w3c credential record + await this.w3cCredentialRepository.save(agentContext, w3cCredentialRecord) + + return w3cCredentialRecord + } + + public async removeCredentialRecord(agentContext: AgentContext, id: string) { + await this.w3cCredentialRepository.deleteById(agentContext, id) + } + + public async getAllCredentialRecords(agentContext: AgentContext): Promise { + return await this.w3cCredentialRepository.getAll(agentContext) + } + + public async getCredentialRecordById(agentContext: AgentContext, id: string): Promise { + return await this.w3cCredentialRepository.getById(agentContext, id) + } + + public async findCredentialsByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ): Promise { + const result = await this.w3cCredentialRepository.findByQuery(agentContext, query, queryOptions) + return result.map((record) => record.credential) + } + + public async findCredentialRecordByQuery( + agentContext: AgentContext, + query: Query + ): Promise { + const result = await this.w3cCredentialRepository.findSingleByQuery(agentContext, query) + return result?.credential + } +} diff --git a/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts new file mode 100644 index 0000000000..3a9b892e89 --- /dev/null +++ b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts @@ -0,0 +1,193 @@ +import type { ProofPurpose, W3cJsonLdVerifiablePresentation } from './data-integrity' +import type { W3cJsonLdVerifiableCredential } from './data-integrity/models/W3cJsonLdVerifiableCredential' +import type { W3cJwtVerifiableCredential } from './jwt-vc/W3cJwtVerifiableCredential' +import type { W3cJwtVerifiablePresentation } from './jwt-vc/W3cJwtVerifiablePresentation' +import type { ClaimFormat, W3cVerifiableCredential } from './models' +import type { W3cCredential } from './models/credential/W3cCredential' +import type { W3cPresentation } from './models/presentation/W3cPresentation' +import type { JwaSignatureAlgorithm } from '../../crypto/jose/jwa' +import type { SingleOrArray } from '../../utils/type' + +export type W3cSignCredentialOptions = + Format extends ClaimFormat.JwtVc + ? W3cJwtSignCredentialOptions + : Format extends ClaimFormat.LdpVc + ? W3cJsonLdSignCredentialOptions + : W3cJwtSignCredentialOptions | W3cJsonLdSignCredentialOptions +export type W3cVerifyCredentialOptions = + Format extends ClaimFormat.JwtVc + ? W3cJwtVerifyCredentialOptions + : Format extends ClaimFormat.LdpVc + ? W3cJsonLdVerifyCredentialOptions + : W3cJwtVerifyCredentialOptions | W3cJsonLdVerifyCredentialOptions +export type W3cSignPresentationOptions = + Format extends ClaimFormat.JwtVp + ? W3cJwtSignPresentationOptions + : Format extends ClaimFormat.LdpVp + ? W3cJsonLdSignPresentationOptions + : W3cJwtSignPresentationOptions | W3cJsonLdSignPresentationOptions +export type W3cVerifyPresentationOptions = W3cJwtVerifyPresentationOptions | W3cJsonLdVerifyPresentationOptions + +interface W3cSignCredentialOptionsBase { + /** + * The format of the credential to be signed. + * + * @see https://identity.foundation/claim-format-registry + */ + format: ClaimFormat + + /** + * The credential to be signed. + */ + credential: W3cCredential + + /** + * URI of the verificationMethod to be used for signing the credential. + * + * Must be a valid did url pointing to a key. + */ + verificationMethod: string +} + +export interface W3cJwtSignCredentialOptions extends W3cSignCredentialOptionsBase { + format: ClaimFormat.JwtVc + + /** + * The alg to be used for signing the credential. + * + * Must be a valid JWA signature algorithm. + */ + alg: JwaSignatureAlgorithm +} + +export interface W3cJsonLdSignCredentialOptions extends W3cSignCredentialOptionsBase { + /** + * The format of the credential to be signed. Must be either `jwt_vc` or `ldp_vc`. + * @see https://identity.foundation/claim-format-registry + */ + format: ClaimFormat.LdpVc + + /** + * The proofType to be used for signing the credential. + * + * Must be a valid Linked Data Signature suite. + */ + proofType: string + + proofPurpose?: ProofPurpose + created?: string +} + +interface W3cVerifyCredentialOptionsBase { + credential: unknown + + /** + * Whether to verify the credentialStatus, if present. + */ + verifyCredentialStatus?: boolean +} + +export interface W3cJwtVerifyCredentialOptions extends W3cVerifyCredentialOptionsBase { + credential: W3cJwtVerifiableCredential | string // string must be encoded VC JWT +} + +export interface W3cJsonLdVerifyCredentialOptions extends W3cVerifyCredentialOptionsBase { + credential: W3cJsonLdVerifiableCredential + proofPurpose?: ProofPurpose +} + +export interface W3cCreatePresentationOptions { + credentials: SingleOrArray + id?: string + holder?: string +} + +interface W3cSignPresentationOptionsBase { + /** + * The format of the presentation to be signed. + * + * @see https://identity.foundation/claim-format-registry + */ + format: ClaimFormat + + /** + * The presentation to be signed. + */ + presentation: W3cPresentation + + /** + * URI of the verificationMethod to be used for signing the presentation. + * + * Must be a valid did url pointing to a key. + */ + verificationMethod: string + + /** + * The challenge / nonce to be used in the proof to prevent replay attacks. + */ + challenge: string + + /** + * The domain / aud to be used in the proof to assert the intended recipient. + */ + domain?: string +} + +export interface W3cJsonLdSignPresentationOptions extends W3cSignPresentationOptionsBase { + format: ClaimFormat.LdpVp + + /** + * The proofType to be used for signing the presentation. + * + * Must be a valid Linked Data Signature suite. + */ + proofType: string + + proofPurpose: ProofPurpose +} + +export interface W3cJwtSignPresentationOptions extends W3cSignPresentationOptionsBase { + format: ClaimFormat.JwtVp + + /** + * The alg to be used for signing the presentation. + * + * Must be a valid JWA signature algorithm. + */ + alg: JwaSignatureAlgorithm +} + +interface W3cVerifyPresentationOptionsBase { + /** + * The presentation to be verified. + */ + presentation: unknown + + /** + * The challenge / nonce that must present in the presentation prevent replay attacks. + */ + challenge: string + + /** + * The domain / aud to be used in the proof to assert the intended recipient. + */ + domain?: string + + /** + * Whether to verify the credentialStatus, if present. + */ + verifyCredentialStatus?: boolean +} + +export interface W3cJwtVerifyPresentationOptions extends W3cVerifyPresentationOptionsBase { + presentation: W3cJwtVerifiablePresentation | string // string must be encoded VP JWT +} + +export interface W3cJsonLdVerifyPresentationOptions extends W3cVerifyPresentationOptionsBase { + presentation: W3cJsonLdVerifiablePresentation + purpose?: ProofPurpose +} + +export interface StoreCredentialOptions { + credential: W3cVerifiableCredential +} diff --git a/packages/core/src/modules/vc/W3cCredentialsApi.ts b/packages/core/src/modules/vc/W3cCredentialsApi.ts new file mode 100644 index 0000000000..9db8fcf93f --- /dev/null +++ b/packages/core/src/modules/vc/W3cCredentialsApi.ts @@ -0,0 +1,77 @@ +import type { + StoreCredentialOptions, + W3cCreatePresentationOptions, + W3cSignCredentialOptions, + W3cSignPresentationOptions, + W3cVerifyCredentialOptions, + W3cVerifyPresentationOptions, +} from './W3cCredentialServiceOptions' +import type { W3cVerifiableCredential, ClaimFormat } from './models' +import type { W3cCredentialRecord } from './repository' +import type { Query, QueryOptions } from '../../storage/StorageService' + +import { AgentContext } from '../../agent' +import { injectable } from '../../plugins' + +import { W3cCredentialService } from './W3cCredentialService' + +/** + * @public + */ +@injectable() +export class W3cCredentialsApi { + private agentContext: AgentContext + private w3cCredentialService: W3cCredentialService + + public constructor(agentContext: AgentContext, w3cCredentialService: W3cCredentialService) { + this.agentContext = agentContext + this.w3cCredentialService = w3cCredentialService + } + + public async storeCredential(options: StoreCredentialOptions): Promise { + return this.w3cCredentialService.storeCredential(this.agentContext, options) + } + + public async removeCredentialRecord(id: string) { + return this.w3cCredentialService.removeCredentialRecord(this.agentContext, id) + } + + public async getAllCredentialRecords(): Promise { + return this.w3cCredentialService.getAllCredentialRecords(this.agentContext) + } + + public async getCredentialRecordById(id: string): Promise { + return this.w3cCredentialService.getCredentialRecordById(this.agentContext, id) + } + + public async findCredentialRecordsByQuery( + query: Query, + queryOptions?: QueryOptions + ): Promise { + return this.w3cCredentialService.findCredentialsByQuery(this.agentContext, query, queryOptions) + } + + public async signCredential( + options: W3cSignCredentialOptions + ) { + return this.w3cCredentialService.signCredential(this.agentContext, options) + } + + public async verifyCredential(options: W3cVerifyCredentialOptions) { + return this.w3cCredentialService.verifyCredential(this.agentContext, options) + } + + public async createPresentation(options: W3cCreatePresentationOptions) { + return this.w3cCredentialService.createPresentation(options) + } + + public async signPresentation( + options: W3cSignPresentationOptions + ) { + return this.w3cCredentialService.signPresentation(this.agentContext, options) + } + + public async verifyPresentation(options: W3cVerifyPresentationOptions) { + return this.w3cCredentialService.verifyPresentation(this.agentContext, options) + } +} diff --git a/packages/core/src/modules/vc/W3cCredentialsModule.ts b/packages/core/src/modules/vc/W3cCredentialsModule.ts new file mode 100644 index 0000000000..3b1fd7da8b --- /dev/null +++ b/packages/core/src/modules/vc/W3cCredentialsModule.ts @@ -0,0 +1,52 @@ +import type { W3cCredentialsModuleConfigOptions } from './W3cCredentialsModuleConfig' +import type { DependencyManager, Module } from '../../plugins' + +import { KeyType } from '../../crypto' +import { + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, +} from '../dids' + +import { W3cCredentialService } from './W3cCredentialService' +import { W3cCredentialsApi } from './W3cCredentialsApi' +import { W3cCredentialsModuleConfig } from './W3cCredentialsModuleConfig' +import { SignatureSuiteRegistry, SignatureSuiteToken } from './data-integrity/SignatureSuiteRegistry' +import { W3cJsonLdCredentialService } from './data-integrity/W3cJsonLdCredentialService' +import { Ed25519Signature2018 } from './data-integrity/signature-suites' +import { W3cJwtCredentialService } from './jwt-vc' +import { W3cCredentialRepository } from './repository/W3cCredentialRepository' + +/** + * @public + */ +export class W3cCredentialsModule implements Module { + public readonly config: W3cCredentialsModuleConfig + public readonly api = W3cCredentialsApi + + public constructor(config?: W3cCredentialsModuleConfigOptions) { + this.config = new W3cCredentialsModuleConfig(config) + } + + public register(dependencyManager: DependencyManager) { + dependencyManager.registerSingleton(W3cCredentialService) + dependencyManager.registerSingleton(W3cJwtCredentialService) + dependencyManager.registerSingleton(W3cJsonLdCredentialService) + dependencyManager.registerSingleton(W3cCredentialRepository) + + dependencyManager.registerSingleton(SignatureSuiteRegistry) + + // Register the config + dependencyManager.registerInstance(W3cCredentialsModuleConfig, this.config) + + // Always register ed25519 signature suite + dependencyManager.registerInstance(SignatureSuiteToken, { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + verificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ], + keyTypes: [KeyType.Ed25519], + }) + } +} diff --git a/packages/core/src/modules/vc/W3cCredentialsModuleConfig.ts b/packages/core/src/modules/vc/W3cCredentialsModuleConfig.ts new file mode 100644 index 0000000000..3bb81b75a2 --- /dev/null +++ b/packages/core/src/modules/vc/W3cCredentialsModuleConfig.ts @@ -0,0 +1,46 @@ +import type { DocumentLoaderWithContext } from './data-integrity/libraries/documentLoader' + +import { defaultDocumentLoader } from './data-integrity/libraries/documentLoader' + +/** + * W3cCredentialsModuleConfigOptions defines the interface for the options of the W3cCredentialsModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface W3cCredentialsModuleConfigOptions { + /** + * Document loader to use for resolving JSON-LD objects. Takes a {@link AgentContext} as parameter, + * and must return a {@link DocumentLoader} function. + * + * @example + * ``` + * const myDocumentLoader = (agentContext: AgentContext) => { + * return async (url) => { + * if (url !== 'https://example.org') throw new Error("I don't know how to load this document") + * + * return { + * contextUrl: null, + * documentUrl: url, + * document: null + * } + * } + * } + * ``` + * + * + * @default {@link defaultDocumentLoader} + */ + documentLoader?: DocumentLoaderWithContext +} + +export class W3cCredentialsModuleConfig { + private options: W3cCredentialsModuleConfigOptions + + public constructor(options?: W3cCredentialsModuleConfigOptions) { + this.options = options ?? {} + } + + /** See {@link W3cCredentialsModuleConfigOptions.documentLoader} */ + public get documentLoader() { + return this.options.documentLoader ?? defaultDocumentLoader + } +} diff --git a/packages/core/src/modules/vc/__tests__/W3CredentialsModule.test.ts b/packages/core/src/modules/vc/__tests__/W3CredentialsModule.test.ts new file mode 100644 index 0000000000..263d9f03ee --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/W3CredentialsModule.test.ts @@ -0,0 +1,40 @@ +import { KeyType } from '../../../crypto' +import { DependencyManager } from '../../../plugins/DependencyManager' +import { W3cCredentialService } from '../W3cCredentialService' +import { W3cCredentialsModule } from '../W3cCredentialsModule' +import { W3cCredentialsModuleConfig } from '../W3cCredentialsModuleConfig' +import { SignatureSuiteRegistry, SignatureSuiteToken } from '../data-integrity/SignatureSuiteRegistry' +import { W3cJsonLdCredentialService } from '../data-integrity/W3cJsonLdCredentialService' +import { Ed25519Signature2018 } from '../data-integrity/signature-suites' +import { W3cJwtCredentialService } from '../jwt-vc' +import { W3cCredentialRepository } from '../repository' + +jest.mock('../../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +describe('W3cCredentialsModule', () => { + test('registers dependencies on the dependency manager', () => { + const module = new W3cCredentialsModule() + + module.register(dependencyManager) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(5) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(W3cCredentialService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(W3cJsonLdCredentialService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(W3cJwtCredentialService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(W3cCredentialRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(SignatureSuiteRegistry) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(W3cCredentialsModuleConfig, module.config) + + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(SignatureSuiteToken, { + suiteClass: Ed25519Signature2018, + verificationMethodTypes: ['Ed25519VerificationKey2018', 'Ed25519VerificationKey2020'], + proofType: 'Ed25519Signature2018', + keyTypes: [KeyType.Ed25519], + }) + }) +}) diff --git a/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts new file mode 100644 index 0000000000..f708a56e96 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts @@ -0,0 +1,175 @@ +import type { AgentContext } from '../../../agent' +import type { Wallet } from '../../../wallet' + +import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../tests' +import { JwsService } from '../../../crypto' +import { JsonTransformer, asArray } from '../../../utils' +import { W3cCredentialService } from '../W3cCredentialService' +import { W3cCredentialsModuleConfig } from '../W3cCredentialsModuleConfig' +import { W3cJsonLdVerifiableCredential } from '../data-integrity' +import { SignatureSuiteRegistry } from '../data-integrity/SignatureSuiteRegistry' +import { W3cJsonLdCredentialService } from '../data-integrity/W3cJsonLdCredentialService' +import { customDocumentLoader } from '../data-integrity/__tests__/documentLoader' +import { Ed25519Signature2018Fixtures } from '../data-integrity/__tests__/fixtures' +import jsonld from '../data-integrity/libraries/jsonld' +import { W3cJwtCredentialService } from '../jwt-vc' +import { W3cPresentation } from '../models' +import { W3cCredentialRepository, W3cCredentialRecord } from '../repository' + +jest.mock('../repository/W3cCredentialRepository') +const W3cCredentialsRepositoryMock = W3cCredentialRepository as jest.Mock + +const agentConfig = getAgentConfig('W3cCredentialServiceTest') + +// Helper func +const credentialRecordFactory = async (credential: W3cJsonLdVerifiableCredential) => { + const expandedTypes = ( + await jsonld.expand(JsonTransformer.toJSON(credential), { documentLoader: customDocumentLoader() }) + )[0]['@type'] + + // Create an instance of the w3cCredentialRecord + return new W3cCredentialRecord({ + tags: { expandedTypes: asArray(expandedTypes) }, + credential: credential, + }) +} + +const credentialsModuleConfig = new W3cCredentialsModuleConfig({ + documentLoader: customDocumentLoader, +}) + +describe('W3cCredentialsService', () => { + let wallet: Wallet + let agentContext: AgentContext + let w3cCredentialService: W3cCredentialService + let w3cCredentialsRepository: W3cCredentialRepository + + beforeAll(async () => { + wallet = new InMemoryWallet() + await wallet.createAndOpen(agentConfig.walletConfig) + agentContext = getAgentContext({ + agentConfig, + wallet, + }) + w3cCredentialsRepository = new W3cCredentialsRepositoryMock() + w3cCredentialService = new W3cCredentialService( + w3cCredentialsRepository, + new W3cJsonLdCredentialService(new SignatureSuiteRegistry([]), credentialsModuleConfig), + new W3cJwtCredentialService(new JwsService()) + ) + }) + + afterAll(async () => { + await wallet.delete() + }) + + describe('createPresentation', () => { + it('should successfully create a presentation from single verifiable credential', async () => { + const vc = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ) + const result = await w3cCredentialService.createPresentation({ credentials: [vc] }) + + expect(result).toBeInstanceOf(W3cPresentation) + + expect(result.type).toEqual(expect.arrayContaining(['VerifiablePresentation'])) + + expect(result.verifiableCredential).toHaveLength(1) + expect(result.verifiableCredential).toEqual([vc]) + }) + + it('should successfully create a presentation from two verifiable credential', async () => { + const vc1 = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ) + const vc2 = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ) + + const vcs = [vc1, vc2] + const result = await w3cCredentialService.createPresentation({ credentials: vcs }) + + expect(result).toBeInstanceOf(W3cPresentation) + + expect(result.type).toEqual(expect.arrayContaining(['VerifiablePresentation'])) + + expect(result.verifiableCredential).toHaveLength(2) + expect(result.verifiableCredential).toEqual(expect.arrayContaining([vc1, vc2])) + }) + }) + + describe('Credential Storage', () => { + let w3cCredentialRecord: W3cCredentialRecord + let w3cCredentialRepositoryDeleteMock: jest.MockedFunction<(typeof w3cCredentialsRepository)['deleteById']> + + beforeEach(async () => { + const credential = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ) + + w3cCredentialRecord = await credentialRecordFactory(credential) + + mockFunction(w3cCredentialsRepository.getById).mockResolvedValue(w3cCredentialRecord) + mockFunction(w3cCredentialsRepository.getAll).mockResolvedValue([w3cCredentialRecord]) + w3cCredentialRepositoryDeleteMock = mockFunction(w3cCredentialsRepository.deleteById).mockResolvedValue() + }) + describe('storeCredential', () => { + it('should store a credential and expand the tags correctly', async () => { + const credential = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ) + + w3cCredentialRecord = await w3cCredentialService.storeCredential(agentContext, { credential: credential }) + + expect(w3cCredentialRecord).toMatchObject({ + type: 'W3cCredentialRecord', + id: expect.any(String), + createdAt: expect.any(Date), + credential: expect.any(W3cJsonLdVerifiableCredential), + }) + + expect(w3cCredentialRecord.getTags()).toMatchObject({ + expandedTypes: [ + 'https://www.w3.org/2018/credentials#VerifiableCredential', + 'https://example.org/examples#UniversityDegreeCredential', + ], + }) + }) + }) + + describe('removeCredentialRecord', () => { + it('should remove a credential', async () => { + await w3cCredentialService.removeCredentialRecord(agentContext, 'some-id') + + expect(w3cCredentialRepositoryDeleteMock).toBeCalledWith(agentContext, 'some-id') + }) + }) + + describe('getAllCredentialRecords', () => { + it('should retrieve all W3cCredentialRecords', async () => { + const credential = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ) + await w3cCredentialService.storeCredential(agentContext, { credential: credential }) + + const records = await w3cCredentialService.getAllCredentialRecords(agentContext) + + expect(records.length).toEqual(1) + }) + }) + describe('getCredentialRecordById', () => { + it('should retrieve a W3cCredentialRecord by id', async () => { + const credential = await w3cCredentialService.getCredentialRecordById(agentContext, w3cCredentialRecord.id) + + expect(credential.id).toEqual(w3cCredentialRecord.id) + }) + }) + }) +}) diff --git a/packages/core/src/modules/vc/__tests__/W3cCredentialsApi.test.ts b/packages/core/src/modules/vc/__tests__/W3cCredentialsApi.test.ts new file mode 100644 index 0000000000..c39f408452 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/W3cCredentialsApi.test.ts @@ -0,0 +1,93 @@ +import { getInMemoryAgentOptions } from '../../../../tests' +import { Agent } from '../../../agent/Agent' +import { JsonTransformer } from '../../../utils' +import { W3cCredentialService } from '../W3cCredentialService' +import { W3cCredentialsModule } from '../W3cCredentialsModule' +import { customDocumentLoader } from '../data-integrity/__tests__/documentLoader' +import { Ed25519Signature2018Fixtures } from '../data-integrity/__tests__/fixtures' +import { W3cJsonLdVerifiableCredential } from '../data-integrity/models' +import { W3cCredentialRepository } from '../repository' + +const agentOptions = getInMemoryAgentOptions( + 'W3cCredentialsApi', + {}, + { + w3cCredentials: new W3cCredentialsModule({ + documentLoader: customDocumentLoader, + }), + } +) + +const agent = new Agent(agentOptions) + +let w3cCredentialRepository: W3cCredentialRepository +let w3cCredentialService: W3cCredentialService + +const testCredential = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential +) + +describe('W3cCredentialsApi', () => { + beforeAll(() => { + w3cCredentialRepository = agent.dependencyManager.resolve(W3cCredentialRepository) + w3cCredentialService = agent.dependencyManager.resolve(W3cCredentialService) + }) + + beforeEach(async () => { + await agent.initialize() + }) + + afterEach(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + it('Should successfully store a credential', async () => { + const repoSpy = jest.spyOn(w3cCredentialRepository, 'save') + const serviceSpy = jest.spyOn(w3cCredentialService, 'storeCredential') + + await agent.w3cCredentials.storeCredential({ + credential: testCredential, + }) + + expect(repoSpy).toHaveBeenCalledTimes(1) + expect(serviceSpy).toHaveBeenCalledTimes(1) + }) + + it('Should successfully retrieve a credential by id', async () => { + const repoSpy = jest.spyOn(w3cCredentialRepository, 'getById') + const serviceSpy = jest.spyOn(w3cCredentialService, 'getCredentialRecordById') + + const storedCredential = await agent.w3cCredentials.storeCredential({ + credential: testCredential, + }) + + const retrievedCredential = await agent.w3cCredentials.getCredentialRecordById(storedCredential.id) + expect(storedCredential.id).toEqual(retrievedCredential.id) + + expect(repoSpy).toHaveBeenCalledTimes(1) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(repoSpy).toHaveBeenCalledWith((agent as any).agentContext, storedCredential.id) + expect(serviceSpy).toHaveBeenCalledTimes(1) + }) + + it('Should successfully remove a credential by id', async () => { + const repoSpy = jest.spyOn(w3cCredentialRepository, 'deleteById') + const serviceSpy = jest.spyOn(w3cCredentialService, 'removeCredentialRecord') + + const storedCredential = await agent.w3cCredentials.storeCredential({ + credential: testCredential, + }) + + await agent.w3cCredentials.removeCredentialRecord(storedCredential.id) + + expect(repoSpy).toHaveBeenCalledTimes(1) + expect(serviceSpy).toHaveBeenCalledTimes(1) + expect(serviceSpy).toHaveBeenCalledWith(agent.context, storedCredential.id) + + const allCredentials = await agent.w3cCredentials.getAllCredentialRecords() + expect(allCredentials).toHaveLength(0) + }) +}) diff --git a/packages/core/src/modules/vc/__tests__/W3cCredentialsModuleConfig.test.ts b/packages/core/src/modules/vc/__tests__/W3cCredentialsModuleConfig.test.ts new file mode 100644 index 0000000000..bda3448a7a --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/W3cCredentialsModuleConfig.test.ts @@ -0,0 +1,19 @@ +import { W3cCredentialsModuleConfig } from '../W3cCredentialsModuleConfig' +import { defaultDocumentLoader } from '../data-integrity/libraries/documentLoader' + +describe('W3cCredentialsModuleConfig', () => { + test('sets default values', () => { + const config = new W3cCredentialsModuleConfig() + + expect(config.documentLoader).toBe(defaultDocumentLoader) + }) + + test('sets values', () => { + const documentLoader = jest.fn() + const config = new W3cCredentialsModuleConfig({ + documentLoader, + }) + + expect(config.documentLoader).toBe(documentLoader) + }) +}) diff --git a/packages/core/src/modules/vc/__tests__/dids/did_example_489398593.ts b/packages/core/src/modules/vc/__tests__/dids/did_example_489398593.ts new file mode 100644 index 0000000000..9cee4d0e2c --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_example_489398593.ts @@ -0,0 +1,13 @@ +export const DID_EXAMPLE_48939859 = { + '@context': 'https://www.w3.org/ns/did/v1', + id: 'did:example:489398593', + assertionMethod: [ + { + id: 'did:example:489398593#test', + type: 'Bls12381G2Key2020', + controller: 'did:example:489398593', + publicKeyBase58: + 'oqpWYKaZD9M1Kbe94BVXpr8WTdFBNZyKv48cziTiQUeuhm7sBhCABMyYG4kcMrseC68YTFFgyhiNeBKjzdKk9MiRWuLv5H4FFujQsQK2KTAtzU8qTBiZqBHMmnLF4PL7Ytu', + }, + ], +} diff --git a/packages/core/src/modules/vc/__tests__/dids/did_sov_QqEfJxe752NCmWqR5TssZ5.ts b/packages/core/src/modules/vc/__tests__/dids/did_sov_QqEfJxe752NCmWqR5TssZ5.ts new file mode 100644 index 0000000000..0246796da6 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_sov_QqEfJxe752NCmWqR5TssZ5.ts @@ -0,0 +1,24 @@ +export const DID_SOV_QqEfJxe752NCmWqR5TssZ5 = { + '@context': 'https://www.w3.org/ns/did/v1', + id: 'did:sov:QqEfJxe752NCmWqR5TssZ5', + verificationMethod: [ + { + id: 'did:sov:QqEfJxe752NCmWqR5TssZ5#key-1', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:QqEfJxe752NCmWqR5TssZ5', + publicKeyBase58: 'DzNC1pbarUzgGXmxRsccNJDBjWgCaiy6uSXgPPJZGWCL', + }, + ], + authentication: ['did:sov:QqEfJxe752NCmWqR5TssZ5#key-1'], + assertionMethod: ['did:sov:QqEfJxe752NCmWqR5TssZ5#key-1'], + service: [ + { + id: 'did:sov:QqEfJxe752NCmWqR5TssZ5#did-communication', + type: 'did-communication', + serviceEndpoint: 'http://localhost:3002', + recipientKeys: ['did:sov:QqEfJxe752NCmWqR5TssZ5#key-1'], + routingKeys: [], + priority: 0, + }, + ], +} diff --git a/packages/core/src/modules/vc/__tests__/dids/did_web_launchpad.ts b/packages/core/src/modules/vc/__tests__/dids/did_web_launchpad.ts new file mode 100644 index 0000000000..81c02d5555 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_web_launchpad.ts @@ -0,0 +1,24 @@ +export const DID_WEB_LAUNCHPAD = { + id: 'did:web:launchpad.vii.electron.mattrlabs.io', + '@context': ['https://w3.org/ns/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + verificationMethod: [ + { + id: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg', + type: 'Ed25519VerificationKey2018', + controller: 'did:web:launchpad.vii.electron.mattrlabs.io', + publicKeyBase58: '6BhFMCGTJg9DnpXZe7zbiTrtuwion5FVV6Z2NUpwDMVT', + }, + ], + keyAgreement: [ + { + id: 'did:web:launchpad.vii.electron.mattrlabs.io#9eS8Tqsus1', + type: 'X25519KeyAgreementKey2019', + controller: 'did:web:launchpad.vii.electron.mattrlabs.io', + publicKeyBase58: '9eS8Tqsus1uJmQpf37S8CnEeBrEehsC3qz8RMq67KoLB', + }, + ], + authentication: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], + assertionMethod: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], + capabilityDelegation: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], + capabilityInvocation: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], +} diff --git a/packages/core/src/modules/vc/__tests__/dids/did_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL.ts b/packages/core/src/modules/vc/__tests__/dids/did_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL.ts new file mode 100644 index 0000000000..f3bb1e0b1d --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL.ts @@ -0,0 +1,45 @@ +export const DID_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL = { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + alsoKnownAs: [], + controller: [], + verificationMethod: [ + { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + publicKeyBase58: '3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx', + }, + ], + service: [], + authentication: [ + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + ], + assertionMethod: [ + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + ], + keyAgreement: [ + { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + publicKeyBase58: '3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx', + }, + { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6LSbkodSr6SU2trs8VUgnrnWtSm7BAPG245ggrBmSrxbv1R', + type: 'X25519KeyAgreementKey2019', + controller: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + publicKeyBase58: '5dTvYHaNaB7mk7iA9LqCJEHG2dGZQsvoi8WGzDRtYEf', + }, + ], + capabilityInvocation: [ + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + ], + capabilityDelegation: [ + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + ], + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', +} diff --git a/packages/core/src/modules/vc/__tests__/dids/did_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV.ts b/packages/core/src/modules/vc/__tests__/dids/did_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV.ts new file mode 100644 index 0000000000..2c8d443940 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV.ts @@ -0,0 +1,45 @@ +export const DID_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV = { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + alsoKnownAs: [], + controller: [], + verificationMethod: [ + { + id: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + publicKeyBase58: 'HC8vuuvP8x9kVJizh2eujQjo2JwFQJz6w63szzdbu1Q7', + }, + ], + service: [], + authentication: [ + 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + ], + assertionMethod: [ + 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + ], + keyAgreement: [ + { + id: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + publicKeyBase58: 'HC8vuuvP8x9kVJizh2eujQjo2JwFQJz6w63szzdbu1Q7', + }, + { + id: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6LSsJwtXqeVHCtCR9QMyX58hfBNY62wQooE4VPYmwyyesov', + type: 'X25519KeyAgreementKey2019', + controller: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + publicKeyBase58: 'Gdmj1XqdBkATKm2bSsZBP4xtgwVpiCd5BWfsHVLSwW3A', + }, + ], + capabilityInvocation: [ + 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + ], + capabilityDelegation: [ + 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + ], + id: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', +} diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC729nNiUKQ4pHHNYovae25gkkuvtsZmtpjnLYUj1r8Yd4ZRn3FaswicUWs2NYNuWXxQ7MgzAX7dqXxAFZXFvn2jhqGKpjm5xLwESYfhcDGdSrc9mgfu51w939BjmKmng5HvYK.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC729nNiUKQ4pHHNYovae25gkkuvtsZmtpjnLYUj1r8Yd4ZRn3FaswicUWs2NYNuWXxQ7MgzAX7dqXxAFZXFvn2jhqGKpjm5xLwESYfhcDGdSrc9mgfu51w939BjmKmng5HvYK.ts new file mode 100644 index 0000000000..46ae84b94e --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC729nNiUKQ4pHHNYovae25gkkuvtsZmtpjnLYUj1r8Yd4ZRn3FaswicUWs2NYNuWXxQ7MgzAX7dqXxAFZXFvn2jhqGKpjm5xLwESYfhcDGdSrc9mgfu51w939BjmKmng5HvYK.ts @@ -0,0 +1,30 @@ +export const DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4 = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + verificationMethod: [ + { + id: 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'rvmIn58iMglCOixwxv7snWjuu8ooQteghivgqrchuIDH8DbG7pzF5io_k2t5HOW1DjcsVioEXLnIdSdUz8jJQq2r-B8zyw4CEiWAM9LUPnmmRDeVFVtA0YVaLo7DdkOn', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + authentication: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + capabilityInvocation: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + capabilityDelegation: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa.ts new file mode 100644 index 0000000000..472bc1e84c --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa.ts @@ -0,0 +1,54 @@ +export const DID_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa = + { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/bbs/v1'], + alsoKnownAs: [], + controller: [], + verificationMethod: [ + { + id: 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + type: 'Bls12381G2Key2020', + controller: + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + publicKeyBase58: + 'nZZe9Nizhaz9JGpgjysaNkWGg5TNEhpib5j6WjTUHJ5K46dedUrZ57PUFZBq9Xckv8mFJjx6G6Vvj2rPspq22BagdADEEEy2F8AVLE1DhuwWC5vHFa4fUhUwxMkH7B6joqG', + publicKeyBase64: undefined, + publicKeyJwk: undefined, + publicKeyHex: undefined, + publicKeyMultibase: undefined, + publicKeyPem: undefined, + blockchainAccountId: undefined, + ethereumAddress: undefined, + }, + ], + service: [], + authentication: [ + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + ], + assertionMethod: [ + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + ], + keyAgreement: [ + { + id: 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + type: 'Bls12381G2Key2020', + controller: + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + publicKeyBase58: + 'nZZe9Nizhaz9JGpgjysaNkWGg5TNEhpib5j6WjTUHJ5K46dedUrZ57PUFZBq9Xckv8mFJjx6G6Vvj2rPspq22BagdADEEEy2F8AVLE1DhuwWC5vHFa4fUhUwxMkH7B6joqG', + publicKeyBase64: undefined, + publicKeyJwk: undefined, + publicKeyHex: undefined, + publicKeyMultibase: undefined, + publicKeyPem: undefined, + blockchainAccountId: undefined, + ethereumAddress: undefined, + }, + ], + capabilityInvocation: [ + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + ], + capabilityDelegation: [ + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + ], + id: 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD.ts new file mode 100644 index 0000000000..968aec92bc --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD.ts @@ -0,0 +1,30 @@ +export const DID_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + verificationMethod: [ + { + id: 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'hbLuuV4otX1HEALBmUGy_ryyTIcY4TsoZYm_UZPCPgITbXvn8YlvlVM_T6_D0ZrUByvZELEX6wXzKhSkCwEqawZOEhUk4iWFID4MR6nRD4icGm97LC4d58WHTfCZ5bXw', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + ], + authentication: [ + 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + ], + capabilityInvocation: [ + 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + ], + capabilityDelegation: [ + 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh.ts new file mode 100644 index 0000000000..b3072fa575 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh.ts @@ -0,0 +1,30 @@ +export const DID_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + verificationMethod: [ + { + id: 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'huBQv7qpuF5FI5bvaku1B8JSPHeHKPI-hhvcJ97I5vNdGtafbPfrPncV4NNXidkzDDASYgt22eMSVKX9Kc9iWFnPmprzDNUt1HhvtBrldXLlRegT93LOogEh7BwoKVGW', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + ], + authentication: [ + 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + ], + capabilityInvocation: [ + 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + ], + capabilityDelegation: [ + 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn.ts new file mode 100644 index 0000000000..c2861e2a1a --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn.ts @@ -0,0 +1,30 @@ +export const DID_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + verificationMethod: [ + { + id: 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'h5pno-Wq71ExNSbjZ91OJavpe0tA871-20TigCvQAs9jHtIV6KjXtX17Cmoz01dQBlPUFPOB5ILw2JeZ2MYtMOzCCYtnuour5XDuyYs6KTAXgYQ2nAlIFfmXXr9Jc48z', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + ], + authentication: [ + 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + ], + capabilityInvocation: [ + 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + ], + capabilityDelegation: [ + 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN.ts new file mode 100644 index 0000000000..3991dcd28b --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN.ts @@ -0,0 +1,27 @@ +export const DID_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/bls12381-2020/v1'], + id: 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + verificationMethod: [ + { + id: 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + type: 'Bls12381G2Key2020', + controller: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + publicKeyBase58: + 'pegxn1a43zphf3uqGT4cx1bz8Ebb9QmoSWhQyP1qYTSeRuvWLGKJ5KcqaymnSj53YhCFbjr3tJAhqcaxxZ4Lry7KxkpLeA6GVf3Zb1x999dYp3k4jQzYa1PQXC6x1uCd9s4', + }, + ], + assertionMethod: [ + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + ], + authentication: [ + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + ], + capabilityInvocation: [ + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + ], + capabilityDelegation: [ + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ.ts new file mode 100644 index 0000000000..d369808fc9 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ.ts @@ -0,0 +1,30 @@ +export const DID_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + verificationMethod: [ + { + id: 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'kSN7z0XGmPGn81aqNhL4zE-jF799YUzc7nl730o0nBsMZiZzwlqyNvemMYrWAGq5FCoaN0jpCkefgdRrMRPPD_6IK3w0g3ieFxNxdwX7NcGR8aihA9stCdTe0kx-ePJr', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + ], + authentication: [ + 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + ], + capabilityInvocation: [ + 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + ], + capabilityDelegation: [ + 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox.ts new file mode 100644 index 0000000000..5288ec249c --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox.ts @@ -0,0 +1,30 @@ +export const DID_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + verificationMethod: [ + { + id: 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'pA1LXe8EGRU8PTpXfnG3fpJoIW394wpGpx8Q3V5Keh3PUM7j_PRLbk6XN3KJTv7cFesQeo_Q-knymniIm0Ugk9-RGKn65pRIy65aMa1ACfKfGTnnnTuJP4tWRHW2BaHb', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + ], + authentication: [ + 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + ], + capabilityInvocation: [ + 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + ], + capabilityDelegation: [ + 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F.ts new file mode 100644 index 0000000000..3e4bac3b13 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F.ts @@ -0,0 +1,30 @@ +export const DID_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + verificationMethod: [ + { + id: 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'qULVOptm5i4PfW7r6Hu6wzw6BZRywAQcCi3V0q1VDidrf0bZ-rFUaP72vXRa1WkPAoWpjMjM-uYbDQJBQbgVXoFm4L5Qz3YG5ziHRGdVWChY_5TX8yV3fQOsLJDSnfZy', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + ], + authentication: [ + 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + ], + capabilityInvocation: [ + 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + ], + capabilityDelegation: [ + 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4.ts new file mode 100644 index 0000000000..46ae84b94e --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4.ts @@ -0,0 +1,30 @@ +export const DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4 = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + verificationMethod: [ + { + id: 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'rvmIn58iMglCOixwxv7snWjuu8ooQteghivgqrchuIDH8DbG7pzF5io_k2t5HOW1DjcsVioEXLnIdSdUz8jJQq2r-B8zyw4CEiWAM9LUPnmmRDeVFVtA0YVaLo7DdkOn', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + authentication: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + capabilityInvocation: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + capabilityDelegation: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + } diff --git a/packages/core/src/modules/vc/constants.ts b/packages/core/src/modules/vc/constants.ts new file mode 100644 index 0000000000..3dacc0548a --- /dev/null +++ b/packages/core/src/modules/vc/constants.ts @@ -0,0 +1,16 @@ +export const SECURITY_CONTEXT_V1_URL = 'https://w3id.org/security/v1' +export const SECURITY_CONTEXT_V2_URL = 'https://w3id.org/security/v2' +export const SECURITY_CONTEXT_V3_URL = 'https://w3id.org/security/v3-unstable' +export const SECURITY_CONTEXT_URL = SECURITY_CONTEXT_V2_URL +export const SECURITY_X25519_CONTEXT_URL = 'https://w3id.org/security/suites/x25519-2019/v1' +export const DID_V1_CONTEXT_URL = 'https://www.w3.org/ns/did/v1' +export const CREDENTIALS_CONTEXT_V1_URL = 'https://www.w3.org/2018/credentials/v1' +export const SECURITY_CONTEXT_BBS_URL = 'https://w3id.org/security/bbs/v1' +export const CREDENTIALS_ISSUER_URL = 'https://www.w3.org/2018/credentials#issuer' +export const SECURITY_PROOF_URL = 'https://w3id.org/security#proof' +export const SECURITY_SIGNATURE_URL = 'https://w3id.org/security#signature' +export const VERIFIABLE_CREDENTIAL_TYPE = 'VerifiableCredential' +export const VERIFIABLE_PRESENTATION_TYPE = 'VerifiablePresentation' +export const EXPANDED_TYPE_CREDENTIALS_CONTEXT_V1_VC_TYPE = 'https://www.w3.org/2018/credentials#VerifiableCredential' +export const SECURITY_JWS_CONTEXT_URL = 'https://w3id.org/security/suites/jws-2020/v1' +export const SECURITY_CONTEXT_SECP256k1_URL = 'https://w3id.org/security/suites/secp256k1-2019/v1' diff --git a/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts b/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts new file mode 100644 index 0000000000..24c2089567 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts @@ -0,0 +1,60 @@ +import type { KeyType } from '../../../crypto' + +import { CredoError } from '../../../error' +import { injectable, injectAll } from '../../../plugins' + +import { suites } from './libraries/jsonld-signatures' + +const LinkedDataSignature = suites.LinkedDataSignature + +export const SignatureSuiteToken = Symbol('SignatureSuiteToken') +export interface SuiteInfo { + suiteClass: typeof LinkedDataSignature + proofType: string + verificationMethodTypes: string[] + keyTypes: KeyType[] +} + +@injectable() +export class SignatureSuiteRegistry { + private suiteMapping: SuiteInfo[] + + public constructor(@injectAll(SignatureSuiteToken) suites: Array) { + this.suiteMapping = suites.filter((suite): suite is SuiteInfo => suite !== 'default') + } + + public get supportedProofTypes(): string[] { + return this.suiteMapping.map((x) => x.proofType) + } + + /** + * @deprecated recommended to always search by key type instead as that will have broader support + */ + public getByVerificationMethodType(verificationMethodType: string) { + return this.suiteMapping.find((x) => x.verificationMethodTypes.includes(verificationMethodType)) + } + + public getAllByKeyType(keyType: KeyType) { + return this.suiteMapping.filter((x) => x.keyTypes.includes(keyType)) + } + + public getByProofType(proofType: string) { + const suiteInfo = this.suiteMapping.find((x) => x.proofType === proofType) + + if (!suiteInfo) { + throw new CredoError(`No signature suite for proof type: ${proofType}`) + } + + return suiteInfo + } + + public getVerificationMethodTypesByProofType(proofType: string): string[] { + const suiteInfo = this.suiteMapping.find((suiteInfo) => suiteInfo.proofType === proofType) + + if (!suiteInfo) { + throw new CredoError(`No verification method type found for proof type: ${proofType}`) + } + + return suiteInfo.verificationMethodTypes + } +} diff --git a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts new file mode 100644 index 0000000000..3f5622af6c --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts @@ -0,0 +1,384 @@ +import type { W3cJsonLdDeriveProofOptions } from './deriveProof' +import type { AgentContext } from '../../../agent/context' +import type { Key } from '../../../crypto/Key' +import type { SingleOrArray } from '../../../utils' +import type { + W3cJsonLdSignCredentialOptions, + W3cJsonLdSignPresentationOptions, + W3cJsonLdVerifyCredentialOptions, + W3cJsonLdVerifyPresentationOptions, +} from '../W3cCredentialServiceOptions' +import type { W3cVerifyCredentialResult, W3cVerifyPresentationResult } from '../models' +import type { W3cJsonCredential } from '../models/credential/W3cJsonCredential' + +import { createWalletKeyPairClass } from '../../../crypto/WalletKeyPair' +import { CredoError } from '../../../error' +import { injectable } from '../../../plugins' +import { asArray, JsonTransformer } from '../../../utils' +import { DidsApi, VerificationMethod } from '../../dids' +import { getKeyFromVerificationMethod } from '../../dids/domain/key-type' +import { W3cCredentialsModuleConfig } from '../W3cCredentialsModuleConfig' +import { w3cDate } from '../util' + +import { SignatureSuiteRegistry } from './SignatureSuiteRegistry' +import { deriveProof } from './deriveProof' +import { assertOnlyW3cJsonLdVerifiableCredentials } from './jsonldUtil' +import jsonld from './libraries/jsonld' +import vc from './libraries/vc' +import { W3cJsonLdVerifiableCredential } from './models' +import { W3cJsonLdVerifiablePresentation } from './models/W3cJsonLdVerifiablePresentation' + +/** + * Supports signing and verification of credentials according to the [Verifiable Credential Data Model](https://www.w3.org/TR/vc-data-model) + * using [Data Integrity Proof](https://www.w3.org/TR/vc-data-model/#data-integrity-proofs). + */ +@injectable() +export class W3cJsonLdCredentialService { + private signatureSuiteRegistry: SignatureSuiteRegistry + private w3cCredentialsModuleConfig: W3cCredentialsModuleConfig + + public constructor( + signatureSuiteRegistry: SignatureSuiteRegistry, + w3cCredentialsModuleConfig: W3cCredentialsModuleConfig + ) { + this.signatureSuiteRegistry = signatureSuiteRegistry + this.w3cCredentialsModuleConfig = w3cCredentialsModuleConfig + } + + /** + * Signs a credential + */ + public async signCredential( + agentContext: AgentContext, + options: W3cJsonLdSignCredentialOptions + ): Promise { + const WalletKeyPair = createWalletKeyPairClass(agentContext.wallet) + + const signingKey = await this.getPublicKeyFromVerificationMethod(agentContext, options.verificationMethod) + const suiteInfo = this.signatureSuiteRegistry.getByProofType(options.proofType) + + if (!suiteInfo.keyTypes.includes(signingKey.keyType)) { + throw new CredoError('The key type of the verification method does not match the suite') + } + + const keyPair = new WalletKeyPair({ + controller: options.credential.issuerId, // should we check this against the verificationMethod.controller? + id: options.verificationMethod, + key: signingKey, + wallet: agentContext.wallet, + }) + + const SuiteClass = suiteInfo.suiteClass + + const suite = new SuiteClass({ + key: keyPair, + LDKeyClass: WalletKeyPair, + proof: { + verificationMethod: options.verificationMethod, + }, + useNativeCanonize: false, + date: options.created ?? w3cDate(), + }) + + const result = await vc.issue({ + credential: JsonTransformer.toJSON(options.credential), + suite: suite, + purpose: options.proofPurpose, + documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), + }) + + return JsonTransformer.fromJSON(result, W3cJsonLdVerifiableCredential) + } + + /** + * Verifies the signature(s) of a credential + * + * @param credential the credential to be verified + * @returns the verification result + */ + public async verifyCredential( + agentContext: AgentContext, + options: W3cJsonLdVerifyCredentialOptions + ): Promise { + try { + const verifyCredentialStatus = options.verifyCredentialStatus ?? true + + const suites = this.getSignatureSuitesForCredential(agentContext, options.credential) + + const verifyOptions: Record = { + credential: JsonTransformer.toJSON(options.credential), + suite: suites, + documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), + checkStatus: ({ credential }: { credential: W3cJsonCredential }) => { + // Only throw error if credentialStatus is present + if (verifyCredentialStatus && 'credentialStatus' in credential) { + throw new CredoError('Verifying credential status for JSON-LD credentials is currently not supported') + } + return { + verified: true, + } + }, + } + + // this is a hack because vcjs throws if purpose is passed as undefined or null + if (options.proofPurpose) { + verifyOptions['purpose'] = options.proofPurpose + } + + const result = await vc.verifyCredential(verifyOptions) + + const { verified: isValid, ...remainingResult } = result + + if (!isValid) { + agentContext.config.logger.debug(`Credential verification failed: ${result.error?.message}`, { + stack: result.error?.stack, + }) + } + + // We map the result to our own result type to make it easier to work with + // however, for now we just add a single vcJs validation result as we don't + // have access to the internal validation results of vc-js + return { + isValid, + validations: { + vcJs: { + isValid, + ...remainingResult, + }, + }, + error: result.error, + } + } catch (error) { + return { + isValid: false, + validations: {}, + error, + } + } + } + + /** + * Signs a presentation including the credentials it includes + * + * @param presentation the presentation to be signed + * @returns the signed presentation + */ + public async signPresentation( + agentContext: AgentContext, + options: W3cJsonLdSignPresentationOptions + ): Promise { + // create keyPair + const WalletKeyPair = createWalletKeyPairClass(agentContext.wallet) + + const suiteInfo = this.signatureSuiteRegistry.getByProofType(options.proofType) + + if (!suiteInfo) { + throw new CredoError(`The requested proofType ${options.proofType} is not supported`) + } + + const signingKey = await this.getPublicKeyFromVerificationMethod(agentContext, options.verificationMethod) + + if (!suiteInfo.keyTypes.includes(signingKey.keyType)) { + throw new CredoError('The key type of the verification method does not match the suite') + } + + const documentLoader = this.w3cCredentialsModuleConfig.documentLoader(agentContext) + const verificationMethodObject = (await documentLoader(options.verificationMethod)).document as Record< + string, + unknown + > + + const keyPair = new WalletKeyPair({ + controller: verificationMethodObject['controller'] as string, + id: options.verificationMethod, + key: signingKey, + wallet: agentContext.wallet, + }) + + const suite = new suiteInfo.suiteClass({ + LDKeyClass: WalletKeyPair, + proof: { + verificationMethod: options.verificationMethod, + }, + date: new Date().toISOString(), + key: keyPair, + useNativeCanonize: false, + }) + + const result = await vc.signPresentation({ + presentation: JsonTransformer.toJSON(options.presentation), + suite: suite, + challenge: options.challenge, + domain: options.domain, + documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), + }) + + return JsonTransformer.fromJSON(result, W3cJsonLdVerifiablePresentation) + } + + /** + * Verifies a presentation including the credentials it includes + * + * @param presentation the presentation to be verified + * @returns the verification result + */ + public async verifyPresentation( + agentContext: AgentContext, + options: W3cJsonLdVerifyPresentationOptions + ): Promise { + try { + // create keyPair + const WalletKeyPair = createWalletKeyPairClass(agentContext.wallet) + + let proofs = options.presentation.proof + + if (!Array.isArray(proofs)) { + proofs = [proofs] + } + if (options.purpose) { + proofs = proofs.filter((proof) => proof.proofPurpose === options.purpose.term) + } + + const presentationSuites = proofs.map((proof) => { + const SuiteClass = this.signatureSuiteRegistry.getByProofType(proof.type).suiteClass + return new SuiteClass({ + LDKeyClass: WalletKeyPair, + proof: { + verificationMethod: proof.verificationMethod, + }, + date: proof.created, + useNativeCanonize: false, + }) + }) + + const credentials = asArray(options.presentation.verifiableCredential) + assertOnlyW3cJsonLdVerifiableCredentials(credentials) + + const credentialSuites = credentials.map((credential) => + this.getSignatureSuitesForCredential(agentContext, credential) + ) + const allSuites = presentationSuites.concat(...credentialSuites) + + const verifyOptions: Record = { + presentation: JsonTransformer.toJSON(options.presentation), + suite: allSuites, + challenge: options.challenge, + domain: options.domain, + documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), + } + + // this is a hack because vcjs throws if purpose is passed as undefined or null + if (options.purpose) { + verifyOptions['presentationPurpose'] = options.purpose + } + + const result = await vc.verify(verifyOptions) + + const { verified: isValid, ...remainingResult } = result + + // We map the result to our own result type to make it easier to work with + // however, for now we just add a single vcJs validation result as we don't + // have access to the internal validation results of vc-js + return { + isValid, + validations: { + vcJs: { + isValid, + ...remainingResult, + }, + }, + error: result.error, + } + } catch (error) { + return { + isValid: false, + validations: {}, + error, + } + } + } + + public async deriveProof( + agentContext: AgentContext, + options: W3cJsonLdDeriveProofOptions + ): Promise { + // TODO: make suite dynamic + const suiteInfo = this.signatureSuiteRegistry.getByProofType('BbsBlsSignatureProof2020') + const SuiteClass = suiteInfo.suiteClass + + const suite = new SuiteClass() + + const proof = await deriveProof(JsonTransformer.toJSON(options.credential), options.revealDocument, { + suite: suite, + documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), + }) + + return proof + } + + public getVerificationMethodTypesByProofType(proofType: string): string[] { + return this.signatureSuiteRegistry.getByProofType(proofType).verificationMethodTypes + } + + public getKeyTypesByProofType(proofType: string): string[] { + return this.signatureSuiteRegistry.getByProofType(proofType).keyTypes + } + + public async getExpandedTypesForCredential(agentContext: AgentContext, credential: W3cJsonLdVerifiableCredential) { + // Get the expanded types + const expandedTypes: SingleOrArray = ( + await jsonld.expand(JsonTransformer.toJSON(credential), { + documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), + }) + )[0]['@type'] + + return asArray(expandedTypes) + } + + private async getPublicKeyFromVerificationMethod( + agentContext: AgentContext, + verificationMethod: string + ): Promise { + if (!verificationMethod.startsWith('did:')) { + const documentLoader = this.w3cCredentialsModuleConfig.documentLoader(agentContext) + const verificationMethodObject = await documentLoader(verificationMethod) + const verificationMethodClass = JsonTransformer.fromJSON(verificationMethodObject.document, VerificationMethod) + + const key = getKeyFromVerificationMethod(verificationMethodClass) + return key + } else { + const [did, keyid] = verificationMethod.split('#') + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const doc = await didsApi.resolve(did) + if (doc.didDocument) { + const verificationMethodClass = doc.didDocument.dereferenceKey(keyid) + return getKeyFromVerificationMethod(verificationMethodClass) + } + throw new CredoError(`Could not resolve verification method with id ${verificationMethod}`) + } + } + + private getSignatureSuitesForCredential(agentContext: AgentContext, credential: W3cJsonLdVerifiableCredential) { + const WalletKeyPair = createWalletKeyPairClass(agentContext.wallet) + + let proofs = credential.proof + + if (!Array.isArray(proofs)) { + proofs = [proofs] + } + + return proofs.map((proof) => { + const SuiteClass = this.signatureSuiteRegistry.getByProofType(proof.type)?.suiteClass + if (SuiteClass) { + return new SuiteClass({ + LDKeyClass: WalletKeyPair, + proof: { + verificationMethod: proof.verificationMethod, + }, + date: proof.created, + useNativeCanonize: false, + }) + } + }) + } +} diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts b/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts new file mode 100644 index 0000000000..b6170844f7 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts @@ -0,0 +1,338 @@ +import type { AgentContext } from '../../../../agent' +import type { Wallet } from '../../../../wallet' + +import { InMemoryWallet } from '../../../../../../../tests/InMemoryWallet' +import { getAgentConfig, getAgentContext } from '../../../../../tests/helpers' +import { KeyType } from '../../../../crypto' +import { asArray, TypedArrayEncoder } from '../../../../utils' +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { WalletError } from '../../../../wallet/error' +import { + DidKey, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, +} from '../../../dids' +import { W3cCredentialsModuleConfig } from '../../W3cCredentialsModuleConfig' +import { ClaimFormat, W3cCredential } from '../../models' +import { W3cPresentation } from '../../models/presentation/W3cPresentation' +import { SignatureSuiteRegistry } from '../SignatureSuiteRegistry' +import { W3cJsonLdCredentialService } from '../W3cJsonLdCredentialService' +import { W3cJsonLdVerifiableCredential } from '../models' +import { LinkedDataProof } from '../models/LinkedDataProof' +import { W3cJsonLdVerifiablePresentation } from '../models/W3cJsonLdVerifiablePresentation' +import { CredentialIssuancePurpose } from '../proof-purposes/CredentialIssuancePurpose' +import { Ed25519Signature2018 } from '../signature-suites' + +import { customDocumentLoader } from './documentLoader' +import { Ed25519Signature2018Fixtures } from './fixtures' + +const signatureSuiteRegistry = new SignatureSuiteRegistry([ + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + + verificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ], + keyTypes: [KeyType.Ed25519], + }, +]) + +const agentConfig = getAgentConfig('W3cJsonLdCredentialServiceTest') + +describe('W3cJsonLdCredentialsService', () => { + let wallet: Wallet + let agentContext: AgentContext + let w3cJsonLdCredentialService: W3cJsonLdCredentialService + const privateKey = TypedArrayEncoder.fromString('testseed000000000000000000000001') + + beforeAll(async () => { + wallet = new InMemoryWallet() + await wallet.createAndOpen(agentConfig.walletConfig) + agentContext = getAgentContext({ + agentConfig, + wallet, + }) + w3cJsonLdCredentialService = new W3cJsonLdCredentialService( + signatureSuiteRegistry, + new W3cCredentialsModuleConfig({ + documentLoader: customDocumentLoader, + }) + ) + }) + + afterAll(async () => { + await wallet.delete() + }) + + describe('Utility methods', () => { + describe('getKeyTypesByProofType', () => { + it('should return the correct key types for Ed25519Signature2018 proof type', async () => { + const keyTypes = w3cJsonLdCredentialService.getKeyTypesByProofType('Ed25519Signature2018') + expect(keyTypes).toEqual([KeyType.Ed25519]) + }) + }) + + describe('getVerificationMethodTypesByProofType', () => { + it('should return the correct key types for Ed25519Signature2018 proof type', async () => { + const verificationMethodTypes = + w3cJsonLdCredentialService.getVerificationMethodTypesByProofType('Ed25519Signature2018') + expect(verificationMethodTypes).toEqual([ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ]) + }) + }) + }) + + describe('Ed25519Signature2018', () => { + let issuerDidKey: DidKey + let verificationMethod: string + beforeAll(async () => { + // TODO: update to use did registrar + const issuerKey = await wallet.createKey({ + keyType: KeyType.Ed25519, + privateKey, + }) + issuerDidKey = new DidKey(issuerKey) + verificationMethod = `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}` + }) + + describe('signCredential', () => { + it('should return a successfully signed credential', async () => { + const credentialJson = Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT + + const credential = JsonTransformer.fromJSON(credentialJson, W3cCredential) + + const vc = await w3cJsonLdCredentialService.signCredential(agentContext, { + format: ClaimFormat.LdpVc, + credential, + proofType: 'Ed25519Signature2018', + verificationMethod: verificationMethod, + }) + + expect(vc).toBeInstanceOf(W3cJsonLdVerifiableCredential) + expect(vc.issuer).toEqual(issuerDidKey.did) + expect(Array.isArray(vc.proof)).toBe(false) + expect(vc.proof).toBeInstanceOf(LinkedDataProof) + + expect(asArray(vc.proof)[0].verificationMethod).toEqual(verificationMethod) + }) + + it('should throw because of verificationMethod does not belong to this wallet', async () => { + const credentialJson = Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT + credentialJson.issuer = issuerDidKey.did + + const credential = JsonTransformer.fromJSON(credentialJson, W3cCredential) + + expect(async () => { + await w3cJsonLdCredentialService.signCredential(agentContext, { + format: ClaimFormat.LdpVc, + credential, + proofType: 'Ed25519Signature2018', + verificationMethod: + 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + }) + }).rejects.toThrowError(WalletError) + }) + }) + + describe('verifyCredential', () => { + it('should verify a credential successfully', async () => { + const vc = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ) + const result = await w3cJsonLdCredentialService.verifyCredential(agentContext, { credential: vc }) + + expect(result).toEqual({ + isValid: true, + error: undefined, + validations: { + vcJs: { + isValid: true, + results: expect.any(Array), + log: [ + { + id: 'expiration', + valid: true, + }, + { + id: 'valid_signature', + valid: true, + }, + { + id: 'issuer_did_resolves', + valid: true, + }, + { + id: 'revocation_status', + valid: true, + }, + ], + statusResult: { + verified: true, + }, + }, + }, + }) + }) + + it('should fail because of invalid signature', async () => { + const vc = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_BAD_SIGNED, + W3cJsonLdVerifiableCredential + ) + const result = await w3cJsonLdCredentialService.verifyCredential(agentContext, { credential: vc }) + + expect(result).toEqual({ + isValid: false, + error: expect.any(Error), + validations: { + vcJs: { + error: expect.any(Error), + isValid: false, + results: expect.any(Array), + }, + }, + }) + }) + + it('should fail because of an unsigned statement', async () => { + const vcJson = { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + credentialSubject: { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED.credentialSubject, + alumniOf: 'oops', + }, + } + + const vc = JsonTransformer.fromJSON(vcJson, W3cJsonLdVerifiableCredential) + const result = await w3cJsonLdCredentialService.verifyCredential(agentContext, { credential: vc }) + + expect(result).toEqual({ + isValid: false, + error: expect.any(Error), + validations: { + vcJs: { + error: expect.any(Error), + isValid: false, + results: expect.any(Array), + }, + }, + }) + }) + + it('should fail because of a changed statement', async () => { + const vcJson = { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + credentialSubject: { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED.credentialSubject, + degree: { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED.credentialSubject.degree, + name: 'oops', + }, + }, + } + + const vc = JsonTransformer.fromJSON(vcJson, W3cJsonLdVerifiableCredential) + const result = await w3cJsonLdCredentialService.verifyCredential(agentContext, { credential: vc }) + + expect(result).toEqual({ + isValid: false, + error: expect.any(Error), + validations: { + vcJs: { + error: expect.any(Error), + isValid: false, + results: expect.any(Array), + }, + }, + }) + }) + }) + + describe('signPresentation', () => { + it('should successfully create a presentation from single verifiable credential', async () => { + const presentation = JsonTransformer.fromJSON(Ed25519Signature2018Fixtures.TEST_VP_DOCUMENT, W3cPresentation) + + const purpose = new CredentialIssuancePurpose({ + controller: { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + }, + date: new Date().toISOString(), + }) + + const verifiablePresentation = await w3cJsonLdCredentialService.signPresentation(agentContext, { + format: ClaimFormat.LdpVp, + presentation: presentation, + proofPurpose: purpose, + proofType: 'Ed25519Signature2018', + challenge: '7bf32d0b-39d4-41f3-96b6-45de52988e4c', + domain: 'issuer.example.com', + verificationMethod: verificationMethod, + }) + + expect(verifiablePresentation).toBeInstanceOf(W3cJsonLdVerifiablePresentation) + }) + }) + + describe('verifyPresentation', () => { + it('should successfully verify a presentation containing a single verifiable credential', async () => { + const vp = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_VP_DOCUMENT_SIGNED, + W3cJsonLdVerifiablePresentation + ) + + const result = await w3cJsonLdCredentialService.verifyPresentation(agentContext, { + presentation: vp, + challenge: '7bf32d0b-39d4-41f3-96b6-45de52988e4c', + }) + + expect(result).toEqual({ + isValid: true, + error: undefined, + validations: { + vcJs: { + isValid: true, + presentationResult: expect.any(Object), + credentialResults: expect.any(Array), + }, + }, + }) + }) + + it('should fail when presentation signature is not valid', async () => { + const vp = JsonTransformer.fromJSON( + { + ...Ed25519Signature2018Fixtures.TEST_VP_DOCUMENT_SIGNED, + proof: { + ...Ed25519Signature2018Fixtures.TEST_VP_DOCUMENT_SIGNED.proof, + jws: Ed25519Signature2018Fixtures.TEST_VP_DOCUMENT_SIGNED.proof.jws + 'a', + }, + }, + W3cJsonLdVerifiablePresentation + ) + + const result = await w3cJsonLdCredentialService.verifyPresentation(agentContext, { + presentation: vp, + challenge: '7bf32d0b-39d4-41f3-96b6-45de52988e4c', + }) + + expect(result).toEqual({ + isValid: false, + error: expect.any(Error), + validations: { + vcJs: { + isValid: false, + credentialResults: expect.any(Array), + presentationResult: expect.any(Object), + error: expect.any(Error), + }, + }, + }) + }) + }) + }) +}) diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/contexts/citizenship_v1.ts b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/citizenship_v1.ts new file mode 100644 index 0000000000..13eb72776c --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/citizenship_v1.ts @@ -0,0 +1,45 @@ +export const CITIZENSHIP_V1 = { + '@context': { + '@version': 1.1, + '@protected': true, + name: 'http://schema.org/name', + description: 'http://schema.org/description', + identifier: 'http://schema.org/identifier', + image: { '@id': 'http://schema.org/image', '@type': '@id' }, + PermanentResidentCard: { + '@id': 'https://w3id.org/citizenship#PermanentResidentCard', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + name: 'http://schema.org/name', + identifier: 'http://schema.org/identifier', + image: { '@id': 'http://schema.org/image', '@type': '@id' }, + }, + }, + PermanentResident: { + '@id': 'https://w3id.org/citizenship#PermanentResident', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + ctzn: 'https://w3id.org/citizenship#', + schema: 'http://schema.org/', + xsd: 'http://www.w3.org/2001/XMLSchema#', + birthCountry: 'ctzn:birthCountry', + birthDate: { '@id': 'schema:birthDate', '@type': 'xsd:dateTime' }, + commuterClassification: 'ctzn:commuterClassification', + familyName: 'schema:familyName', + gender: 'schema:gender', + givenName: 'schema:givenName', + lprCategory: 'ctzn:lprCategory', + lprNumber: 'ctzn:lprNumber', + residentSince: { '@id': 'ctzn:residentSince', '@type': 'xsd:dateTime' }, + }, + }, + Person: 'http://schema.org/Person', + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/contexts/citizenship_v2.ts b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/citizenship_v2.ts new file mode 100644 index 0000000000..667571bea2 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/citizenship_v2.ts @@ -0,0 +1,45 @@ +export const CITIZENSHIP_V2 = { + '@context': { + '@version': 1.1, + '@protected': true, + name: 'http://schema.org/name', + description: 'http://schema.org/description', + identifier: 'http://schema.org/identifier', + image: { '@id': 'http://schema.org/image', '@type': '@id' }, + PermanentResidentCard: { + '@id': 'https://w3id.org/citizenship#PermanentResidentCard', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + name: 'http://schema.org/name', + identifier: 'http://schema.org/identifier', + image: { '@id': 'http://schema.org/image', '@type': '@id' }, + }, + }, + PermanentResident: { + '@id': 'https://w3id.org/citizenship#PermanentResident', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + ctzn: 'https://w3id.org/citizenship#', + schema: 'http://schema.org/', + xsd: 'http://www.w3.org/2001/XMLSchema#', + birthCountry: 'ctzn:birthCountry', + birthDate: { '@id': 'schema:birthDate', '@type': 'xsd:dateTime' }, + commuterClassification: 'ctzn:commuterClassification', + familyName: 'schema:familyName', + gender: 'schema:gender', + givenName: 'schema:givenName', + lprCategory: 'ctzn:lprCategory', + lprNumber: 'ctzn:lprNumber', + residentSince: { '@id': 'ctzn:residentSince', '@type': 'xsd:dateTime' }, + }, + }, + Person: 'http://schema.org/Person', + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/contexts/examples_v1.ts b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/examples_v1.ts new file mode 100644 index 0000000000..c0894b4553 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/examples_v1.ts @@ -0,0 +1,46 @@ +export const EXAMPLES_V1 = { + '@context': [ + { '@version': 1.1 }, + 'https://www.w3.org/ns/odrl.jsonld', + { + ex: 'https://example.org/examples#', + schema: 'http://schema.org/', + rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + '3rdPartyCorrelation': 'ex:3rdPartyCorrelation', + AllVerifiers: 'ex:AllVerifiers', + Archival: 'ex:Archival', + BachelorDegree: 'ex:BachelorDegree', + Child: 'ex:Child', + CLCredentialDefinition2019: 'ex:CLCredentialDefinition2019', + CLSignature2019: 'ex:CLSignature2019', + IssuerPolicy: 'ex:IssuerPolicy', + HolderPolicy: 'ex:HolderPolicy', + Mother: 'ex:Mother', + RelationshipCredential: 'ex:RelationshipCredential', + UniversityDegreeCredential: 'ex:UniversityDegreeCredential', + ZkpExampleSchema2018: 'ex:ZkpExampleSchema2018', + issuerData: 'ex:issuerData', + attributes: 'ex:attributes', + signature: 'ex:signature', + signatureCorrectnessProof: 'ex:signatureCorrectnessProof', + primaryProof: 'ex:primaryProof', + nonRevocationProof: 'ex:nonRevocationProof', + alumniOf: { '@id': 'schema:alumniOf', '@type': 'rdf:HTML' }, + child: { '@id': 'ex:child', '@type': '@id' }, + degree: 'ex:degree', + degreeType: 'ex:degreeType', + degreeSchool: 'ex:degreeSchool', + college: 'ex:college', + name: { '@id': 'schema:name', '@type': 'rdf:HTML' }, + givenName: 'schema:givenName', + familyName: 'schema:familyName', + parent: { '@id': 'ex:parent', '@type': '@id' }, + referenceId: 'ex:referenceId', + documentPresence: 'ex:documentPresence', + evidenceDocument: 'ex:evidenceDocument', + spouse: 'schema:spouse', + subjectPresence: 'ex:subjectPresence', + verifier: { '@id': 'ex:verifier', '@type': '@id' }, + }, + ], +} diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/contexts/index.ts b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/index.ts new file mode 100644 index 0000000000..42a043c93e --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/index.ts @@ -0,0 +1,5 @@ +export * from './citizenship_v1' +export * from './examples_v1' +export * from './security_v3_unstable' +export * from './vaccination_v1' +export * from './vaccination_v2' diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/contexts/mattr_vc_extension_v1.ts b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/mattr_vc_extension_v1.ts new file mode 100644 index 0000000000..aaadf21bb5 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/mattr_vc_extension_v1.ts @@ -0,0 +1,17 @@ +export const MATTR_VC_EXTENSION_V1 = { + '@context': { + '@version': 1.1, + '@protected': true, + VerifiableCredentialExtension: { + '@id': 'https://mattr.global/contexts/vc-extensions/v1#VerifiableCredentialExtension', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + name: 'https://mattr.global/contexts/vc-extensions/v1#name', + description: 'https://mattr.global/contexts/vc-extensions/v1#description', + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/contexts/security_v3_unstable.ts b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/security_v3_unstable.ts new file mode 100644 index 0000000000..e24d51defe --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/security_v3_unstable.ts @@ -0,0 +1,680 @@ +export const SECURITY_V3_UNSTABLE = { + '@context': [ + { + '@version': 1.1, + id: '@id', + type: '@type', + '@protected': true, + JsonWebKey2020: { '@id': 'https://w3id.org/security#JsonWebKey2020' }, + JsonWebSignature2020: { + '@id': 'https://w3id.org/security#JsonWebSignature2020', + '@context': { + '@version': 1.1, + id: '@id', + type: '@type', + '@protected': true, + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + jws: 'https://w3id.org/security#jws', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + Ed25519VerificationKey2020: { + '@id': 'https://w3id.org/security#Ed25519VerificationKey2020', + }, + Ed25519Signature2020: { + '@id': 'https://w3id.org/security#Ed25519Signature2020', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: { + '@id': 'https://w3id.org/security#proofValue', + '@type': 'https://w3id.org/security#multibase', + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + publicKeyJwk: { + '@id': 'https://w3id.org/security#publicKeyJwk', + '@type': '@json', + }, + ethereumAddress: { '@id': 'https://w3id.org/security#ethereumAddress' }, + publicKeyHex: { '@id': 'https://w3id.org/security#publicKeyHex' }, + blockchainAccountId: { + '@id': 'https://w3id.org/security#blockchainAccountId', + }, + MerkleProof2019: { '@id': 'https://w3id.org/security#MerkleProof2019' }, + Bls12381G1Key2020: { '@id': 'https://w3id.org/security#Bls12381G1Key2020' }, + Bls12381G2Key2020: { '@id': 'https://w3id.org/security#Bls12381G2Key2020' }, + BbsBlsSignature2020: { + '@id': 'https://w3id.org/security#BbsBlsSignature2020', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + BbsBlsSignatureProof2020: { + '@id': 'https://w3id.org/security#BbsBlsSignatureProof2020', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + EcdsaKoblitzSignature2016: 'https://w3id.org/security#EcdsaKoblitzSignature2016', + Ed25519Signature2018: { + '@id': 'https://w3id.org/security#Ed25519Signature2018', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + jws: 'https://w3id.org/security#jws', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + EncryptedMessage: 'https://w3id.org/security#EncryptedMessage', + GraphSignature2012: 'https://w3id.org/security#GraphSignature2012', + LinkedDataSignature2015: 'https://w3id.org/security#LinkedDataSignature2015', + LinkedDataSignature2016: 'https://w3id.org/security#LinkedDataSignature2016', + CryptographicKey: 'https://w3id.org/security#Key', + authenticationTag: 'https://w3id.org/security#authenticationTag', + canonicalizationAlgorithm: 'https://w3id.org/security#canonicalizationAlgorithm', + cipherAlgorithm: 'https://w3id.org/security#cipherAlgorithm', + cipherData: 'https://w3id.org/security#cipherData', + cipherKey: 'https://w3id.org/security#cipherKey', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + creator: { '@id': 'http://purl.org/dc/terms/creator', '@type': '@id' }, + digestAlgorithm: 'https://w3id.org/security#digestAlgorithm', + digestValue: 'https://w3id.org/security#digestValue', + domain: 'https://w3id.org/security#domain', + encryptionKey: 'https://w3id.org/security#encryptionKey', + expiration: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + initializationVector: 'https://w3id.org/security#initializationVector', + iterationCount: 'https://w3id.org/security#iterationCount', + nonce: 'https://w3id.org/security#nonce', + normalizationAlgorithm: 'https://w3id.org/security#normalizationAlgorithm', + owner: 'https://w3id.org/security#owner', + password: 'https://w3id.org/security#password', + privateKey: 'https://w3id.org/security#privateKey', + privateKeyPem: 'https://w3id.org/security#privateKeyPem', + publicKey: 'https://w3id.org/security#publicKey', + publicKeyBase58: 'https://w3id.org/security#publicKeyBase58', + publicKeyPem: 'https://w3id.org/security#publicKeyPem', + publicKeyWif: 'https://w3id.org/security#publicKeyWif', + publicKeyService: 'https://w3id.org/security#publicKeyService', + revoked: { + '@id': 'https://w3id.org/security#revoked', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + salt: 'https://w3id.org/security#salt', + signature: 'https://w3id.org/security#signature', + signatureAlgorithm: 'https://w3id.org/security#signingAlgorithm', + signatureValue: 'https://w3id.org/security#signatureValue', + proofValue: 'https://w3id.org/security#proofValue', + AesKeyWrappingKey2019: 'https://w3id.org/security#AesKeyWrappingKey2019', + DeleteKeyOperation: 'https://w3id.org/security#DeleteKeyOperation', + DeriveSecretOperation: 'https://w3id.org/security#DeriveSecretOperation', + EcdsaSecp256k1Signature2019: { + '@id': 'https://w3id.org/security#EcdsaSecp256k1Signature2019', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + jws: 'https://w3id.org/security#jws', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + EcdsaSecp256r1Signature2019: { + '@id': 'https://w3id.org/security#EcdsaSecp256r1Signature2019', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + jws: 'https://w3id.org/security#jws', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + EcdsaSecp256k1VerificationKey2019: 'https://w3id.org/security#EcdsaSecp256k1VerificationKey2019', + EcdsaSecp256r1VerificationKey2019: 'https://w3id.org/security#EcdsaSecp256r1VerificationKey2019', + Ed25519VerificationKey2018: 'https://w3id.org/security#Ed25519VerificationKey2018', + EquihashProof2018: 'https://w3id.org/security#EquihashProof2018', + ExportKeyOperation: 'https://w3id.org/security#ExportKeyOperation', + GenerateKeyOperation: 'https://w3id.org/security#GenerateKeyOperation', + KmsOperation: 'https://w3id.org/security#KmsOperation', + RevokeKeyOperation: 'https://w3id.org/security#RevokeKeyOperation', + RsaSignature2018: { + '@id': 'https://w3id.org/security#RsaSignature2018', + '@context': { + '@protected': true, + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + jws: 'https://w3id.org/security#jws', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + RsaVerificationKey2018: 'https://w3id.org/security#RsaVerificationKey2018', + Sha256HmacKey2019: 'https://w3id.org/security#Sha256HmacKey2019', + SignOperation: 'https://w3id.org/security#SignOperation', + UnwrapKeyOperation: 'https://w3id.org/security#UnwrapKeyOperation', + VerifyOperation: 'https://w3id.org/security#VerifyOperation', + WrapKeyOperation: 'https://w3id.org/security#WrapKeyOperation', + X25519KeyAgreementKey2019: 'https://w3id.org/security#X25519KeyAgreementKey2019', + allowedAction: 'https://w3id.org/security#allowedAction', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capability: { + '@id': 'https://w3id.org/security#capability', + '@type': '@id', + }, + capabilityAction: 'https://w3id.org/security#capabilityAction', + capabilityChain: { + '@id': 'https://w3id.org/security#capabilityChain', + '@type': '@id', + '@container': '@list', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + caveat: { + '@id': 'https://w3id.org/security#caveat', + '@type': '@id', + '@container': '@set', + }, + challenge: 'https://w3id.org/security#challenge', + ciphertext: 'https://w3id.org/security#ciphertext', + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + delegator: { '@id': 'https://w3id.org/security#delegator', '@type': '@id' }, + equihashParameterK: { + '@id': 'https://w3id.org/security#equihashParameterK', + '@type': 'http://www.w3.org/2001/XMLSchema#:integer', + }, + equihashParameterN: { + '@id': 'https://w3id.org/security#equihashParameterN', + '@type': 'http://www.w3.org/2001/XMLSchema#:integer', + }, + invocationTarget: { + '@id': 'https://w3id.org/security#invocationTarget', + '@type': '@id', + }, + invoker: { '@id': 'https://w3id.org/security#invoker', '@type': '@id' }, + jws: 'https://w3id.org/security#jws', + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + kmsModule: { '@id': 'https://w3id.org/security#kmsModule' }, + parentCapability: { + '@id': 'https://w3id.org/security#parentCapability', + '@type': '@id', + }, + plaintext: 'https://w3id.org/security#plaintext', + proof: { + '@id': 'https://w3id.org/security#proof', + '@type': '@id', + '@container': '@graph', + }, + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + referenceId: 'https://w3id.org/security#referenceId', + unwrappedKey: 'https://w3id.org/security#unwrappedKey', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + verifyData: 'https://w3id.org/security#verifyData', + wrappedKey: 'https://w3id.org/security#wrappedKey', + }, + ], +} diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/contexts/vaccination_v1.ts b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/vaccination_v1.ts new file mode 100644 index 0000000000..ce7d65f499 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/vaccination_v1.ts @@ -0,0 +1,88 @@ +export const VACCINATION_V1 = { + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + identifier: 'http://schema.org/identifier', + name: 'http://schema.org/name', + image: 'http://schema.org/image', + VaccinationCertificate: { + '@id': 'https://w3id.org/vaccination#VaccinationCertificate', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + identifier: 'http://schema.org/identifier', + name: 'http://schema.org/name', + image: 'http://schema.org/image', + }, + }, + VaccinationEvent: { + '@id': 'https://w3id.org/vaccination#VaccinationEvent', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + administeringCentre: 'https://w3id.org/vaccination#administeringCentre', + batchNumber: 'https://w3id.org/vaccination#batchNumber', + countryOfVaccination: 'https://w3id.org/vaccination#countryOfVaccination', + dateOfVaccination: { + '@id': 'https://w3id.org/vaccination#dateOfVaccination', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + healthProfessional: 'https://w3id.org/vaccination#healthProfessional', + nextVaccinationDate: { + '@id': 'https://w3id.org/vaccination#nextVaccinationDate', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + order: 'https://w3id.org/vaccination#order', + recipient: { + '@id': 'https://w3id.org/vaccination#recipient', + '@type': 'https://w3id.org/vaccination#VaccineRecipient', + }, + vaccine: { + '@id': 'https://w3id.org/vaccination#VaccineEventVaccine', + '@type': 'https://w3id.org/vaccination#Vaccine', + }, + }, + }, + VaccineRecipient: { + '@id': 'https://w3id.org/vaccination#VaccineRecipient', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + birthDate: { + '@id': 'http://schema.org/birthDate', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + familyName: 'http://schema.org/familyName', + gender: 'http://schema.org/gender', + givenName: 'http://schema.org/givenName', + }, + }, + Vaccine: { + '@id': 'https://w3id.org/vaccination#Vaccine', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + atcCode: 'https://w3id.org/vaccination#atc-code', + disease: 'https://w3id.org/vaccination#disease', + event: { + '@id': 'https://w3id.org/vaccination#VaccineRecipientVaccineEvent', + '@type': 'https://w3id.org/vaccination#VaccineEvent', + }, + marketingAuthorizationHolder: 'https://w3id.org/vaccination#marketingAuthorizationHolder', + medicinalProductName: 'https://w3id.org/vaccination#medicinalProductName', + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/contexts/vaccination_v2.ts b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/vaccination_v2.ts new file mode 100644 index 0000000000..483c87134b --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/__tests__/contexts/vaccination_v2.ts @@ -0,0 +1,88 @@ +export const VACCINATION_V2 = { + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + identifier: 'http://schema.org/identifier', + name: 'http://schema.org/name', + image: 'http://schema.org/image', + VaccinationCertificate: { + '@id': 'https://w3id.org/vaccination#VaccinationCertificate', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + identifier: 'http://schema.org/identifier', + name: 'http://schema.org/name', + image: 'http://schema.org/image', + }, + }, + VaccinationEvent: { + '@id': 'https://w3id.org/vaccination#VaccinationEvent', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + administeringCentre: 'https://w3id.org/vaccination#administeringCentre', + batchNumber: 'https://w3id.org/vaccination#batchNumber', + countryOfVaccination: 'https://w3id.org/vaccination#countryOfVaccination', + dateOfVaccination: { + '@id': 'https://w3id.org/vaccination#dateOfVaccination', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + healthProfessional: 'https://w3id.org/vaccination#healthProfessional', + nextVaccinationDate: { + '@id': 'https://w3id.org/vaccination#nextVaccinationDate', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + order: 'https://w3id.org/vaccination#order', + recipient: { + '@id': 'https://w3id.org/vaccination#recipient', + '@type': 'https://w3id.org/vaccination#VaccineRecipient', + }, + vaccine: { + '@id': 'https://w3id.org/vaccination#VaccineEventVaccine', + '@type': 'https://w3id.org/vaccination#Vaccine', + }, + }, + }, + VaccineRecipient: { + '@id': 'https://w3id.org/vaccination#VaccineRecipient', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + birthDate: { + '@id': 'http://schema.org/birthDate', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + familyName: 'http://schema.org/familyName', + gender: 'http://schema.org/gender', + givenName: 'http://schema.org/givenName', + }, + }, + Vaccine: { + '@id': 'https://w3id.org/vaccination#Vaccine', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + atcCode: 'https://w3id.org/vaccination#atc-code', + disease: 'https://w3id.org/vaccination#disease', + event: { + '@id': 'https://w3id.org/vaccination#VaccineRecipientVaccineEvent', + '@type': 'https://w3id.org/vaccination#VaccineEvent', + }, + marketingAuthorizationHolder: 'https://w3id.org/vaccination#marketingAuthorizationHolder', + medicinalProductName: 'https://w3id.org/vaccination#medicinalProductName', + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/documentLoader.ts b/packages/core/src/modules/vc/data-integrity/__tests__/documentLoader.ts new file mode 100644 index 0000000000..cfcaa0b285 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/__tests__/documentLoader.ts @@ -0,0 +1,120 @@ +import type { AgentContext } from '../../../../agent/context/AgentContext' +import type { JsonObject } from '../../../../types' +import type { DocumentLoaderResult } from '../libraries/jsonld' + +import { isDid } from '../../../../utils' +import { DID_EXAMPLE_48939859 } from '../../__tests__/dids/did_example_489398593' +import { DID_SOV_QqEfJxe752NCmWqR5TssZ5 } from '../../__tests__/dids/did_sov_QqEfJxe752NCmWqR5TssZ5' +import { DID_WEB_LAUNCHPAD } from '../../__tests__/dids/did_web_launchpad' +import { DID_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL } from '../../__tests__/dids/did_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL' +import { DID_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV } from '../../__tests__/dids/did_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV' +import { DID_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa } from '../../__tests__/dids/did_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa' +import { DID_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD } from '../../__tests__/dids/did_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD' +import { DID_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh } from '../../__tests__/dids/did_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh' +import { DID_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn } from '../../__tests__/dids/did_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn' +import { DID_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN } from '../../__tests__/dids/did_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN' +import { DID_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ } from '../../__tests__/dids/did_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ' +import { DID_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox } from '../../__tests__/dids/did_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox' +import { DID_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F } from '../../__tests__/dids/did_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F' +import { DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4 } from '../../__tests__/dids/did_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4' +import { SECURITY_CONTEXT_V3_URL } from '../../constants' +import { DEFAULT_CONTEXTS } from '../libraries/contexts' +import jsonld from '../libraries/jsonld' + +import { EXAMPLES_V1, VACCINATION_V1, VACCINATION_V2 } from './contexts' +import { CITIZENSHIP_V1 } from './contexts/citizenship_v1' +import { CITIZENSHIP_V2 } from './contexts/citizenship_v2' +import { MATTR_VC_EXTENSION_V1 } from './contexts/mattr_vc_extension_v1' +import { SECURITY_V3_UNSTABLE } from './contexts/security_v3_unstable' + +export const DOCUMENTS = { + ...DEFAULT_CONTEXTS, + [DID_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL['id']]: DID_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL, + [DID_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV['id']]: DID_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV, + [DID_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa[ + 'id' + ]]: + DID_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa, + [DID_EXAMPLE_48939859['id']]: DID_EXAMPLE_48939859, + [DID_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh[ + 'id' + ]]: + DID_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh, + [DID_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn[ + 'id' + ]]: + DID_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn, + [DID_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ[ + 'id' + ]]: + DID_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ, + [DID_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox[ + 'id' + ]]: + DID_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox, + [DID_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F[ + 'id' + ]]: + DID_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F, + [DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4[ + 'id' + ]]: + DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4, + [DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4[ + 'id' + ]]: + DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4, + [DID_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD[ + 'id' + ]]: + DID_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD, + [DID_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN[ + 'id' + ]]: + DID_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN, + [DID_SOV_QqEfJxe752NCmWqR5TssZ5['id']]: DID_SOV_QqEfJxe752NCmWqR5TssZ5, + [DID_WEB_LAUNCHPAD['id']]: DID_WEB_LAUNCHPAD, + [SECURITY_CONTEXT_V3_URL]: SECURITY_V3_UNSTABLE, + 'https://www.w3.org/2018/credentials/examples/v1': EXAMPLES_V1, + 'https://w3id.org/citizenship/v1': CITIZENSHIP_V1, + 'https://w3id.org/citizenship/v2': CITIZENSHIP_V2, + 'https://w3id.org/vaccination/v1': VACCINATION_V1, + 'https://w3id.org/vaccination/v2': VACCINATION_V2, + 'https://mattr.global/contexts/vc-extensions/v1': MATTR_VC_EXTENSION_V1, +} + +async function _customDocumentLoader(url: string): Promise { + let result = DOCUMENTS[url as keyof typeof DOCUMENTS] + + if (!result) { + const withoutFragment = url.split('#')[0] + result = DOCUMENTS[withoutFragment as keyof typeof DOCUMENTS] + } + + if (!result) { + throw new Error(`Document not found: ${url}`) + } + + if (isDid(url)) { + result = await jsonld.frame( + result, + { + '@context': result['@context'], + '@embed': '@never', + id: url, + }, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + { documentLoader: this } + ) + } + + return { + contextUrl: null, + documentUrl: url, + document: result as JsonObject, + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const customDocumentLoader = (agentContext?: AgentContext) => _customDocumentLoader.bind(_customDocumentLoader) diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/fixtures.ts b/packages/core/src/modules/vc/data-integrity/__tests__/fixtures.ts new file mode 100644 index 0000000000..e06481460a --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/__tests__/fixtures.ts @@ -0,0 +1,147 @@ +import { CREDENTIALS_CONTEXT_V1_URL } from '../../constants' + +export const Ed25519Signature2018Fixtures = { + TEST_LD_DOCUMENT: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + }, + TEST_LD_DOCUMENT_2: { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/suites/ed25519-2020/v1', + 'https://w3id.org/citizenship/v1', + ], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: '', + identifier: '83627465', + name: 'Permanent Resident Card', + description: 'Government of Example Permanent Resident Card.', + issuanceDate: '2019-12-03T12:19:52Z', + expirationDate: '2029-12-03T12:19:52Z', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['PermanentResident', 'Person'], + givenName: 'JOHN', + familyName: 'SMITH', + gender: 'Male', + image: 'data:image/png;base64,iVBORw0KGgokJggg==', + residentSince: '2015-01-01', + lprCategory: 'C09', + lprNumber: '999-999-999', + commuterClassification: 'C1', + birthCountry: 'Bahamas', + birthDate: '1958-07-17', + }, + }, + + TEST_LD_DOCUMENT_SIGNED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + proof: { + verificationMethod: + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519Signature2018', + created: '2022-04-18T23:13:10Z', + proofPurpose: 'assertionMethod', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..ECQsj_lABelr1jkehSkqaYpc5CBvbSjbi3ZvgiVVKxZFDYfj5xZmeXb_awa4aw_cGEVaoypeN2uCFmeG6WKkBw', + }, + }, + TEST_LD_DOCUMENT_BAD_SIGNED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + proof: { + verificationMethod: + 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + type: 'Ed25519Signature2018', + created: '2022-03-28T15:54:59Z', + proofPurpose: 'assertionMethod', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..Ej5aEUBTgeNm3_a4uO_AuNnisldnYTMMGMom4xLb-_TmoYe7467Yo046Bw2QqdfdBja6y-HBbBj4SonOlwswAg', + }, + }, + TEST_VP_DOCUMENT: { + '@context': [CREDENTIALS_CONTEXT_V1_URL], + type: ['VerifiablePresentation'], + verifiableCredential: [ + { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + proof: { + verificationMethod: + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519Signature2018', + created: '2022-04-18T23:13:10Z', + proofPurpose: 'assertionMethod', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..ECQsj_lABelr1jkehSkqaYpc5CBvbSjbi3ZvgiVVKxZFDYfj5xZmeXb_awa4aw_cGEVaoypeN2uCFmeG6WKkBw', + }, + }, + ], + }, + TEST_VP_DOCUMENT_SIGNED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL], + type: ['VerifiablePresentation'], + verifiableCredential: [ + { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + proof: { + verificationMethod: + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519Signature2018', + created: '2022-04-18T23:13:10Z', + proofPurpose: 'assertionMethod', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..ECQsj_lABelr1jkehSkqaYpc5CBvbSjbi3ZvgiVVKxZFDYfj5xZmeXb_awa4aw_cGEVaoypeN2uCFmeG6WKkBw', + }, + }, + ], + proof: { + verificationMethod: + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519Signature2018', + created: '2022-04-20T17:31:49Z', + proofPurpose: 'authentication', + challenge: '7bf32d0b-39d4-41f3-96b6-45de52988e4c', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..yNSkNCfVv6_1-P6CtldiqS2bDe_8DPKBIP3Do9qi0LF2DU_d70pWajevJIBH5NZ8K4AawDYx_irlhdz4aiH3Bw', + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/deriveProof.ts b/packages/core/src/modules/vc/data-integrity/deriveProof.ts new file mode 100644 index 0000000000..47d0dc550b --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/deriveProof.ts @@ -0,0 +1,133 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { JsonObject } from '../../../types' + +import { JsonTransformer } from '../../../utils' +import { SECURITY_PROOF_URL } from '../constants' + +import { getProofs, getTypeInfo } from './jsonldUtil' +import jsonld from './libraries/jsonld' +import { W3cJsonLdVerifiableCredential } from './models/W3cJsonLdVerifiableCredential' + +export interface W3cJsonLdDeriveProofOptions { + credential: W3cJsonLdVerifiableCredential + revealDocument: JsonObject + verificationMethod: string +} + +/** + * Derives a proof from a document featuring a supported linked data proof + * + * NOTE - This is a temporary API extending JSON-LD signatures + * + * @param proofDocument A document featuring a linked data proof capable of proof derivation + * @param revealDocument A document of the form of a JSON-LD frame describing the terms to selectively derive from the proof document + * @param options Options for proof derivation + */ +export const deriveProof = async ( + proofDocument: JsonObject, + revealDocument: JsonObject, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + { suite, skipProofCompaction, documentLoader, nonce }: any +): Promise => { + if (!suite) { + throw new TypeError('"options.suite" is required.') + } + + if (Array.isArray(proofDocument)) { + throw new TypeError('proofDocument should be an object not an array.') + } + + const { proofs, document } = await getProofs({ + document: proofDocument, + proofType: suite.supportedDeriveProofType, + documentLoader, + }) + + if (proofs.length === 0) { + throw new Error(`There were not any proofs provided that can be used to derive a proof with this suite.`) + } + + let derivedProof = await suite.deriveProof({ + document, + proof: proofs[0], + revealDocument, + documentLoader, + + nonce, + }) + + if (proofs.length > 1) { + // convert the proof property value from object ot array of objects + derivedProof = { ...derivedProof, proof: [derivedProof.proof] } + + // drop the first proof because it's already been processed + proofs.splice(0, 1) + + // add all the additional proofs to the derivedProof document + for (const proof of proofs) { + const additionalDerivedProofValue = await suite.deriveProof({ + document, + proof, + revealDocument, + documentLoader, + }) + derivedProof.proof.push(additionalDerivedProofValue.proof) + } + } + + if (!skipProofCompaction) { + /* eslint-disable prefer-const */ + let expandedProof: Record = { + [SECURITY_PROOF_URL]: { + '@graph': derivedProof.proof, + }, + } + + // account for type-scoped `proof` definition by getting document types + const { types, alias } = await getTypeInfo(derivedProof.document, { + documentLoader, + }) + + expandedProof['@type'] = types + + const ctx = jsonld.getValues(derivedProof.document, '@context') + + const compactProof = await jsonld.compact(expandedProof, ctx, { + documentLoader, + compactToRelative: false, + }) + + delete compactProof[alias] + delete compactProof['@context'] + + /** + * removes the @included tag when multiple proofs exist because the + * @included tag messes up the canonicalized bytes leading to a bad + * signature that won't verify. + **/ + if (compactProof.proof?.['@included']) { + compactProof.proof = compactProof.proof['@included'] + } + + // add proof to document + const key = Object.keys(compactProof)[0] + jsonld.addValue(derivedProof.document, key, compactProof[key]) + } else { + delete derivedProof.proof['@context'] + jsonld.addValue(derivedProof.document, 'proof', derivedProof.proof) + } + + return JsonTransformer.fromJSON(derivedProof.document, W3cJsonLdVerifiableCredential) +} diff --git a/packages/core/src/modules/vc/data-integrity/index.ts b/packages/core/src/modules/vc/data-integrity/index.ts new file mode 100644 index 0000000000..d2cfc71995 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/index.ts @@ -0,0 +1,7 @@ +export type { DocumentLoader, Proof } from './jsonldUtil' +export { SuiteInfo, SignatureSuiteToken, SignatureSuiteRegistry } from './SignatureSuiteRegistry' +export * from './signature-suites' +export * from './libraries' +export * from './models' +export * from './proof-purposes' +export * from './deriveProof' diff --git a/packages/core/src/modules/vc/data-integrity/jsonldUtil.ts b/packages/core/src/modules/vc/data-integrity/jsonldUtil.ts new file mode 100644 index 0000000000..960edc5549 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/jsonldUtil.ts @@ -0,0 +1,145 @@ +import type { GetProofsOptions } from './models/GetProofsOptions' +import type { GetProofsResult } from './models/GetProofsResult' +import type { GetTypeOptions } from './models/GetTypeOptions' +import type { JsonObject, JsonValue } from '../../../types' + +import { CredoError } from '../../../error' +import { SECURITY_CONTEXT_URL } from '../constants' + +import jsonld from './libraries/jsonld' +import { W3cJsonLdVerifiableCredential } from './models/W3cJsonLdVerifiableCredential' + +export type JsonLdDoc = Record +export interface VerificationMethod extends JsonObject { + id: string + [key: string]: JsonValue +} + +export interface Proof extends JsonObject { + verificationMethod: string | VerificationMethod + [key: string]: JsonValue +} + +export interface DocumentLoaderResult { + contextUrl?: string | null + documentUrl: string + document: Record +} + +export type DocumentLoader = (url: string) => Promise + +export const _includesContext = (options: { document: JsonLdDoc; contextUrl: string }) => { + const context = options.document['@context'] + + return context === options.contextUrl || (Array.isArray(context) && context.includes(options.contextUrl)) +} + +export function assertOnlyW3cJsonLdVerifiableCredentials( + credentials: unknown[] +): asserts credentials is W3cJsonLdVerifiableCredential[] { + if (credentials.some((c) => !(c instanceof W3cJsonLdVerifiableCredential))) { + throw new CredoError('JSON-LD VPs can only contain JSON-LD VCs') + } +} + +/* + * The code in this file originated from + * @see https://github.com/digitalbazaar/jsonld-signatures + * Hence the following copyright notice applies + * + * Copyright (c) 2017-2018 Digital Bazaar, Inc. All rights reserved. + */ + +/** + * The property identifying the linked data proof + * Note - this will not work for legacy systems that + * relying on `signature` + */ +const PROOF_PROPERTY = 'proof' + +/** + * Gets a supported linked data proof from a JSON-LD Document + * Note - unless instructed not to the document will be compacted + * against the security v2 context @see https://w3id.org/security/v2 + * + * @param options Options for extracting the proof from the document + * + * @returns {GetProofsResult} An object containing the matched proofs and the JSON-LD document + */ +export const getProofs = async (options: GetProofsOptions): Promise => { + const { proofType, skipProofCompaction, documentLoader } = options + let { document } = options + + let proofs + if (!skipProofCompaction) { + // If we must compact the proof then we must first compact the input + // document to find the proof + document = await jsonld.compact(document, SECURITY_CONTEXT_URL, { + documentLoader, + compactToRelative: false, + }) + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - needed because getValues is not part of the public API. + proofs = jsonld.getValues(document, PROOF_PROPERTY) + delete document[PROOF_PROPERTY] + + if (typeof proofType === 'string') { + proofs = proofs.filter((_: Record) => _.type == proofType) + } + if (Array.isArray(proofType)) { + proofs = proofs.filter((_: Record) => proofType.includes(_.type)) + } + + proofs = proofs.map((matchedProof: Record) => ({ + '@context': SECURITY_CONTEXT_URL, + ...matchedProof, + })) + + return { + proofs, + document, + } +} + +/** + * Gets the JSON-LD type information for a document + * @param document {any} JSON-LD document to extract the type information from + * @param options {GetTypeInfoOptions} Options for extracting the JSON-LD document + * + * @returns {object} Type info for the JSON-LD document + */ +export const getTypeInfo = async ( + document: JsonObject, + options: GetTypeOptions +): Promise<{ types: string[]; alias: string }> => { + const { documentLoader } = options + + // determine `@type` alias, if any + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - needed because getValues is not part of the public API. + const context = jsonld.getValues(document, '@context') + + const compacted = await jsonld.compact({ '@type': '_:b0' }, context, { + documentLoader, + }) + + delete compacted['@context'] + + const alias = Object.keys(compacted)[0] + + // optimize: expand only `@type` and `type` values + /* eslint-disable prefer-const */ + let toExpand: Record = { '@context': context } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - needed because getValues is not part of the public API. + toExpand['@type'] = jsonld.getValues(document, '@type').concat(jsonld.getValues(document, alias)) + + const expanded = (await jsonld.expand(toExpand, { documentLoader }))[0] || {} + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - needed because getValues is not part of the public API. + return { types: jsonld.getValues(expanded, '@type'), alias } +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/X25519_v1.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/X25519_v1.ts new file mode 100644 index 0000000000..3a5b8bf768 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/X25519_v1.ts @@ -0,0 +1,26 @@ +export const X25519_V1 = { + '@context': { + id: '@id', + type: '@type', + '@protected': true, + X25519KeyAgreementKey2019: { + '@id': 'https://w3id.org/security#X25519KeyAgreementKey2019', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + revoked: { + '@id': 'https://w3id.org/security#revoked', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + publicKeyBase58: { + '@id': 'https://w3id.org/security#publicKeyBase58', + }, + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/bbs_v1.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/bbs_v1.ts new file mode 100644 index 0000000000..897de0a4eb --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/bbs_v1.ts @@ -0,0 +1,129 @@ +export const BBS_V1 = { + '@context': { + '@version': 1.1, + id: '@id', + type: '@type', + BbsBlsSignature2020: { + '@id': 'https://w3id.org/security#BbsBlsSignature2020', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + proofValue: 'https://w3id.org/security#proofValue', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + BbsBlsSignatureProof2020: { + '@id': 'https://w3id.org/security#BbsBlsSignatureProof2020', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + Bls12381G1Key2020: { + '@id': 'https://w3id.org/security#Bls12381G1Key2020', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + revoked: { + '@id': 'https://w3id.org/security#revoked', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + publicKeyBase58: { + '@id': 'https://w3id.org/security#publicKeyBase58', + }, + }, + }, + Bls12381G2Key2020: { + '@id': 'https://w3id.org/security#Bls12381G2Key2020', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + revoked: { + '@id': 'https://w3id.org/security#revoked', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + publicKeyBase58: { + '@id': 'https://w3id.org/security#publicKeyBase58', + }, + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/credentials_v1.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/credentials_v1.ts new file mode 100644 index 0000000000..f401fb33f5 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/credentials_v1.ts @@ -0,0 +1,250 @@ +export const CREDENTIALS_V1 = { + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + VerifiableCredential: { + '@id': 'https://www.w3.org/2018/credentials#VerifiableCredential', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + cred: 'https://www.w3.org/2018/credentials#', + sec: 'https://w3id.org/security#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + credentialSchema: { + '@id': 'cred:credentialSchema', + '@type': '@id', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + cred: 'https://www.w3.org/2018/credentials#', + JsonSchemaValidator2018: 'cred:JsonSchemaValidator2018', + }, + }, + credentialStatus: { '@id': 'cred:credentialStatus', '@type': '@id' }, + credentialSubject: { '@id': 'cred:credentialSubject', '@type': '@id' }, + evidence: { '@id': 'cred:evidence', '@type': '@id' }, + expirationDate: { + '@id': 'cred:expirationDate', + '@type': 'xsd:dateTime', + }, + holder: { '@id': 'cred:holder', '@type': '@id' }, + issued: { '@id': 'cred:issued', '@type': 'xsd:dateTime' }, + issuer: { '@id': 'cred:issuer', '@type': '@id' }, + issuanceDate: { '@id': 'cred:issuanceDate', '@type': 'xsd:dateTime' }, + proof: { '@id': 'sec:proof', '@type': '@id', '@container': '@graph' }, + refreshService: { + '@id': 'cred:refreshService', + '@type': '@id', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + cred: 'https://www.w3.org/2018/credentials#', + ManualRefreshService2018: 'cred:ManualRefreshService2018', + }, + }, + termsOfUse: { '@id': 'cred:termsOfUse', '@type': '@id' }, + validFrom: { '@id': 'cred:validFrom', '@type': 'xsd:dateTime' }, + validUntil: { '@id': 'cred:validUntil', '@type': 'xsd:dateTime' }, + }, + }, + VerifiablePresentation: { + '@id': 'https://www.w3.org/2018/credentials#VerifiablePresentation', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + cred: 'https://www.w3.org/2018/credentials#', + sec: 'https://w3id.org/security#', + holder: { '@id': 'cred:holder', '@type': '@id' }, + proof: { '@id': 'sec:proof', '@type': '@id', '@container': '@graph' }, + verifiableCredential: { + '@id': 'cred:verifiableCredential', + '@type': '@id', + '@container': '@graph', + }, + }, + }, + EcdsaSecp256k1Signature2019: { + '@id': 'https://w3id.org/security#EcdsaSecp256k1Signature2019', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + challenge: 'sec:challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'xsd:dateTime', + }, + domain: 'sec:domain', + expires: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + jws: 'sec:jws', + nonce: 'sec:nonce', + proofPurpose: { + '@id': 'sec:proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + assertionMethod: { + '@id': 'sec:assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'sec:authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'sec:proofValue', + verificationMethod: { '@id': 'sec:verificationMethod', '@type': '@id' }, + }, + }, + EcdsaSecp256r1Signature2019: { + '@id': 'https://w3id.org/security#EcdsaSecp256r1Signature2019', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + challenge: 'sec:challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'xsd:dateTime', + }, + domain: 'sec:domain', + expires: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + jws: 'sec:jws', + nonce: 'sec:nonce', + proofPurpose: { + '@id': 'sec:proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + assertionMethod: { + '@id': 'sec:assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'sec:authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'sec:proofValue', + verificationMethod: { '@id': 'sec:verificationMethod', '@type': '@id' }, + }, + }, + Ed25519Signature2018: { + '@id': 'https://w3id.org/security#Ed25519Signature2018', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + challenge: 'sec:challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'xsd:dateTime', + }, + domain: 'sec:domain', + expires: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + jws: 'sec:jws', + nonce: 'sec:nonce', + proofPurpose: { + '@id': 'sec:proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + assertionMethod: { + '@id': 'sec:assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'sec:authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'sec:proofValue', + verificationMethod: { '@id': 'sec:verificationMethod', '@type': '@id' }, + }, + }, + RsaSignature2018: { + '@id': 'https://w3id.org/security#RsaSignature2018', + '@context': { + '@version': 1.1, + '@protected': true, + challenge: 'sec:challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'xsd:dateTime', + }, + domain: 'sec:domain', + expires: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + jws: 'sec:jws', + nonce: 'sec:nonce', + proofPurpose: { + '@id': 'sec:proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + assertionMethod: { + '@id': 'sec:assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'sec:authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'sec:proofValue', + verificationMethod: { '@id': 'sec:verificationMethod', '@type': '@id' }, + }, + }, + proof: { + '@id': 'https://w3id.org/security#proof', + '@type': '@id', + '@container': '@graph', + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/dataIntegrity_v2.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/dataIntegrity_v2.ts new file mode 100644 index 0000000000..d7aa6dff6d --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/dataIntegrity_v2.ts @@ -0,0 +1,81 @@ +export const DATA_INTEGRITY_V2 = { + '@context': { + id: '@id', + type: '@type', + '@protected': true, + proof: { + '@id': 'https://w3id.org/security#proof', + '@type': '@id', + '@container': '@graph', + }, + DataIntegrityProof: { + '@id': 'https://w3id.org/security#DataIntegrityProof', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + nonce: 'https://w3id.org/security#nonce', + previousProof: { + '@id': 'https://w3id.org/security#previousProof', + '@type': '@id', + }, + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + cryptosuite: { + '@id': 'https://w3id.org/security#cryptosuite', + '@type': 'https://w3id.org/security#cryptosuiteString', + }, + proofValue: { + '@id': 'https://w3id.org/security#proofValue', + '@type': 'https://w3id.org/security#multibase', + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/defaultContexts.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/defaultContexts.ts new file mode 100644 index 0000000000..210a67a04e --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/defaultContexts.ts @@ -0,0 +1,34 @@ +import { X25519_V1 } from './X25519_v1' +import { BBS_V1 } from './bbs_v1' +import { CREDENTIALS_V1 } from './credentials_v1' +import { DATA_INTEGRITY_V2 } from './dataIntegrity_v2' +import { DID_V1 } from './did_v1' +import { ED25519_V1 } from './ed25519_v1' +import { ODRL } from './odrl' +import { PURL_OB_V3P0 } from './purl_ob_v3po' +import { SCHEMA_ORG } from './schema_org' +import { SECP256K1_V1 } from './secp256k1_v1' +import { SECURITY_V1 } from './security_v1' +import { SECURITY_V2 } from './security_v2' +import { PRESENTATION_SUBMISSION } from './submission' +import { VC_REVOCATION_LIST_2020 } from './vc_revocation_list_2020' + +export const DEFAULT_CONTEXTS = { + 'https://w3id.org/security/suites/bls12381-2020/v1': BBS_V1, + 'https://w3id.org/security/bbs/v1': BBS_V1, + 'https://w3id.org/security/v1': SECURITY_V1, + 'https://w3id.org/security/v2': SECURITY_V2, + 'https://w3id.org/security/suites/x25519-2019/v1': X25519_V1, + 'https://w3id.org/security/suites/ed25519-2018/v1': ED25519_V1, + 'https://w3id.org/security/suites/secp256k1-2019/v1': SECP256K1_V1, + 'https://www.w3.org/2018/credentials/v1': CREDENTIALS_V1, + 'https://w3id.org/did/v1': DID_V1, + 'https://www.w3.org/ns/did/v1': DID_V1, + 'https://w3.org/ns/did/v1': DID_V1, + 'https://www.w3.org/ns/odrl.jsonld': ODRL, + 'http://schema.org/': SCHEMA_ORG, + 'https://identity.foundation/presentation-exchange/submission/v1': PRESENTATION_SUBMISSION, + 'https://purl.imsglobal.org/spec/ob/v3p0/context.json': PURL_OB_V3P0, + 'https://w3c-ccg.github.io/vc-status-rl-2020/contexts/vc-revocation-list-2020/v1.jsonld': VC_REVOCATION_LIST_2020, + 'https://w3id.org/security/data-integrity/v2': DATA_INTEGRITY_V2, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/did_v1.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/did_v1.ts new file mode 100644 index 0000000000..2a431389b3 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/did_v1.ts @@ -0,0 +1,56 @@ +export const DID_V1 = { + '@context': { + '@protected': true, + id: '@id', + type: '@type', + alsoKnownAs: { + '@id': 'https://www.w3.org/ns/activitystreams#alsoKnownAs', + '@type': '@id', + }, + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + service: { + '@id': 'https://www.w3.org/ns/did#service', + '@type': '@id', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + serviceEndpoint: { + '@id': 'https://www.w3.org/ns/did#serviceEndpoint', + '@type': '@id', + }, + }, + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/ed25519_v1.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/ed25519_v1.ts new file mode 100644 index 0000000000..29e09035d4 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/ed25519_v1.ts @@ -0,0 +1,91 @@ +export const ED25519_V1 = { + '@context': { + id: '@id', + type: '@type', + '@protected': true, + proof: { + '@id': 'https://w3id.org/security#proof', + '@type': '@id', + '@container': '@graph', + }, + Ed25519VerificationKey2018: { + '@id': 'https://w3id.org/security#Ed25519VerificationKey2018', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + revoked: { + '@id': 'https://w3id.org/security#revoked', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + publicKeyBase58: { + '@id': 'https://w3id.org/security#publicKeyBase58', + }, + }, + }, + Ed25519Signature2018: { + '@id': 'https://w3id.org/security#Ed25519Signature2018', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + jws: { + '@id': 'https://w3id.org/security#jws', + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/index.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/index.ts new file mode 100644 index 0000000000..e1834fa3b7 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/index.ts @@ -0,0 +1 @@ +export { DEFAULT_CONTEXTS } from './defaultContexts' diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/odrl.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/odrl.ts new file mode 100644 index 0000000000..6efea2320e --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/odrl.ts @@ -0,0 +1,181 @@ +export const ODRL = { + '@context': { + odrl: 'http://www.w3.org/ns/odrl/2/', + rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + rdfs: 'http://www.w3.org/2000/01/rdf-schema#', + owl: 'http://www.w3.org/2002/07/owl#', + skos: 'http://www.w3.org/2004/02/skos/core#', + dct: 'http://purl.org/dc/terms/', + xsd: 'http://www.w3.org/2001/XMLSchema#', + vcard: 'http://www.w3.org/2006/vcard/ns#', + foaf: 'http://xmlns.com/foaf/0.1/', + schema: 'http://schema.org/', + cc: 'http://creativecommons.org/ns#', + uid: '@id', + type: '@type', + Policy: 'odrl:Policy', + Rule: 'odrl:Rule', + profile: { '@type': '@id', '@id': 'odrl:profile' }, + inheritFrom: { '@type': '@id', '@id': 'odrl:inheritFrom' }, + ConflictTerm: 'odrl:ConflictTerm', + conflict: { '@type': '@vocab', '@id': 'odrl:conflict' }, + perm: 'odrl:perm', + prohibit: 'odrl:prohibit', + invalid: 'odrl:invalid', + Agreement: 'odrl:Agreement', + Assertion: 'odrl:Assertion', + Offer: 'odrl:Offer', + Privacy: 'odrl:Privacy', + Request: 'odrl:Request', + Set: 'odrl:Set', + Ticket: 'odrl:Ticket', + Asset: 'odrl:Asset', + AssetCollection: 'odrl:AssetCollection', + relation: { '@type': '@id', '@id': 'odrl:relation' }, + hasPolicy: { '@type': '@id', '@id': 'odrl:hasPolicy' }, + target: { '@type': '@id', '@id': 'odrl:target' }, + output: { '@type': '@id', '@id': 'odrl:output' }, + partOf: { '@type': '@id', '@id': 'odrl:partOf' }, + source: { '@type': '@id', '@id': 'odrl:source' }, + Party: 'odrl:Party', + PartyCollection: 'odrl:PartyCollection', + function: { '@type': '@vocab', '@id': 'odrl:function' }, + PartyScope: 'odrl:PartyScope', + assignee: { '@type': '@id', '@id': 'odrl:assignee' }, + assigner: { '@type': '@id', '@id': 'odrl:assigner' }, + assigneeOf: { '@type': '@id', '@id': 'odrl:assigneeOf' }, + assignerOf: { '@type': '@id', '@id': 'odrl:assignerOf' }, + attributedParty: { '@type': '@id', '@id': 'odrl:attributedParty' }, + attributingParty: { '@type': '@id', '@id': 'odrl:attributingParty' }, + compensatedParty: { '@type': '@id', '@id': 'odrl:compensatedParty' }, + compensatingParty: { '@type': '@id', '@id': 'odrl:compensatingParty' }, + consentingParty: { '@type': '@id', '@id': 'odrl:consentingParty' }, + consentedParty: { '@type': '@id', '@id': 'odrl:consentedParty' }, + informedParty: { '@type': '@id', '@id': 'odrl:informedParty' }, + informingParty: { '@type': '@id', '@id': 'odrl:informingParty' }, + trackingParty: { '@type': '@id', '@id': 'odrl:trackingParty' }, + trackedParty: { '@type': '@id', '@id': 'odrl:trackedParty' }, + contractingParty: { '@type': '@id', '@id': 'odrl:contractingParty' }, + contractedParty: { '@type': '@id', '@id': 'odrl:contractedParty' }, + Action: 'odrl:Action', + action: { '@type': '@vocab', '@id': 'odrl:action' }, + includedIn: { '@type': '@id', '@id': 'odrl:includedIn' }, + implies: { '@type': '@id', '@id': 'odrl:implies' }, + Permission: 'odrl:Permission', + permission: { '@type': '@id', '@id': 'odrl:permission' }, + Prohibition: 'odrl:Prohibition', + prohibition: { '@type': '@id', '@id': 'odrl:prohibition' }, + obligation: { '@type': '@id', '@id': 'odrl:obligation' }, + use: 'odrl:use', + grantUse: 'odrl:grantUse', + aggregate: 'odrl:aggregate', + annotate: 'odrl:annotate', + anonymize: 'odrl:anonymize', + archive: 'odrl:archive', + concurrentUse: 'odrl:concurrentUse', + derive: 'odrl:derive', + digitize: 'odrl:digitize', + display: 'odrl:display', + distribute: 'odrl:distribute', + execute: 'odrl:execute', + extract: 'odrl:extract', + give: 'odrl:give', + index: 'odrl:index', + install: 'odrl:install', + modify: 'odrl:modify', + move: 'odrl:move', + play: 'odrl:play', + present: 'odrl:present', + print: 'odrl:print', + read: 'odrl:read', + reproduce: 'odrl:reproduce', + sell: 'odrl:sell', + stream: 'odrl:stream', + textToSpeech: 'odrl:textToSpeech', + transfer: 'odrl:transfer', + transform: 'odrl:transform', + translate: 'odrl:translate', + Duty: 'odrl:Duty', + duty: { '@type': '@id', '@id': 'odrl:duty' }, + consequence: { '@type': '@id', '@id': 'odrl:consequence' }, + remedy: { '@type': '@id', '@id': 'odrl:remedy' }, + acceptTracking: 'odrl:acceptTracking', + attribute: 'odrl:attribute', + compensate: 'odrl:compensate', + delete: 'odrl:delete', + ensureExclusivity: 'odrl:ensureExclusivity', + include: 'odrl:include', + inform: 'odrl:inform', + nextPolicy: 'odrl:nextPolicy', + obtainConsent: 'odrl:obtainConsent', + reviewPolicy: 'odrl:reviewPolicy', + uninstall: 'odrl:uninstall', + watermark: 'odrl:watermark', + Constraint: 'odrl:Constraint', + LogicalConstraint: 'odrl:LogicalConstraint', + constraint: { '@type': '@id', '@id': 'odrl:constraint' }, + refinement: { '@type': '@id', '@id': 'odrl:refinement' }, + Operator: 'odrl:Operator', + operator: { '@type': '@vocab', '@id': 'odrl:operator' }, + RightOperand: 'odrl:RightOperand', + rightOperand: 'odrl:rightOperand', + rightOperandReference: { + '@type': 'xsd:anyURI', + '@id': 'odrl:rightOperandReference', + }, + LeftOperand: 'odrl:LeftOperand', + leftOperand: { '@type': '@vocab', '@id': 'odrl:leftOperand' }, + unit: 'odrl:unit', + dataType: { '@type': 'xsd:anyType', '@id': 'odrl:datatype' }, + status: 'odrl:status', + absolutePosition: 'odrl:absolutePosition', + absoluteSpatialPosition: 'odrl:absoluteSpatialPosition', + absoluteTemporalPosition: 'odrl:absoluteTemporalPosition', + absoluteSize: 'odrl:absoluteSize', + count: 'odrl:count', + dateTime: 'odrl:dateTime', + delayPeriod: 'odrl:delayPeriod', + deliveryChannel: 'odrl:deliveryChannel', + elapsedTime: 'odrl:elapsedTime', + event: 'odrl:event', + fileFormat: 'odrl:fileFormat', + industry: 'odrl:industry:', + language: 'odrl:language', + media: 'odrl:media', + meteredTime: 'odrl:meteredTime', + payAmount: 'odrl:payAmount', + percentage: 'odrl:percentage', + product: 'odrl:product', + purpose: 'odrl:purpose', + recipient: 'odrl:recipient', + relativePosition: 'odrl:relativePosition', + relativeSpatialPosition: 'odrl:relativeSpatialPosition', + relativeTemporalPosition: 'odrl:relativeTemporalPosition', + relativeSize: 'odrl:relativeSize', + resolution: 'odrl:resolution', + spatial: 'odrl:spatial', + spatialCoordinates: 'odrl:spatialCoordinates', + systemDevice: 'odrl:systemDevice', + timeInterval: 'odrl:timeInterval', + unitOfCount: 'odrl:unitOfCount', + version: 'odrl:version', + virtualLocation: 'odrl:virtualLocation', + eq: 'odrl:eq', + gt: 'odrl:gt', + gteq: 'odrl:gteq', + lt: 'odrl:lt', + lteq: 'odrl:lteq', + neq: 'odrl:neg', + isA: 'odrl:isA', + hasPart: 'odrl:hasPart', + isPartOf: 'odrl:isPartOf', + isAllOf: 'odrl:isAllOf', + isAnyOf: 'odrl:isAnyOf', + isNoneOf: 'odrl:isNoneOf', + or: 'odrl:or', + xone: 'odrl:xone', + and: 'odrl:and', + andSequence: 'odrl:andSequence', + policyUsage: 'odrl:policyUsage', + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/purl_ob_v3po.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/purl_ob_v3po.ts new file mode 100644 index 0000000000..3b2ffe28f6 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/purl_ob_v3po.ts @@ -0,0 +1,438 @@ +export const PURL_OB_V3P0 = { + '@context': { + id: '@id', + type: '@type', + xsd: 'https://www.w3.org/2001/XMLSchema#', + OpenBadgeCredential: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#OpenBadgeCredential', + }, + Achievement: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Achievement', + '@context': { + achievementType: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#achievementType', + '@type': 'xsd:string', + }, + alignment: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#alignment', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Alignment', + '@container': '@set', + }, + creator: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Profile', + }, + creditsAvailable: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#creditsAvailable', + '@type': 'xsd:float', + }, + criteria: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Criteria', + '@type': '@id', + }, + fieldOfStudy: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#fieldOfStudy', + '@type': 'xsd:string', + }, + humanCode: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#humanCode', + '@type': 'xsd:string', + }, + otherIdentifier: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#otherIdentifier', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#IdentifierEntry', + '@container': '@set', + }, + related: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#related', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Related', + '@container': '@set', + }, + resultDescription: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#resultDescription', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#ResultDescription', + '@container': '@set', + }, + specialization: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#specialization', + '@type': 'xsd:string', + }, + tag: { + '@id': 'https://schema.org/keywords', + '@type': 'xsd:string', + '@container': '@set', + }, + version: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#version', + '@type': 'xsd:string', + }, + }, + }, + AchievementCredential: { + '@id': 'OpenBadgeCredential', + }, + AchievementSubject: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#AchievementSubject', + '@context': { + achievement: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Achievement', + }, + activityEndDate: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#activityEndDate', + '@type': 'xsd:date', + }, + activityStartDate: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#activityStartDate', + '@type': 'xsd:date', + }, + creditsEarned: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#creditsEarned', + '@type': 'xsd:float', + }, + identifier: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#identifier', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#IdentityObject', + '@container': '@set', + }, + licenseNumber: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#licenseNumber', + '@type': 'xsd:string', + }, + result: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#result', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Result', + '@container': '@set', + }, + role: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#role', + '@type': 'xsd:string', + }, + source: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#source', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Profile', + }, + term: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#term', + '@type': 'xsd:string', + }, + }, + }, + Address: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Address', + '@context': { + addressCountry: { + '@id': 'https://schema.org/addressCountry', + '@type': 'xsd:string', + }, + addressCountryCode: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#CountryCode', + '@type': 'xsd:string', + }, + addressLocality: { + '@id': 'https://schema.org/addressLocality', + '@type': 'xsd:string', + }, + addressRegion: { + '@id': 'https://schema.org/addressRegion', + '@type': 'xsd:string', + }, + geo: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#GeoCoordinates', + }, + postOfficeBoxNumber: { + '@id': 'https://schema.org/postOfficeBoxNumber', + '@type': 'xsd:string', + }, + postalCode: { + '@id': 'https://schema.org/postalCode', + '@type': 'xsd:string', + }, + streetAddress: { + '@id': 'https://schema.org/streetAddress', + '@type': 'xsd:string', + }, + }, + }, + Alignment: { + '@id': 'https://schema.org/Alignment', + '@context': { + targetCode: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#targetCode', + '@type': 'xsd:string', + }, + targetDescription: { + '@id': 'https://schema.org/targetDescription', + '@type': 'xsd:string', + }, + targetFramework: { + '@id': 'https://schema.org/targetFramework', + '@type': 'xsd:string', + }, + targetName: { + '@id': 'https://schema.org/targetName', + '@type': 'xsd:string', + }, + targetType: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#targetType', + '@type': 'xsd:string', + }, + targetUrl: { + '@id': 'https://schema.org/targetUrl', + '@type': 'xsd:anyURI', + }, + }, + }, + Criteria: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Criteria', + }, + EndorsementCredential: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#EndorsementCredential', + }, + EndorsementSubject: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#EndorsementSubject', + '@context': { + endorsementComment: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#endorsementComment', + '@type': 'xsd:string', + }, + }, + }, + Evidence: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Evidence', + '@context': { + audience: { + '@id': 'https://schema.org/audience', + '@type': 'xsd:string', + }, + genre: { + '@id': 'https://schema.org/genre', + '@type': 'xsd:string', + }, + }, + }, + GeoCoordinates: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#GeoCoordinates', + '@context': { + latitude: { + '@id': 'https://schema.org/latitude', + '@type': 'xsd:string', + }, + longitude: { + '@id': 'https://schema.org/longitude', + '@type': 'xsd:string', + }, + }, + }, + IdentifierEntry: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#IdentifierEntry', + '@context': { + identifier: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#identifier', + '@type': 'xsd:string', + }, + identifierType: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#identifierType', + '@type': 'xsd:string', + }, + }, + }, + IdentityObject: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#IdentityObject', + '@context': { + hashed: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#hashed', + '@type': 'xsd:boolean', + }, + identityHash: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#identityHash', + '@type': 'xsd:string', + }, + identityType: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#identityType', + '@type': 'xsd:string', + }, + salt: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#salt', + '@type': 'xsd:string', + }, + }, + }, + Image: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Image', + '@context': { + caption: { + '@id': 'https://schema.org/caption', + '@type': 'xsd:string', + }, + }, + }, + Profile: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Profile', + '@context': { + additionalName: { + '@id': 'https://schema.org/additionalName', + '@type': 'xsd:string', + }, + address: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Address', + }, + dateOfBirth: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#dateOfBirth', + '@type': 'xsd:date', + }, + email: { + '@id': 'https://schema.org/email', + '@type': 'xsd:string', + }, + familyName: { + '@id': 'https://schema.org/familyName', + '@type': 'xsd:string', + }, + familyNamePrefix: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#familyNamePrefix', + '@type': 'xsd:string', + }, + givenName: { + '@id': 'https://schema.org/givenName', + '@type': 'xsd:string', + }, + honorificPrefix: { + '@id': 'https://schema.org/honorificPrefix', + '@type': 'xsd:string', + }, + honorificSuffix: { + '@id': 'https://schema.org/honorificSuffix', + '@type': 'xsd:string', + }, + otherIdentifier: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#otherIdentifier', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#IdentifierEntry', + '@container': '@set', + }, + parentOrg: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#parentOrg', + '@type': 'xsd:string', + }, + patronymicName: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#patronymicName', + '@type': 'xsd:string', + }, + phone: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#PhoneNumber', + '@type': 'xsd:string', + }, + official: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#official', + '@type': 'xsd:string', + }, + }, + }, + Related: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Related', + '@context': { + version: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#version', + '@type': 'xsd:string', + }, + }, + }, + Result: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Result', + '@context': { + achievedLevel: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#achievedLevel', + '@type': 'xsd:anyURI', + }, + resultDescription: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#resultDescription', + '@type': 'xsd:anyURI', + }, + status: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#status', + '@type': 'xsd:string', + }, + value: { + '@id': 'https://schema.org/value', + '@type': 'xsd:string', + }, + }, + }, + ResultDescription: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#ResultDescription', + '@context': { + allowedValue: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#allowedValue', + '@type': 'xsd:string', + '@container': '@set', + }, + requiredLevel: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#requiredLevel', + '@type': 'xsd:anyURI', + }, + requiredValue: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#requiredValue', + '@type': 'xsd:string', + }, + resultType: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#resultType', + '@type': 'xsd:string', + }, + rubricCriterionLevel: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#rubricCriterionLevel', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#RubricCriterionLevel', + '@container': '@set', + }, + valueMax: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#valueMax', + '@type': 'xsd:string', + }, + valueMin: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#valueMin', + '@type': 'xsd:string', + }, + }, + }, + RubricCriterionLevel: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#RubricCriterionLevel', + '@context': { + level: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#level', + '@type': 'xsd:string', + }, + points: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#points', + '@type': 'xsd:string', + }, + }, + }, + alignment: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#alignment', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Alignment', + '@container': '@set', + }, + description: { + '@id': 'https://schema.org/description', + '@type': 'xsd:string', + }, + endorsement: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#endorsement', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#EndorsementCredential', + '@container': '@set', + }, + image: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#image', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Image', + }, + name: { + '@id': 'https://schema.org/name', + '@type': 'xsd:string', + }, + narrative: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#narrative', + '@type': 'xsd:string', + }, + url: { + '@id': 'https://schema.org/url', + '@type': 'xsd:anyURI', + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/schema_org.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/schema_org.ts new file mode 100644 index 0000000000..818951da70 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/schema_org.ts @@ -0,0 +1,2838 @@ +export const SCHEMA_ORG = { + '@context': { + type: '@type', + id: '@id', + HTML: { '@id': 'rdf:HTML' }, + '@vocab': 'http://schema.org/', + rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + rdfs: 'http://www.w3.org/2000/01/rdf-schema#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + schema: 'http://schema.org/', + owl: 'http://www.w3.org/2002/07/owl#', + dc: 'http://purl.org/dc/elements/1.1/', + dct: 'http://purl.org/dc/terms/', + dctype: 'http://purl.org/dc/dcmitype/', + void: 'http://rdfs.org/ns/void#', + dcat: 'http://www.w3.org/ns/dcat#', + '3DModel': { '@id': 'schema:3DModel' }, + AMRadioChannel: { '@id': 'schema:AMRadioChannel' }, + APIReference: { '@id': 'schema:APIReference' }, + Abdomen: { '@id': 'schema:Abdomen' }, + AboutPage: { '@id': 'schema:AboutPage' }, + AcceptAction: { '@id': 'schema:AcceptAction' }, + Accommodation: { '@id': 'schema:Accommodation' }, + AccountingService: { '@id': 'schema:AccountingService' }, + AchieveAction: { '@id': 'schema:AchieveAction' }, + Action: { '@id': 'schema:Action' }, + ActionAccessSpecification: { '@id': 'schema:ActionAccessSpecification' }, + ActionStatusType: { '@id': 'schema:ActionStatusType' }, + ActivateAction: { '@id': 'schema:ActivateAction' }, + ActivationFee: { '@id': 'schema:ActivationFee' }, + ActiveActionStatus: { '@id': 'schema:ActiveActionStatus' }, + ActiveNotRecruiting: { '@id': 'schema:ActiveNotRecruiting' }, + AddAction: { '@id': 'schema:AddAction' }, + AdministrativeArea: { '@id': 'schema:AdministrativeArea' }, + AdultEntertainment: { '@id': 'schema:AdultEntertainment' }, + AdvertiserContentArticle: { '@id': 'schema:AdvertiserContentArticle' }, + AerobicActivity: { '@id': 'schema:AerobicActivity' }, + AggregateOffer: { '@id': 'schema:AggregateOffer' }, + AggregateRating: { '@id': 'schema:AggregateRating' }, + AgreeAction: { '@id': 'schema:AgreeAction' }, + Airline: { '@id': 'schema:Airline' }, + Airport: { '@id': 'schema:Airport' }, + AlbumRelease: { '@id': 'schema:AlbumRelease' }, + AlignmentObject: { '@id': 'schema:AlignmentObject' }, + AllWheelDriveConfiguration: { '@id': 'schema:AllWheelDriveConfiguration' }, + AllergiesHealthAspect: { '@id': 'schema:AllergiesHealthAspect' }, + AllocateAction: { '@id': 'schema:AllocateAction' }, + AmpStory: { '@id': 'schema:AmpStory' }, + AmusementPark: { '@id': 'schema:AmusementPark' }, + AnaerobicActivity: { '@id': 'schema:AnaerobicActivity' }, + AnalysisNewsArticle: { '@id': 'schema:AnalysisNewsArticle' }, + AnatomicalStructure: { '@id': 'schema:AnatomicalStructure' }, + AnatomicalSystem: { '@id': 'schema:AnatomicalSystem' }, + Anesthesia: { '@id': 'schema:Anesthesia' }, + AnimalShelter: { '@id': 'schema:AnimalShelter' }, + Answer: { '@id': 'schema:Answer' }, + Apartment: { '@id': 'schema:Apartment' }, + ApartmentComplex: { '@id': 'schema:ApartmentComplex' }, + Appearance: { '@id': 'schema:Appearance' }, + AppendAction: { '@id': 'schema:AppendAction' }, + ApplyAction: { '@id': 'schema:ApplyAction' }, + ApprovedIndication: { '@id': 'schema:ApprovedIndication' }, + Aquarium: { '@id': 'schema:Aquarium' }, + ArchiveComponent: { '@id': 'schema:ArchiveComponent' }, + ArchiveOrganization: { '@id': 'schema:ArchiveOrganization' }, + ArriveAction: { '@id': 'schema:ArriveAction' }, + ArtGallery: { '@id': 'schema:ArtGallery' }, + Artery: { '@id': 'schema:Artery' }, + Article: { '@id': 'schema:Article' }, + AskAction: { '@id': 'schema:AskAction' }, + AskPublicNewsArticle: { '@id': 'schema:AskPublicNewsArticle' }, + AssessAction: { '@id': 'schema:AssessAction' }, + AssignAction: { '@id': 'schema:AssignAction' }, + Atlas: { '@id': 'schema:Atlas' }, + Attorney: { '@id': 'schema:Attorney' }, + Audience: { '@id': 'schema:Audience' }, + AudioObject: { '@id': 'schema:AudioObject' }, + Audiobook: { '@id': 'schema:Audiobook' }, + AudiobookFormat: { '@id': 'schema:AudiobookFormat' }, + AuthoritativeLegalValue: { '@id': 'schema:AuthoritativeLegalValue' }, + AuthorizeAction: { '@id': 'schema:AuthorizeAction' }, + AutoBodyShop: { '@id': 'schema:AutoBodyShop' }, + AutoDealer: { '@id': 'schema:AutoDealer' }, + AutoPartsStore: { '@id': 'schema:AutoPartsStore' }, + AutoRental: { '@id': 'schema:AutoRental' }, + AutoRepair: { '@id': 'schema:AutoRepair' }, + AutoWash: { '@id': 'schema:AutoWash' }, + AutomatedTeller: { '@id': 'schema:AutomatedTeller' }, + AutomotiveBusiness: { '@id': 'schema:AutomotiveBusiness' }, + Ayurvedic: { '@id': 'schema:Ayurvedic' }, + BackOrder: { '@id': 'schema:BackOrder' }, + BackgroundNewsArticle: { '@id': 'schema:BackgroundNewsArticle' }, + Bacteria: { '@id': 'schema:Bacteria' }, + Bakery: { '@id': 'schema:Bakery' }, + Balance: { '@id': 'schema:Balance' }, + BankAccount: { '@id': 'schema:BankAccount' }, + BankOrCreditUnion: { '@id': 'schema:BankOrCreditUnion' }, + BarOrPub: { '@id': 'schema:BarOrPub' }, + Barcode: { '@id': 'schema:Barcode' }, + BasicIncome: { '@id': 'schema:BasicIncome' }, + Beach: { '@id': 'schema:Beach' }, + BeautySalon: { '@id': 'schema:BeautySalon' }, + BedAndBreakfast: { '@id': 'schema:BedAndBreakfast' }, + BedDetails: { '@id': 'schema:BedDetails' }, + BedType: { '@id': 'schema:BedType' }, + BefriendAction: { '@id': 'schema:BefriendAction' }, + BenefitsHealthAspect: { '@id': 'schema:BenefitsHealthAspect' }, + BikeStore: { '@id': 'schema:BikeStore' }, + Blog: { '@id': 'schema:Blog' }, + BlogPosting: { '@id': 'schema:BlogPosting' }, + BloodTest: { '@id': 'schema:BloodTest' }, + BoardingPolicyType: { '@id': 'schema:BoardingPolicyType' }, + BoatReservation: { '@id': 'schema:BoatReservation' }, + BoatTerminal: { '@id': 'schema:BoatTerminal' }, + BoatTrip: { '@id': 'schema:BoatTrip' }, + BodyMeasurementArm: { '@id': 'schema:BodyMeasurementArm' }, + BodyMeasurementBust: { '@id': 'schema:BodyMeasurementBust' }, + BodyMeasurementChest: { '@id': 'schema:BodyMeasurementChest' }, + BodyMeasurementFoot: { '@id': 'schema:BodyMeasurementFoot' }, + BodyMeasurementHand: { '@id': 'schema:BodyMeasurementHand' }, + BodyMeasurementHead: { '@id': 'schema:BodyMeasurementHead' }, + BodyMeasurementHeight: { '@id': 'schema:BodyMeasurementHeight' }, + BodyMeasurementHips: { '@id': 'schema:BodyMeasurementHips' }, + BodyMeasurementInsideLeg: { '@id': 'schema:BodyMeasurementInsideLeg' }, + BodyMeasurementNeck: { '@id': 'schema:BodyMeasurementNeck' }, + BodyMeasurementTypeEnumeration: { + '@id': 'schema:BodyMeasurementTypeEnumeration', + }, + BodyMeasurementUnderbust: { '@id': 'schema:BodyMeasurementUnderbust' }, + BodyMeasurementWaist: { '@id': 'schema:BodyMeasurementWaist' }, + BodyMeasurementWeight: { '@id': 'schema:BodyMeasurementWeight' }, + BodyOfWater: { '@id': 'schema:BodyOfWater' }, + Bone: { '@id': 'schema:Bone' }, + Book: { '@id': 'schema:Book' }, + BookFormatType: { '@id': 'schema:BookFormatType' }, + BookSeries: { '@id': 'schema:BookSeries' }, + BookStore: { '@id': 'schema:BookStore' }, + BookmarkAction: { '@id': 'schema:BookmarkAction' }, + Boolean: { '@id': 'schema:Boolean' }, + BorrowAction: { '@id': 'schema:BorrowAction' }, + BowlingAlley: { '@id': 'schema:BowlingAlley' }, + BrainStructure: { '@id': 'schema:BrainStructure' }, + Brand: { '@id': 'schema:Brand' }, + BreadcrumbList: { '@id': 'schema:BreadcrumbList' }, + Brewery: { '@id': 'schema:Brewery' }, + Bridge: { '@id': 'schema:Bridge' }, + BroadcastChannel: { '@id': 'schema:BroadcastChannel' }, + BroadcastEvent: { '@id': 'schema:BroadcastEvent' }, + BroadcastFrequencySpecification: { + '@id': 'schema:BroadcastFrequencySpecification', + }, + BroadcastRelease: { '@id': 'schema:BroadcastRelease' }, + BroadcastService: { '@id': 'schema:BroadcastService' }, + BrokerageAccount: { '@id': 'schema:BrokerageAccount' }, + BuddhistTemple: { '@id': 'schema:BuddhistTemple' }, + BusOrCoach: { '@id': 'schema:BusOrCoach' }, + BusReservation: { '@id': 'schema:BusReservation' }, + BusStation: { '@id': 'schema:BusStation' }, + BusStop: { '@id': 'schema:BusStop' }, + BusTrip: { '@id': 'schema:BusTrip' }, + BusinessAudience: { '@id': 'schema:BusinessAudience' }, + BusinessEntityType: { '@id': 'schema:BusinessEntityType' }, + BusinessEvent: { '@id': 'schema:BusinessEvent' }, + BusinessFunction: { '@id': 'schema:BusinessFunction' }, + BusinessSupport: { '@id': 'schema:BusinessSupport' }, + BuyAction: { '@id': 'schema:BuyAction' }, + CDCPMDRecord: { '@id': 'schema:CDCPMDRecord' }, + CDFormat: { '@id': 'schema:CDFormat' }, + CT: { '@id': 'schema:CT' }, + CableOrSatelliteService: { '@id': 'schema:CableOrSatelliteService' }, + CafeOrCoffeeShop: { '@id': 'schema:CafeOrCoffeeShop' }, + Campground: { '@id': 'schema:Campground' }, + CampingPitch: { '@id': 'schema:CampingPitch' }, + Canal: { '@id': 'schema:Canal' }, + CancelAction: { '@id': 'schema:CancelAction' }, + Car: { '@id': 'schema:Car' }, + CarUsageType: { '@id': 'schema:CarUsageType' }, + Cardiovascular: { '@id': 'schema:Cardiovascular' }, + CardiovascularExam: { '@id': 'schema:CardiovascularExam' }, + CaseSeries: { '@id': 'schema:CaseSeries' }, + Casino: { '@id': 'schema:Casino' }, + CassetteFormat: { '@id': 'schema:CassetteFormat' }, + CategoryCode: { '@id': 'schema:CategoryCode' }, + CategoryCodeSet: { '@id': 'schema:CategoryCodeSet' }, + CatholicChurch: { '@id': 'schema:CatholicChurch' }, + CausesHealthAspect: { '@id': 'schema:CausesHealthAspect' }, + Cemetery: { '@id': 'schema:Cemetery' }, + Chapter: { '@id': 'schema:Chapter' }, + CharitableIncorporatedOrganization: { + '@id': 'schema:CharitableIncorporatedOrganization', + }, + CheckAction: { '@id': 'schema:CheckAction' }, + CheckInAction: { '@id': 'schema:CheckInAction' }, + CheckOutAction: { '@id': 'schema:CheckOutAction' }, + CheckoutPage: { '@id': 'schema:CheckoutPage' }, + ChildCare: { '@id': 'schema:ChildCare' }, + ChildrensEvent: { '@id': 'schema:ChildrensEvent' }, + Chiropractic: { '@id': 'schema:Chiropractic' }, + ChooseAction: { '@id': 'schema:ChooseAction' }, + Church: { '@id': 'schema:Church' }, + City: { '@id': 'schema:City' }, + CityHall: { '@id': 'schema:CityHall' }, + CivicStructure: { '@id': 'schema:CivicStructure' }, + Claim: { '@id': 'schema:Claim' }, + ClaimReview: { '@id': 'schema:ClaimReview' }, + Class: { '@id': 'schema:Class' }, + CleaningFee: { '@id': 'schema:CleaningFee' }, + Clinician: { '@id': 'schema:Clinician' }, + Clip: { '@id': 'schema:Clip' }, + ClothingStore: { '@id': 'schema:ClothingStore' }, + CoOp: { '@id': 'schema:CoOp' }, + Code: { '@id': 'schema:Code' }, + CohortStudy: { '@id': 'schema:CohortStudy' }, + Collection: { '@id': 'schema:Collection' }, + CollectionPage: { '@id': 'schema:CollectionPage' }, + CollegeOrUniversity: { '@id': 'schema:CollegeOrUniversity' }, + ComedyClub: { '@id': 'schema:ComedyClub' }, + ComedyEvent: { '@id': 'schema:ComedyEvent' }, + ComicCoverArt: { '@id': 'schema:ComicCoverArt' }, + ComicIssue: { '@id': 'schema:ComicIssue' }, + ComicSeries: { '@id': 'schema:ComicSeries' }, + ComicStory: { '@id': 'schema:ComicStory' }, + Comment: { '@id': 'schema:Comment' }, + CommentAction: { '@id': 'schema:CommentAction' }, + CommentPermission: { '@id': 'schema:CommentPermission' }, + CommunicateAction: { '@id': 'schema:CommunicateAction' }, + CommunityHealth: { '@id': 'schema:CommunityHealth' }, + CompilationAlbum: { '@id': 'schema:CompilationAlbum' }, + CompleteDataFeed: { '@id': 'schema:CompleteDataFeed' }, + Completed: { '@id': 'schema:Completed' }, + CompletedActionStatus: { '@id': 'schema:CompletedActionStatus' }, + CompoundPriceSpecification: { '@id': 'schema:CompoundPriceSpecification' }, + ComputerLanguage: { '@id': 'schema:ComputerLanguage' }, + ComputerStore: { '@id': 'schema:ComputerStore' }, + ConfirmAction: { '@id': 'schema:ConfirmAction' }, + Consortium: { '@id': 'schema:Consortium' }, + ConsumeAction: { '@id': 'schema:ConsumeAction' }, + ContactPage: { '@id': 'schema:ContactPage' }, + ContactPoint: { '@id': 'schema:ContactPoint' }, + ContactPointOption: { '@id': 'schema:ContactPointOption' }, + ContagiousnessHealthAspect: { '@id': 'schema:ContagiousnessHealthAspect' }, + Continent: { '@id': 'schema:Continent' }, + ControlAction: { '@id': 'schema:ControlAction' }, + ConvenienceStore: { '@id': 'schema:ConvenienceStore' }, + Conversation: { '@id': 'schema:Conversation' }, + CookAction: { '@id': 'schema:CookAction' }, + Corporation: { '@id': 'schema:Corporation' }, + CorrectionComment: { '@id': 'schema:CorrectionComment' }, + Country: { '@id': 'schema:Country' }, + Course: { '@id': 'schema:Course' }, + CourseInstance: { '@id': 'schema:CourseInstance' }, + Courthouse: { '@id': 'schema:Courthouse' }, + CoverArt: { '@id': 'schema:CoverArt' }, + CovidTestingFacility: { '@id': 'schema:CovidTestingFacility' }, + CreateAction: { '@id': 'schema:CreateAction' }, + CreativeWork: { '@id': 'schema:CreativeWork' }, + CreativeWorkSeason: { '@id': 'schema:CreativeWorkSeason' }, + CreativeWorkSeries: { '@id': 'schema:CreativeWorkSeries' }, + CreditCard: { '@id': 'schema:CreditCard' }, + Crematorium: { '@id': 'schema:Crematorium' }, + CriticReview: { '@id': 'schema:CriticReview' }, + CrossSectional: { '@id': 'schema:CrossSectional' }, + CssSelectorType: { '@id': 'schema:CssSelectorType' }, + CurrencyConversionService: { '@id': 'schema:CurrencyConversionService' }, + DDxElement: { '@id': 'schema:DDxElement' }, + DJMixAlbum: { '@id': 'schema:DJMixAlbum' }, + DVDFormat: { '@id': 'schema:DVDFormat' }, + DamagedCondition: { '@id': 'schema:DamagedCondition' }, + DanceEvent: { '@id': 'schema:DanceEvent' }, + DanceGroup: { '@id': 'schema:DanceGroup' }, + DataCatalog: { '@id': 'schema:DataCatalog' }, + DataDownload: { '@id': 'schema:DataDownload' }, + DataFeed: { '@id': 'schema:DataFeed' }, + DataFeedItem: { '@id': 'schema:DataFeedItem' }, + DataType: { '@id': 'schema:DataType' }, + Dataset: { '@id': 'schema:Dataset' }, + Date: { '@id': 'schema:Date' }, + DateTime: { '@id': 'schema:DateTime' }, + DatedMoneySpecification: { '@id': 'schema:DatedMoneySpecification' }, + DayOfWeek: { '@id': 'schema:DayOfWeek' }, + DaySpa: { '@id': 'schema:DaySpa' }, + DeactivateAction: { '@id': 'schema:DeactivateAction' }, + DecontextualizedContent: { '@id': 'schema:DecontextualizedContent' }, + DefenceEstablishment: { '@id': 'schema:DefenceEstablishment' }, + DefinedRegion: { '@id': 'schema:DefinedRegion' }, + DefinedTerm: { '@id': 'schema:DefinedTerm' }, + DefinedTermSet: { '@id': 'schema:DefinedTermSet' }, + DefinitiveLegalValue: { '@id': 'schema:DefinitiveLegalValue' }, + DeleteAction: { '@id': 'schema:DeleteAction' }, + DeliveryChargeSpecification: { '@id': 'schema:DeliveryChargeSpecification' }, + DeliveryEvent: { '@id': 'schema:DeliveryEvent' }, + DeliveryMethod: { '@id': 'schema:DeliveryMethod' }, + DeliveryTimeSettings: { '@id': 'schema:DeliveryTimeSettings' }, + Demand: { '@id': 'schema:Demand' }, + DemoAlbum: { '@id': 'schema:DemoAlbum' }, + Dentist: { '@id': 'schema:Dentist' }, + Dentistry: { '@id': 'schema:Dentistry' }, + DepartAction: { '@id': 'schema:DepartAction' }, + DepartmentStore: { '@id': 'schema:DepartmentStore' }, + DepositAccount: { '@id': 'schema:DepositAccount' }, + Dermatologic: { '@id': 'schema:Dermatologic' }, + Dermatology: { '@id': 'schema:Dermatology' }, + DiabeticDiet: { '@id': 'schema:DiabeticDiet' }, + Diagnostic: { '@id': 'schema:Diagnostic' }, + DiagnosticLab: { '@id': 'schema:DiagnosticLab' }, + DiagnosticProcedure: { '@id': 'schema:DiagnosticProcedure' }, + Diet: { '@id': 'schema:Diet' }, + DietNutrition: { '@id': 'schema:DietNutrition' }, + DietarySupplement: { '@id': 'schema:DietarySupplement' }, + DigitalAudioTapeFormat: { '@id': 'schema:DigitalAudioTapeFormat' }, + DigitalDocument: { '@id': 'schema:DigitalDocument' }, + DigitalDocumentPermission: { '@id': 'schema:DigitalDocumentPermission' }, + DigitalDocumentPermissionType: { + '@id': 'schema:DigitalDocumentPermissionType', + }, + DigitalFormat: { '@id': 'schema:DigitalFormat' }, + DisabilitySupport: { '@id': 'schema:DisabilitySupport' }, + DisagreeAction: { '@id': 'schema:DisagreeAction' }, + Discontinued: { '@id': 'schema:Discontinued' }, + DiscoverAction: { '@id': 'schema:DiscoverAction' }, + DiscussionForumPosting: { '@id': 'schema:DiscussionForumPosting' }, + DislikeAction: { '@id': 'schema:DislikeAction' }, + Distance: { '@id': 'schema:Distance' }, + DistanceFee: { '@id': 'schema:DistanceFee' }, + Distillery: { '@id': 'schema:Distillery' }, + DonateAction: { '@id': 'schema:DonateAction' }, + DoseSchedule: { '@id': 'schema:DoseSchedule' }, + DoubleBlindedTrial: { '@id': 'schema:DoubleBlindedTrial' }, + DownloadAction: { '@id': 'schema:DownloadAction' }, + Downpayment: { '@id': 'schema:Downpayment' }, + DrawAction: { '@id': 'schema:DrawAction' }, + Drawing: { '@id': 'schema:Drawing' }, + DrinkAction: { '@id': 'schema:DrinkAction' }, + DriveWheelConfigurationValue: { '@id': 'schema:DriveWheelConfigurationValue' }, + DrivingSchoolVehicleUsage: { '@id': 'schema:DrivingSchoolVehicleUsage' }, + Drug: { '@id': 'schema:Drug' }, + DrugClass: { '@id': 'schema:DrugClass' }, + DrugCost: { '@id': 'schema:DrugCost' }, + DrugCostCategory: { '@id': 'schema:DrugCostCategory' }, + DrugLegalStatus: { '@id': 'schema:DrugLegalStatus' }, + DrugPregnancyCategory: { '@id': 'schema:DrugPregnancyCategory' }, + DrugPrescriptionStatus: { '@id': 'schema:DrugPrescriptionStatus' }, + DrugStrength: { '@id': 'schema:DrugStrength' }, + DryCleaningOrLaundry: { '@id': 'schema:DryCleaningOrLaundry' }, + Duration: { '@id': 'schema:Duration' }, + EBook: { '@id': 'schema:EBook' }, + EPRelease: { '@id': 'schema:EPRelease' }, + EUEnergyEfficiencyCategoryA: { '@id': 'schema:EUEnergyEfficiencyCategoryA' }, + EUEnergyEfficiencyCategoryA1Plus: { + '@id': 'schema:EUEnergyEfficiencyCategoryA1Plus', + }, + EUEnergyEfficiencyCategoryA2Plus: { + '@id': 'schema:EUEnergyEfficiencyCategoryA2Plus', + }, + EUEnergyEfficiencyCategoryA3Plus: { + '@id': 'schema:EUEnergyEfficiencyCategoryA3Plus', + }, + EUEnergyEfficiencyCategoryB: { '@id': 'schema:EUEnergyEfficiencyCategoryB' }, + EUEnergyEfficiencyCategoryC: { '@id': 'schema:EUEnergyEfficiencyCategoryC' }, + EUEnergyEfficiencyCategoryD: { '@id': 'schema:EUEnergyEfficiencyCategoryD' }, + EUEnergyEfficiencyCategoryE: { '@id': 'schema:EUEnergyEfficiencyCategoryE' }, + EUEnergyEfficiencyCategoryF: { '@id': 'schema:EUEnergyEfficiencyCategoryF' }, + EUEnergyEfficiencyCategoryG: { '@id': 'schema:EUEnergyEfficiencyCategoryG' }, + EUEnergyEfficiencyEnumeration: { + '@id': 'schema:EUEnergyEfficiencyEnumeration', + }, + Ear: { '@id': 'schema:Ear' }, + EatAction: { '@id': 'schema:EatAction' }, + EditedOrCroppedContent: { '@id': 'schema:EditedOrCroppedContent' }, + EducationEvent: { '@id': 'schema:EducationEvent' }, + EducationalAudience: { '@id': 'schema:EducationalAudience' }, + EducationalOccupationalCredential: { + '@id': 'schema:EducationalOccupationalCredential', + }, + EducationalOccupationalProgram: { + '@id': 'schema:EducationalOccupationalProgram', + }, + EducationalOrganization: { '@id': 'schema:EducationalOrganization' }, + EffectivenessHealthAspect: { '@id': 'schema:EffectivenessHealthAspect' }, + Electrician: { '@id': 'schema:Electrician' }, + ElectronicsStore: { '@id': 'schema:ElectronicsStore' }, + ElementarySchool: { '@id': 'schema:ElementarySchool' }, + EmailMessage: { '@id': 'schema:EmailMessage' }, + Embassy: { '@id': 'schema:Embassy' }, + Emergency: { '@id': 'schema:Emergency' }, + EmergencyService: { '@id': 'schema:EmergencyService' }, + EmployeeRole: { '@id': 'schema:EmployeeRole' }, + EmployerAggregateRating: { '@id': 'schema:EmployerAggregateRating' }, + EmployerReview: { '@id': 'schema:EmployerReview' }, + EmploymentAgency: { '@id': 'schema:EmploymentAgency' }, + Endocrine: { '@id': 'schema:Endocrine' }, + EndorseAction: { '@id': 'schema:EndorseAction' }, + EndorsementRating: { '@id': 'schema:EndorsementRating' }, + Energy: { '@id': 'schema:Energy' }, + EnergyConsumptionDetails: { '@id': 'schema:EnergyConsumptionDetails' }, + EnergyEfficiencyEnumeration: { '@id': 'schema:EnergyEfficiencyEnumeration' }, + EnergyStarCertified: { '@id': 'schema:EnergyStarCertified' }, + EnergyStarEnergyEfficiencyEnumeration: { + '@id': 'schema:EnergyStarEnergyEfficiencyEnumeration', + }, + EngineSpecification: { '@id': 'schema:EngineSpecification' }, + EnrollingByInvitation: { '@id': 'schema:EnrollingByInvitation' }, + EntertainmentBusiness: { '@id': 'schema:EntertainmentBusiness' }, + EntryPoint: { '@id': 'schema:EntryPoint' }, + Enumeration: { '@id': 'schema:Enumeration' }, + Episode: { '@id': 'schema:Episode' }, + Event: { '@id': 'schema:Event' }, + EventAttendanceModeEnumeration: { + '@id': 'schema:EventAttendanceModeEnumeration', + }, + EventCancelled: { '@id': 'schema:EventCancelled' }, + EventMovedOnline: { '@id': 'schema:EventMovedOnline' }, + EventPostponed: { '@id': 'schema:EventPostponed' }, + EventRescheduled: { '@id': 'schema:EventRescheduled' }, + EventReservation: { '@id': 'schema:EventReservation' }, + EventScheduled: { '@id': 'schema:EventScheduled' }, + EventSeries: { '@id': 'schema:EventSeries' }, + EventStatusType: { '@id': 'schema:EventStatusType' }, + EventVenue: { '@id': 'schema:EventVenue' }, + EvidenceLevelA: { '@id': 'schema:EvidenceLevelA' }, + EvidenceLevelB: { '@id': 'schema:EvidenceLevelB' }, + EvidenceLevelC: { '@id': 'schema:EvidenceLevelC' }, + ExchangeRateSpecification: { '@id': 'schema:ExchangeRateSpecification' }, + ExchangeRefund: { '@id': 'schema:ExchangeRefund' }, + ExerciseAction: { '@id': 'schema:ExerciseAction' }, + ExerciseGym: { '@id': 'schema:ExerciseGym' }, + ExercisePlan: { '@id': 'schema:ExercisePlan' }, + ExhibitionEvent: { '@id': 'schema:ExhibitionEvent' }, + Eye: { '@id': 'schema:Eye' }, + FAQPage: { '@id': 'schema:FAQPage' }, + FDAcategoryA: { '@id': 'schema:FDAcategoryA' }, + FDAcategoryB: { '@id': 'schema:FDAcategoryB' }, + FDAcategoryC: { '@id': 'schema:FDAcategoryC' }, + FDAcategoryD: { '@id': 'schema:FDAcategoryD' }, + FDAcategoryX: { '@id': 'schema:FDAcategoryX' }, + FDAnotEvaluated: { '@id': 'schema:FDAnotEvaluated' }, + FMRadioChannel: { '@id': 'schema:FMRadioChannel' }, + FailedActionStatus: { '@id': 'schema:FailedActionStatus' }, + False: { '@id': 'schema:False' }, + FastFoodRestaurant: { '@id': 'schema:FastFoodRestaurant' }, + Female: { '@id': 'schema:Female' }, + Festival: { '@id': 'schema:Festival' }, + FilmAction: { '@id': 'schema:FilmAction' }, + FinancialProduct: { '@id': 'schema:FinancialProduct' }, + FinancialService: { '@id': 'schema:FinancialService' }, + FindAction: { '@id': 'schema:FindAction' }, + FireStation: { '@id': 'schema:FireStation' }, + Flexibility: { '@id': 'schema:Flexibility' }, + Flight: { '@id': 'schema:Flight' }, + FlightReservation: { '@id': 'schema:FlightReservation' }, + Float: { '@id': 'schema:Float' }, + FloorPlan: { '@id': 'schema:FloorPlan' }, + Florist: { '@id': 'schema:Florist' }, + FollowAction: { '@id': 'schema:FollowAction' }, + FoodEstablishment: { '@id': 'schema:FoodEstablishment' }, + FoodEstablishmentReservation: { '@id': 'schema:FoodEstablishmentReservation' }, + FoodEvent: { '@id': 'schema:FoodEvent' }, + FoodService: { '@id': 'schema:FoodService' }, + FourWheelDriveConfiguration: { '@id': 'schema:FourWheelDriveConfiguration' }, + Friday: { '@id': 'schema:Friday' }, + FrontWheelDriveConfiguration: { '@id': 'schema:FrontWheelDriveConfiguration' }, + FullRefund: { '@id': 'schema:FullRefund' }, + FundingAgency: { '@id': 'schema:FundingAgency' }, + FundingScheme: { '@id': 'schema:FundingScheme' }, + Fungus: { '@id': 'schema:Fungus' }, + FurnitureStore: { '@id': 'schema:FurnitureStore' }, + Game: { '@id': 'schema:Game' }, + GamePlayMode: { '@id': 'schema:GamePlayMode' }, + GameServer: { '@id': 'schema:GameServer' }, + GameServerStatus: { '@id': 'schema:GameServerStatus' }, + GardenStore: { '@id': 'schema:GardenStore' }, + GasStation: { '@id': 'schema:GasStation' }, + Gastroenterologic: { '@id': 'schema:Gastroenterologic' }, + GatedResidenceCommunity: { '@id': 'schema:GatedResidenceCommunity' }, + GenderType: { '@id': 'schema:GenderType' }, + GeneralContractor: { '@id': 'schema:GeneralContractor' }, + Genetic: { '@id': 'schema:Genetic' }, + Genitourinary: { '@id': 'schema:Genitourinary' }, + GeoCircle: { '@id': 'schema:GeoCircle' }, + GeoCoordinates: { '@id': 'schema:GeoCoordinates' }, + GeoShape: { '@id': 'schema:GeoShape' }, + GeospatialGeometry: { '@id': 'schema:GeospatialGeometry' }, + Geriatric: { '@id': 'schema:Geriatric' }, + GettingAccessHealthAspect: { '@id': 'schema:GettingAccessHealthAspect' }, + GiveAction: { '@id': 'schema:GiveAction' }, + GlutenFreeDiet: { '@id': 'schema:GlutenFreeDiet' }, + GolfCourse: { '@id': 'schema:GolfCourse' }, + GovernmentBenefitsType: { '@id': 'schema:GovernmentBenefitsType' }, + GovernmentBuilding: { '@id': 'schema:GovernmentBuilding' }, + GovernmentOffice: { '@id': 'schema:GovernmentOffice' }, + GovernmentOrganization: { '@id': 'schema:GovernmentOrganization' }, + GovernmentPermit: { '@id': 'schema:GovernmentPermit' }, + GovernmentService: { '@id': 'schema:GovernmentService' }, + Grant: { '@id': 'schema:Grant' }, + GraphicNovel: { '@id': 'schema:GraphicNovel' }, + GroceryStore: { '@id': 'schema:GroceryStore' }, + GroupBoardingPolicy: { '@id': 'schema:GroupBoardingPolicy' }, + Guide: { '@id': 'schema:Guide' }, + Gynecologic: { '@id': 'schema:Gynecologic' }, + HVACBusiness: { '@id': 'schema:HVACBusiness' }, + Hackathon: { '@id': 'schema:Hackathon' }, + HairSalon: { '@id': 'schema:HairSalon' }, + HalalDiet: { '@id': 'schema:HalalDiet' }, + Hardcover: { '@id': 'schema:Hardcover' }, + HardwareStore: { '@id': 'schema:HardwareStore' }, + Head: { '@id': 'schema:Head' }, + HealthAndBeautyBusiness: { '@id': 'schema:HealthAndBeautyBusiness' }, + HealthAspectEnumeration: { '@id': 'schema:HealthAspectEnumeration' }, + HealthCare: { '@id': 'schema:HealthCare' }, + HealthClub: { '@id': 'schema:HealthClub' }, + HealthInsurancePlan: { '@id': 'schema:HealthInsurancePlan' }, + HealthPlanCostSharingSpecification: { + '@id': 'schema:HealthPlanCostSharingSpecification', + }, + HealthPlanFormulary: { '@id': 'schema:HealthPlanFormulary' }, + HealthPlanNetwork: { '@id': 'schema:HealthPlanNetwork' }, + HealthTopicContent: { '@id': 'schema:HealthTopicContent' }, + HearingImpairedSupported: { '@id': 'schema:HearingImpairedSupported' }, + Hematologic: { '@id': 'schema:Hematologic' }, + HighSchool: { '@id': 'schema:HighSchool' }, + HinduDiet: { '@id': 'schema:HinduDiet' }, + HinduTemple: { '@id': 'schema:HinduTemple' }, + HobbyShop: { '@id': 'schema:HobbyShop' }, + HomeAndConstructionBusiness: { '@id': 'schema:HomeAndConstructionBusiness' }, + HomeGoodsStore: { '@id': 'schema:HomeGoodsStore' }, + Homeopathic: { '@id': 'schema:Homeopathic' }, + Hospital: { '@id': 'schema:Hospital' }, + Hostel: { '@id': 'schema:Hostel' }, + Hotel: { '@id': 'schema:Hotel' }, + HotelRoom: { '@id': 'schema:HotelRoom' }, + House: { '@id': 'schema:House' }, + HousePainter: { '@id': 'schema:HousePainter' }, + HowItWorksHealthAspect: { '@id': 'schema:HowItWorksHealthAspect' }, + HowOrWhereHealthAspect: { '@id': 'schema:HowOrWhereHealthAspect' }, + HowTo: { '@id': 'schema:HowTo' }, + HowToDirection: { '@id': 'schema:HowToDirection' }, + HowToItem: { '@id': 'schema:HowToItem' }, + HowToSection: { '@id': 'schema:HowToSection' }, + HowToStep: { '@id': 'schema:HowToStep' }, + HowToSupply: { '@id': 'schema:HowToSupply' }, + HowToTip: { '@id': 'schema:HowToTip' }, + HowToTool: { '@id': 'schema:HowToTool' }, + HyperToc: { '@id': 'schema:HyperToc' }, + HyperTocEntry: { '@id': 'schema:HyperTocEntry' }, + IceCreamShop: { '@id': 'schema:IceCreamShop' }, + IgnoreAction: { '@id': 'schema:IgnoreAction' }, + ImageGallery: { '@id': 'schema:ImageGallery' }, + ImageObject: { '@id': 'schema:ImageObject' }, + ImagingTest: { '@id': 'schema:ImagingTest' }, + InForce: { '@id': 'schema:InForce' }, + InStock: { '@id': 'schema:InStock' }, + InStoreOnly: { '@id': 'schema:InStoreOnly' }, + IndividualProduct: { '@id': 'schema:IndividualProduct' }, + Infectious: { '@id': 'schema:Infectious' }, + InfectiousAgentClass: { '@id': 'schema:InfectiousAgentClass' }, + InfectiousDisease: { '@id': 'schema:InfectiousDisease' }, + InformAction: { '@id': 'schema:InformAction' }, + IngredientsHealthAspect: { '@id': 'schema:IngredientsHealthAspect' }, + InsertAction: { '@id': 'schema:InsertAction' }, + InstallAction: { '@id': 'schema:InstallAction' }, + Installment: { '@id': 'schema:Installment' }, + InsuranceAgency: { '@id': 'schema:InsuranceAgency' }, + Intangible: { '@id': 'schema:Intangible' }, + Integer: { '@id': 'schema:Integer' }, + InteractAction: { '@id': 'schema:InteractAction' }, + InteractionCounter: { '@id': 'schema:InteractionCounter' }, + InternationalTrial: { '@id': 'schema:InternationalTrial' }, + InternetCafe: { '@id': 'schema:InternetCafe' }, + InvestmentFund: { '@id': 'schema:InvestmentFund' }, + InvestmentOrDeposit: { '@id': 'schema:InvestmentOrDeposit' }, + InviteAction: { '@id': 'schema:InviteAction' }, + Invoice: { '@id': 'schema:Invoice' }, + InvoicePrice: { '@id': 'schema:InvoicePrice' }, + ItemAvailability: { '@id': 'schema:ItemAvailability' }, + ItemList: { '@id': 'schema:ItemList' }, + ItemListOrderAscending: { '@id': 'schema:ItemListOrderAscending' }, + ItemListOrderDescending: { '@id': 'schema:ItemListOrderDescending' }, + ItemListOrderType: { '@id': 'schema:ItemListOrderType' }, + ItemListUnordered: { '@id': 'schema:ItemListUnordered' }, + ItemPage: { '@id': 'schema:ItemPage' }, + JewelryStore: { '@id': 'schema:JewelryStore' }, + JobPosting: { '@id': 'schema:JobPosting' }, + JoinAction: { '@id': 'schema:JoinAction' }, + Joint: { '@id': 'schema:Joint' }, + KosherDiet: { '@id': 'schema:KosherDiet' }, + LaboratoryScience: { '@id': 'schema:LaboratoryScience' }, + LakeBodyOfWater: { '@id': 'schema:LakeBodyOfWater' }, + Landform: { '@id': 'schema:Landform' }, + LandmarksOrHistoricalBuildings: { + '@id': 'schema:LandmarksOrHistoricalBuildings', + }, + Language: { '@id': 'schema:Language' }, + LaserDiscFormat: { '@id': 'schema:LaserDiscFormat' }, + LearningResource: { '@id': 'schema:LearningResource' }, + LeaveAction: { '@id': 'schema:LeaveAction' }, + LeftHandDriving: { '@id': 'schema:LeftHandDriving' }, + LegalForceStatus: { '@id': 'schema:LegalForceStatus' }, + LegalService: { '@id': 'schema:LegalService' }, + LegalValueLevel: { '@id': 'schema:LegalValueLevel' }, + Legislation: { '@id': 'schema:Legislation' }, + LegislationObject: { '@id': 'schema:LegislationObject' }, + LegislativeBuilding: { '@id': 'schema:LegislativeBuilding' }, + LeisureTimeActivity: { '@id': 'schema:LeisureTimeActivity' }, + LendAction: { '@id': 'schema:LendAction' }, + Library: { '@id': 'schema:Library' }, + LibrarySystem: { '@id': 'schema:LibrarySystem' }, + LifestyleModification: { '@id': 'schema:LifestyleModification' }, + Ligament: { '@id': 'schema:Ligament' }, + LikeAction: { '@id': 'schema:LikeAction' }, + LimitedAvailability: { '@id': 'schema:LimitedAvailability' }, + LimitedByGuaranteeCharity: { '@id': 'schema:LimitedByGuaranteeCharity' }, + LinkRole: { '@id': 'schema:LinkRole' }, + LiquorStore: { '@id': 'schema:LiquorStore' }, + ListItem: { '@id': 'schema:ListItem' }, + ListPrice: { '@id': 'schema:ListPrice' }, + ListenAction: { '@id': 'schema:ListenAction' }, + LiteraryEvent: { '@id': 'schema:LiteraryEvent' }, + LiveAlbum: { '@id': 'schema:LiveAlbum' }, + LiveBlogPosting: { '@id': 'schema:LiveBlogPosting' }, + LivingWithHealthAspect: { '@id': 'schema:LivingWithHealthAspect' }, + LoanOrCredit: { '@id': 'schema:LoanOrCredit' }, + LocalBusiness: { '@id': 'schema:LocalBusiness' }, + LocationFeatureSpecification: { '@id': 'schema:LocationFeatureSpecification' }, + LockerDelivery: { '@id': 'schema:LockerDelivery' }, + Locksmith: { '@id': 'schema:Locksmith' }, + LodgingBusiness: { '@id': 'schema:LodgingBusiness' }, + LodgingReservation: { '@id': 'schema:LodgingReservation' }, + Longitudinal: { '@id': 'schema:Longitudinal' }, + LoseAction: { '@id': 'schema:LoseAction' }, + LowCalorieDiet: { '@id': 'schema:LowCalorieDiet' }, + LowFatDiet: { '@id': 'schema:LowFatDiet' }, + LowLactoseDiet: { '@id': 'schema:LowLactoseDiet' }, + LowSaltDiet: { '@id': 'schema:LowSaltDiet' }, + Lung: { '@id': 'schema:Lung' }, + LymphaticVessel: { '@id': 'schema:LymphaticVessel' }, + MRI: { '@id': 'schema:MRI' }, + MSRP: { '@id': 'schema:MSRP' }, + Male: { '@id': 'schema:Male' }, + Manuscript: { '@id': 'schema:Manuscript' }, + Map: { '@id': 'schema:Map' }, + MapCategoryType: { '@id': 'schema:MapCategoryType' }, + MarryAction: { '@id': 'schema:MarryAction' }, + Mass: { '@id': 'schema:Mass' }, + MathSolver: { '@id': 'schema:MathSolver' }, + MaximumDoseSchedule: { '@id': 'schema:MaximumDoseSchedule' }, + MayTreatHealthAspect: { '@id': 'schema:MayTreatHealthAspect' }, + MeasurementTypeEnumeration: { '@id': 'schema:MeasurementTypeEnumeration' }, + MediaGallery: { '@id': 'schema:MediaGallery' }, + MediaManipulationRatingEnumeration: { + '@id': 'schema:MediaManipulationRatingEnumeration', + }, + MediaObject: { '@id': 'schema:MediaObject' }, + MediaReview: { '@id': 'schema:MediaReview' }, + MediaSubscription: { '@id': 'schema:MediaSubscription' }, + MedicalAudience: { '@id': 'schema:MedicalAudience' }, + MedicalAudienceType: { '@id': 'schema:MedicalAudienceType' }, + MedicalBusiness: { '@id': 'schema:MedicalBusiness' }, + MedicalCause: { '@id': 'schema:MedicalCause' }, + MedicalClinic: { '@id': 'schema:MedicalClinic' }, + MedicalCode: { '@id': 'schema:MedicalCode' }, + MedicalCondition: { '@id': 'schema:MedicalCondition' }, + MedicalConditionStage: { '@id': 'schema:MedicalConditionStage' }, + MedicalContraindication: { '@id': 'schema:MedicalContraindication' }, + MedicalDevice: { '@id': 'schema:MedicalDevice' }, + MedicalDevicePurpose: { '@id': 'schema:MedicalDevicePurpose' }, + MedicalEntity: { '@id': 'schema:MedicalEntity' }, + MedicalEnumeration: { '@id': 'schema:MedicalEnumeration' }, + MedicalEvidenceLevel: { '@id': 'schema:MedicalEvidenceLevel' }, + MedicalGuideline: { '@id': 'schema:MedicalGuideline' }, + MedicalGuidelineContraindication: { + '@id': 'schema:MedicalGuidelineContraindication', + }, + MedicalGuidelineRecommendation: { + '@id': 'schema:MedicalGuidelineRecommendation', + }, + MedicalImagingTechnique: { '@id': 'schema:MedicalImagingTechnique' }, + MedicalIndication: { '@id': 'schema:MedicalIndication' }, + MedicalIntangible: { '@id': 'schema:MedicalIntangible' }, + MedicalObservationalStudy: { '@id': 'schema:MedicalObservationalStudy' }, + MedicalObservationalStudyDesign: { + '@id': 'schema:MedicalObservationalStudyDesign', + }, + MedicalOrganization: { '@id': 'schema:MedicalOrganization' }, + MedicalProcedure: { '@id': 'schema:MedicalProcedure' }, + MedicalProcedureType: { '@id': 'schema:MedicalProcedureType' }, + MedicalResearcher: { '@id': 'schema:MedicalResearcher' }, + MedicalRiskCalculator: { '@id': 'schema:MedicalRiskCalculator' }, + MedicalRiskEstimator: { '@id': 'schema:MedicalRiskEstimator' }, + MedicalRiskFactor: { '@id': 'schema:MedicalRiskFactor' }, + MedicalRiskScore: { '@id': 'schema:MedicalRiskScore' }, + MedicalScholarlyArticle: { '@id': 'schema:MedicalScholarlyArticle' }, + MedicalSign: { '@id': 'schema:MedicalSign' }, + MedicalSignOrSymptom: { '@id': 'schema:MedicalSignOrSymptom' }, + MedicalSpecialty: { '@id': 'schema:MedicalSpecialty' }, + MedicalStudy: { '@id': 'schema:MedicalStudy' }, + MedicalStudyStatus: { '@id': 'schema:MedicalStudyStatus' }, + MedicalSymptom: { '@id': 'schema:MedicalSymptom' }, + MedicalTest: { '@id': 'schema:MedicalTest' }, + MedicalTestPanel: { '@id': 'schema:MedicalTestPanel' }, + MedicalTherapy: { '@id': 'schema:MedicalTherapy' }, + MedicalTrial: { '@id': 'schema:MedicalTrial' }, + MedicalTrialDesign: { '@id': 'schema:MedicalTrialDesign' }, + MedicalWebPage: { '@id': 'schema:MedicalWebPage' }, + MedicineSystem: { '@id': 'schema:MedicineSystem' }, + MeetingRoom: { '@id': 'schema:MeetingRoom' }, + MensClothingStore: { '@id': 'schema:MensClothingStore' }, + Menu: { '@id': 'schema:Menu' }, + MenuItem: { '@id': 'schema:MenuItem' }, + MenuSection: { '@id': 'schema:MenuSection' }, + MerchantReturnEnumeration: { '@id': 'schema:MerchantReturnEnumeration' }, + MerchantReturnFiniteReturnWindow: { + '@id': 'schema:MerchantReturnFiniteReturnWindow', + }, + MerchantReturnNotPermitted: { '@id': 'schema:MerchantReturnNotPermitted' }, + MerchantReturnPolicy: { '@id': 'schema:MerchantReturnPolicy' }, + MerchantReturnUnlimitedWindow: { + '@id': 'schema:MerchantReturnUnlimitedWindow', + }, + MerchantReturnUnspecified: { '@id': 'schema:MerchantReturnUnspecified' }, + Message: { '@id': 'schema:Message' }, + MiddleSchool: { '@id': 'schema:MiddleSchool' }, + Midwifery: { '@id': 'schema:Midwifery' }, + MinimumAdvertisedPrice: { '@id': 'schema:MinimumAdvertisedPrice' }, + MisconceptionsHealthAspect: { '@id': 'schema:MisconceptionsHealthAspect' }, + MixedEventAttendanceMode: { '@id': 'schema:MixedEventAttendanceMode' }, + MixtapeAlbum: { '@id': 'schema:MixtapeAlbum' }, + MobileApplication: { '@id': 'schema:MobileApplication' }, + MobilePhoneStore: { '@id': 'schema:MobilePhoneStore' }, + Monday: { '@id': 'schema:Monday' }, + MonetaryAmount: { '@id': 'schema:MonetaryAmount' }, + MonetaryAmountDistribution: { '@id': 'schema:MonetaryAmountDistribution' }, + MonetaryGrant: { '@id': 'schema:MonetaryGrant' }, + MoneyTransfer: { '@id': 'schema:MoneyTransfer' }, + MortgageLoan: { '@id': 'schema:MortgageLoan' }, + Mosque: { '@id': 'schema:Mosque' }, + Motel: { '@id': 'schema:Motel' }, + Motorcycle: { '@id': 'schema:Motorcycle' }, + MotorcycleDealer: { '@id': 'schema:MotorcycleDealer' }, + MotorcycleRepair: { '@id': 'schema:MotorcycleRepair' }, + MotorizedBicycle: { '@id': 'schema:MotorizedBicycle' }, + Mountain: { '@id': 'schema:Mountain' }, + MoveAction: { '@id': 'schema:MoveAction' }, + Movie: { '@id': 'schema:Movie' }, + MovieClip: { '@id': 'schema:MovieClip' }, + MovieRentalStore: { '@id': 'schema:MovieRentalStore' }, + MovieSeries: { '@id': 'schema:MovieSeries' }, + MovieTheater: { '@id': 'schema:MovieTheater' }, + MovingCompany: { '@id': 'schema:MovingCompany' }, + MultiCenterTrial: { '@id': 'schema:MultiCenterTrial' }, + MultiPlayer: { '@id': 'schema:MultiPlayer' }, + MulticellularParasite: { '@id': 'schema:MulticellularParasite' }, + Muscle: { '@id': 'schema:Muscle' }, + Musculoskeletal: { '@id': 'schema:Musculoskeletal' }, + MusculoskeletalExam: { '@id': 'schema:MusculoskeletalExam' }, + Museum: { '@id': 'schema:Museum' }, + MusicAlbum: { '@id': 'schema:MusicAlbum' }, + MusicAlbumProductionType: { '@id': 'schema:MusicAlbumProductionType' }, + MusicAlbumReleaseType: { '@id': 'schema:MusicAlbumReleaseType' }, + MusicComposition: { '@id': 'schema:MusicComposition' }, + MusicEvent: { '@id': 'schema:MusicEvent' }, + MusicGroup: { '@id': 'schema:MusicGroup' }, + MusicPlaylist: { '@id': 'schema:MusicPlaylist' }, + MusicRecording: { '@id': 'schema:MusicRecording' }, + MusicRelease: { '@id': 'schema:MusicRelease' }, + MusicReleaseFormatType: { '@id': 'schema:MusicReleaseFormatType' }, + MusicStore: { '@id': 'schema:MusicStore' }, + MusicVenue: { '@id': 'schema:MusicVenue' }, + MusicVideoObject: { '@id': 'schema:MusicVideoObject' }, + NGO: { '@id': 'schema:NGO' }, + NLNonprofitType: { '@id': 'schema:NLNonprofitType' }, + NailSalon: { '@id': 'schema:NailSalon' }, + Neck: { '@id': 'schema:Neck' }, + Nerve: { '@id': 'schema:Nerve' }, + Neuro: { '@id': 'schema:Neuro' }, + Neurologic: { '@id': 'schema:Neurologic' }, + NewCondition: { '@id': 'schema:NewCondition' }, + NewsArticle: { '@id': 'schema:NewsArticle' }, + NewsMediaOrganization: { '@id': 'schema:NewsMediaOrganization' }, + Newspaper: { '@id': 'schema:Newspaper' }, + NightClub: { '@id': 'schema:NightClub' }, + NoninvasiveProcedure: { '@id': 'schema:NoninvasiveProcedure' }, + Nonprofit501a: { '@id': 'schema:Nonprofit501a' }, + Nonprofit501c1: { '@id': 'schema:Nonprofit501c1' }, + Nonprofit501c10: { '@id': 'schema:Nonprofit501c10' }, + Nonprofit501c11: { '@id': 'schema:Nonprofit501c11' }, + Nonprofit501c12: { '@id': 'schema:Nonprofit501c12' }, + Nonprofit501c13: { '@id': 'schema:Nonprofit501c13' }, + Nonprofit501c14: { '@id': 'schema:Nonprofit501c14' }, + Nonprofit501c15: { '@id': 'schema:Nonprofit501c15' }, + Nonprofit501c16: { '@id': 'schema:Nonprofit501c16' }, + Nonprofit501c17: { '@id': 'schema:Nonprofit501c17' }, + Nonprofit501c18: { '@id': 'schema:Nonprofit501c18' }, + Nonprofit501c19: { '@id': 'schema:Nonprofit501c19' }, + Nonprofit501c2: { '@id': 'schema:Nonprofit501c2' }, + Nonprofit501c20: { '@id': 'schema:Nonprofit501c20' }, + Nonprofit501c21: { '@id': 'schema:Nonprofit501c21' }, + Nonprofit501c22: { '@id': 'schema:Nonprofit501c22' }, + Nonprofit501c23: { '@id': 'schema:Nonprofit501c23' }, + Nonprofit501c24: { '@id': 'schema:Nonprofit501c24' }, + Nonprofit501c25: { '@id': 'schema:Nonprofit501c25' }, + Nonprofit501c26: { '@id': 'schema:Nonprofit501c26' }, + Nonprofit501c27: { '@id': 'schema:Nonprofit501c27' }, + Nonprofit501c28: { '@id': 'schema:Nonprofit501c28' }, + Nonprofit501c3: { '@id': 'schema:Nonprofit501c3' }, + Nonprofit501c4: { '@id': 'schema:Nonprofit501c4' }, + Nonprofit501c5: { '@id': 'schema:Nonprofit501c5' }, + Nonprofit501c6: { '@id': 'schema:Nonprofit501c6' }, + Nonprofit501c7: { '@id': 'schema:Nonprofit501c7' }, + Nonprofit501c8: { '@id': 'schema:Nonprofit501c8' }, + Nonprofit501c9: { '@id': 'schema:Nonprofit501c9' }, + Nonprofit501d: { '@id': 'schema:Nonprofit501d' }, + Nonprofit501e: { '@id': 'schema:Nonprofit501e' }, + Nonprofit501f: { '@id': 'schema:Nonprofit501f' }, + Nonprofit501k: { '@id': 'schema:Nonprofit501k' }, + Nonprofit501n: { '@id': 'schema:Nonprofit501n' }, + Nonprofit501q: { '@id': 'schema:Nonprofit501q' }, + Nonprofit527: { '@id': 'schema:Nonprofit527' }, + NonprofitANBI: { '@id': 'schema:NonprofitANBI' }, + NonprofitSBBI: { '@id': 'schema:NonprofitSBBI' }, + NonprofitType: { '@id': 'schema:NonprofitType' }, + Nose: { '@id': 'schema:Nose' }, + NotInForce: { '@id': 'schema:NotInForce' }, + NotYetRecruiting: { '@id': 'schema:NotYetRecruiting' }, + Notary: { '@id': 'schema:Notary' }, + NoteDigitalDocument: { '@id': 'schema:NoteDigitalDocument' }, + Number: { '@id': 'schema:Number' }, + Nursing: { '@id': 'schema:Nursing' }, + NutritionInformation: { '@id': 'schema:NutritionInformation' }, + OTC: { '@id': 'schema:OTC' }, + Observation: { '@id': 'schema:Observation' }, + Observational: { '@id': 'schema:Observational' }, + Obstetric: { '@id': 'schema:Obstetric' }, + Occupation: { '@id': 'schema:Occupation' }, + OccupationalActivity: { '@id': 'schema:OccupationalActivity' }, + OccupationalExperienceRequirements: { + '@id': 'schema:OccupationalExperienceRequirements', + }, + OccupationalTherapy: { '@id': 'schema:OccupationalTherapy' }, + OceanBodyOfWater: { '@id': 'schema:OceanBodyOfWater' }, + Offer: { '@id': 'schema:Offer' }, + OfferCatalog: { '@id': 'schema:OfferCatalog' }, + OfferForLease: { '@id': 'schema:OfferForLease' }, + OfferForPurchase: { '@id': 'schema:OfferForPurchase' }, + OfferItemCondition: { '@id': 'schema:OfferItemCondition' }, + OfferShippingDetails: { '@id': 'schema:OfferShippingDetails' }, + OfficeEquipmentStore: { '@id': 'schema:OfficeEquipmentStore' }, + OfficialLegalValue: { '@id': 'schema:OfficialLegalValue' }, + OfflineEventAttendanceMode: { '@id': 'schema:OfflineEventAttendanceMode' }, + OfflinePermanently: { '@id': 'schema:OfflinePermanently' }, + OfflineTemporarily: { '@id': 'schema:OfflineTemporarily' }, + OnDemandEvent: { '@id': 'schema:OnDemandEvent' }, + OnSitePickup: { '@id': 'schema:OnSitePickup' }, + Oncologic: { '@id': 'schema:Oncologic' }, + OneTimePayments: { '@id': 'schema:OneTimePayments' }, + Online: { '@id': 'schema:Online' }, + OnlineEventAttendanceMode: { '@id': 'schema:OnlineEventAttendanceMode' }, + OnlineFull: { '@id': 'schema:OnlineFull' }, + OnlineOnly: { '@id': 'schema:OnlineOnly' }, + OpenTrial: { '@id': 'schema:OpenTrial' }, + OpeningHoursSpecification: { '@id': 'schema:OpeningHoursSpecification' }, + OpinionNewsArticle: { '@id': 'schema:OpinionNewsArticle' }, + Optician: { '@id': 'schema:Optician' }, + Optometric: { '@id': 'schema:Optometric' }, + Order: { '@id': 'schema:Order' }, + OrderAction: { '@id': 'schema:OrderAction' }, + OrderCancelled: { '@id': 'schema:OrderCancelled' }, + OrderDelivered: { '@id': 'schema:OrderDelivered' }, + OrderInTransit: { '@id': 'schema:OrderInTransit' }, + OrderItem: { '@id': 'schema:OrderItem' }, + OrderPaymentDue: { '@id': 'schema:OrderPaymentDue' }, + OrderPickupAvailable: { '@id': 'schema:OrderPickupAvailable' }, + OrderProblem: { '@id': 'schema:OrderProblem' }, + OrderProcessing: { '@id': 'schema:OrderProcessing' }, + OrderReturned: { '@id': 'schema:OrderReturned' }, + OrderStatus: { '@id': 'schema:OrderStatus' }, + Organization: { '@id': 'schema:Organization' }, + OrganizationRole: { '@id': 'schema:OrganizationRole' }, + OrganizeAction: { '@id': 'schema:OrganizeAction' }, + OriginalMediaContent: { '@id': 'schema:OriginalMediaContent' }, + OriginalShippingFees: { '@id': 'schema:OriginalShippingFees' }, + Osteopathic: { '@id': 'schema:Osteopathic' }, + Otolaryngologic: { '@id': 'schema:Otolaryngologic' }, + OutOfStock: { '@id': 'schema:OutOfStock' }, + OutletStore: { '@id': 'schema:OutletStore' }, + OverviewHealthAspect: { '@id': 'schema:OverviewHealthAspect' }, + OwnershipInfo: { '@id': 'schema:OwnershipInfo' }, + PET: { '@id': 'schema:PET' }, + PaidLeave: { '@id': 'schema:PaidLeave' }, + PaintAction: { '@id': 'schema:PaintAction' }, + Painting: { '@id': 'schema:Painting' }, + PalliativeProcedure: { '@id': 'schema:PalliativeProcedure' }, + Paperback: { '@id': 'schema:Paperback' }, + ParcelDelivery: { '@id': 'schema:ParcelDelivery' }, + ParcelService: { '@id': 'schema:ParcelService' }, + ParentAudience: { '@id': 'schema:ParentAudience' }, + ParentalSupport: { '@id': 'schema:ParentalSupport' }, + Park: { '@id': 'schema:Park' }, + ParkingFacility: { '@id': 'schema:ParkingFacility' }, + ParkingMap: { '@id': 'schema:ParkingMap' }, + PartiallyInForce: { '@id': 'schema:PartiallyInForce' }, + Pathology: { '@id': 'schema:Pathology' }, + PathologyTest: { '@id': 'schema:PathologyTest' }, + Patient: { '@id': 'schema:Patient' }, + PatientExperienceHealthAspect: { + '@id': 'schema:PatientExperienceHealthAspect', + }, + PawnShop: { '@id': 'schema:PawnShop' }, + PayAction: { '@id': 'schema:PayAction' }, + PaymentAutomaticallyApplied: { '@id': 'schema:PaymentAutomaticallyApplied' }, + PaymentCard: { '@id': 'schema:PaymentCard' }, + PaymentChargeSpecification: { '@id': 'schema:PaymentChargeSpecification' }, + PaymentComplete: { '@id': 'schema:PaymentComplete' }, + PaymentDeclined: { '@id': 'schema:PaymentDeclined' }, + PaymentDue: { '@id': 'schema:PaymentDue' }, + PaymentMethod: { '@id': 'schema:PaymentMethod' }, + PaymentPastDue: { '@id': 'schema:PaymentPastDue' }, + PaymentService: { '@id': 'schema:PaymentService' }, + PaymentStatusType: { '@id': 'schema:PaymentStatusType' }, + Pediatric: { '@id': 'schema:Pediatric' }, + PeopleAudience: { '@id': 'schema:PeopleAudience' }, + PercutaneousProcedure: { '@id': 'schema:PercutaneousProcedure' }, + PerformAction: { '@id': 'schema:PerformAction' }, + PerformanceRole: { '@id': 'schema:PerformanceRole' }, + PerformingArtsTheater: { '@id': 'schema:PerformingArtsTheater' }, + PerformingGroup: { '@id': 'schema:PerformingGroup' }, + Periodical: { '@id': 'schema:Periodical' }, + Permit: { '@id': 'schema:Permit' }, + Person: { '@id': 'schema:Person' }, + PetStore: { '@id': 'schema:PetStore' }, + Pharmacy: { '@id': 'schema:Pharmacy' }, + PharmacySpecialty: { '@id': 'schema:PharmacySpecialty' }, + Photograph: { '@id': 'schema:Photograph' }, + PhotographAction: { '@id': 'schema:PhotographAction' }, + PhysicalActivity: { '@id': 'schema:PhysicalActivity' }, + PhysicalActivityCategory: { '@id': 'schema:PhysicalActivityCategory' }, + PhysicalExam: { '@id': 'schema:PhysicalExam' }, + PhysicalTherapy: { '@id': 'schema:PhysicalTherapy' }, + Physician: { '@id': 'schema:Physician' }, + Physiotherapy: { '@id': 'schema:Physiotherapy' }, + Place: { '@id': 'schema:Place' }, + PlaceOfWorship: { '@id': 'schema:PlaceOfWorship' }, + PlaceboControlledTrial: { '@id': 'schema:PlaceboControlledTrial' }, + PlanAction: { '@id': 'schema:PlanAction' }, + PlasticSurgery: { '@id': 'schema:PlasticSurgery' }, + Play: { '@id': 'schema:Play' }, + PlayAction: { '@id': 'schema:PlayAction' }, + Playground: { '@id': 'schema:Playground' }, + Plumber: { '@id': 'schema:Plumber' }, + PodcastEpisode: { '@id': 'schema:PodcastEpisode' }, + PodcastSeason: { '@id': 'schema:PodcastSeason' }, + PodcastSeries: { '@id': 'schema:PodcastSeries' }, + Podiatric: { '@id': 'schema:Podiatric' }, + PoliceStation: { '@id': 'schema:PoliceStation' }, + Pond: { '@id': 'schema:Pond' }, + PostOffice: { '@id': 'schema:PostOffice' }, + PostalAddress: { '@id': 'schema:PostalAddress' }, + PostalCodeRangeSpecification: { '@id': 'schema:PostalCodeRangeSpecification' }, + Poster: { '@id': 'schema:Poster' }, + PotentialActionStatus: { '@id': 'schema:PotentialActionStatus' }, + PreOrder: { '@id': 'schema:PreOrder' }, + PreOrderAction: { '@id': 'schema:PreOrderAction' }, + PreSale: { '@id': 'schema:PreSale' }, + PregnancyHealthAspect: { '@id': 'schema:PregnancyHealthAspect' }, + PrependAction: { '@id': 'schema:PrependAction' }, + Preschool: { '@id': 'schema:Preschool' }, + PrescriptionOnly: { '@id': 'schema:PrescriptionOnly' }, + PresentationDigitalDocument: { '@id': 'schema:PresentationDigitalDocument' }, + PreventionHealthAspect: { '@id': 'schema:PreventionHealthAspect' }, + PreventionIndication: { '@id': 'schema:PreventionIndication' }, + PriceComponentTypeEnumeration: { + '@id': 'schema:PriceComponentTypeEnumeration', + }, + PriceSpecification: { '@id': 'schema:PriceSpecification' }, + PriceTypeEnumeration: { '@id': 'schema:PriceTypeEnumeration' }, + PrimaryCare: { '@id': 'schema:PrimaryCare' }, + Prion: { '@id': 'schema:Prion' }, + Product: { '@id': 'schema:Product' }, + ProductCollection: { '@id': 'schema:ProductCollection' }, + ProductGroup: { '@id': 'schema:ProductGroup' }, + ProductModel: { '@id': 'schema:ProductModel' }, + ProductReturnEnumeration: { '@id': 'schema:ProductReturnEnumeration' }, + ProductReturnFiniteReturnWindow: { + '@id': 'schema:ProductReturnFiniteReturnWindow', + }, + ProductReturnNotPermitted: { '@id': 'schema:ProductReturnNotPermitted' }, + ProductReturnPolicy: { '@id': 'schema:ProductReturnPolicy' }, + ProductReturnUnlimitedWindow: { '@id': 'schema:ProductReturnUnlimitedWindow' }, + ProductReturnUnspecified: { '@id': 'schema:ProductReturnUnspecified' }, + ProfessionalService: { '@id': 'schema:ProfessionalService' }, + ProfilePage: { '@id': 'schema:ProfilePage' }, + PrognosisHealthAspect: { '@id': 'schema:PrognosisHealthAspect' }, + ProgramMembership: { '@id': 'schema:ProgramMembership' }, + Project: { '@id': 'schema:Project' }, + PronounceableText: { '@id': 'schema:PronounceableText' }, + Property: { '@id': 'schema:Property' }, + PropertyValue: { '@id': 'schema:PropertyValue' }, + PropertyValueSpecification: { '@id': 'schema:PropertyValueSpecification' }, + Protozoa: { '@id': 'schema:Protozoa' }, + Psychiatric: { '@id': 'schema:Psychiatric' }, + PsychologicalTreatment: { '@id': 'schema:PsychologicalTreatment' }, + PublicHealth: { '@id': 'schema:PublicHealth' }, + PublicHolidays: { '@id': 'schema:PublicHolidays' }, + PublicSwimmingPool: { '@id': 'schema:PublicSwimmingPool' }, + PublicToilet: { '@id': 'schema:PublicToilet' }, + PublicationEvent: { '@id': 'schema:PublicationEvent' }, + PublicationIssue: { '@id': 'schema:PublicationIssue' }, + PublicationVolume: { '@id': 'schema:PublicationVolume' }, + Pulmonary: { '@id': 'schema:Pulmonary' }, + QAPage: { '@id': 'schema:QAPage' }, + QualitativeValue: { '@id': 'schema:QualitativeValue' }, + QuantitativeValue: { '@id': 'schema:QuantitativeValue' }, + QuantitativeValueDistribution: { + '@id': 'schema:QuantitativeValueDistribution', + }, + Quantity: { '@id': 'schema:Quantity' }, + Question: { '@id': 'schema:Question' }, + Quiz: { '@id': 'schema:Quiz' }, + Quotation: { '@id': 'schema:Quotation' }, + QuoteAction: { '@id': 'schema:QuoteAction' }, + RVPark: { '@id': 'schema:RVPark' }, + RadiationTherapy: { '@id': 'schema:RadiationTherapy' }, + RadioBroadcastService: { '@id': 'schema:RadioBroadcastService' }, + RadioChannel: { '@id': 'schema:RadioChannel' }, + RadioClip: { '@id': 'schema:RadioClip' }, + RadioEpisode: { '@id': 'schema:RadioEpisode' }, + RadioSeason: { '@id': 'schema:RadioSeason' }, + RadioSeries: { '@id': 'schema:RadioSeries' }, + RadioStation: { '@id': 'schema:RadioStation' }, + Radiography: { '@id': 'schema:Radiography' }, + RandomizedTrial: { '@id': 'schema:RandomizedTrial' }, + Rating: { '@id': 'schema:Rating' }, + ReactAction: { '@id': 'schema:ReactAction' }, + ReadAction: { '@id': 'schema:ReadAction' }, + ReadPermission: { '@id': 'schema:ReadPermission' }, + RealEstateAgent: { '@id': 'schema:RealEstateAgent' }, + RealEstateListing: { '@id': 'schema:RealEstateListing' }, + RearWheelDriveConfiguration: { '@id': 'schema:RearWheelDriveConfiguration' }, + ReceiveAction: { '@id': 'schema:ReceiveAction' }, + Recipe: { '@id': 'schema:Recipe' }, + Recommendation: { '@id': 'schema:Recommendation' }, + RecommendedDoseSchedule: { '@id': 'schema:RecommendedDoseSchedule' }, + Recruiting: { '@id': 'schema:Recruiting' }, + RecyclingCenter: { '@id': 'schema:RecyclingCenter' }, + RefundTypeEnumeration: { '@id': 'schema:RefundTypeEnumeration' }, + RefurbishedCondition: { '@id': 'schema:RefurbishedCondition' }, + RegisterAction: { '@id': 'schema:RegisterAction' }, + Registry: { '@id': 'schema:Registry' }, + ReimbursementCap: { '@id': 'schema:ReimbursementCap' }, + RejectAction: { '@id': 'schema:RejectAction' }, + RelatedTopicsHealthAspect: { '@id': 'schema:RelatedTopicsHealthAspect' }, + RemixAlbum: { '@id': 'schema:RemixAlbum' }, + Renal: { '@id': 'schema:Renal' }, + RentAction: { '@id': 'schema:RentAction' }, + RentalCarReservation: { '@id': 'schema:RentalCarReservation' }, + RentalVehicleUsage: { '@id': 'schema:RentalVehicleUsage' }, + RepaymentSpecification: { '@id': 'schema:RepaymentSpecification' }, + ReplaceAction: { '@id': 'schema:ReplaceAction' }, + ReplyAction: { '@id': 'schema:ReplyAction' }, + Report: { '@id': 'schema:Report' }, + ReportageNewsArticle: { '@id': 'schema:ReportageNewsArticle' }, + ReportedDoseSchedule: { '@id': 'schema:ReportedDoseSchedule' }, + ResearchProject: { '@id': 'schema:ResearchProject' }, + Researcher: { '@id': 'schema:Researcher' }, + Reservation: { '@id': 'schema:Reservation' }, + ReservationCancelled: { '@id': 'schema:ReservationCancelled' }, + ReservationConfirmed: { '@id': 'schema:ReservationConfirmed' }, + ReservationHold: { '@id': 'schema:ReservationHold' }, + ReservationPackage: { '@id': 'schema:ReservationPackage' }, + ReservationPending: { '@id': 'schema:ReservationPending' }, + ReservationStatusType: { '@id': 'schema:ReservationStatusType' }, + ReserveAction: { '@id': 'schema:ReserveAction' }, + Reservoir: { '@id': 'schema:Reservoir' }, + Residence: { '@id': 'schema:Residence' }, + Resort: { '@id': 'schema:Resort' }, + RespiratoryTherapy: { '@id': 'schema:RespiratoryTherapy' }, + Restaurant: { '@id': 'schema:Restaurant' }, + RestockingFees: { '@id': 'schema:RestockingFees' }, + RestrictedDiet: { '@id': 'schema:RestrictedDiet' }, + ResultsAvailable: { '@id': 'schema:ResultsAvailable' }, + ResultsNotAvailable: { '@id': 'schema:ResultsNotAvailable' }, + ResumeAction: { '@id': 'schema:ResumeAction' }, + Retail: { '@id': 'schema:Retail' }, + ReturnAction: { '@id': 'schema:ReturnAction' }, + ReturnFeesEnumeration: { '@id': 'schema:ReturnFeesEnumeration' }, + ReturnShippingFees: { '@id': 'schema:ReturnShippingFees' }, + Review: { '@id': 'schema:Review' }, + ReviewAction: { '@id': 'schema:ReviewAction' }, + ReviewNewsArticle: { '@id': 'schema:ReviewNewsArticle' }, + Rheumatologic: { '@id': 'schema:Rheumatologic' }, + RightHandDriving: { '@id': 'schema:RightHandDriving' }, + RisksOrComplicationsHealthAspect: { + '@id': 'schema:RisksOrComplicationsHealthAspect', + }, + RiverBodyOfWater: { '@id': 'schema:RiverBodyOfWater' }, + Role: { '@id': 'schema:Role' }, + RoofingContractor: { '@id': 'schema:RoofingContractor' }, + Room: { '@id': 'schema:Room' }, + RsvpAction: { '@id': 'schema:RsvpAction' }, + RsvpResponseMaybe: { '@id': 'schema:RsvpResponseMaybe' }, + RsvpResponseNo: { '@id': 'schema:RsvpResponseNo' }, + RsvpResponseType: { '@id': 'schema:RsvpResponseType' }, + RsvpResponseYes: { '@id': 'schema:RsvpResponseYes' }, + SRP: { '@id': 'schema:SRP' }, + SafetyHealthAspect: { '@id': 'schema:SafetyHealthAspect' }, + SaleEvent: { '@id': 'schema:SaleEvent' }, + SalePrice: { '@id': 'schema:SalePrice' }, + SatireOrParodyContent: { '@id': 'schema:SatireOrParodyContent' }, + SatiricalArticle: { '@id': 'schema:SatiricalArticle' }, + Saturday: { '@id': 'schema:Saturday' }, + Schedule: { '@id': 'schema:Schedule' }, + ScheduleAction: { '@id': 'schema:ScheduleAction' }, + ScholarlyArticle: { '@id': 'schema:ScholarlyArticle' }, + School: { '@id': 'schema:School' }, + SchoolDistrict: { '@id': 'schema:SchoolDistrict' }, + ScreeningEvent: { '@id': 'schema:ScreeningEvent' }, + ScreeningHealthAspect: { '@id': 'schema:ScreeningHealthAspect' }, + Sculpture: { '@id': 'schema:Sculpture' }, + SeaBodyOfWater: { '@id': 'schema:SeaBodyOfWater' }, + SearchAction: { '@id': 'schema:SearchAction' }, + SearchResultsPage: { '@id': 'schema:SearchResultsPage' }, + Season: { '@id': 'schema:Season' }, + Seat: { '@id': 'schema:Seat' }, + SeatingMap: { '@id': 'schema:SeatingMap' }, + SeeDoctorHealthAspect: { '@id': 'schema:SeeDoctorHealthAspect' }, + SeekToAction: { '@id': 'schema:SeekToAction' }, + SelfCareHealthAspect: { '@id': 'schema:SelfCareHealthAspect' }, + SelfStorage: { '@id': 'schema:SelfStorage' }, + SellAction: { '@id': 'schema:SellAction' }, + SendAction: { '@id': 'schema:SendAction' }, + Series: { '@id': 'schema:Series' }, + Service: { '@id': 'schema:Service' }, + ServiceChannel: { '@id': 'schema:ServiceChannel' }, + ShareAction: { '@id': 'schema:ShareAction' }, + SheetMusic: { '@id': 'schema:SheetMusic' }, + ShippingDeliveryTime: { '@id': 'schema:ShippingDeliveryTime' }, + ShippingRateSettings: { '@id': 'schema:ShippingRateSettings' }, + ShoeStore: { '@id': 'schema:ShoeStore' }, + ShoppingCenter: { '@id': 'schema:ShoppingCenter' }, + ShortStory: { '@id': 'schema:ShortStory' }, + SideEffectsHealthAspect: { '@id': 'schema:SideEffectsHealthAspect' }, + SingleBlindedTrial: { '@id': 'schema:SingleBlindedTrial' }, + SingleCenterTrial: { '@id': 'schema:SingleCenterTrial' }, + SingleFamilyResidence: { '@id': 'schema:SingleFamilyResidence' }, + SinglePlayer: { '@id': 'schema:SinglePlayer' }, + SingleRelease: { '@id': 'schema:SingleRelease' }, + SiteNavigationElement: { '@id': 'schema:SiteNavigationElement' }, + SizeGroupEnumeration: { '@id': 'schema:SizeGroupEnumeration' }, + SizeSpecification: { '@id': 'schema:SizeSpecification' }, + SizeSystemEnumeration: { '@id': 'schema:SizeSystemEnumeration' }, + SizeSystemImperial: { '@id': 'schema:SizeSystemImperial' }, + SizeSystemMetric: { '@id': 'schema:SizeSystemMetric' }, + SkiResort: { '@id': 'schema:SkiResort' }, + Skin: { '@id': 'schema:Skin' }, + SocialEvent: { '@id': 'schema:SocialEvent' }, + SocialMediaPosting: { '@id': 'schema:SocialMediaPosting' }, + SoftwareApplication: { '@id': 'schema:SoftwareApplication' }, + SoftwareSourceCode: { '@id': 'schema:SoftwareSourceCode' }, + SoldOut: { '@id': 'schema:SoldOut' }, + SolveMathAction: { '@id': 'schema:SolveMathAction' }, + SomeProducts: { '@id': 'schema:SomeProducts' }, + SoundtrackAlbum: { '@id': 'schema:SoundtrackAlbum' }, + SpeakableSpecification: { '@id': 'schema:SpeakableSpecification' }, + SpecialAnnouncement: { '@id': 'schema:SpecialAnnouncement' }, + Specialty: { '@id': 'schema:Specialty' }, + SpeechPathology: { '@id': 'schema:SpeechPathology' }, + SpokenWordAlbum: { '@id': 'schema:SpokenWordAlbum' }, + SportingGoodsStore: { '@id': 'schema:SportingGoodsStore' }, + SportsActivityLocation: { '@id': 'schema:SportsActivityLocation' }, + SportsClub: { '@id': 'schema:SportsClub' }, + SportsEvent: { '@id': 'schema:SportsEvent' }, + SportsOrganization: { '@id': 'schema:SportsOrganization' }, + SportsTeam: { '@id': 'schema:SportsTeam' }, + SpreadsheetDigitalDocument: { '@id': 'schema:SpreadsheetDigitalDocument' }, + StadiumOrArena: { '@id': 'schema:StadiumOrArena' }, + StagedContent: { '@id': 'schema:StagedContent' }, + StagesHealthAspect: { '@id': 'schema:StagesHealthAspect' }, + State: { '@id': 'schema:State' }, + StatisticalPopulation: { '@id': 'schema:StatisticalPopulation' }, + StatusEnumeration: { '@id': 'schema:StatusEnumeration' }, + SteeringPositionValue: { '@id': 'schema:SteeringPositionValue' }, + Store: { '@id': 'schema:Store' }, + StoreCreditRefund: { '@id': 'schema:StoreCreditRefund' }, + StrengthTraining: { '@id': 'schema:StrengthTraining' }, + StructuredValue: { '@id': 'schema:StructuredValue' }, + StudioAlbum: { '@id': 'schema:StudioAlbum' }, + StupidType: { '@id': 'schema:StupidType' }, + SubscribeAction: { '@id': 'schema:SubscribeAction' }, + Subscription: { '@id': 'schema:Subscription' }, + Substance: { '@id': 'schema:Substance' }, + SubwayStation: { '@id': 'schema:SubwayStation' }, + Suite: { '@id': 'schema:Suite' }, + Sunday: { '@id': 'schema:Sunday' }, + SuperficialAnatomy: { '@id': 'schema:SuperficialAnatomy' }, + Surgical: { '@id': 'schema:Surgical' }, + SurgicalProcedure: { '@id': 'schema:SurgicalProcedure' }, + SuspendAction: { '@id': 'schema:SuspendAction' }, + Suspended: { '@id': 'schema:Suspended' }, + SymptomsHealthAspect: { '@id': 'schema:SymptomsHealthAspect' }, + Synagogue: { '@id': 'schema:Synagogue' }, + TVClip: { '@id': 'schema:TVClip' }, + TVEpisode: { '@id': 'schema:TVEpisode' }, + TVSeason: { '@id': 'schema:TVSeason' }, + TVSeries: { '@id': 'schema:TVSeries' }, + Table: { '@id': 'schema:Table' }, + TakeAction: { '@id': 'schema:TakeAction' }, + TattooParlor: { '@id': 'schema:TattooParlor' }, + Taxi: { '@id': 'schema:Taxi' }, + TaxiReservation: { '@id': 'schema:TaxiReservation' }, + TaxiService: { '@id': 'schema:TaxiService' }, + TaxiStand: { '@id': 'schema:TaxiStand' }, + TaxiVehicleUsage: { '@id': 'schema:TaxiVehicleUsage' }, + TechArticle: { '@id': 'schema:TechArticle' }, + TelevisionChannel: { '@id': 'schema:TelevisionChannel' }, + TelevisionStation: { '@id': 'schema:TelevisionStation' }, + TennisComplex: { '@id': 'schema:TennisComplex' }, + Terminated: { '@id': 'schema:Terminated' }, + Text: { '@id': 'schema:Text' }, + TextDigitalDocument: { '@id': 'schema:TextDigitalDocument' }, + TheaterEvent: { '@id': 'schema:TheaterEvent' }, + TheaterGroup: { '@id': 'schema:TheaterGroup' }, + Therapeutic: { '@id': 'schema:Therapeutic' }, + TherapeuticProcedure: { '@id': 'schema:TherapeuticProcedure' }, + Thesis: { '@id': 'schema:Thesis' }, + Thing: { '@id': 'schema:Thing' }, + Throat: { '@id': 'schema:Throat' }, + Thursday: { '@id': 'schema:Thursday' }, + Ticket: { '@id': 'schema:Ticket' }, + TieAction: { '@id': 'schema:TieAction' }, + Time: { '@id': 'schema:Time' }, + TipAction: { '@id': 'schema:TipAction' }, + TireShop: { '@id': 'schema:TireShop' }, + TollFree: { '@id': 'schema:TollFree' }, + TouristAttraction: { '@id': 'schema:TouristAttraction' }, + TouristDestination: { '@id': 'schema:TouristDestination' }, + TouristInformationCenter: { '@id': 'schema:TouristInformationCenter' }, + TouristTrip: { '@id': 'schema:TouristTrip' }, + Toxicologic: { '@id': 'schema:Toxicologic' }, + ToyStore: { '@id': 'schema:ToyStore' }, + TrackAction: { '@id': 'schema:TrackAction' }, + TradeAction: { '@id': 'schema:TradeAction' }, + TraditionalChinese: { '@id': 'schema:TraditionalChinese' }, + TrainReservation: { '@id': 'schema:TrainReservation' }, + TrainStation: { '@id': 'schema:TrainStation' }, + TrainTrip: { '@id': 'schema:TrainTrip' }, + TransferAction: { '@id': 'schema:TransferAction' }, + TransformedContent: { '@id': 'schema:TransformedContent' }, + TransitMap: { '@id': 'schema:TransitMap' }, + TravelAction: { '@id': 'schema:TravelAction' }, + TravelAgency: { '@id': 'schema:TravelAgency' }, + TreatmentIndication: { '@id': 'schema:TreatmentIndication' }, + TreatmentsHealthAspect: { '@id': 'schema:TreatmentsHealthAspect' }, + Trip: { '@id': 'schema:Trip' }, + TripleBlindedTrial: { '@id': 'schema:TripleBlindedTrial' }, + True: { '@id': 'schema:True' }, + Tuesday: { '@id': 'schema:Tuesday' }, + TypeAndQuantityNode: { '@id': 'schema:TypeAndQuantityNode' }, + TypesHealthAspect: { '@id': 'schema:TypesHealthAspect' }, + UKNonprofitType: { '@id': 'schema:UKNonprofitType' }, + UKTrust: { '@id': 'schema:UKTrust' }, + URL: { '@id': 'schema:URL' }, + USNonprofitType: { '@id': 'schema:USNonprofitType' }, + Ultrasound: { '@id': 'schema:Ultrasound' }, + UnRegisterAction: { '@id': 'schema:UnRegisterAction' }, + UnemploymentSupport: { '@id': 'schema:UnemploymentSupport' }, + UnincorporatedAssociationCharity: { + '@id': 'schema:UnincorporatedAssociationCharity', + }, + UnitPriceSpecification: { '@id': 'schema:UnitPriceSpecification' }, + UnofficialLegalValue: { '@id': 'schema:UnofficialLegalValue' }, + UpdateAction: { '@id': 'schema:UpdateAction' }, + Urologic: { '@id': 'schema:Urologic' }, + UsageOrScheduleHealthAspect: { '@id': 'schema:UsageOrScheduleHealthAspect' }, + UseAction: { '@id': 'schema:UseAction' }, + UsedCondition: { '@id': 'schema:UsedCondition' }, + UserBlocks: { '@id': 'schema:UserBlocks' }, + UserCheckins: { '@id': 'schema:UserCheckins' }, + UserComments: { '@id': 'schema:UserComments' }, + UserDownloads: { '@id': 'schema:UserDownloads' }, + UserInteraction: { '@id': 'schema:UserInteraction' }, + UserLikes: { '@id': 'schema:UserLikes' }, + UserPageVisits: { '@id': 'schema:UserPageVisits' }, + UserPlays: { '@id': 'schema:UserPlays' }, + UserPlusOnes: { '@id': 'schema:UserPlusOnes' }, + UserReview: { '@id': 'schema:UserReview' }, + UserTweets: { '@id': 'schema:UserTweets' }, + VeganDiet: { '@id': 'schema:VeganDiet' }, + VegetarianDiet: { '@id': 'schema:VegetarianDiet' }, + Vehicle: { '@id': 'schema:Vehicle' }, + Vein: { '@id': 'schema:Vein' }, + VenueMap: { '@id': 'schema:VenueMap' }, + Vessel: { '@id': 'schema:Vessel' }, + VeterinaryCare: { '@id': 'schema:VeterinaryCare' }, + VideoGallery: { '@id': 'schema:VideoGallery' }, + VideoGame: { '@id': 'schema:VideoGame' }, + VideoGameClip: { '@id': 'schema:VideoGameClip' }, + VideoGameSeries: { '@id': 'schema:VideoGameSeries' }, + VideoObject: { '@id': 'schema:VideoObject' }, + ViewAction: { '@id': 'schema:ViewAction' }, + VinylFormat: { '@id': 'schema:VinylFormat' }, + VirtualLocation: { '@id': 'schema:VirtualLocation' }, + Virus: { '@id': 'schema:Virus' }, + VisualArtsEvent: { '@id': 'schema:VisualArtsEvent' }, + VisualArtwork: { '@id': 'schema:VisualArtwork' }, + VitalSign: { '@id': 'schema:VitalSign' }, + Volcano: { '@id': 'schema:Volcano' }, + VoteAction: { '@id': 'schema:VoteAction' }, + WPAdBlock: { '@id': 'schema:WPAdBlock' }, + WPFooter: { '@id': 'schema:WPFooter' }, + WPHeader: { '@id': 'schema:WPHeader' }, + WPSideBar: { '@id': 'schema:WPSideBar' }, + WantAction: { '@id': 'schema:WantAction' }, + WarrantyPromise: { '@id': 'schema:WarrantyPromise' }, + WarrantyScope: { '@id': 'schema:WarrantyScope' }, + WatchAction: { '@id': 'schema:WatchAction' }, + Waterfall: { '@id': 'schema:Waterfall' }, + WearAction: { '@id': 'schema:WearAction' }, + WearableMeasurementBack: { '@id': 'schema:WearableMeasurementBack' }, + WearableMeasurementChestOrBust: { + '@id': 'schema:WearableMeasurementChestOrBust', + }, + WearableMeasurementCollar: { '@id': 'schema:WearableMeasurementCollar' }, + WearableMeasurementCup: { '@id': 'schema:WearableMeasurementCup' }, + WearableMeasurementHeight: { '@id': 'schema:WearableMeasurementHeight' }, + WearableMeasurementHips: { '@id': 'schema:WearableMeasurementHips' }, + WearableMeasurementInseam: { '@id': 'schema:WearableMeasurementInseam' }, + WearableMeasurementLength: { '@id': 'schema:WearableMeasurementLength' }, + WearableMeasurementOutsideLeg: { + '@id': 'schema:WearableMeasurementOutsideLeg', + }, + WearableMeasurementSleeve: { '@id': 'schema:WearableMeasurementSleeve' }, + WearableMeasurementTypeEnumeration: { + '@id': 'schema:WearableMeasurementTypeEnumeration', + }, + WearableMeasurementWaist: { '@id': 'schema:WearableMeasurementWaist' }, + WearableMeasurementWidth: { '@id': 'schema:WearableMeasurementWidth' }, + WearableSizeGroupBig: { '@id': 'schema:WearableSizeGroupBig' }, + WearableSizeGroupBoys: { '@id': 'schema:WearableSizeGroupBoys' }, + WearableSizeGroupEnumeration: { '@id': 'schema:WearableSizeGroupEnumeration' }, + WearableSizeGroupExtraShort: { '@id': 'schema:WearableSizeGroupExtraShort' }, + WearableSizeGroupExtraTall: { '@id': 'schema:WearableSizeGroupExtraTall' }, + WearableSizeGroupGirls: { '@id': 'schema:WearableSizeGroupGirls' }, + WearableSizeGroupHusky: { '@id': 'schema:WearableSizeGroupHusky' }, + WearableSizeGroupInfants: { '@id': 'schema:WearableSizeGroupInfants' }, + WearableSizeGroupJuniors: { '@id': 'schema:WearableSizeGroupJuniors' }, + WearableSizeGroupMaternity: { '@id': 'schema:WearableSizeGroupMaternity' }, + WearableSizeGroupMens: { '@id': 'schema:WearableSizeGroupMens' }, + WearableSizeGroupMisses: { '@id': 'schema:WearableSizeGroupMisses' }, + WearableSizeGroupPetite: { '@id': 'schema:WearableSizeGroupPetite' }, + WearableSizeGroupPlus: { '@id': 'schema:WearableSizeGroupPlus' }, + WearableSizeGroupRegular: { '@id': 'schema:WearableSizeGroupRegular' }, + WearableSizeGroupShort: { '@id': 'schema:WearableSizeGroupShort' }, + WearableSizeGroupTall: { '@id': 'schema:WearableSizeGroupTall' }, + WearableSizeGroupWomens: { '@id': 'schema:WearableSizeGroupWomens' }, + WearableSizeSystemAU: { '@id': 'schema:WearableSizeSystemAU' }, + WearableSizeSystemBR: { '@id': 'schema:WearableSizeSystemBR' }, + WearableSizeSystemCN: { '@id': 'schema:WearableSizeSystemCN' }, + WearableSizeSystemContinental: { + '@id': 'schema:WearableSizeSystemContinental', + }, + WearableSizeSystemDE: { '@id': 'schema:WearableSizeSystemDE' }, + WearableSizeSystemEN13402: { '@id': 'schema:WearableSizeSystemEN13402' }, + WearableSizeSystemEnumeration: { + '@id': 'schema:WearableSizeSystemEnumeration', + }, + WearableSizeSystemEurope: { '@id': 'schema:WearableSizeSystemEurope' }, + WearableSizeSystemFR: { '@id': 'schema:WearableSizeSystemFR' }, + WearableSizeSystemGS1: { '@id': 'schema:WearableSizeSystemGS1' }, + WearableSizeSystemIT: { '@id': 'schema:WearableSizeSystemIT' }, + WearableSizeSystemJP: { '@id': 'schema:WearableSizeSystemJP' }, + WearableSizeSystemMX: { '@id': 'schema:WearableSizeSystemMX' }, + WearableSizeSystemUK: { '@id': 'schema:WearableSizeSystemUK' }, + WearableSizeSystemUS: { '@id': 'schema:WearableSizeSystemUS' }, + WebAPI: { '@id': 'schema:WebAPI' }, + WebApplication: { '@id': 'schema:WebApplication' }, + WebContent: { '@id': 'schema:WebContent' }, + WebPage: { '@id': 'schema:WebPage' }, + WebPageElement: { '@id': 'schema:WebPageElement' }, + WebSite: { '@id': 'schema:WebSite' }, + Wednesday: { '@id': 'schema:Wednesday' }, + WesternConventional: { '@id': 'schema:WesternConventional' }, + Wholesale: { '@id': 'schema:Wholesale' }, + WholesaleStore: { '@id': 'schema:WholesaleStore' }, + WinAction: { '@id': 'schema:WinAction' }, + Winery: { '@id': 'schema:Winery' }, + Withdrawn: { '@id': 'schema:Withdrawn' }, + WorkBasedProgram: { '@id': 'schema:WorkBasedProgram' }, + WorkersUnion: { '@id': 'schema:WorkersUnion' }, + WriteAction: { '@id': 'schema:WriteAction' }, + WritePermission: { '@id': 'schema:WritePermission' }, + XPathType: { '@id': 'schema:XPathType' }, + XRay: { '@id': 'schema:XRay' }, + ZoneBoardingPolicy: { '@id': 'schema:ZoneBoardingPolicy' }, + Zoo: { '@id': 'schema:Zoo' }, + about: { '@id': 'schema:about' }, + abridged: { '@id': 'schema:abridged' }, + abstract: { '@id': 'schema:abstract' }, + accelerationTime: { '@id': 'schema:accelerationTime' }, + acceptedAnswer: { '@id': 'schema:acceptedAnswer' }, + acceptedOffer: { '@id': 'schema:acceptedOffer' }, + acceptedPaymentMethod: { '@id': 'schema:acceptedPaymentMethod' }, + acceptsReservations: { '@id': 'schema:acceptsReservations' }, + accessCode: { '@id': 'schema:accessCode' }, + accessMode: { '@id': 'schema:accessMode' }, + accessModeSufficient: { '@id': 'schema:accessModeSufficient' }, + accessibilityAPI: { '@id': 'schema:accessibilityAPI' }, + accessibilityControl: { '@id': 'schema:accessibilityControl' }, + accessibilityFeature: { '@id': 'schema:accessibilityFeature' }, + accessibilityHazard: { '@id': 'schema:accessibilityHazard' }, + accessibilitySummary: { '@id': 'schema:accessibilitySummary' }, + accommodationCategory: { '@id': 'schema:accommodationCategory' }, + accommodationFloorPlan: { '@id': 'schema:accommodationFloorPlan' }, + accountId: { '@id': 'schema:accountId' }, + accountMinimumInflow: { '@id': 'schema:accountMinimumInflow' }, + accountOverdraftLimit: { '@id': 'schema:accountOverdraftLimit' }, + accountablePerson: { '@id': 'schema:accountablePerson' }, + acquireLicensePage: { '@id': 'schema:acquireLicensePage', '@type': '@id' }, + acquiredFrom: { '@id': 'schema:acquiredFrom' }, + acrissCode: { '@id': 'schema:acrissCode' }, + actionAccessibilityRequirement: { + '@id': 'schema:actionAccessibilityRequirement', + }, + actionApplication: { '@id': 'schema:actionApplication' }, + actionOption: { '@id': 'schema:actionOption' }, + actionPlatform: { '@id': 'schema:actionPlatform' }, + actionStatus: { '@id': 'schema:actionStatus' }, + actionableFeedbackPolicy: { + '@id': 'schema:actionableFeedbackPolicy', + '@type': '@id', + }, + activeIngredient: { '@id': 'schema:activeIngredient' }, + activityDuration: { '@id': 'schema:activityDuration' }, + activityFrequency: { '@id': 'schema:activityFrequency' }, + actor: { '@id': 'schema:actor' }, + actors: { '@id': 'schema:actors' }, + addOn: { '@id': 'schema:addOn' }, + additionalName: { '@id': 'schema:additionalName' }, + additionalNumberOfGuests: { '@id': 'schema:additionalNumberOfGuests' }, + additionalProperty: { '@id': 'schema:additionalProperty' }, + additionalType: { '@id': 'schema:additionalType', '@type': '@id' }, + additionalVariable: { '@id': 'schema:additionalVariable' }, + address: { '@id': 'schema:address' }, + addressCountry: { '@id': 'schema:addressCountry' }, + addressLocality: { '@id': 'schema:addressLocality' }, + addressRegion: { '@id': 'schema:addressRegion' }, + administrationRoute: { '@id': 'schema:administrationRoute' }, + advanceBookingRequirement: { '@id': 'schema:advanceBookingRequirement' }, + adverseOutcome: { '@id': 'schema:adverseOutcome' }, + affectedBy: { '@id': 'schema:affectedBy' }, + affiliation: { '@id': 'schema:affiliation' }, + afterMedia: { '@id': 'schema:afterMedia', '@type': '@id' }, + agent: { '@id': 'schema:agent' }, + aggregateRating: { '@id': 'schema:aggregateRating' }, + aircraft: { '@id': 'schema:aircraft' }, + album: { '@id': 'schema:album' }, + albumProductionType: { '@id': 'schema:albumProductionType' }, + albumRelease: { '@id': 'schema:albumRelease' }, + albumReleaseType: { '@id': 'schema:albumReleaseType' }, + albums: { '@id': 'schema:albums' }, + alcoholWarning: { '@id': 'schema:alcoholWarning' }, + algorithm: { '@id': 'schema:algorithm' }, + alignmentType: { '@id': 'schema:alignmentType' }, + alternateName: { '@id': 'schema:alternateName' }, + alternativeHeadline: { '@id': 'schema:alternativeHeadline' }, + alumni: { '@id': 'schema:alumni' }, + alumniOf: { '@id': 'schema:alumniOf' }, + amenityFeature: { '@id': 'schema:amenityFeature' }, + amount: { '@id': 'schema:amount' }, + amountOfThisGood: { '@id': 'schema:amountOfThisGood' }, + announcementLocation: { '@id': 'schema:announcementLocation' }, + annualPercentageRate: { '@id': 'schema:annualPercentageRate' }, + answerCount: { '@id': 'schema:answerCount' }, + answerExplanation: { '@id': 'schema:answerExplanation' }, + antagonist: { '@id': 'schema:antagonist' }, + appearance: { '@id': 'schema:appearance' }, + applicableLocation: { '@id': 'schema:applicableLocation' }, + applicantLocationRequirements: { + '@id': 'schema:applicantLocationRequirements', + }, + application: { '@id': 'schema:application' }, + applicationCategory: { '@id': 'schema:applicationCategory' }, + applicationContact: { '@id': 'schema:applicationContact' }, + applicationDeadline: { '@id': 'schema:applicationDeadline', '@type': 'Date' }, + applicationStartDate: { '@id': 'schema:applicationStartDate', '@type': 'Date' }, + applicationSubCategory: { '@id': 'schema:applicationSubCategory' }, + applicationSuite: { '@id': 'schema:applicationSuite' }, + appliesToDeliveryMethod: { '@id': 'schema:appliesToDeliveryMethod' }, + appliesToPaymentMethod: { '@id': 'schema:appliesToPaymentMethod' }, + archiveHeld: { '@id': 'schema:archiveHeld' }, + area: { '@id': 'schema:area' }, + areaServed: { '@id': 'schema:areaServed' }, + arrivalAirport: { '@id': 'schema:arrivalAirport' }, + arrivalBoatTerminal: { '@id': 'schema:arrivalBoatTerminal' }, + arrivalBusStop: { '@id': 'schema:arrivalBusStop' }, + arrivalGate: { '@id': 'schema:arrivalGate' }, + arrivalPlatform: { '@id': 'schema:arrivalPlatform' }, + arrivalStation: { '@id': 'schema:arrivalStation' }, + arrivalTerminal: { '@id': 'schema:arrivalTerminal' }, + arrivalTime: { '@id': 'schema:arrivalTime' }, + artEdition: { '@id': 'schema:artEdition' }, + artMedium: { '@id': 'schema:artMedium' }, + arterialBranch: { '@id': 'schema:arterialBranch' }, + artform: { '@id': 'schema:artform' }, + articleBody: { '@id': 'schema:articleBody' }, + articleSection: { '@id': 'schema:articleSection' }, + artist: { '@id': 'schema:artist' }, + artworkSurface: { '@id': 'schema:artworkSurface' }, + aspect: { '@id': 'schema:aspect' }, + assembly: { '@id': 'schema:assembly' }, + assemblyVersion: { '@id': 'schema:assemblyVersion' }, + assesses: { '@id': 'schema:assesses' }, + associatedAnatomy: { '@id': 'schema:associatedAnatomy' }, + associatedArticle: { '@id': 'schema:associatedArticle' }, + associatedMedia: { '@id': 'schema:associatedMedia' }, + associatedPathophysiology: { '@id': 'schema:associatedPathophysiology' }, + athlete: { '@id': 'schema:athlete' }, + attendee: { '@id': 'schema:attendee' }, + attendees: { '@id': 'schema:attendees' }, + audience: { '@id': 'schema:audience' }, + audienceType: { '@id': 'schema:audienceType' }, + audio: { '@id': 'schema:audio' }, + authenticator: { '@id': 'schema:authenticator' }, + author: { '@id': 'schema:author' }, + availability: { '@id': 'schema:availability' }, + availabilityEnds: { '@id': 'schema:availabilityEnds', '@type': 'Date' }, + availabilityStarts: { '@id': 'schema:availabilityStarts', '@type': 'Date' }, + availableAtOrFrom: { '@id': 'schema:availableAtOrFrom' }, + availableChannel: { '@id': 'schema:availableChannel' }, + availableDeliveryMethod: { '@id': 'schema:availableDeliveryMethod' }, + availableFrom: { '@id': 'schema:availableFrom' }, + availableIn: { '@id': 'schema:availableIn' }, + availableLanguage: { '@id': 'schema:availableLanguage' }, + availableOnDevice: { '@id': 'schema:availableOnDevice' }, + availableService: { '@id': 'schema:availableService' }, + availableStrength: { '@id': 'schema:availableStrength' }, + availableTest: { '@id': 'schema:availableTest' }, + availableThrough: { '@id': 'schema:availableThrough' }, + award: { '@id': 'schema:award' }, + awards: { '@id': 'schema:awards' }, + awayTeam: { '@id': 'schema:awayTeam' }, + backstory: { '@id': 'schema:backstory' }, + bankAccountType: { '@id': 'schema:bankAccountType' }, + baseSalary: { '@id': 'schema:baseSalary' }, + bccRecipient: { '@id': 'schema:bccRecipient' }, + bed: { '@id': 'schema:bed' }, + beforeMedia: { '@id': 'schema:beforeMedia', '@type': '@id' }, + beneficiaryBank: { '@id': 'schema:beneficiaryBank' }, + benefits: { '@id': 'schema:benefits' }, + benefitsSummaryUrl: { '@id': 'schema:benefitsSummaryUrl', '@type': '@id' }, + bestRating: { '@id': 'schema:bestRating' }, + billingAddress: { '@id': 'schema:billingAddress' }, + billingDuration: { '@id': 'schema:billingDuration' }, + billingIncrement: { '@id': 'schema:billingIncrement' }, + billingPeriod: { '@id': 'schema:billingPeriod' }, + billingStart: { '@id': 'schema:billingStart' }, + biomechnicalClass: { '@id': 'schema:biomechnicalClass' }, + birthDate: { '@id': 'schema:birthDate', '@type': 'Date' }, + birthPlace: { '@id': 'schema:birthPlace' }, + bitrate: { '@id': 'schema:bitrate' }, + blogPost: { '@id': 'schema:blogPost' }, + blogPosts: { '@id': 'schema:blogPosts' }, + bloodSupply: { '@id': 'schema:bloodSupply' }, + boardingGroup: { '@id': 'schema:boardingGroup' }, + boardingPolicy: { '@id': 'schema:boardingPolicy' }, + bodyLocation: { '@id': 'schema:bodyLocation' }, + bodyType: { '@id': 'schema:bodyType' }, + bookEdition: { '@id': 'schema:bookEdition' }, + bookFormat: { '@id': 'schema:bookFormat' }, + bookingAgent: { '@id': 'schema:bookingAgent' }, + bookingTime: { '@id': 'schema:bookingTime' }, + borrower: { '@id': 'schema:borrower' }, + box: { '@id': 'schema:box' }, + branch: { '@id': 'schema:branch' }, + branchCode: { '@id': 'schema:branchCode' }, + branchOf: { '@id': 'schema:branchOf' }, + brand: { '@id': 'schema:brand' }, + breadcrumb: { '@id': 'schema:breadcrumb' }, + breastfeedingWarning: { '@id': 'schema:breastfeedingWarning' }, + broadcastAffiliateOf: { '@id': 'schema:broadcastAffiliateOf' }, + broadcastChannelId: { '@id': 'schema:broadcastChannelId' }, + broadcastDisplayName: { '@id': 'schema:broadcastDisplayName' }, + broadcastFrequency: { '@id': 'schema:broadcastFrequency' }, + broadcastFrequencyValue: { '@id': 'schema:broadcastFrequencyValue' }, + broadcastOfEvent: { '@id': 'schema:broadcastOfEvent' }, + broadcastServiceTier: { '@id': 'schema:broadcastServiceTier' }, + broadcastSignalModulation: { '@id': 'schema:broadcastSignalModulation' }, + broadcastSubChannel: { '@id': 'schema:broadcastSubChannel' }, + broadcastTimezone: { '@id': 'schema:broadcastTimezone' }, + broadcaster: { '@id': 'schema:broadcaster' }, + broker: { '@id': 'schema:broker' }, + browserRequirements: { '@id': 'schema:browserRequirements' }, + busName: { '@id': 'schema:busName' }, + busNumber: { '@id': 'schema:busNumber' }, + businessDays: { '@id': 'schema:businessDays' }, + businessFunction: { '@id': 'schema:businessFunction' }, + buyer: { '@id': 'schema:buyer' }, + byArtist: { '@id': 'schema:byArtist' }, + byDay: { '@id': 'schema:byDay' }, + byMonth: { '@id': 'schema:byMonth' }, + byMonthDay: { '@id': 'schema:byMonthDay' }, + byMonthWeek: { '@id': 'schema:byMonthWeek' }, + callSign: { '@id': 'schema:callSign' }, + calories: { '@id': 'schema:calories' }, + candidate: { '@id': 'schema:candidate' }, + caption: { '@id': 'schema:caption' }, + carbohydrateContent: { '@id': 'schema:carbohydrateContent' }, + cargoVolume: { '@id': 'schema:cargoVolume' }, + carrier: { '@id': 'schema:carrier' }, + carrierRequirements: { '@id': 'schema:carrierRequirements' }, + cashBack: { '@id': 'schema:cashBack' }, + catalog: { '@id': 'schema:catalog' }, + catalogNumber: { '@id': 'schema:catalogNumber' }, + category: { '@id': 'schema:category' }, + causeOf: { '@id': 'schema:causeOf' }, + ccRecipient: { '@id': 'schema:ccRecipient' }, + character: { '@id': 'schema:character' }, + characterAttribute: { '@id': 'schema:characterAttribute' }, + characterName: { '@id': 'schema:characterName' }, + cheatCode: { '@id': 'schema:cheatCode' }, + checkinTime: { '@id': 'schema:checkinTime' }, + checkoutTime: { '@id': 'schema:checkoutTime' }, + childMaxAge: { '@id': 'schema:childMaxAge' }, + childMinAge: { '@id': 'schema:childMinAge' }, + children: { '@id': 'schema:children' }, + cholesterolContent: { '@id': 'schema:cholesterolContent' }, + circle: { '@id': 'schema:circle' }, + citation: { '@id': 'schema:citation' }, + claimReviewed: { '@id': 'schema:claimReviewed' }, + clincalPharmacology: { '@id': 'schema:clincalPharmacology' }, + clinicalPharmacology: { '@id': 'schema:clinicalPharmacology' }, + clipNumber: { '@id': 'schema:clipNumber' }, + closes: { '@id': 'schema:closes' }, + coach: { '@id': 'schema:coach' }, + code: { '@id': 'schema:code' }, + codeRepository: { '@id': 'schema:codeRepository', '@type': '@id' }, + codeSampleType: { '@id': 'schema:codeSampleType' }, + codeValue: { '@id': 'schema:codeValue' }, + codingSystem: { '@id': 'schema:codingSystem' }, + colleague: { '@id': 'schema:colleague', '@type': '@id' }, + colleagues: { '@id': 'schema:colleagues' }, + collection: { '@id': 'schema:collection' }, + collectionSize: { '@id': 'schema:collectionSize' }, + color: { '@id': 'schema:color' }, + colorist: { '@id': 'schema:colorist' }, + comment: { '@id': 'schema:comment' }, + commentCount: { '@id': 'schema:commentCount' }, + commentText: { '@id': 'schema:commentText' }, + commentTime: { '@id': 'schema:commentTime', '@type': 'Date' }, + competencyRequired: { '@id': 'schema:competencyRequired' }, + competitor: { '@id': 'schema:competitor' }, + composer: { '@id': 'schema:composer' }, + comprisedOf: { '@id': 'schema:comprisedOf' }, + conditionsOfAccess: { '@id': 'schema:conditionsOfAccess' }, + confirmationNumber: { '@id': 'schema:confirmationNumber' }, + connectedTo: { '@id': 'schema:connectedTo' }, + constrainingProperty: { '@id': 'schema:constrainingProperty' }, + contactOption: { '@id': 'schema:contactOption' }, + contactPoint: { '@id': 'schema:contactPoint' }, + contactPoints: { '@id': 'schema:contactPoints' }, + contactType: { '@id': 'schema:contactType' }, + contactlessPayment: { '@id': 'schema:contactlessPayment' }, + containedIn: { '@id': 'schema:containedIn' }, + containedInPlace: { '@id': 'schema:containedInPlace' }, + containsPlace: { '@id': 'schema:containsPlace' }, + containsSeason: { '@id': 'schema:containsSeason' }, + contentLocation: { '@id': 'schema:contentLocation' }, + contentRating: { '@id': 'schema:contentRating' }, + contentReferenceTime: { '@id': 'schema:contentReferenceTime' }, + contentSize: { '@id': 'schema:contentSize' }, + contentType: { '@id': 'schema:contentType' }, + contentUrl: { '@id': 'schema:contentUrl', '@type': '@id' }, + contraindication: { '@id': 'schema:contraindication' }, + contributor: { '@id': 'schema:contributor' }, + cookTime: { '@id': 'schema:cookTime' }, + cookingMethod: { '@id': 'schema:cookingMethod' }, + copyrightHolder: { '@id': 'schema:copyrightHolder' }, + copyrightNotice: { '@id': 'schema:copyrightNotice' }, + copyrightYear: { '@id': 'schema:copyrightYear' }, + correction: { '@id': 'schema:correction' }, + correctionsPolicy: { '@id': 'schema:correctionsPolicy', '@type': '@id' }, + costCategory: { '@id': 'schema:costCategory' }, + costCurrency: { '@id': 'schema:costCurrency' }, + costOrigin: { '@id': 'schema:costOrigin' }, + costPerUnit: { '@id': 'schema:costPerUnit' }, + countriesNotSupported: { '@id': 'schema:countriesNotSupported' }, + countriesSupported: { '@id': 'schema:countriesSupported' }, + countryOfOrigin: { '@id': 'schema:countryOfOrigin' }, + course: { '@id': 'schema:course' }, + courseCode: { '@id': 'schema:courseCode' }, + courseMode: { '@id': 'schema:courseMode' }, + coursePrerequisites: { '@id': 'schema:coursePrerequisites' }, + courseWorkload: { '@id': 'schema:courseWorkload' }, + coverageEndTime: { '@id': 'schema:coverageEndTime' }, + coverageStartTime: { '@id': 'schema:coverageStartTime' }, + creativeWorkStatus: { '@id': 'schema:creativeWorkStatus' }, + creator: { '@id': 'schema:creator' }, + credentialCategory: { '@id': 'schema:credentialCategory' }, + creditText: { '@id': 'schema:creditText' }, + creditedTo: { '@id': 'schema:creditedTo' }, + cssSelector: { '@id': 'schema:cssSelector' }, + currenciesAccepted: { '@id': 'schema:currenciesAccepted' }, + currency: { '@id': 'schema:currency' }, + currentExchangeRate: { '@id': 'schema:currentExchangeRate' }, + customer: { '@id': 'schema:customer' }, + cutoffTime: { '@id': 'schema:cutoffTime' }, + cvdCollectionDate: { '@id': 'schema:cvdCollectionDate' }, + cvdFacilityCounty: { '@id': 'schema:cvdFacilityCounty' }, + cvdFacilityId: { '@id': 'schema:cvdFacilityId' }, + cvdNumBeds: { '@id': 'schema:cvdNumBeds' }, + cvdNumBedsOcc: { '@id': 'schema:cvdNumBedsOcc' }, + cvdNumC19Died: { '@id': 'schema:cvdNumC19Died' }, + cvdNumC19HOPats: { '@id': 'schema:cvdNumC19HOPats' }, + cvdNumC19HospPats: { '@id': 'schema:cvdNumC19HospPats' }, + cvdNumC19MechVentPats: { '@id': 'schema:cvdNumC19MechVentPats' }, + cvdNumC19OFMechVentPats: { '@id': 'schema:cvdNumC19OFMechVentPats' }, + cvdNumC19OverflowPats: { '@id': 'schema:cvdNumC19OverflowPats' }, + cvdNumICUBeds: { '@id': 'schema:cvdNumICUBeds' }, + cvdNumICUBedsOcc: { '@id': 'schema:cvdNumICUBedsOcc' }, + cvdNumTotBeds: { '@id': 'schema:cvdNumTotBeds' }, + cvdNumVent: { '@id': 'schema:cvdNumVent' }, + cvdNumVentUse: { '@id': 'schema:cvdNumVentUse' }, + dataFeedElement: { '@id': 'schema:dataFeedElement' }, + dataset: { '@id': 'schema:dataset' }, + datasetTimeInterval: { '@id': 'schema:datasetTimeInterval' }, + dateCreated: { '@id': 'schema:dateCreated', '@type': 'Date' }, + dateDeleted: { '@id': 'schema:dateDeleted', '@type': 'Date' }, + dateIssued: { '@id': 'schema:dateIssued', '@type': 'Date' }, + dateModified: { '@id': 'schema:dateModified', '@type': 'Date' }, + datePosted: { '@id': 'schema:datePosted', '@type': 'Date' }, + datePublished: { '@id': 'schema:datePublished', '@type': 'Date' }, + dateRead: { '@id': 'schema:dateRead', '@type': 'Date' }, + dateReceived: { '@id': 'schema:dateReceived' }, + dateSent: { '@id': 'schema:dateSent' }, + dateVehicleFirstRegistered: { + '@id': 'schema:dateVehicleFirstRegistered', + '@type': 'Date', + }, + dateline: { '@id': 'schema:dateline' }, + dayOfWeek: { '@id': 'schema:dayOfWeek' }, + deathDate: { '@id': 'schema:deathDate', '@type': 'Date' }, + deathPlace: { '@id': 'schema:deathPlace' }, + defaultValue: { '@id': 'schema:defaultValue' }, + deliveryAddress: { '@id': 'schema:deliveryAddress' }, + deliveryLeadTime: { '@id': 'schema:deliveryLeadTime' }, + deliveryMethod: { '@id': 'schema:deliveryMethod' }, + deliveryStatus: { '@id': 'schema:deliveryStatus' }, + deliveryTime: { '@id': 'schema:deliveryTime' }, + department: { '@id': 'schema:department' }, + departureAirport: { '@id': 'schema:departureAirport' }, + departureBoatTerminal: { '@id': 'schema:departureBoatTerminal' }, + departureBusStop: { '@id': 'schema:departureBusStop' }, + departureGate: { '@id': 'schema:departureGate' }, + departurePlatform: { '@id': 'schema:departurePlatform' }, + departureStation: { '@id': 'schema:departureStation' }, + departureTerminal: { '@id': 'schema:departureTerminal' }, + departureTime: { '@id': 'schema:departureTime' }, + dependencies: { '@id': 'schema:dependencies' }, + depth: { '@id': 'schema:depth' }, + description: { '@id': 'schema:description' }, + device: { '@id': 'schema:device' }, + diagnosis: { '@id': 'schema:diagnosis' }, + diagram: { '@id': 'schema:diagram' }, + diet: { '@id': 'schema:diet' }, + dietFeatures: { '@id': 'schema:dietFeatures' }, + differentialDiagnosis: { '@id': 'schema:differentialDiagnosis' }, + director: { '@id': 'schema:director' }, + directors: { '@id': 'schema:directors' }, + disambiguatingDescription: { '@id': 'schema:disambiguatingDescription' }, + discount: { '@id': 'schema:discount' }, + discountCode: { '@id': 'schema:discountCode' }, + discountCurrency: { '@id': 'schema:discountCurrency' }, + discusses: { '@id': 'schema:discusses' }, + discussionUrl: { '@id': 'schema:discussionUrl', '@type': '@id' }, + diseasePreventionInfo: { + '@id': 'schema:diseasePreventionInfo', + '@type': '@id', + }, + diseaseSpreadStatistics: { + '@id': 'schema:diseaseSpreadStatistics', + '@type': '@id', + }, + dissolutionDate: { '@id': 'schema:dissolutionDate', '@type': 'Date' }, + distance: { '@id': 'schema:distance' }, + distinguishingSign: { '@id': 'schema:distinguishingSign' }, + distribution: { '@id': 'schema:distribution' }, + diversityPolicy: { '@id': 'schema:diversityPolicy', '@type': '@id' }, + diversityStaffingReport: { + '@id': 'schema:diversityStaffingReport', + '@type': '@id', + }, + documentation: { '@id': 'schema:documentation', '@type': '@id' }, + doesNotShip: { '@id': 'schema:doesNotShip' }, + domainIncludes: { '@id': 'schema:domainIncludes' }, + domiciledMortgage: { '@id': 'schema:domiciledMortgage' }, + doorTime: { '@id': 'schema:doorTime' }, + dosageForm: { '@id': 'schema:dosageForm' }, + doseSchedule: { '@id': 'schema:doseSchedule' }, + doseUnit: { '@id': 'schema:doseUnit' }, + doseValue: { '@id': 'schema:doseValue' }, + downPayment: { '@id': 'schema:downPayment' }, + downloadUrl: { '@id': 'schema:downloadUrl', '@type': '@id' }, + downvoteCount: { '@id': 'schema:downvoteCount' }, + drainsTo: { '@id': 'schema:drainsTo' }, + driveWheelConfiguration: { '@id': 'schema:driveWheelConfiguration' }, + dropoffLocation: { '@id': 'schema:dropoffLocation' }, + dropoffTime: { '@id': 'schema:dropoffTime' }, + drug: { '@id': 'schema:drug' }, + drugClass: { '@id': 'schema:drugClass' }, + drugUnit: { '@id': 'schema:drugUnit' }, + duns: { '@id': 'schema:duns' }, + duplicateTherapy: { '@id': 'schema:duplicateTherapy' }, + duration: { '@id': 'schema:duration' }, + durationOfWarranty: { '@id': 'schema:durationOfWarranty' }, + duringMedia: { '@id': 'schema:duringMedia', '@type': '@id' }, + earlyPrepaymentPenalty: { '@id': 'schema:earlyPrepaymentPenalty' }, + editEIDR: { '@id': 'schema:editEIDR' }, + editor: { '@id': 'schema:editor' }, + eduQuestionType: { '@id': 'schema:eduQuestionType' }, + educationRequirements: { '@id': 'schema:educationRequirements' }, + educationalAlignment: { '@id': 'schema:educationalAlignment' }, + educationalCredentialAwarded: { '@id': 'schema:educationalCredentialAwarded' }, + educationalFramework: { '@id': 'schema:educationalFramework' }, + educationalLevel: { '@id': 'schema:educationalLevel' }, + educationalProgramMode: { '@id': 'schema:educationalProgramMode' }, + educationalRole: { '@id': 'schema:educationalRole' }, + educationalUse: { '@id': 'schema:educationalUse' }, + elevation: { '@id': 'schema:elevation' }, + eligibilityToWorkRequirement: { '@id': 'schema:eligibilityToWorkRequirement' }, + eligibleCustomerType: { '@id': 'schema:eligibleCustomerType' }, + eligibleDuration: { '@id': 'schema:eligibleDuration' }, + eligibleQuantity: { '@id': 'schema:eligibleQuantity' }, + eligibleRegion: { '@id': 'schema:eligibleRegion' }, + eligibleTransactionVolume: { '@id': 'schema:eligibleTransactionVolume' }, + email: { '@id': 'schema:email' }, + embedUrl: { '@id': 'schema:embedUrl', '@type': '@id' }, + emissionsCO2: { '@id': 'schema:emissionsCO2' }, + employee: { '@id': 'schema:employee' }, + employees: { '@id': 'schema:employees' }, + employerOverview: { '@id': 'schema:employerOverview' }, + employmentType: { '@id': 'schema:employmentType' }, + employmentUnit: { '@id': 'schema:employmentUnit' }, + encodesCreativeWork: { '@id': 'schema:encodesCreativeWork' }, + encoding: { '@id': 'schema:encoding' }, + encodingFormat: { '@id': 'schema:encodingFormat' }, + encodingType: { '@id': 'schema:encodingType' }, + encodings: { '@id': 'schema:encodings' }, + endDate: { '@id': 'schema:endDate', '@type': 'Date' }, + endOffset: { '@id': 'schema:endOffset' }, + endTime: { '@id': 'schema:endTime' }, + endorsee: { '@id': 'schema:endorsee' }, + endorsers: { '@id': 'schema:endorsers' }, + energyEfficiencyScaleMax: { '@id': 'schema:energyEfficiencyScaleMax' }, + energyEfficiencyScaleMin: { '@id': 'schema:energyEfficiencyScaleMin' }, + engineDisplacement: { '@id': 'schema:engineDisplacement' }, + enginePower: { '@id': 'schema:enginePower' }, + engineType: { '@id': 'schema:engineType' }, + entertainmentBusiness: { '@id': 'schema:entertainmentBusiness' }, + epidemiology: { '@id': 'schema:epidemiology' }, + episode: { '@id': 'schema:episode' }, + episodeNumber: { '@id': 'schema:episodeNumber' }, + episodes: { '@id': 'schema:episodes' }, + equal: { '@id': 'schema:equal' }, + error: { '@id': 'schema:error' }, + estimatedCost: { '@id': 'schema:estimatedCost' }, + estimatedFlightDuration: { '@id': 'schema:estimatedFlightDuration' }, + estimatedSalary: { '@id': 'schema:estimatedSalary' }, + estimatesRiskOf: { '@id': 'schema:estimatesRiskOf' }, + ethicsPolicy: { '@id': 'schema:ethicsPolicy', '@type': '@id' }, + event: { '@id': 'schema:event' }, + eventAttendanceMode: { '@id': 'schema:eventAttendanceMode' }, + eventSchedule: { '@id': 'schema:eventSchedule' }, + eventStatus: { '@id': 'schema:eventStatus' }, + events: { '@id': 'schema:events' }, + evidenceLevel: { '@id': 'schema:evidenceLevel' }, + evidenceOrigin: { '@id': 'schema:evidenceOrigin' }, + exampleOfWork: { '@id': 'schema:exampleOfWork' }, + exceptDate: { '@id': 'schema:exceptDate', '@type': 'Date' }, + exchangeRateSpread: { '@id': 'schema:exchangeRateSpread' }, + executableLibraryName: { '@id': 'schema:executableLibraryName' }, + exerciseCourse: { '@id': 'schema:exerciseCourse' }, + exercisePlan: { '@id': 'schema:exercisePlan' }, + exerciseRelatedDiet: { '@id': 'schema:exerciseRelatedDiet' }, + exerciseType: { '@id': 'schema:exerciseType' }, + exifData: { '@id': 'schema:exifData' }, + expectedArrivalFrom: { '@id': 'schema:expectedArrivalFrom', '@type': 'Date' }, + expectedArrivalUntil: { '@id': 'schema:expectedArrivalUntil', '@type': 'Date' }, + expectedPrognosis: { '@id': 'schema:expectedPrognosis' }, + expectsAcceptanceOf: { '@id': 'schema:expectsAcceptanceOf' }, + experienceInPlaceOfEducation: { '@id': 'schema:experienceInPlaceOfEducation' }, + experienceRequirements: { '@id': 'schema:experienceRequirements' }, + expertConsiderations: { '@id': 'schema:expertConsiderations' }, + expires: { '@id': 'schema:expires', '@type': 'Date' }, + familyName: { '@id': 'schema:familyName' }, + fatContent: { '@id': 'schema:fatContent' }, + faxNumber: { '@id': 'schema:faxNumber' }, + featureList: { '@id': 'schema:featureList' }, + feesAndCommissionsSpecification: { + '@id': 'schema:feesAndCommissionsSpecification', + }, + fiberContent: { '@id': 'schema:fiberContent' }, + fileFormat: { '@id': 'schema:fileFormat' }, + fileSize: { '@id': 'schema:fileSize' }, + financialAidEligible: { '@id': 'schema:financialAidEligible' }, + firstAppearance: { '@id': 'schema:firstAppearance' }, + firstPerformance: { '@id': 'schema:firstPerformance' }, + flightDistance: { '@id': 'schema:flightDistance' }, + flightNumber: { '@id': 'schema:flightNumber' }, + floorLevel: { '@id': 'schema:floorLevel' }, + floorLimit: { '@id': 'schema:floorLimit' }, + floorSize: { '@id': 'schema:floorSize' }, + followee: { '@id': 'schema:followee' }, + follows: { '@id': 'schema:follows' }, + followup: { '@id': 'schema:followup' }, + foodEstablishment: { '@id': 'schema:foodEstablishment' }, + foodEvent: { '@id': 'schema:foodEvent' }, + foodWarning: { '@id': 'schema:foodWarning' }, + founder: { '@id': 'schema:founder' }, + founders: { '@id': 'schema:founders' }, + foundingDate: { '@id': 'schema:foundingDate', '@type': 'Date' }, + foundingLocation: { '@id': 'schema:foundingLocation' }, + free: { '@id': 'schema:free' }, + freeShippingThreshold: { '@id': 'schema:freeShippingThreshold' }, + frequency: { '@id': 'schema:frequency' }, + fromLocation: { '@id': 'schema:fromLocation' }, + fuelCapacity: { '@id': 'schema:fuelCapacity' }, + fuelConsumption: { '@id': 'schema:fuelConsumption' }, + fuelEfficiency: { '@id': 'schema:fuelEfficiency' }, + fuelType: { '@id': 'schema:fuelType' }, + functionalClass: { '@id': 'schema:functionalClass' }, + fundedItem: { '@id': 'schema:fundedItem' }, + funder: { '@id': 'schema:funder' }, + game: { '@id': 'schema:game' }, + gameItem: { '@id': 'schema:gameItem' }, + gameLocation: { '@id': 'schema:gameLocation', '@type': '@id' }, + gamePlatform: { '@id': 'schema:gamePlatform' }, + gameServer: { '@id': 'schema:gameServer' }, + gameTip: { '@id': 'schema:gameTip' }, + gender: { '@id': 'schema:gender' }, + genre: { '@id': 'schema:genre' }, + geo: { '@id': 'schema:geo' }, + geoContains: { '@id': 'schema:geoContains' }, + geoCoveredBy: { '@id': 'schema:geoCoveredBy' }, + geoCovers: { '@id': 'schema:geoCovers' }, + geoCrosses: { '@id': 'schema:geoCrosses' }, + geoDisjoint: { '@id': 'schema:geoDisjoint' }, + geoEquals: { '@id': 'schema:geoEquals' }, + geoIntersects: { '@id': 'schema:geoIntersects' }, + geoMidpoint: { '@id': 'schema:geoMidpoint' }, + geoOverlaps: { '@id': 'schema:geoOverlaps' }, + geoRadius: { '@id': 'schema:geoRadius' }, + geoTouches: { '@id': 'schema:geoTouches' }, + geoWithin: { '@id': 'schema:geoWithin' }, + geographicArea: { '@id': 'schema:geographicArea' }, + gettingTestedInfo: { '@id': 'schema:gettingTestedInfo', '@type': '@id' }, + givenName: { '@id': 'schema:givenName' }, + globalLocationNumber: { '@id': 'schema:globalLocationNumber' }, + governmentBenefitsInfo: { '@id': 'schema:governmentBenefitsInfo' }, + gracePeriod: { '@id': 'schema:gracePeriod' }, + grantee: { '@id': 'schema:grantee' }, + greater: { '@id': 'schema:greater' }, + greaterOrEqual: { '@id': 'schema:greaterOrEqual' }, + gtin: { '@id': 'schema:gtin' }, + gtin12: { '@id': 'schema:gtin12' }, + gtin13: { '@id': 'schema:gtin13' }, + gtin14: { '@id': 'schema:gtin14' }, + gtin8: { '@id': 'schema:gtin8' }, + guideline: { '@id': 'schema:guideline' }, + guidelineDate: { '@id': 'schema:guidelineDate', '@type': 'Date' }, + guidelineSubject: { '@id': 'schema:guidelineSubject' }, + handlingTime: { '@id': 'schema:handlingTime' }, + hasBroadcastChannel: { '@id': 'schema:hasBroadcastChannel' }, + hasCategoryCode: { '@id': 'schema:hasCategoryCode' }, + hasCourse: { '@id': 'schema:hasCourse' }, + hasCourseInstance: { '@id': 'schema:hasCourseInstance' }, + hasCredential: { '@id': 'schema:hasCredential' }, + hasDefinedTerm: { '@id': 'schema:hasDefinedTerm' }, + hasDeliveryMethod: { '@id': 'schema:hasDeliveryMethod' }, + hasDigitalDocumentPermission: { '@id': 'schema:hasDigitalDocumentPermission' }, + hasDriveThroughService: { '@id': 'schema:hasDriveThroughService' }, + hasEnergyConsumptionDetails: { '@id': 'schema:hasEnergyConsumptionDetails' }, + hasEnergyEfficiencyCategory: { '@id': 'schema:hasEnergyEfficiencyCategory' }, + hasHealthAspect: { '@id': 'schema:hasHealthAspect' }, + hasMap: { '@id': 'schema:hasMap', '@type': '@id' }, + hasMeasurement: { '@id': 'schema:hasMeasurement' }, + hasMenu: { '@id': 'schema:hasMenu' }, + hasMenuItem: { '@id': 'schema:hasMenuItem' }, + hasMenuSection: { '@id': 'schema:hasMenuSection' }, + hasMerchantReturnPolicy: { '@id': 'schema:hasMerchantReturnPolicy' }, + hasOccupation: { '@id': 'schema:hasOccupation' }, + hasOfferCatalog: { '@id': 'schema:hasOfferCatalog' }, + hasPOS: { '@id': 'schema:hasPOS' }, + hasPart: { '@id': 'schema:hasPart' }, + hasProductReturnPolicy: { '@id': 'schema:hasProductReturnPolicy' }, + hasVariant: { '@id': 'schema:hasVariant' }, + headline: { '@id': 'schema:headline' }, + healthCondition: { '@id': 'schema:healthCondition' }, + healthPlanCoinsuranceOption: { '@id': 'schema:healthPlanCoinsuranceOption' }, + healthPlanCoinsuranceRate: { '@id': 'schema:healthPlanCoinsuranceRate' }, + healthPlanCopay: { '@id': 'schema:healthPlanCopay' }, + healthPlanCopayOption: { '@id': 'schema:healthPlanCopayOption' }, + healthPlanCostSharing: { '@id': 'schema:healthPlanCostSharing' }, + healthPlanDrugOption: { '@id': 'schema:healthPlanDrugOption' }, + healthPlanDrugTier: { '@id': 'schema:healthPlanDrugTier' }, + healthPlanId: { '@id': 'schema:healthPlanId' }, + healthPlanMarketingUrl: { + '@id': 'schema:healthPlanMarketingUrl', + '@type': '@id', + }, + healthPlanNetworkId: { '@id': 'schema:healthPlanNetworkId' }, + healthPlanNetworkTier: { '@id': 'schema:healthPlanNetworkTier' }, + healthPlanPharmacyCategory: { '@id': 'schema:healthPlanPharmacyCategory' }, + healthcareReportingData: { '@id': 'schema:healthcareReportingData' }, + height: { '@id': 'schema:height' }, + highPrice: { '@id': 'schema:highPrice' }, + hiringOrganization: { '@id': 'schema:hiringOrganization' }, + holdingArchive: { '@id': 'schema:holdingArchive' }, + homeLocation: { '@id': 'schema:homeLocation' }, + homeTeam: { '@id': 'schema:homeTeam' }, + honorificPrefix: { '@id': 'schema:honorificPrefix' }, + honorificSuffix: { '@id': 'schema:honorificSuffix' }, + hospitalAffiliation: { '@id': 'schema:hospitalAffiliation' }, + hostingOrganization: { '@id': 'schema:hostingOrganization' }, + hoursAvailable: { '@id': 'schema:hoursAvailable' }, + howPerformed: { '@id': 'schema:howPerformed' }, + httpMethod: { '@id': 'schema:httpMethod' }, + iataCode: { '@id': 'schema:iataCode' }, + icaoCode: { '@id': 'schema:icaoCode' }, + identifier: { '@id': 'schema:identifier' }, + identifyingExam: { '@id': 'schema:identifyingExam' }, + identifyingTest: { '@id': 'schema:identifyingTest' }, + illustrator: { '@id': 'schema:illustrator' }, + image: { '@id': 'schema:image', '@type': '@id' }, + imagingTechnique: { '@id': 'schema:imagingTechnique' }, + inAlbum: { '@id': 'schema:inAlbum' }, + inBroadcastLineup: { '@id': 'schema:inBroadcastLineup' }, + inCodeSet: { '@id': 'schema:inCodeSet', '@type': '@id' }, + inDefinedTermSet: { '@id': 'schema:inDefinedTermSet', '@type': '@id' }, + inLanguage: { '@id': 'schema:inLanguage' }, + inPlaylist: { '@id': 'schema:inPlaylist' }, + inProductGroupWithID: { '@id': 'schema:inProductGroupWithID' }, + inStoreReturnsOffered: { '@id': 'schema:inStoreReturnsOffered' }, + inSupportOf: { '@id': 'schema:inSupportOf' }, + incentiveCompensation: { '@id': 'schema:incentiveCompensation' }, + incentives: { '@id': 'schema:incentives' }, + includedComposition: { '@id': 'schema:includedComposition' }, + includedDataCatalog: { '@id': 'schema:includedDataCatalog' }, + includedInDataCatalog: { '@id': 'schema:includedInDataCatalog' }, + includedInHealthInsurancePlan: { + '@id': 'schema:includedInHealthInsurancePlan', + }, + includedRiskFactor: { '@id': 'schema:includedRiskFactor' }, + includesAttraction: { '@id': 'schema:includesAttraction' }, + includesHealthPlanFormulary: { '@id': 'schema:includesHealthPlanFormulary' }, + includesHealthPlanNetwork: { '@id': 'schema:includesHealthPlanNetwork' }, + includesObject: { '@id': 'schema:includesObject' }, + increasesRiskOf: { '@id': 'schema:increasesRiskOf' }, + industry: { '@id': 'schema:industry' }, + ineligibleRegion: { '@id': 'schema:ineligibleRegion' }, + infectiousAgent: { '@id': 'schema:infectiousAgent' }, + infectiousAgentClass: { '@id': 'schema:infectiousAgentClass' }, + ingredients: { '@id': 'schema:ingredients' }, + inker: { '@id': 'schema:inker' }, + insertion: { '@id': 'schema:insertion' }, + installUrl: { '@id': 'schema:installUrl', '@type': '@id' }, + instructor: { '@id': 'schema:instructor' }, + instrument: { '@id': 'schema:instrument' }, + intensity: { '@id': 'schema:intensity' }, + interactingDrug: { '@id': 'schema:interactingDrug' }, + interactionCount: { '@id': 'schema:interactionCount' }, + interactionService: { '@id': 'schema:interactionService' }, + interactionStatistic: { '@id': 'schema:interactionStatistic' }, + interactionType: { '@id': 'schema:interactionType' }, + interactivityType: { '@id': 'schema:interactivityType' }, + interestRate: { '@id': 'schema:interestRate' }, + inventoryLevel: { '@id': 'schema:inventoryLevel' }, + inverseOf: { '@id': 'schema:inverseOf' }, + isAcceptingNewPatients: { '@id': 'schema:isAcceptingNewPatients' }, + isAccessibleForFree: { '@id': 'schema:isAccessibleForFree' }, + isAccessoryOrSparePartFor: { '@id': 'schema:isAccessoryOrSparePartFor' }, + isAvailableGenerically: { '@id': 'schema:isAvailableGenerically' }, + isBasedOn: { '@id': 'schema:isBasedOn', '@type': '@id' }, + isBasedOnUrl: { '@id': 'schema:isBasedOnUrl', '@type': '@id' }, + isConsumableFor: { '@id': 'schema:isConsumableFor' }, + isFamilyFriendly: { '@id': 'schema:isFamilyFriendly' }, + isGift: { '@id': 'schema:isGift' }, + isLiveBroadcast: { '@id': 'schema:isLiveBroadcast' }, + isPartOf: { '@id': 'schema:isPartOf', '@type': '@id' }, + isPlanForApartment: { '@id': 'schema:isPlanForApartment' }, + isProprietary: { '@id': 'schema:isProprietary' }, + isRelatedTo: { '@id': 'schema:isRelatedTo' }, + isResizable: { '@id': 'schema:isResizable' }, + isSimilarTo: { '@id': 'schema:isSimilarTo' }, + isUnlabelledFallback: { '@id': 'schema:isUnlabelledFallback' }, + isVariantOf: { '@id': 'schema:isVariantOf' }, + isbn: { '@id': 'schema:isbn' }, + isicV4: { '@id': 'schema:isicV4' }, + isrcCode: { '@id': 'schema:isrcCode' }, + issn: { '@id': 'schema:issn' }, + issueNumber: { '@id': 'schema:issueNumber' }, + issuedBy: { '@id': 'schema:issuedBy' }, + issuedThrough: { '@id': 'schema:issuedThrough' }, + iswcCode: { '@id': 'schema:iswcCode' }, + item: { '@id': 'schema:item' }, + itemCondition: { '@id': 'schema:itemCondition' }, + itemListElement: { '@id': 'schema:itemListElement' }, + itemListOrder: { '@id': 'schema:itemListOrder' }, + itemLocation: { '@id': 'schema:itemLocation' }, + itemOffered: { '@id': 'schema:itemOffered' }, + itemReviewed: { '@id': 'schema:itemReviewed' }, + itemShipped: { '@id': 'schema:itemShipped' }, + itinerary: { '@id': 'schema:itinerary' }, + jobBenefits: { '@id': 'schema:jobBenefits' }, + jobImmediateStart: { '@id': 'schema:jobImmediateStart' }, + jobLocation: { '@id': 'schema:jobLocation' }, + jobLocationType: { '@id': 'schema:jobLocationType' }, + jobStartDate: { '@id': 'schema:jobStartDate' }, + jobTitle: { '@id': 'schema:jobTitle' }, + jurisdiction: { '@id': 'schema:jurisdiction' }, + keywords: { '@id': 'schema:keywords' }, + knownVehicleDamages: { '@id': 'schema:knownVehicleDamages' }, + knows: { '@id': 'schema:knows' }, + knowsAbout: { '@id': 'schema:knowsAbout' }, + knowsLanguage: { '@id': 'schema:knowsLanguage' }, + labelDetails: { '@id': 'schema:labelDetails', '@type': '@id' }, + landlord: { '@id': 'schema:landlord' }, + language: { '@id': 'schema:language' }, + lastReviewed: { '@id': 'schema:lastReviewed', '@type': 'Date' }, + latitude: { '@id': 'schema:latitude' }, + layoutImage: { '@id': 'schema:layoutImage', '@type': '@id' }, + learningResourceType: { '@id': 'schema:learningResourceType' }, + leaseLength: { '@id': 'schema:leaseLength' }, + legalName: { '@id': 'schema:legalName' }, + legalStatus: { '@id': 'schema:legalStatus' }, + legislationApplies: { '@id': 'schema:legislationApplies' }, + legislationChanges: { '@id': 'schema:legislationChanges' }, + legislationConsolidates: { '@id': 'schema:legislationConsolidates' }, + legislationDate: { '@id': 'schema:legislationDate', '@type': 'Date' }, + legislationDateVersion: { + '@id': 'schema:legislationDateVersion', + '@type': 'Date', + }, + legislationIdentifier: { '@id': 'schema:legislationIdentifier' }, + legislationJurisdiction: { '@id': 'schema:legislationJurisdiction' }, + legislationLegalForce: { '@id': 'schema:legislationLegalForce' }, + legislationLegalValue: { '@id': 'schema:legislationLegalValue' }, + legislationPassedBy: { '@id': 'schema:legislationPassedBy' }, + legislationResponsible: { '@id': 'schema:legislationResponsible' }, + legislationTransposes: { '@id': 'schema:legislationTransposes' }, + legislationType: { '@id': 'schema:legislationType' }, + leiCode: { '@id': 'schema:leiCode' }, + lender: { '@id': 'schema:lender' }, + lesser: { '@id': 'schema:lesser' }, + lesserOrEqual: { '@id': 'schema:lesserOrEqual' }, + letterer: { '@id': 'schema:letterer' }, + license: { '@id': 'schema:license', '@type': '@id' }, + line: { '@id': 'schema:line' }, + linkRelationship: { '@id': 'schema:linkRelationship' }, + liveBlogUpdate: { '@id': 'schema:liveBlogUpdate' }, + loanMortgageMandateAmount: { '@id': 'schema:loanMortgageMandateAmount' }, + loanPaymentAmount: { '@id': 'schema:loanPaymentAmount' }, + loanPaymentFrequency: { '@id': 'schema:loanPaymentFrequency' }, + loanRepaymentForm: { '@id': 'schema:loanRepaymentForm' }, + loanTerm: { '@id': 'schema:loanTerm' }, + loanType: { '@id': 'schema:loanType' }, + location: { '@id': 'schema:location' }, + locationCreated: { '@id': 'schema:locationCreated' }, + lodgingUnitDescription: { '@id': 'schema:lodgingUnitDescription' }, + lodgingUnitType: { '@id': 'schema:lodgingUnitType' }, + logo: { '@id': 'schema:logo', '@type': '@id' }, + longitude: { '@id': 'schema:longitude' }, + loser: { '@id': 'schema:loser' }, + lowPrice: { '@id': 'schema:lowPrice' }, + lyricist: { '@id': 'schema:lyricist' }, + lyrics: { '@id': 'schema:lyrics' }, + mainContentOfPage: { '@id': 'schema:mainContentOfPage' }, + mainEntity: { '@id': 'schema:mainEntity' }, + mainEntityOfPage: { '@id': 'schema:mainEntityOfPage', '@type': '@id' }, + maintainer: { '@id': 'schema:maintainer' }, + makesOffer: { '@id': 'schema:makesOffer' }, + manufacturer: { '@id': 'schema:manufacturer' }, + map: { '@id': 'schema:map', '@type': '@id' }, + mapType: { '@id': 'schema:mapType' }, + maps: { '@id': 'schema:maps', '@type': '@id' }, + marginOfError: { '@id': 'schema:marginOfError' }, + masthead: { '@id': 'schema:masthead', '@type': '@id' }, + material: { '@id': 'schema:material' }, + materialExtent: { '@id': 'schema:materialExtent' }, + mathExpression: { '@id': 'schema:mathExpression' }, + maxPrice: { '@id': 'schema:maxPrice' }, + maxValue: { '@id': 'schema:maxValue' }, + maximumAttendeeCapacity: { '@id': 'schema:maximumAttendeeCapacity' }, + maximumEnrollment: { '@id': 'schema:maximumEnrollment' }, + maximumIntake: { '@id': 'schema:maximumIntake' }, + maximumPhysicalAttendeeCapacity: { + '@id': 'schema:maximumPhysicalAttendeeCapacity', + }, + maximumVirtualAttendeeCapacity: { + '@id': 'schema:maximumVirtualAttendeeCapacity', + }, + mealService: { '@id': 'schema:mealService' }, + measuredProperty: { '@id': 'schema:measuredProperty' }, + measuredValue: { '@id': 'schema:measuredValue' }, + measurementTechnique: { '@id': 'schema:measurementTechnique' }, + mechanismOfAction: { '@id': 'schema:mechanismOfAction' }, + mediaAuthenticityCategory: { '@id': 'schema:mediaAuthenticityCategory' }, + median: { '@id': 'schema:median' }, + medicalAudience: { '@id': 'schema:medicalAudience' }, + medicalSpecialty: { '@id': 'schema:medicalSpecialty' }, + medicineSystem: { '@id': 'schema:medicineSystem' }, + meetsEmissionStandard: { '@id': 'schema:meetsEmissionStandard' }, + member: { '@id': 'schema:member' }, + memberOf: { '@id': 'schema:memberOf' }, + members: { '@id': 'schema:members' }, + membershipNumber: { '@id': 'schema:membershipNumber' }, + membershipPointsEarned: { '@id': 'schema:membershipPointsEarned' }, + memoryRequirements: { '@id': 'schema:memoryRequirements' }, + mentions: { '@id': 'schema:mentions' }, + menu: { '@id': 'schema:menu' }, + menuAddOn: { '@id': 'schema:menuAddOn' }, + merchant: { '@id': 'schema:merchant' }, + merchantReturnDays: { '@id': 'schema:merchantReturnDays' }, + merchantReturnLink: { '@id': 'schema:merchantReturnLink', '@type': '@id' }, + messageAttachment: { '@id': 'schema:messageAttachment' }, + mileageFromOdometer: { '@id': 'schema:mileageFromOdometer' }, + minPrice: { '@id': 'schema:minPrice' }, + minValue: { '@id': 'schema:minValue' }, + minimumPaymentDue: { '@id': 'schema:minimumPaymentDue' }, + missionCoveragePrioritiesPolicy: { + '@id': 'schema:missionCoveragePrioritiesPolicy', + '@type': '@id', + }, + model: { '@id': 'schema:model' }, + modelDate: { '@id': 'schema:modelDate', '@type': 'Date' }, + modifiedTime: { '@id': 'schema:modifiedTime' }, + monthlyMinimumRepaymentAmount: { + '@id': 'schema:monthlyMinimumRepaymentAmount', + }, + monthsOfExperience: { '@id': 'schema:monthsOfExperience' }, + mpn: { '@id': 'schema:mpn' }, + multipleValues: { '@id': 'schema:multipleValues' }, + muscleAction: { '@id': 'schema:muscleAction' }, + musicArrangement: { '@id': 'schema:musicArrangement' }, + musicBy: { '@id': 'schema:musicBy' }, + musicCompositionForm: { '@id': 'schema:musicCompositionForm' }, + musicGroupMember: { '@id': 'schema:musicGroupMember' }, + musicReleaseFormat: { '@id': 'schema:musicReleaseFormat' }, + musicalKey: { '@id': 'schema:musicalKey' }, + naics: { '@id': 'schema:naics' }, + name: { '@id': 'schema:name' }, + namedPosition: { '@id': 'schema:namedPosition' }, + nationality: { '@id': 'schema:nationality' }, + naturalProgression: { '@id': 'schema:naturalProgression' }, + nerve: { '@id': 'schema:nerve' }, + nerveMotor: { '@id': 'schema:nerveMotor' }, + netWorth: { '@id': 'schema:netWorth' }, + newsUpdatesAndGuidelines: { + '@id': 'schema:newsUpdatesAndGuidelines', + '@type': '@id', + }, + nextItem: { '@id': 'schema:nextItem' }, + noBylinesPolicy: { '@id': 'schema:noBylinesPolicy', '@type': '@id' }, + nonEqual: { '@id': 'schema:nonEqual' }, + nonProprietaryName: { '@id': 'schema:nonProprietaryName' }, + nonprofitStatus: { '@id': 'schema:nonprofitStatus' }, + normalRange: { '@id': 'schema:normalRange' }, + nsn: { '@id': 'schema:nsn' }, + numAdults: { '@id': 'schema:numAdults' }, + numChildren: { '@id': 'schema:numChildren' }, + numConstraints: { '@id': 'schema:numConstraints' }, + numTracks: { '@id': 'schema:numTracks' }, + numberOfAccommodationUnits: { '@id': 'schema:numberOfAccommodationUnits' }, + numberOfAirbags: { '@id': 'schema:numberOfAirbags' }, + numberOfAvailableAccommodationUnits: { + '@id': 'schema:numberOfAvailableAccommodationUnits', + }, + numberOfAxles: { '@id': 'schema:numberOfAxles' }, + numberOfBathroomsTotal: { '@id': 'schema:numberOfBathroomsTotal' }, + numberOfBedrooms: { '@id': 'schema:numberOfBedrooms' }, + numberOfBeds: { '@id': 'schema:numberOfBeds' }, + numberOfCredits: { '@id': 'schema:numberOfCredits' }, + numberOfDoors: { '@id': 'schema:numberOfDoors' }, + numberOfEmployees: { '@id': 'schema:numberOfEmployees' }, + numberOfEpisodes: { '@id': 'schema:numberOfEpisodes' }, + numberOfForwardGears: { '@id': 'schema:numberOfForwardGears' }, + numberOfFullBathrooms: { '@id': 'schema:numberOfFullBathrooms' }, + numberOfItems: { '@id': 'schema:numberOfItems' }, + numberOfLoanPayments: { '@id': 'schema:numberOfLoanPayments' }, + numberOfPages: { '@id': 'schema:numberOfPages' }, + numberOfPartialBathrooms: { '@id': 'schema:numberOfPartialBathrooms' }, + numberOfPlayers: { '@id': 'schema:numberOfPlayers' }, + numberOfPreviousOwners: { '@id': 'schema:numberOfPreviousOwners' }, + numberOfRooms: { '@id': 'schema:numberOfRooms' }, + numberOfSeasons: { '@id': 'schema:numberOfSeasons' }, + numberedPosition: { '@id': 'schema:numberedPosition' }, + nutrition: { '@id': 'schema:nutrition' }, + object: { '@id': 'schema:object' }, + observationDate: { '@id': 'schema:observationDate' }, + observedNode: { '@id': 'schema:observedNode' }, + occupancy: { '@id': 'schema:occupancy' }, + occupationLocation: { '@id': 'schema:occupationLocation' }, + occupationalCategory: { '@id': 'schema:occupationalCategory' }, + occupationalCredentialAwarded: { + '@id': 'schema:occupationalCredentialAwarded', + }, + offerCount: { '@id': 'schema:offerCount' }, + offeredBy: { '@id': 'schema:offeredBy' }, + offers: { '@id': 'schema:offers' }, + offersPrescriptionByMail: { '@id': 'schema:offersPrescriptionByMail' }, + openingHours: { '@id': 'schema:openingHours' }, + openingHoursSpecification: { '@id': 'schema:openingHoursSpecification' }, + opens: { '@id': 'schema:opens' }, + operatingSystem: { '@id': 'schema:operatingSystem' }, + opponent: { '@id': 'schema:opponent' }, + option: { '@id': 'schema:option' }, + orderDate: { '@id': 'schema:orderDate', '@type': 'Date' }, + orderDelivery: { '@id': 'schema:orderDelivery' }, + orderItemNumber: { '@id': 'schema:orderItemNumber' }, + orderItemStatus: { '@id': 'schema:orderItemStatus' }, + orderNumber: { '@id': 'schema:orderNumber' }, + orderQuantity: { '@id': 'schema:orderQuantity' }, + orderStatus: { '@id': 'schema:orderStatus' }, + orderedItem: { '@id': 'schema:orderedItem' }, + organizer: { '@id': 'schema:organizer' }, + originAddress: { '@id': 'schema:originAddress' }, + originatesFrom: { '@id': 'schema:originatesFrom' }, + overdosage: { '@id': 'schema:overdosage' }, + ownedFrom: { '@id': 'schema:ownedFrom' }, + ownedThrough: { '@id': 'schema:ownedThrough' }, + ownershipFundingInfo: { '@id': 'schema:ownershipFundingInfo' }, + owns: { '@id': 'schema:owns' }, + pageEnd: { '@id': 'schema:pageEnd' }, + pageStart: { '@id': 'schema:pageStart' }, + pagination: { '@id': 'schema:pagination' }, + parent: { '@id': 'schema:parent' }, + parentItem: { '@id': 'schema:parentItem' }, + parentOrganization: { '@id': 'schema:parentOrganization' }, + parentService: { '@id': 'schema:parentService' }, + parents: { '@id': 'schema:parents' }, + partOfEpisode: { '@id': 'schema:partOfEpisode' }, + partOfInvoice: { '@id': 'schema:partOfInvoice' }, + partOfOrder: { '@id': 'schema:partOfOrder' }, + partOfSeason: { '@id': 'schema:partOfSeason' }, + partOfSeries: { '@id': 'schema:partOfSeries' }, + partOfSystem: { '@id': 'schema:partOfSystem' }, + partOfTVSeries: { '@id': 'schema:partOfTVSeries' }, + partOfTrip: { '@id': 'schema:partOfTrip' }, + participant: { '@id': 'schema:participant' }, + partySize: { '@id': 'schema:partySize' }, + passengerPriorityStatus: { '@id': 'schema:passengerPriorityStatus' }, + passengerSequenceNumber: { '@id': 'schema:passengerSequenceNumber' }, + pathophysiology: { '@id': 'schema:pathophysiology' }, + pattern: { '@id': 'schema:pattern' }, + payload: { '@id': 'schema:payload' }, + paymentAccepted: { '@id': 'schema:paymentAccepted' }, + paymentDue: { '@id': 'schema:paymentDue' }, + paymentDueDate: { '@id': 'schema:paymentDueDate', '@type': 'Date' }, + paymentMethod: { '@id': 'schema:paymentMethod' }, + paymentMethodId: { '@id': 'schema:paymentMethodId' }, + paymentStatus: { '@id': 'schema:paymentStatus' }, + paymentUrl: { '@id': 'schema:paymentUrl', '@type': '@id' }, + penciler: { '@id': 'schema:penciler' }, + percentile10: { '@id': 'schema:percentile10' }, + percentile25: { '@id': 'schema:percentile25' }, + percentile75: { '@id': 'schema:percentile75' }, + percentile90: { '@id': 'schema:percentile90' }, + performTime: { '@id': 'schema:performTime' }, + performer: { '@id': 'schema:performer' }, + performerIn: { '@id': 'schema:performerIn' }, + performers: { '@id': 'schema:performers' }, + permissionType: { '@id': 'schema:permissionType' }, + permissions: { '@id': 'schema:permissions' }, + permitAudience: { '@id': 'schema:permitAudience' }, + permittedUsage: { '@id': 'schema:permittedUsage' }, + petsAllowed: { '@id': 'schema:petsAllowed' }, + phoneticText: { '@id': 'schema:phoneticText' }, + photo: { '@id': 'schema:photo' }, + photos: { '@id': 'schema:photos' }, + physicalRequirement: { '@id': 'schema:physicalRequirement' }, + physiologicalBenefits: { '@id': 'schema:physiologicalBenefits' }, + pickupLocation: { '@id': 'schema:pickupLocation' }, + pickupTime: { '@id': 'schema:pickupTime' }, + playMode: { '@id': 'schema:playMode' }, + playerType: { '@id': 'schema:playerType' }, + playersOnline: { '@id': 'schema:playersOnline' }, + polygon: { '@id': 'schema:polygon' }, + populationType: { '@id': 'schema:populationType' }, + position: { '@id': 'schema:position' }, + possibleComplication: { '@id': 'schema:possibleComplication' }, + possibleTreatment: { '@id': 'schema:possibleTreatment' }, + postOfficeBoxNumber: { '@id': 'schema:postOfficeBoxNumber' }, + postOp: { '@id': 'schema:postOp' }, + postalCode: { '@id': 'schema:postalCode' }, + postalCodeBegin: { '@id': 'schema:postalCodeBegin' }, + postalCodeEnd: { '@id': 'schema:postalCodeEnd' }, + postalCodePrefix: { '@id': 'schema:postalCodePrefix' }, + postalCodeRange: { '@id': 'schema:postalCodeRange' }, + potentialAction: { '@id': 'schema:potentialAction' }, + preOp: { '@id': 'schema:preOp' }, + predecessorOf: { '@id': 'schema:predecessorOf' }, + pregnancyCategory: { '@id': 'schema:pregnancyCategory' }, + pregnancyWarning: { '@id': 'schema:pregnancyWarning' }, + prepTime: { '@id': 'schema:prepTime' }, + preparation: { '@id': 'schema:preparation' }, + prescribingInfo: { '@id': 'schema:prescribingInfo', '@type': '@id' }, + prescriptionStatus: { '@id': 'schema:prescriptionStatus' }, + previousItem: { '@id': 'schema:previousItem' }, + previousStartDate: { '@id': 'schema:previousStartDate', '@type': 'Date' }, + price: { '@id': 'schema:price' }, + priceComponent: { '@id': 'schema:priceComponent' }, + priceComponentType: { '@id': 'schema:priceComponentType' }, + priceCurrency: { '@id': 'schema:priceCurrency' }, + priceRange: { '@id': 'schema:priceRange' }, + priceSpecification: { '@id': 'schema:priceSpecification' }, + priceType: { '@id': 'schema:priceType' }, + priceValidUntil: { '@id': 'schema:priceValidUntil', '@type': 'Date' }, + primaryImageOfPage: { '@id': 'schema:primaryImageOfPage' }, + primaryPrevention: { '@id': 'schema:primaryPrevention' }, + printColumn: { '@id': 'schema:printColumn' }, + printEdition: { '@id': 'schema:printEdition' }, + printPage: { '@id': 'schema:printPage' }, + printSection: { '@id': 'schema:printSection' }, + procedure: { '@id': 'schema:procedure' }, + procedureType: { '@id': 'schema:procedureType' }, + processingTime: { '@id': 'schema:processingTime' }, + processorRequirements: { '@id': 'schema:processorRequirements' }, + producer: { '@id': 'schema:producer' }, + produces: { '@id': 'schema:produces' }, + productGroupID: { '@id': 'schema:productGroupID' }, + productID: { '@id': 'schema:productID' }, + productReturnDays: { '@id': 'schema:productReturnDays' }, + productReturnLink: { '@id': 'schema:productReturnLink', '@type': '@id' }, + productSupported: { '@id': 'schema:productSupported' }, + productionCompany: { '@id': 'schema:productionCompany' }, + productionDate: { '@id': 'schema:productionDate', '@type': 'Date' }, + proficiencyLevel: { '@id': 'schema:proficiencyLevel' }, + programMembershipUsed: { '@id': 'schema:programMembershipUsed' }, + programName: { '@id': 'schema:programName' }, + programPrerequisites: { '@id': 'schema:programPrerequisites' }, + programType: { '@id': 'schema:programType' }, + programmingLanguage: { '@id': 'schema:programmingLanguage' }, + programmingModel: { '@id': 'schema:programmingModel' }, + propertyID: { '@id': 'schema:propertyID' }, + proprietaryName: { '@id': 'schema:proprietaryName' }, + proteinContent: { '@id': 'schema:proteinContent' }, + provider: { '@id': 'schema:provider' }, + providerMobility: { '@id': 'schema:providerMobility' }, + providesBroadcastService: { '@id': 'schema:providesBroadcastService' }, + providesService: { '@id': 'schema:providesService' }, + publicAccess: { '@id': 'schema:publicAccess' }, + publicTransportClosuresInfo: { + '@id': 'schema:publicTransportClosuresInfo', + '@type': '@id', + }, + publication: { '@id': 'schema:publication' }, + publicationType: { '@id': 'schema:publicationType' }, + publishedBy: { '@id': 'schema:publishedBy' }, + publishedOn: { '@id': 'schema:publishedOn' }, + publisher: { '@id': 'schema:publisher' }, + publisherImprint: { '@id': 'schema:publisherImprint' }, + publishingPrinciples: { '@id': 'schema:publishingPrinciples', '@type': '@id' }, + purchaseDate: { '@id': 'schema:purchaseDate', '@type': 'Date' }, + qualifications: { '@id': 'schema:qualifications' }, + quarantineGuidelines: { '@id': 'schema:quarantineGuidelines', '@type': '@id' }, + query: { '@id': 'schema:query' }, + quest: { '@id': 'schema:quest' }, + question: { '@id': 'schema:question' }, + rangeIncludes: { '@id': 'schema:rangeIncludes' }, + ratingCount: { '@id': 'schema:ratingCount' }, + ratingExplanation: { '@id': 'schema:ratingExplanation' }, + ratingValue: { '@id': 'schema:ratingValue' }, + readBy: { '@id': 'schema:readBy' }, + readonlyValue: { '@id': 'schema:readonlyValue' }, + realEstateAgent: { '@id': 'schema:realEstateAgent' }, + recipe: { '@id': 'schema:recipe' }, + recipeCategory: { '@id': 'schema:recipeCategory' }, + recipeCuisine: { '@id': 'schema:recipeCuisine' }, + recipeIngredient: { '@id': 'schema:recipeIngredient' }, + recipeInstructions: { '@id': 'schema:recipeInstructions' }, + recipeYield: { '@id': 'schema:recipeYield' }, + recipient: { '@id': 'schema:recipient' }, + recognizedBy: { '@id': 'schema:recognizedBy' }, + recognizingAuthority: { '@id': 'schema:recognizingAuthority' }, + recommendationStrength: { '@id': 'schema:recommendationStrength' }, + recommendedIntake: { '@id': 'schema:recommendedIntake' }, + recordLabel: { '@id': 'schema:recordLabel' }, + recordedAs: { '@id': 'schema:recordedAs' }, + recordedAt: { '@id': 'schema:recordedAt' }, + recordedIn: { '@id': 'schema:recordedIn' }, + recordingOf: { '@id': 'schema:recordingOf' }, + recourseLoan: { '@id': 'schema:recourseLoan' }, + referenceQuantity: { '@id': 'schema:referenceQuantity' }, + referencesOrder: { '@id': 'schema:referencesOrder' }, + refundType: { '@id': 'schema:refundType' }, + regionDrained: { '@id': 'schema:regionDrained' }, + regionsAllowed: { '@id': 'schema:regionsAllowed' }, + relatedAnatomy: { '@id': 'schema:relatedAnatomy' }, + relatedCondition: { '@id': 'schema:relatedCondition' }, + relatedDrug: { '@id': 'schema:relatedDrug' }, + relatedLink: { '@id': 'schema:relatedLink', '@type': '@id' }, + relatedStructure: { '@id': 'schema:relatedStructure' }, + relatedTherapy: { '@id': 'schema:relatedTherapy' }, + relatedTo: { '@id': 'schema:relatedTo' }, + releaseDate: { '@id': 'schema:releaseDate', '@type': 'Date' }, + releaseNotes: { '@id': 'schema:releaseNotes' }, + releaseOf: { '@id': 'schema:releaseOf' }, + releasedEvent: { '@id': 'schema:releasedEvent' }, + relevantOccupation: { '@id': 'schema:relevantOccupation' }, + relevantSpecialty: { '@id': 'schema:relevantSpecialty' }, + remainingAttendeeCapacity: { '@id': 'schema:remainingAttendeeCapacity' }, + renegotiableLoan: { '@id': 'schema:renegotiableLoan' }, + repeatCount: { '@id': 'schema:repeatCount' }, + repeatFrequency: { '@id': 'schema:repeatFrequency' }, + repetitions: { '@id': 'schema:repetitions' }, + replacee: { '@id': 'schema:replacee' }, + replacer: { '@id': 'schema:replacer' }, + replyToUrl: { '@id': 'schema:replyToUrl', '@type': '@id' }, + reportNumber: { '@id': 'schema:reportNumber' }, + representativeOfPage: { '@id': 'schema:representativeOfPage' }, + requiredCollateral: { '@id': 'schema:requiredCollateral' }, + requiredGender: { '@id': 'schema:requiredGender' }, + requiredMaxAge: { '@id': 'schema:requiredMaxAge' }, + requiredMinAge: { '@id': 'schema:requiredMinAge' }, + requiredQuantity: { '@id': 'schema:requiredQuantity' }, + requirements: { '@id': 'schema:requirements' }, + requiresSubscription: { '@id': 'schema:requiresSubscription' }, + reservationFor: { '@id': 'schema:reservationFor' }, + reservationId: { '@id': 'schema:reservationId' }, + reservationStatus: { '@id': 'schema:reservationStatus' }, + reservedTicket: { '@id': 'schema:reservedTicket' }, + responsibilities: { '@id': 'schema:responsibilities' }, + restPeriods: { '@id': 'schema:restPeriods' }, + result: { '@id': 'schema:result' }, + resultComment: { '@id': 'schema:resultComment' }, + resultReview: { '@id': 'schema:resultReview' }, + returnFees: { '@id': 'schema:returnFees' }, + returnPolicyCategory: { '@id': 'schema:returnPolicyCategory' }, + review: { '@id': 'schema:review' }, + reviewAspect: { '@id': 'schema:reviewAspect' }, + reviewBody: { '@id': 'schema:reviewBody' }, + reviewCount: { '@id': 'schema:reviewCount' }, + reviewRating: { '@id': 'schema:reviewRating' }, + reviewedBy: { '@id': 'schema:reviewedBy' }, + reviews: { '@id': 'schema:reviews' }, + riskFactor: { '@id': 'schema:riskFactor' }, + risks: { '@id': 'schema:risks' }, + roleName: { '@id': 'schema:roleName' }, + roofLoad: { '@id': 'schema:roofLoad' }, + rsvpResponse: { '@id': 'schema:rsvpResponse' }, + runsTo: { '@id': 'schema:runsTo' }, + runtime: { '@id': 'schema:runtime' }, + runtimePlatform: { '@id': 'schema:runtimePlatform' }, + rxcui: { '@id': 'schema:rxcui' }, + safetyConsideration: { '@id': 'schema:safetyConsideration' }, + salaryCurrency: { '@id': 'schema:salaryCurrency' }, + salaryUponCompletion: { '@id': 'schema:salaryUponCompletion' }, + sameAs: { '@id': 'schema:sameAs', '@type': '@id' }, + sampleType: { '@id': 'schema:sampleType' }, + saturatedFatContent: { '@id': 'schema:saturatedFatContent' }, + scheduleTimezone: { '@id': 'schema:scheduleTimezone' }, + scheduledPaymentDate: { '@id': 'schema:scheduledPaymentDate', '@type': 'Date' }, + scheduledTime: { '@id': 'schema:scheduledTime' }, + schemaVersion: { '@id': 'schema:schemaVersion' }, + schoolClosuresInfo: { '@id': 'schema:schoolClosuresInfo', '@type': '@id' }, + screenCount: { '@id': 'schema:screenCount' }, + screenshot: { '@id': 'schema:screenshot', '@type': '@id' }, + sdDatePublished: { '@id': 'schema:sdDatePublished', '@type': 'Date' }, + sdLicense: { '@id': 'schema:sdLicense', '@type': '@id' }, + sdPublisher: { '@id': 'schema:sdPublisher' }, + season: { '@id': 'schema:season', '@type': '@id' }, + seasonNumber: { '@id': 'schema:seasonNumber' }, + seasons: { '@id': 'schema:seasons' }, + seatNumber: { '@id': 'schema:seatNumber' }, + seatRow: { '@id': 'schema:seatRow' }, + seatSection: { '@id': 'schema:seatSection' }, + seatingCapacity: { '@id': 'schema:seatingCapacity' }, + seatingType: { '@id': 'schema:seatingType' }, + secondaryPrevention: { '@id': 'schema:secondaryPrevention' }, + securityClearanceRequirement: { '@id': 'schema:securityClearanceRequirement' }, + securityScreening: { '@id': 'schema:securityScreening' }, + seeks: { '@id': 'schema:seeks' }, + seller: { '@id': 'schema:seller' }, + sender: { '@id': 'schema:sender' }, + sensoryRequirement: { '@id': 'schema:sensoryRequirement' }, + sensoryUnit: { '@id': 'schema:sensoryUnit' }, + serialNumber: { '@id': 'schema:serialNumber' }, + seriousAdverseOutcome: { '@id': 'schema:seriousAdverseOutcome' }, + serverStatus: { '@id': 'schema:serverStatus' }, + servesCuisine: { '@id': 'schema:servesCuisine' }, + serviceArea: { '@id': 'schema:serviceArea' }, + serviceAudience: { '@id': 'schema:serviceAudience' }, + serviceLocation: { '@id': 'schema:serviceLocation' }, + serviceOperator: { '@id': 'schema:serviceOperator' }, + serviceOutput: { '@id': 'schema:serviceOutput' }, + servicePhone: { '@id': 'schema:servicePhone' }, + servicePostalAddress: { '@id': 'schema:servicePostalAddress' }, + serviceSmsNumber: { '@id': 'schema:serviceSmsNumber' }, + serviceType: { '@id': 'schema:serviceType' }, + serviceUrl: { '@id': 'schema:serviceUrl', '@type': '@id' }, + servingSize: { '@id': 'schema:servingSize' }, + sharedContent: { '@id': 'schema:sharedContent' }, + shippingDestination: { '@id': 'schema:shippingDestination' }, + shippingDetails: { '@id': 'schema:shippingDetails' }, + shippingLabel: { '@id': 'schema:shippingLabel' }, + shippingRate: { '@id': 'schema:shippingRate' }, + shippingSettingsLink: { '@id': 'schema:shippingSettingsLink', '@type': '@id' }, + sibling: { '@id': 'schema:sibling' }, + siblings: { '@id': 'schema:siblings' }, + signDetected: { '@id': 'schema:signDetected' }, + signOrSymptom: { '@id': 'schema:signOrSymptom' }, + significance: { '@id': 'schema:significance' }, + significantLink: { '@id': 'schema:significantLink', '@type': '@id' }, + significantLinks: { '@id': 'schema:significantLinks', '@type': '@id' }, + size: { '@id': 'schema:size' }, + sizeGroup: { '@id': 'schema:sizeGroup' }, + sizeSystem: { '@id': 'schema:sizeSystem' }, + skills: { '@id': 'schema:skills' }, + sku: { '@id': 'schema:sku' }, + slogan: { '@id': 'schema:slogan' }, + smokingAllowed: { '@id': 'schema:smokingAllowed' }, + sodiumContent: { '@id': 'schema:sodiumContent' }, + softwareAddOn: { '@id': 'schema:softwareAddOn' }, + softwareHelp: { '@id': 'schema:softwareHelp' }, + softwareRequirements: { '@id': 'schema:softwareRequirements' }, + softwareVersion: { '@id': 'schema:softwareVersion' }, + sourceOrganization: { '@id': 'schema:sourceOrganization' }, + sourcedFrom: { '@id': 'schema:sourcedFrom' }, + spatial: { '@id': 'schema:spatial' }, + spatialCoverage: { '@id': 'schema:spatialCoverage' }, + speakable: { '@id': 'schema:speakable', '@type': '@id' }, + specialCommitments: { '@id': 'schema:specialCommitments' }, + specialOpeningHoursSpecification: { + '@id': 'schema:specialOpeningHoursSpecification', + }, + specialty: { '@id': 'schema:specialty' }, + speechToTextMarkup: { '@id': 'schema:speechToTextMarkup' }, + speed: { '@id': 'schema:speed' }, + spokenByCharacter: { '@id': 'schema:spokenByCharacter' }, + sponsor: { '@id': 'schema:sponsor' }, + sport: { '@id': 'schema:sport' }, + sportsActivityLocation: { '@id': 'schema:sportsActivityLocation' }, + sportsEvent: { '@id': 'schema:sportsEvent' }, + sportsTeam: { '@id': 'schema:sportsTeam' }, + spouse: { '@id': 'schema:spouse' }, + stage: { '@id': 'schema:stage' }, + stageAsNumber: { '@id': 'schema:stageAsNumber' }, + starRating: { '@id': 'schema:starRating' }, + startDate: { '@id': 'schema:startDate', '@type': 'Date' }, + startOffset: { '@id': 'schema:startOffset' }, + startTime: { '@id': 'schema:startTime' }, + status: { '@id': 'schema:status' }, + steeringPosition: { '@id': 'schema:steeringPosition' }, + step: { '@id': 'schema:step' }, + stepValue: { '@id': 'schema:stepValue' }, + steps: { '@id': 'schema:steps' }, + storageRequirements: { '@id': 'schema:storageRequirements' }, + streetAddress: { '@id': 'schema:streetAddress' }, + strengthUnit: { '@id': 'schema:strengthUnit' }, + strengthValue: { '@id': 'schema:strengthValue' }, + structuralClass: { '@id': 'schema:structuralClass' }, + study: { '@id': 'schema:study' }, + studyDesign: { '@id': 'schema:studyDesign' }, + studyLocation: { '@id': 'schema:studyLocation' }, + studySubject: { '@id': 'schema:studySubject' }, + stupidProperty: { '@id': 'schema:stupidProperty' }, + subEvent: { '@id': 'schema:subEvent' }, + subEvents: { '@id': 'schema:subEvents' }, + subOrganization: { '@id': 'schema:subOrganization' }, + subReservation: { '@id': 'schema:subReservation' }, + subStageSuffix: { '@id': 'schema:subStageSuffix' }, + subStructure: { '@id': 'schema:subStructure' }, + subTest: { '@id': 'schema:subTest' }, + subTrip: { '@id': 'schema:subTrip' }, + subjectOf: { '@id': 'schema:subjectOf' }, + subtitleLanguage: { '@id': 'schema:subtitleLanguage' }, + successorOf: { '@id': 'schema:successorOf' }, + sugarContent: { '@id': 'schema:sugarContent' }, + suggestedAge: { '@id': 'schema:suggestedAge' }, + suggestedAnswer: { '@id': 'schema:suggestedAnswer' }, + suggestedGender: { '@id': 'schema:suggestedGender' }, + suggestedMaxAge: { '@id': 'schema:suggestedMaxAge' }, + suggestedMeasurement: { '@id': 'schema:suggestedMeasurement' }, + suggestedMinAge: { '@id': 'schema:suggestedMinAge' }, + suitableForDiet: { '@id': 'schema:suitableForDiet' }, + superEvent: { '@id': 'schema:superEvent' }, + supersededBy: { '@id': 'schema:supersededBy' }, + supply: { '@id': 'schema:supply' }, + supplyTo: { '@id': 'schema:supplyTo' }, + supportingData: { '@id': 'schema:supportingData' }, + surface: { '@id': 'schema:surface' }, + target: { '@id': 'schema:target' }, + targetCollection: { '@id': 'schema:targetCollection' }, + targetDescription: { '@id': 'schema:targetDescription' }, + targetName: { '@id': 'schema:targetName' }, + targetPlatform: { '@id': 'schema:targetPlatform' }, + targetPopulation: { '@id': 'schema:targetPopulation' }, + targetProduct: { '@id': 'schema:targetProduct' }, + targetUrl: { '@id': 'schema:targetUrl', '@type': '@id' }, + taxID: { '@id': 'schema:taxID' }, + teaches: { '@id': 'schema:teaches' }, + telephone: { '@id': 'schema:telephone' }, + temporal: { '@id': 'schema:temporal' }, + temporalCoverage: { '@id': 'schema:temporalCoverage' }, + termCode: { '@id': 'schema:termCode' }, + termDuration: { '@id': 'schema:termDuration' }, + termsOfService: { '@id': 'schema:termsOfService' }, + termsPerYear: { '@id': 'schema:termsPerYear' }, + text: { '@id': 'schema:text' }, + textValue: { '@id': 'schema:textValue' }, + thumbnail: { '@id': 'schema:thumbnail' }, + thumbnailUrl: { '@id': 'schema:thumbnailUrl', '@type': '@id' }, + tickerSymbol: { '@id': 'schema:tickerSymbol' }, + ticketNumber: { '@id': 'schema:ticketNumber' }, + ticketToken: { '@id': 'schema:ticketToken' }, + ticketedSeat: { '@id': 'schema:ticketedSeat' }, + timeOfDay: { '@id': 'schema:timeOfDay' }, + timeRequired: { '@id': 'schema:timeRequired' }, + timeToComplete: { '@id': 'schema:timeToComplete' }, + tissueSample: { '@id': 'schema:tissueSample' }, + title: { '@id': 'schema:title' }, + titleEIDR: { '@id': 'schema:titleEIDR' }, + toLocation: { '@id': 'schema:toLocation' }, + toRecipient: { '@id': 'schema:toRecipient' }, + tocContinuation: { '@id': 'schema:tocContinuation' }, + tocEntry: { '@id': 'schema:tocEntry' }, + tongueWeight: { '@id': 'schema:tongueWeight' }, + tool: { '@id': 'schema:tool' }, + torque: { '@id': 'schema:torque' }, + totalJobOpenings: { '@id': 'schema:totalJobOpenings' }, + totalPaymentDue: { '@id': 'schema:totalPaymentDue' }, + totalPrice: { '@id': 'schema:totalPrice' }, + totalTime: { '@id': 'schema:totalTime' }, + tourBookingPage: { '@id': 'schema:tourBookingPage', '@type': '@id' }, + touristType: { '@id': 'schema:touristType' }, + track: { '@id': 'schema:track' }, + trackingNumber: { '@id': 'schema:trackingNumber' }, + trackingUrl: { '@id': 'schema:trackingUrl', '@type': '@id' }, + tracks: { '@id': 'schema:tracks' }, + trailer: { '@id': 'schema:trailer' }, + trailerWeight: { '@id': 'schema:trailerWeight' }, + trainName: { '@id': 'schema:trainName' }, + trainNumber: { '@id': 'schema:trainNumber' }, + trainingSalary: { '@id': 'schema:trainingSalary' }, + transFatContent: { '@id': 'schema:transFatContent' }, + transcript: { '@id': 'schema:transcript' }, + transitTime: { '@id': 'schema:transitTime' }, + transitTimeLabel: { '@id': 'schema:transitTimeLabel' }, + translationOfWork: { '@id': 'schema:translationOfWork' }, + translator: { '@id': 'schema:translator' }, + transmissionMethod: { '@id': 'schema:transmissionMethod' }, + travelBans: { '@id': 'schema:travelBans', '@type': '@id' }, + trialDesign: { '@id': 'schema:trialDesign' }, + tributary: { '@id': 'schema:tributary' }, + typeOfBed: { '@id': 'schema:typeOfBed' }, + typeOfGood: { '@id': 'schema:typeOfGood' }, + typicalAgeRange: { '@id': 'schema:typicalAgeRange' }, + typicalCreditsPerTerm: { '@id': 'schema:typicalCreditsPerTerm' }, + typicalTest: { '@id': 'schema:typicalTest' }, + underName: { '@id': 'schema:underName' }, + unitCode: { '@id': 'schema:unitCode' }, + unitText: { '@id': 'schema:unitText' }, + unnamedSourcesPolicy: { '@id': 'schema:unnamedSourcesPolicy', '@type': '@id' }, + unsaturatedFatContent: { '@id': 'schema:unsaturatedFatContent' }, + uploadDate: { '@id': 'schema:uploadDate', '@type': 'Date' }, + upvoteCount: { '@id': 'schema:upvoteCount' }, + url: { '@id': 'schema:url', '@type': '@id' }, + urlTemplate: { '@id': 'schema:urlTemplate' }, + usageInfo: { '@id': 'schema:usageInfo', '@type': '@id' }, + usedToDiagnose: { '@id': 'schema:usedToDiagnose' }, + userInteractionCount: { '@id': 'schema:userInteractionCount' }, + usesDevice: { '@id': 'schema:usesDevice' }, + usesHealthPlanIdStandard: { '@id': 'schema:usesHealthPlanIdStandard' }, + utterances: { '@id': 'schema:utterances' }, + validFor: { '@id': 'schema:validFor' }, + validFrom: { '@id': 'schema:validFrom', '@type': 'Date' }, + validIn: { '@id': 'schema:validIn' }, + validThrough: { '@id': 'schema:validThrough', '@type': 'Date' }, + validUntil: { '@id': 'schema:validUntil', '@type': 'Date' }, + value: { '@id': 'schema:value' }, + valueAddedTaxIncluded: { '@id': 'schema:valueAddedTaxIncluded' }, + valueMaxLength: { '@id': 'schema:valueMaxLength' }, + valueMinLength: { '@id': 'schema:valueMinLength' }, + valueName: { '@id': 'schema:valueName' }, + valuePattern: { '@id': 'schema:valuePattern' }, + valueReference: { '@id': 'schema:valueReference' }, + valueRequired: { '@id': 'schema:valueRequired' }, + variableMeasured: { '@id': 'schema:variableMeasured' }, + variablesMeasured: { '@id': 'schema:variablesMeasured' }, + variantCover: { '@id': 'schema:variantCover' }, + variesBy: { '@id': 'schema:variesBy' }, + vatID: { '@id': 'schema:vatID' }, + vehicleConfiguration: { '@id': 'schema:vehicleConfiguration' }, + vehicleEngine: { '@id': 'schema:vehicleEngine' }, + vehicleIdentificationNumber: { '@id': 'schema:vehicleIdentificationNumber' }, + vehicleInteriorColor: { '@id': 'schema:vehicleInteriorColor' }, + vehicleInteriorType: { '@id': 'schema:vehicleInteriorType' }, + vehicleModelDate: { '@id': 'schema:vehicleModelDate', '@type': 'Date' }, + vehicleSeatingCapacity: { '@id': 'schema:vehicleSeatingCapacity' }, + vehicleSpecialUsage: { '@id': 'schema:vehicleSpecialUsage' }, + vehicleTransmission: { '@id': 'schema:vehicleTransmission' }, + vendor: { '@id': 'schema:vendor' }, + verificationFactCheckingPolicy: { + '@id': 'schema:verificationFactCheckingPolicy', + '@type': '@id', + }, + version: { '@id': 'schema:version' }, + video: { '@id': 'schema:video' }, + videoFormat: { '@id': 'schema:videoFormat' }, + videoFrameSize: { '@id': 'schema:videoFrameSize' }, + videoQuality: { '@id': 'schema:videoQuality' }, + volumeNumber: { '@id': 'schema:volumeNumber' }, + warning: { '@id': 'schema:warning' }, + warranty: { '@id': 'schema:warranty' }, + warrantyPromise: { '@id': 'schema:warrantyPromise' }, + warrantyScope: { '@id': 'schema:warrantyScope' }, + webCheckinTime: { '@id': 'schema:webCheckinTime' }, + webFeed: { '@id': 'schema:webFeed', '@type': '@id' }, + weight: { '@id': 'schema:weight' }, + weightTotal: { '@id': 'schema:weightTotal' }, + wheelbase: { '@id': 'schema:wheelbase' }, + width: { '@id': 'schema:width' }, + winner: { '@id': 'schema:winner' }, + wordCount: { '@id': 'schema:wordCount' }, + workExample: { '@id': 'schema:workExample' }, + workFeatured: { '@id': 'schema:workFeatured' }, + workHours: { '@id': 'schema:workHours' }, + workLocation: { '@id': 'schema:workLocation' }, + workPerformed: { '@id': 'schema:workPerformed' }, + workPresented: { '@id': 'schema:workPresented' }, + workTranslation: { '@id': 'schema:workTranslation' }, + workload: { '@id': 'schema:workload' }, + worksFor: { '@id': 'schema:worksFor' }, + worstRating: { '@id': 'schema:worstRating' }, + xpath: { '@id': 'schema:xpath' }, + yearBuilt: { '@id': 'schema:yearBuilt' }, + yearlyRevenue: { '@id': 'schema:yearlyRevenue' }, + yearsInOperation: { '@id': 'schema:yearsInOperation' }, + yield: { '@id': 'schema:yield' }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/secp256k1_v1.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/secp256k1_v1.ts new file mode 100644 index 0000000000..bf6b5b1723 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/secp256k1_v1.ts @@ -0,0 +1,102 @@ +export const SECP256K1_V1 = { + '@context': { + id: '@id', + type: '@type', + '@protected': true, + proof: { + '@id': 'https://w3id.org/security#proof', + '@type': '@id', + '@container': '@graph', + }, + EcdsaSecp256k1VerificationKey2019: { + '@id': 'https://w3id.org/security#EcdsaSecp256k1VerificationKey2019', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + revoked: { + '@id': 'https://w3id.org/security#revoked', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + blockchainAccountId: { + '@id': 'https://w3id.org/security#blockchainAccountId', + }, + publicKeyJwk: { + '@id': 'https://w3id.org/security#publicKeyJwk', + '@type': '@json', + }, + publicKeyBase58: { + '@id': 'https://w3id.org/security#publicKeyBase58', + }, + publicKeyMultibase: { + '@id': 'https://w3id.org/security#publicKeyMultibase', + '@type': 'https://w3id.org/security#multibase', + }, + }, + }, + EcdsaSecp256k1Signature2019: { + '@id': 'https://w3id.org/security#EcdsaSecp256k1Signature2019', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + jws: { + '@id': 'https://w3id.org/security#jws', + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/security_v1.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/security_v1.ts new file mode 100644 index 0000000000..070779f190 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/security_v1.ts @@ -0,0 +1,47 @@ +export const SECURITY_V1 = { + '@context': { + id: '@id', + type: '@type', + dc: 'http://purl.org/dc/terms/', + sec: 'https://w3id.org/security#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + EcdsaKoblitzSignature2016: 'sec:EcdsaKoblitzSignature2016', + Ed25519Signature2018: 'sec:Ed25519Signature2018', + EncryptedMessage: 'sec:EncryptedMessage', + GraphSignature2012: 'sec:GraphSignature2012', + LinkedDataSignature2015: 'sec:LinkedDataSignature2015', + LinkedDataSignature2016: 'sec:LinkedDataSignature2016', + CryptographicKey: 'sec:Key', + authenticationTag: 'sec:authenticationTag', + canonicalizationAlgorithm: 'sec:canonicalizationAlgorithm', + cipherAlgorithm: 'sec:cipherAlgorithm', + cipherData: 'sec:cipherData', + cipherKey: 'sec:cipherKey', + created: { '@id': 'dc:created', '@type': 'xsd:dateTime' }, + creator: { '@id': 'dc:creator', '@type': '@id' }, + digestAlgorithm: 'sec:digestAlgorithm', + digestValue: 'sec:digestValue', + domain: 'sec:domain', + encryptionKey: 'sec:encryptionKey', + expiration: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + expires: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + initializationVector: 'sec:initializationVector', + iterationCount: 'sec:iterationCount', + nonce: 'sec:nonce', + normalizationAlgorithm: 'sec:normalizationAlgorithm', + owner: { '@id': 'sec:owner', '@type': '@id' }, + password: 'sec:password', + privateKey: { '@id': 'sec:privateKey', '@type': '@id' }, + privateKeyPem: 'sec:privateKeyPem', + publicKey: { '@id': 'sec:publicKey', '@type': '@id' }, + publicKeyBase58: 'sec:publicKeyBase58', + publicKeyPem: 'sec:publicKeyPem', + publicKeyWif: 'sec:publicKeyWif', + publicKeyService: { '@id': 'sec:publicKeyService', '@type': '@id' }, + revoked: { '@id': 'sec:revoked', '@type': 'xsd:dateTime' }, + salt: 'sec:salt', + signature: 'sec:signature', + signatureAlgorithm: 'sec:signingAlgorithm', + signatureValue: 'sec:signatureValue', + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/security_v2.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/security_v2.ts new file mode 100644 index 0000000000..5da116cae1 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/security_v2.ts @@ -0,0 +1,90 @@ +export const SECURITY_V2 = { + '@context': [ + { '@version': 1.1 }, + 'https://w3id.org/security/v1', + { + AesKeyWrappingKey2019: 'sec:AesKeyWrappingKey2019', + DeleteKeyOperation: 'sec:DeleteKeyOperation', + DeriveSecretOperation: 'sec:DeriveSecretOperation', + EcdsaSecp256k1Signature2019: 'sec:EcdsaSecp256k1Signature2019', + EcdsaSecp256r1Signature2019: 'sec:EcdsaSecp256r1Signature2019', + EcdsaSecp256k1VerificationKey2019: 'sec:EcdsaSecp256k1VerificationKey2019', + EcdsaSecp256r1VerificationKey2019: 'sec:EcdsaSecp256r1VerificationKey2019', + Ed25519Signature2018: 'sec:Ed25519Signature2018', + Ed25519VerificationKey2018: 'sec:Ed25519VerificationKey2018', + EquihashProof2018: 'sec:EquihashProof2018', + ExportKeyOperation: 'sec:ExportKeyOperation', + GenerateKeyOperation: 'sec:GenerateKeyOperation', + KmsOperation: 'sec:KmsOperation', + RevokeKeyOperation: 'sec:RevokeKeyOperation', + RsaSignature2018: 'sec:RsaSignature2018', + RsaVerificationKey2018: 'sec:RsaVerificationKey2018', + Sha256HmacKey2019: 'sec:Sha256HmacKey2019', + SignOperation: 'sec:SignOperation', + UnwrapKeyOperation: 'sec:UnwrapKeyOperation', + VerifyOperation: 'sec:VerifyOperation', + WrapKeyOperation: 'sec:WrapKeyOperation', + X25519KeyAgreementKey2019: 'sec:X25519KeyAgreementKey2019', + allowedAction: 'sec:allowedAction', + assertionMethod: { + '@id': 'sec:assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'sec:authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capability: { '@id': 'sec:capability', '@type': '@id' }, + capabilityAction: 'sec:capabilityAction', + capabilityChain: { + '@id': 'sec:capabilityChain', + '@type': '@id', + '@container': '@list', + }, + capabilityDelegation: { + '@id': 'sec:capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'sec:capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + caveat: { '@id': 'sec:caveat', '@type': '@id', '@container': '@set' }, + challenge: 'sec:challenge', + ciphertext: 'sec:ciphertext', + controller: { '@id': 'sec:controller', '@type': '@id' }, + delegator: { '@id': 'sec:delegator', '@type': '@id' }, + equihashParameterK: { + '@id': 'sec:equihashParameterK', + '@type': 'xsd:integer', + }, + equihashParameterN: { + '@id': 'sec:equihashParameterN', + '@type': 'xsd:integer', + }, + invocationTarget: { '@id': 'sec:invocationTarget', '@type': '@id' }, + invoker: { '@id': 'sec:invoker', '@type': '@id' }, + jws: 'sec:jws', + keyAgreement: { + '@id': 'sec:keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + kmsModule: { '@id': 'sec:kmsModule' }, + parentCapability: { '@id': 'sec:parentCapability', '@type': '@id' }, + plaintext: 'sec:plaintext', + proof: { '@id': 'sec:proof', '@type': '@id', '@container': '@graph' }, + proofPurpose: { '@id': 'sec:proofPurpose', '@type': '@vocab' }, + proofValue: 'sec:proofValue', + referenceId: 'sec:referenceId', + unwrappedKey: 'sec:unwrappedKey', + verificationMethod: { '@id': 'sec:verificationMethod', '@type': '@id' }, + verifyData: 'sec:verifyData', + wrappedKey: 'sec:wrappedKey', + }, + ], +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/submission.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/submission.ts new file mode 100644 index 0000000000..4df5ca9b4f --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/submission.ts @@ -0,0 +1,15 @@ +export const PRESENTATION_SUBMISSION = { + '@context': { + '@version': 1.1, + PresentationSubmission: { + '@id': 'https://identity.foundation/presentation-exchange/#presentation-submission', + '@context': { + '@version': 1.1, + presentation_submission: { + '@id': 'https://identity.foundation/presentation-exchange/#presentation-submission', + '@type': '@json', + }, + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/vc_revocation_list_2020.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/vc_revocation_list_2020.ts new file mode 100644 index 0000000000..d0646eaa25 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/vc_revocation_list_2020.ts @@ -0,0 +1,37 @@ +export const VC_REVOCATION_LIST_2020 = { + '@context': { + '@protected': true, + RevocationList2020Credential: { + '@id': 'https://w3id.org/vc-revocation-list-2020#RevocationList2020Credential', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + name: 'http://schema.org/name', + }, + }, + RevocationList2020: { + '@id': 'https://w3id.org/vc-revocation-list-2020#RevocationList2020', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + encodedList: 'https://w3id.org/vc-revocation-list-2020#encodedList', + }, + }, + RevocationList2020Status: { + '@id': 'https://w3id.org/vc-revocation-list-2020#RevocationList2020Status', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + revocationListCredential: { + '@id': 'https://w3id.org/vc-revocation-list-2020#revocationListCredential', + '@type': '@id', + }, + revocationListIndex: 'https://w3id.org/vc-revocation-list-2020#revocationListIndex', + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/documentLoader.ts b/packages/core/src/modules/vc/data-integrity/libraries/documentLoader.ts new file mode 100644 index 0000000000..3ae28c94e9 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/documentLoader.ts @@ -0,0 +1,70 @@ +import type { DocumentLoader } from './jsonld' +import type { AgentContext } from '../../../../agent/context/AgentContext' + +import { CredoError } from '../../../../error/CredoError' +import { isDid } from '../../../../utils' +import { DidResolverService } from '../../../dids' + +import { DEFAULT_CONTEXTS } from './contexts' +import jsonld from './jsonld' +import { getNativeDocumentLoader } from './nativeDocumentLoader' + +export type DocumentLoaderWithContext = (agentContext: AgentContext) => DocumentLoader + +export function defaultDocumentLoader(agentContext: AgentContext): DocumentLoader { + const didResolver = agentContext.dependencyManager.resolve(DidResolverService) + + async function loader(url: string) { + // Check if in the default contexts shipped with Credo + if (url in DEFAULT_CONTEXTS) { + return { + contextUrl: null, + documentUrl: url, + document: DEFAULT_CONTEXTS[url as keyof typeof DEFAULT_CONTEXTS], + } + } + + const withoutFragment = url.split('#')[0] + if (withoutFragment in DEFAULT_CONTEXTS) { + return { + contextUrl: null, + documentUrl: url, + document: DEFAULT_CONTEXTS[url as keyof typeof DEFAULT_CONTEXTS], + } + } + + if (isDid(url)) { + const result = await didResolver.resolve(agentContext, url) + + if (result.didResolutionMetadata.error || !result.didDocument) { + throw new CredoError(`Unable to resolve DID: ${url}`) + } + + const framed = await jsonld.frame( + result.didDocument.toJSON(), + { + '@context': result.didDocument.context, + '@embed': '@never', + id: url, + }, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + { documentLoader: this } + ) + + return { + contextUrl: null, + documentUrl: url, + document: framed, + } + } + + // fetches the documentLoader from documentLoader.ts or documentLoader.native.ts depending on the platform at bundle time + const platformLoader = getNativeDocumentLoader() + const nativeLoader = platformLoader.apply(jsonld, []) + + return await nativeLoader(url) + } + + return loader.bind(loader) +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/index.ts b/packages/core/src/modules/vc/data-integrity/libraries/index.ts new file mode 100644 index 0000000000..2a721334a1 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/index.ts @@ -0,0 +1,20 @@ +import * as jsonld from './jsonld' +import * as jsonldSignatures from './jsonld-signatures' +import * as vc from './vc' + +// Temporary re-export of vc libraries. As the libraries don't +// have types, it's inconvenient to import them from non-core packages +// as we would have to re-add the types. We re-export these libraries, +// so they can be imported by other packages. In the future we should look +// at proper types for these libraries so we don't have to re-export them. +export const vcLibraries = { + jsonldSignatures, + jsonld: { + ...jsonld, + ...jsonld.default, + }, + vc: { + ...vc, + ...vc.default, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/jsonld-signatures.ts b/packages/core/src/modules/vc/data-integrity/libraries/jsonld-signatures.ts new file mode 100644 index 0000000000..2ef49a3aa9 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/jsonld-signatures.ts @@ -0,0 +1,27 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { + suites as JsonLdSuites, + purposes as JsonLdPurposes, + constants as JsonLdConstants, + // No type definitions available for this library + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore +} from '@digitalcredentials/jsonld-signatures' + +export interface Suites { + LinkedDataSignature: any + LinkedDataProof: any +} + +export interface Purposes { + AssertionProofPurpose: any +} + +type Constants = any + +export const suites = JsonLdSuites as Suites + +export const purposes = JsonLdPurposes as Purposes + +export const constants = JsonLdConstants as Constants diff --git a/packages/core/src/modules/vc/data-integrity/libraries/jsonld.ts b/packages/core/src/modules/vc/data-integrity/libraries/jsonld.ts new file mode 100644 index 0000000000..a00f793f07 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/jsonld.ts @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +// No type definitions available for this library +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore +import jsonld from '@digitalcredentials/jsonld' + +export interface JsonLd { + compact(document: any, context: any, options?: any): any + fromRDF(document: any): any + frame(document: any, revealDocument: any, options?: any): any + canonize(document: any, options?: any): any + expand(document: any, options?: any): any + getValues(document: any, key: string): any + addValue(document: any, key: string, value: any): void +} + +export interface DocumentLoaderResult { + contextUrl?: string | null + documentUrl: string + document: Record +} + +export type DocumentLoader = (url: string) => Promise + +export default jsonld as unknown as JsonLd diff --git a/packages/core/src/modules/vc/data-integrity/libraries/nativeDocumentLoader.native.ts b/packages/core/src/modules/vc/data-integrity/libraries/nativeDocumentLoader.native.ts new file mode 100644 index 0000000000..79b502872f --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/nativeDocumentLoader.native.ts @@ -0,0 +1,8 @@ +import type { DocumentLoader } from './jsonld' + +export function getNativeDocumentLoader(): () => DocumentLoader { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const loader = require('@digitalcredentials/jsonld/lib/documentLoaders/xhr') + + return loader as () => DocumentLoader +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/nativeDocumentLoader.ts b/packages/core/src/modules/vc/data-integrity/libraries/nativeDocumentLoader.ts new file mode 100644 index 0000000000..9ad2e61701 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/nativeDocumentLoader.ts @@ -0,0 +1,8 @@ +import type { DocumentLoader } from './jsonld' + +export function getNativeDocumentLoader(): () => DocumentLoader { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const loader = require('@digitalcredentials/jsonld/lib/documentLoaders/node') + + return loader as () => DocumentLoader +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/vc.ts b/packages/core/src/modules/vc/data-integrity/libraries/vc.ts new file mode 100644 index 0000000000..fbaad9b3ad --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/vc.ts @@ -0,0 +1,42 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { JsonObject } from '../../../../types' + +// No type definitions available for this package +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import vc from '@digitalcredentials/vc' + +export interface VC { + issue(options: any): Promise> + verifyCredential(options: any): Promise + createPresentation(options: any): Promise> + signPresentation(options: any): Promise> + verify(options: any): Promise +} + +interface W3cVerificationResult { + isValid: boolean + + error?: Error + + verificationMethod?: JsonObject + proof?: JsonObject + purposeResult?: JsonObject +} + +export interface W3cVerifyCredentialResult { + verified: boolean + error?: Error + results: W3cVerificationResult[] +} + +export interface W3cVerifyPresentationResult { + verified: boolean + error?: Error + + presentationResult: W3cVerificationResult + credentialResults: W3cVerifyCredentialResult[] +} + +export default vc as unknown as VC diff --git a/packages/core/src/modules/vc/data-integrity/models/DataIntegrityProof.ts b/packages/core/src/modules/vc/data-integrity/models/DataIntegrityProof.ts new file mode 100644 index 0000000000..c4c3a5fd36 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/models/DataIntegrityProof.ts @@ -0,0 +1,82 @@ +import { IsEnum, IsOptional, IsString } from 'class-validator' + +import { IsUri } from '../../../../utils' + +export interface DataIntegrityProofOptions { + type: string + cryptosuite: string + verificationMethod: string + proofPurpose: string + domain?: string + challenge?: string + nonce?: string + created?: string + expires?: string + proofValue?: string + previousProof?: string +} + +/** + * Linked Data Proof + * @see https://w3c.github.io/vc-data-model/#proofs-signatures + * + * @class LinkedDataProof + */ +export class DataIntegrityProof { + public constructor(options: DataIntegrityProofOptions) { + if (options) { + this.type = options.type + this.cryptosuite = options.cryptosuite + this.verificationMethod = options.verificationMethod + this.proofPurpose = options.proofPurpose + this.domain = options.domain + this.challenge = options.challenge + this.nonce = options.nonce + this.created = options.created + this.expires = options.expires + this.proofValue = options.proofValue + this.previousProof = options.previousProof + } + } + + @IsString() + @IsEnum(['DataIntegrityProof']) + public type!: string + + @IsString() + public cryptosuite!: string + + @IsString() + public proofPurpose!: string + + @IsString() + public verificationMethod!: string + + @IsUri() + @IsOptional() + public domain?: string + + @IsString() + @IsOptional() + public challenge?: string + + @IsString() + @IsOptional() + public nonce?: string + + @IsString() + @IsOptional() + public created?: string + + @IsString() + @IsOptional() + public expires?: string + + @IsString() + @IsOptional() + public proofValue?: string + + @IsString() + @IsOptional() + public previousProof?: string +} diff --git a/packages/core/src/modules/vc/data-integrity/models/GetProofsOptions.ts b/packages/core/src/modules/vc/data-integrity/models/GetProofsOptions.ts new file mode 100644 index 0000000000..76e9dfccb5 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/models/GetProofsOptions.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { JsonObject } from '../../../../types' +import type { DocumentLoader } from '../jsonldUtil' + +/** + * Options for getting a proof from a JSON-LD document + */ +export interface GetProofsOptions { + /** + * The JSON-LD document to extract the proofs from. + */ + readonly document: JsonObject + /** + * Optional the proof type(s) to filter the returned proofs by + */ + readonly proofType?: string | readonly string[] + /** + * Optional custom document loader + */ + documentLoader?(): DocumentLoader + /** + * Optional property to indicate whether to skip compacting the resulting proof + */ + readonly skipProofCompaction?: boolean +} diff --git a/packages/core/src/modules/vc/data-integrity/models/GetProofsResult.ts b/packages/core/src/modules/vc/data-integrity/models/GetProofsResult.ts new file mode 100644 index 0000000000..787b0943cd --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/models/GetProofsResult.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { JsonObject, JsonArray } from '../../../../types' + +/** + * Result for getting proofs from a JSON-LD document + */ +export interface GetProofsResult { + /** + * The JSON-LD document with the linked data proofs removed. + */ + document: JsonObject + /** + * The list of proofs that matched the requested type. + */ + proofs: JsonArray +} diff --git a/packages/core/src/modules/vc/data-integrity/models/GetTypeOptions.ts b/packages/core/src/modules/vc/data-integrity/models/GetTypeOptions.ts new file mode 100644 index 0000000000..da40540e38 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/models/GetTypeOptions.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2020 - MATTR Limited + * 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. + */ + +import type { DocumentLoader } from '../jsonldUtil' + +/** + * Options for getting the type from a JSON-LD document + */ +export interface GetTypeOptions { + /** + * Optional custom document loader + */ + documentLoader?: DocumentLoader +} diff --git a/packages/core/src/modules/vc/data-integrity/models/IAnonCredsDataIntegrityService.ts b/packages/core/src/modules/vc/data-integrity/models/IAnonCredsDataIntegrityService.ts new file mode 100644 index 0000000000..fe1ce40f11 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/models/IAnonCredsDataIntegrityService.ts @@ -0,0 +1,41 @@ +import type { W3cJsonLdVerifiablePresentation } from './W3cJsonLdVerifiablePresentation' +import type { AgentContext } from '../../../../agent' +import type { + DifPresentationExchangeDefinition, + DifPresentationExchangeSubmission, +} from '../../../dif-presentation-exchange' +import type { W3cPresentation } from '../../models' +import type { W3cCredentialRecord } from '../../repository' + +export const ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE = 'anoncreds-2023' as const + +export interface AnoncredsDataIntegrityCreatePresentation { + selectedCredentialRecords: W3cCredentialRecord[] + presentationDefinition: DifPresentationExchangeDefinition + presentationSubmission: DifPresentationExchangeSubmission + challenge: string +} + +export interface AnoncredsDataIntegrityVerifyPresentation { + presentation: W3cJsonLdVerifiablePresentation + presentationDefinition: DifPresentationExchangeDefinition + presentationSubmission: DifPresentationExchangeSubmission + challenge: string +} + +export const AnonCredsDataIntegrityServiceSymbol = Symbol('AnonCredsDataIntegrityService') + +/** + * We keep this standalone and don't integrity it + * with for example the SignatureSuiteRegistry due + * to it's unique properties, in order to not pollute, + * the existing api's. + */ +export interface IAnonCredsDataIntegrityService { + createPresentation( + agentContext: AgentContext, + options: AnoncredsDataIntegrityCreatePresentation + ): Promise + + verifyPresentation(agentContext: AgentContext, options: AnoncredsDataIntegrityVerifyPresentation): Promise +} diff --git a/packages/core/src/modules/vc/data-integrity/models/LdKeyPair.ts b/packages/core/src/modules/vc/data-integrity/models/LdKeyPair.ts new file mode 100644 index 0000000000..080acf5611 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/models/LdKeyPair.ts @@ -0,0 +1,51 @@ +import type { VerificationMethod } from '../../../dids' + +export interface LdKeyPairOptions { + id: string + controller: string +} + +export abstract class LdKeyPair { + public readonly id: string + public readonly controller: string + public abstract type: string + + public constructor(options: LdKeyPairOptions) { + this.id = options.id + this.controller = options.controller + } + + public static async generate(): Promise { + throw new Error('Not implemented') + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public static async from(verificationMethod: VerificationMethod): Promise { + throw new Error('Abstract method from() must be implemented in subclass.') + } + + public export(publicKey = false, privateKey = false) { + if (!publicKey && !privateKey) { + throw new Error('Export requires specifying either "publicKey" or "privateKey".') + } + const key = { + id: this.id, + type: this.type, + controller: this.controller, + } + + return key + } + + public abstract fingerprint(): string + + public abstract verifyFingerprint(fingerprint: string): boolean + + public abstract signer(): { + sign: (data: { data: Uint8Array | Uint8Array[] }) => Promise> + } + + public abstract verifier(): { + verify: (data: { data: Uint8Array | Uint8Array[]; signature: Uint8Array }) => Promise + } +} diff --git a/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts b/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts new file mode 100644 index 0000000000..3cbf05a485 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts @@ -0,0 +1,70 @@ +import { IsOptional, IsString } from 'class-validator' + +import { IsUri } from '../../../../utils' + +export interface LinkedDataProofOptions { + type: string + proofPurpose: string + verificationMethod: string + created: string + domain?: string + challenge?: string + jws?: string + proofValue?: string + nonce?: string + cryptosuite?: never +} + +/** + * Linked Data Proof + * @see https://w3c.github.io/vc-data-model/#proofs-signatures + * + * @class LinkedDataProof + */ +export class LinkedDataProof { + public constructor(options: LinkedDataProofOptions) { + if (options) { + this.type = options.type + this.proofPurpose = options.proofPurpose + this.verificationMethod = options.verificationMethod + this.created = options.created + this.domain = options.domain + this.challenge = options.challenge + this.jws = options.jws + this.proofValue = options.proofValue + this.nonce = options.nonce + } + } + + @IsString() + public type!: string + + @IsString() + public proofPurpose!: string + + @IsString() + public verificationMethod!: string + + @IsString() + public created!: string + + @IsUri() + @IsOptional() + public domain?: string + + @IsString() + @IsOptional() + public challenge?: string + + @IsString() + @IsOptional() + public jws?: string + + @IsString() + @IsOptional() + public proofValue?: string + + @IsString() + @IsOptional() + public nonce?: string +} diff --git a/packages/core/src/modules/vc/data-integrity/models/ProofTransformer.ts b/packages/core/src/modules/vc/data-integrity/models/ProofTransformer.ts new file mode 100644 index 0000000000..d89f04f4d8 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/models/ProofTransformer.ts @@ -0,0 +1,38 @@ +import type { DataIntegrityProofOptions } from './DataIntegrityProof' +import type { LinkedDataProofOptions } from './LinkedDataProof' +import type { SingleOrArray } from '../../../../utils' + +import { Transform, TransformationType, instanceToPlain, plainToInstance } from 'class-transformer' + +import { DataIntegrityProof } from './DataIntegrityProof' +import { LinkedDataProof } from './LinkedDataProof' + +export function ProofTransformer() { + return Transform( + ({ + value, + type, + }: { + value: SingleOrArray + type: TransformationType + }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + const plainOptionsToClass = (v: LinkedDataProofOptions | DataIntegrityProofOptions) => { + if ('cryptosuite' in v) { + return plainToInstance(DataIntegrityProof, v) + } else { + return plainToInstance(LinkedDataProof, v) + } + } + + if (Array.isArray(value)) return value.map(plainOptionsToClass) + return plainOptionsToClass(value) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + if (Array.isArray(value)) return value.map((v) => instanceToPlain(v)) + return instanceToPlain(value) + } + // PLAIN_TO_PLAIN + return value + } + ) +} diff --git a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts new file mode 100644 index 0000000000..c78d43a9d1 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts @@ -0,0 +1,80 @@ +import type { DataIntegrityProofOptions } from './DataIntegrityProof' +import type { LinkedDataProofOptions } from './LinkedDataProof' +import type { W3cCredentialOptions } from '../../models/credential/W3cCredential' +import type { W3cJsonCredential } from '../../models/credential/W3cJsonCredential' + +import { ValidateNested } from 'class-validator' + +import { + IsInstanceOrArrayOfInstances, + SingleOrArray, + asArray, + mapSingleOrArray, + JsonTransformer, +} from '../../../../utils' +import { ClaimFormat } from '../../models/ClaimFormat' +import { W3cCredential } from '../../models/credential/W3cCredential' + +import { DataIntegrityProof } from './DataIntegrityProof' +import { LinkedDataProof } from './LinkedDataProof' +import { ProofTransformer } from './ProofTransformer' + +export interface W3cJsonLdVerifiableCredentialOptions extends W3cCredentialOptions { + proof: SingleOrArray +} + +export class W3cJsonLdVerifiableCredential extends W3cCredential { + public constructor(options: W3cJsonLdVerifiableCredentialOptions) { + super(options) + if (options) { + this.proof = mapSingleOrArray(options.proof, (proof) => { + if (proof.cryptosuite) return new DataIntegrityProof(proof) + else return new LinkedDataProof(proof as LinkedDataProofOptions) + }) + } + } + + @ProofTransformer() + @IsInstanceOrArrayOfInstances({ classType: [LinkedDataProof, DataIntegrityProof] }) + @ValidateNested() + public proof!: SingleOrArray + + public get proofTypes(): Array { + const proofArray = asArray(this.proof) ?? [] + return proofArray.map((proof) => proof.type) + } + + public get dataIntegrityCryptosuites(): Array { + const proofArray = asArray(this.proof) ?? [] + return proofArray + .filter((proof): proof is DataIntegrityProof => proof.type === 'DataIntegrityProof' && 'cryptosuite' in proof) + .map((proof) => proof.cryptosuite) + } + + public toJson() { + return JsonTransformer.toJSON(this) + } + + public static fromJson(json: Record) { + return JsonTransformer.fromJSON(json, W3cJsonLdVerifiableCredential) + } + + /** + * The {@link ClaimFormat} of the credential. For JSON-LD credentials this is always `ldp_vc`. + */ + public get claimFormat(): ClaimFormat.LdpVc { + return ClaimFormat.LdpVc + } + + /** + * Get the encoded variant of the W3C Verifiable Credential. For JSON-LD credentials this is + * a JSON object. + */ + public get encoded() { + return this.toJson() + } + + public get jsonCredential(): W3cJsonCredential { + return this.toJson() as W3cJsonCredential + } +} diff --git a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiablePresentation.ts b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiablePresentation.ts new file mode 100644 index 0000000000..f559cde534 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiablePresentation.ts @@ -0,0 +1,60 @@ +import type { DataIntegrityProofOptions } from './DataIntegrityProof' +import type { LinkedDataProofOptions } from './LinkedDataProof' +import type { W3cPresentationOptions } from '../../models/presentation/W3cPresentation' + +import { SingleOrArray, IsInstanceOrArrayOfInstances, JsonTransformer, asArray } from '../../../../utils' +import { ClaimFormat } from '../../models' +import { W3cPresentation } from '../../models/presentation/W3cPresentation' + +import { DataIntegrityProof } from './DataIntegrityProof' +import { LinkedDataProof } from './LinkedDataProof' +import { ProofTransformer } from './ProofTransformer' + +export interface W3cJsonLdVerifiablePresentationOptions extends W3cPresentationOptions { + proof: LinkedDataProofOptions | DataIntegrityProofOptions +} + +export class W3cJsonLdVerifiablePresentation extends W3cPresentation { + public constructor(options: W3cJsonLdVerifiablePresentationOptions) { + super(options) + if (options) { + if (options.proof.cryptosuite) this.proof = new DataIntegrityProof(options.proof) + else this.proof = new LinkedDataProof(options.proof as LinkedDataProofOptions) + } + } + + @ProofTransformer() + @IsInstanceOrArrayOfInstances({ classType: [LinkedDataProof, DataIntegrityProof] }) + public proof!: SingleOrArray + + public get proofTypes(): Array { + const proofArray = asArray(this.proof) ?? [] + return proofArray.map((proof) => proof.type) + } + + public get dataIntegrityCryptosuites(): Array { + const proofArray = asArray(this.proof) ?? [] + return proofArray + .filter((proof): proof is DataIntegrityProof => proof.type === 'DataIntegrityProof' && 'cryptosuite' in proof) + .map((proof) => proof.cryptosuite) + } + + public toJson() { + return JsonTransformer.toJSON(this) + } + + /** + * The {@link ClaimFormat} of the presentation. For JSON-LD credentials this is always `ldp_vp`. + */ + public get claimFormat(): ClaimFormat.LdpVp { + return ClaimFormat.LdpVp + } + + /** + * Get the encoded variant of the W3C Verifiable Presentation. For JSON-LD presentations this is + * a JSON object. + */ + public get encoded() { + return this.toJson() + } +} diff --git a/packages/core/src/modules/vc/data-integrity/models/index.ts b/packages/core/src/modules/vc/data-integrity/models/index.ts new file mode 100644 index 0000000000..d4d4c76909 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/models/index.ts @@ -0,0 +1,5 @@ +export * from './W3cJsonLdVerifiableCredential' +export * from './W3cJsonLdVerifiablePresentation' +export * from './LdKeyPair' +export * from './IAnonCredsDataIntegrityService' +export * from './DataIntegrityProof' diff --git a/packages/core/src/modules/vc/data-integrity/proof-purposes/CredentialIssuancePurpose.ts b/packages/core/src/modules/vc/data-integrity/proof-purposes/CredentialIssuancePurpose.ts new file mode 100644 index 0000000000..3f79fe92b7 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/proof-purposes/CredentialIssuancePurpose.ts @@ -0,0 +1,86 @@ +import type { JsonObject } from '../../../../types' +import type { Proof, DocumentLoader } from '../jsonldUtil' + +import { suites, purposes } from '../libraries/jsonld-signatures' + +const AssertionProofPurpose = purposes.AssertionProofPurpose +const LinkedDataProof = suites.LinkedDataProof +/** + * Creates a proof purpose that will validate whether or not the verification + * method in a proof was authorized by its declared controller for the + * proof's purpose. + */ +export class CredentialIssuancePurpose extends AssertionProofPurpose { + /** + * @param {object} options - The options to use. + * @param {object} [options.controller] - The description of the controller, + * if it is not to be dereferenced via a `documentLoader`. + * @param {string|Date|number} [options.date] - The expected date for + * the creation of the proof. + * @param {number} [options.maxTimestampDelta=Infinity] - A maximum number + * of seconds that the date on the signature can deviate from. + */ + public constructor(options: { controller?: Record; date: string; maxTimestampDelta?: number }) { + options.maxTimestampDelta = options.maxTimestampDelta || Infinity + super(options) + } + + /** + * Validates the purpose of a proof. This method is called during + * proof verification, after the proof value has been checked against the + * given verification method (in the case of a digital signature, the + * signature has been cryptographically verified against the public key). + * + * @param {object} proof - The proof to validate. + * @param {object} options - The options to use. + * @param {object} options.document - The document whose signature is + * being verified. + * @param {object} options.suite - Signature suite used in + * the proof. + * @param {string} options.verificationMethod - Key id URL to the paired + * public key. + * @param {object} [options.documentLoader] - A document loader. + * + * @throws {Error} If verification method not authorized by controller. + * @throws {Error} If proof's created timestamp is out of range. + * + * @returns {Promise<{valid: boolean, error: Error}>} Resolves on completion. + */ + public async validate( + proof: Proof, + options?: { + document: JsonObject + suite: typeof LinkedDataProof + verificationMethod: string + documentLoader?: DocumentLoader + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise<{ valid: boolean; error?: any }> { + try { + const result = await super.validate(proof, options) + + if (!result.valid) { + throw result.error + } + + // This @ts-ignore is necessary because the .getValues() method is not part of the public API. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + const issuer = jsonld.util.getValues(options.document, 'issuer') + + if (!issuer || issuer.length === 0) { + throw new Error('Credential issuer is required.') + } + + const issuerId = typeof issuer[0] === 'string' ? issuer[0] : issuer[0].id + + if (result.controller.id !== issuerId) { + throw new Error('Credential issuer must match the verification method controller.') + } + + return { valid: true } + } catch (error) { + return { valid: false, error } + } + } +} diff --git a/packages/core/src/modules/vc/data-integrity/proof-purposes/ProofPurpose.ts b/packages/core/src/modules/vc/data-integrity/proof-purposes/ProofPurpose.ts new file mode 100644 index 0000000000..af04ec9f41 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/proof-purposes/ProofPurpose.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ProofPurpose = any diff --git a/packages/core/src/modules/vc/data-integrity/proof-purposes/index.ts b/packages/core/src/modules/vc/data-integrity/proof-purposes/index.ts new file mode 100644 index 0000000000..d224af2583 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/proof-purposes/index.ts @@ -0,0 +1,2 @@ +export * from './CredentialIssuancePurpose' +export * from './ProofPurpose' diff --git a/packages/core/src/modules/vc/data-integrity/signature-suites/JwsLinkedDataSignature.ts b/packages/core/src/modules/vc/data-integrity/signature-suites/JwsLinkedDataSignature.ts new file mode 100644 index 0000000000..a47c238cc2 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/signature-suites/JwsLinkedDataSignature.ts @@ -0,0 +1,261 @@ +/*! + * Copyright (c) 2020-2021 Digital Bazaar, Inc. All rights reserved. + */ +import type { DocumentLoader, Proof, VerificationMethod } from '../jsonldUtil' +import type { LdKeyPair } from '../models/LdKeyPair' + +import { CredoError } from '../../../../error' +import { TypedArrayEncoder, JsonEncoder } from '../../../../utils' +import { suites } from '../libraries/jsonld-signatures' + +const LinkedDataSignature = suites.LinkedDataSignature +export interface JwsLinkedDataSignatureOptions { + type: string + algorithm: string + LDKeyClass: typeof LdKeyPair + key?: LdKeyPair + proof: Proof + date: string + contextUrl: string + useNativeCanonize: boolean +} + +export class JwsLinkedDataSignature extends LinkedDataSignature { + /** + * @param options - Options hashmap. + * @param options.type - Provided by subclass. + * @param options.alg - JWS alg provided by subclass. + * @param [options.LDKeyClass] - Provided by subclass or subclass + * overrides `getVerificationMethod`. + * + * Either a `key` OR at least one of `signer`/`verifier` is required. + * + * @param [options.key] - An optional key object (containing an + * `id` property, and either `signer` or `verifier`, depending on the + * intended operation. Useful for when the application is managing keys + * itself (when using a KMS, you never have access to the private key, + * and so should use the `signer` param instead). + * + * Advanced optional parameters and overrides. + * + * @param [options.proof] - A JSON-LD document with options to use + * for the `proof` node. Any other custom fields can be provided here + * using a context different from `security-v2`. + * @param [options.date] - Signing date to use if not passed. + * @param options.contextUrl - JSON-LD context url that corresponds + * to this signature suite. Used for enforcing suite context during the + * `sign()` operation. + * @param [options.useNativeCanonize] - Whether to use a native + * canonize algorithm. + */ + public constructor(options: JwsLinkedDataSignatureOptions) { + super({ + type: options.type, + LDKeyClass: options.LDKeyClass, + contextUrl: options.contextUrl, + key: options.key, + signer: undefined, + verifier: undefined, + proof: options.proof, + date: options.date, + useNativeCanonize: options.useNativeCanonize, + }) + this.alg = options.algorithm + } + + /** + * @param options - Options hashmap. + * @param options.verifyData - The data to sign. + * @param options.proof - A JSON-LD document with options to use + * for the `proof` node. Any other custom fields can be provided here + * using a context different from `security-v2`. + * + * @returns The proof containing the signature value. + */ + public async sign(options: { verifyData: Uint8Array; proof: Proof }) { + if (!(this.signer && typeof this.signer.sign === 'function')) { + throw new Error('A signer API has not been specified.') + } + // JWS header + const header = { + alg: this.alg, + b64: false, + crit: ['b64'], + } + + /* + +-------+-----------------------------------------------------------+ + | "b64" | JWS Signing Input Formula | + +-------+-----------------------------------------------------------+ + | true | ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || | + | | BASE64URL(JWS Payload)) | + | | | + | false | ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.') || | + | | JWS Payload | + +-------+-----------------------------------------------------------+ + */ + + // create JWS data and sign + const encodedHeader = JsonEncoder.toBase64URL(header) + + const data = _createJws({ encodedHeader, verifyData: options.verifyData }) + + const signature = await this.signer.sign({ data }) + + // create detached content signature + const encodedSignature = TypedArrayEncoder.toBase64URL(signature) + options.proof.jws = encodedHeader + '..' + encodedSignature + return options.proof + } + + /** + * @param options - Options hashmap. + * @param options.verifyData - The data to verify. + * @param options.verificationMethod - A verification method. + * @param options.proof - The proof to be verified. + * + * @returns Resolves with the verification result. + */ + public async verifySignature(options: { + verifyData: Uint8Array + verificationMethod: VerificationMethod + proof: Proof + }) { + if (!(options.proof.jws && typeof options.proof.jws === 'string' && options.proof.jws.includes('.'))) { + throw new TypeError('The proof does not include a valid "jws" property.') + } + // add payload into detached content signature + const [encodedHeader /*payload*/, , encodedSignature] = options.proof.jws.split('.') + + let header + try { + header = JsonEncoder.fromBase64(encodedHeader) + } catch (e) { + throw new Error('Could not parse JWS header; ' + e) + } + if (!(header && typeof header === 'object')) { + throw new Error('Invalid JWS header.') + } + + // confirm header matches all expectations + if ( + !( + header.alg === this.alg && + header.b64 === false && + Array.isArray(header.crit) && + header.crit.length === 1 && + header.crit[0] === 'b64' + ) && + Object.keys(header).length === 3 + ) { + throw new Error(`Invalid JWS header parameters for ${this.type}.`) + } + + // do signature verification + const signature = TypedArrayEncoder.fromBase64(encodedSignature) + + const data = _createJws({ encodedHeader, verifyData: options.verifyData }) + + let { verifier } = this + if (!verifier) { + const key = await this.LDKeyClass.from(options.verificationMethod) + verifier = key.verifier() + } + return verifier.verify({ data, signature }) + } + + public async getVerificationMethod(options: { proof: Proof; documentLoader?: DocumentLoader }) { + if (this.key) { + // This happens most often during sign() operations. For verify(), + // the expectation is that the verification method will be fetched + // by the documentLoader (below), not provided as a `key` parameter. + return this.key.export({ publicKey: true }) + } + + let { verificationMethod } = options.proof + + if (typeof verificationMethod === 'object' && verificationMethod !== null) { + verificationMethod = verificationMethod.id + } + + if (!verificationMethod) { + throw new Error('No "verificationMethod" found in proof.') + } + + if (!options.documentLoader) { + throw new CredoError('Missing custom document loader. This is required for resolving verification methods.') + } + + const { document } = await options.documentLoader(verificationMethod) + + verificationMethod = typeof document === 'string' ? JSON.parse(document) : document + + await this.assertVerificationMethod(verificationMethod) + return verificationMethod + } + + /** + * Checks whether a given proof exists in the document. + * + * @param options - Options hashmap. + * @param options.proof - A proof. + * @param options.document - A JSON-LD document. + * @param options.purpose - A jsonld-signatures ProofPurpose + * instance (e.g. AssertionProofPurpose, AuthenticationProofPurpose, etc). + * @param options.documentLoader - A secure document loader (it is + * recommended to use one that provides static known documents, instead of + * fetching from the web) for returning contexts, controller documents, + * keys, and other relevant URLs needed for the proof. + * + * @returns Whether a match for the proof was found. + */ + public async matchProof(options: { + proof: Proof + document: VerificationMethod + // eslint-disable-next-line @typescript-eslint/no-explicit-any + purpose: any + documentLoader?: DocumentLoader + }) { + const proofMatches = await super.matchProof({ + proof: options.proof, + document: options.document, + purpose: options.purpose, + documentLoader: options.documentLoader, + }) + if (!proofMatches) { + return false + } + // NOTE: When subclassing this suite: Extending suites will need to check + + if (!this.key) { + // no key specified, so assume this suite matches and it can be retrieved + return true + } + + const { verificationMethod } = options.proof + + // only match if the key specified matches the one in the proof + if (typeof verificationMethod === 'object') { + return verificationMethod.id === this.key.id + } + return verificationMethod === this.key.id + } +} + +/** + * Creates the bytes ready for signing. + * + * @param {object} options - Options hashmap. + * @param {string} options.encodedHeader - A base64url encoded JWT header. + * @param {Uint8Array} options.verifyData - Payload to sign/verify. + * @returns {Uint8Array} A combined byte array for signing. + */ +function _createJws(options: { encodedHeader: string; verifyData: Uint8Array }): Uint8Array { + const encodedHeaderBytes = TypedArrayEncoder.fromString(options.encodedHeader + '.') + + // concatenate the two uint8arrays + const data = new Uint8Array(encodedHeaderBytes.length + options.verifyData.length) + data.set(encodedHeaderBytes, 0) + data.set(options.verifyData, encodedHeaderBytes.length) + return data +} diff --git a/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2018.ts b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2018.ts new file mode 100644 index 0000000000..3feed55441 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2018.ts @@ -0,0 +1,230 @@ +import type { DocumentLoader, JsonLdDoc, Proof, VerificationMethod } from '../../jsonldUtil' +import type { JwsLinkedDataSignatureOptions } from '../JwsLinkedDataSignature' + +import { MultiBaseEncoder, TypedArrayEncoder } from '../../../../../utils' +import { CREDENTIALS_CONTEXT_V1_URL, SECURITY_CONTEXT_URL } from '../../../constants' +import { _includesContext } from '../../jsonldUtil' +import jsonld from '../../libraries/jsonld' +import { JwsLinkedDataSignature } from '../JwsLinkedDataSignature' + +import { ED25519_SUITE_CONTEXT_URL_2018, ED25519_SUITE_CONTEXT_URL_2020 } from './constants' +import { ed25519Signature2018Context } from './context' + +type Ed25519Signature2018Options = Pick< + JwsLinkedDataSignatureOptions, + 'key' | 'proof' | 'date' | 'useNativeCanonize' | 'LDKeyClass' +> + +export class Ed25519Signature2018 extends JwsLinkedDataSignature { + public static CONTEXT_URL = ED25519_SUITE_CONTEXT_URL_2018 + public static CONTEXT = ed25519Signature2018Context.get(ED25519_SUITE_CONTEXT_URL_2018) + + /** + * @param {object} options - Options hashmap. + * + * Either a `key` OR at least one of `signer`/`verifier` is required. + * + * @param {object} [options.key] - An optional key object (containing an + * `id` property, and either `signer` or `verifier`, depending on the + * intended operation. Useful for when the application is managing keys + * itself (when using a KMS, you never have access to the private key, + * and so should use the `signer` param instead). + * @param {Function} [options.signer] - Signer function that returns an + * object with an async sign() method. This is useful when interfacing + * with a KMS (since you don't get access to the private key and its + * `signer()`, the KMS client gives you only the signer function to use). + * @param {Function} [options.verifier] - Verifier function that returns + * an object with an async `verify()` method. Useful when working with a + * KMS-provided verifier function. + * + * Advanced optional parameters and overrides. + * + * @param {object} [options.proof] - A JSON-LD document with options to use + * for the `proof` node. Any other custom fields can be provided here + * using a context different from security-v2). + * @param {string|Date} [options.date] - Signing date to use if not passed. + * @param {boolean} [options.useNativeCanonize] - Whether to use a native + * canonize algorithm. + */ + public constructor(options: Ed25519Signature2018Options) { + super({ + type: 'Ed25519Signature2018', + algorithm: 'EdDSA', + LDKeyClass: options.LDKeyClass, + contextUrl: ED25519_SUITE_CONTEXT_URL_2018, + key: options.key, + proof: options.proof, + date: options.date, + useNativeCanonize: options.useNativeCanonize, + }) + this.requiredKeyType = 'Ed25519VerificationKey2018' + } + + public async assertVerificationMethod(document: JsonLdDoc) { + if (!_includesCompatibleContext({ document: document })) { + // For DID Documents, since keys do not have their own contexts, + // the suite context is usually provided by the documentLoader logic + throw new TypeError( + `The '@context' of the verification method (key) MUST contain the context url "${this.contextUrl}".` + ) + } + + if (!_isEd2018Key(document) && !_isEd2020Key(document)) { + const verificationMethodType = jsonld.getValues(document, 'type')[0] + throw new Error( + `Unsupported verification method type '${verificationMethodType}'. Verification method type MUST be 'Ed25519VerificationKey2018' or 'Ed25519VerificationKey2020'.` + ) + } else if (_isEd2018Key(document) && !_includesEd2018Context(document)) { + throw new Error( + `For verification method type 'Ed25519VerificationKey2018' the '@context' MUST contain the context url "${ED25519_SUITE_CONTEXT_URL_2018}".` + ) + } else if (_isEd2020Key(document) && !_includesEd2020Context(document)) { + throw new Error( + `For verification method type 'Ed25519VerificationKey2020' the '@context' MUST contain the context url "${ED25519_SUITE_CONTEXT_URL_2020}".` + ) + } + + // ensure verification method has not been revoked + if (document.revoked !== undefined) { + throw new Error('The verification method has been revoked.') + } + } + + public async getVerificationMethod(options: { proof: Proof; documentLoader?: DocumentLoader }) { + let verificationMethod = await super.getVerificationMethod({ + proof: options.proof, + documentLoader: options.documentLoader, + }) + + // convert Ed25519VerificationKey2020 to Ed25519VerificationKey2018 + if (_isEd2020Key(verificationMethod) && _includesEd2020Context(verificationMethod)) { + // -- convert multibase to base58 -- + const publicKeyBuffer = MultiBaseEncoder.decode(verificationMethod.publicKeyMultibase) + + // -- update context -- + // remove 2020 context + const context2020Index = verificationMethod['@context'].indexOf(ED25519_SUITE_CONTEXT_URL_2020) + verificationMethod['@context'].splice(context2020Index, 1) + + // add 2018 context + verificationMethod['@context'].push(ED25519_SUITE_CONTEXT_URL_2018) + + // -- update type + verificationMethod.type = 'Ed25519VerificationKey2018' + + verificationMethod = { + ...verificationMethod, + publicKeyMultibase: undefined, + publicKeyBase58: TypedArrayEncoder.toBase58(publicKeyBuffer.data), + } + } + + return verificationMethod + } + + /** + * Ensures the document to be signed contains the required signature suite + * specific `@context`, by either adding it (if `addSuiteContext` is true), + * or throwing an error if it's missing. + * + * @override + * + * @param {object} options - Options hashmap. + * @param {object} options.document - JSON-LD document to be signed. + * @param {boolean} options.addSuiteContext - Add suite context? + */ + public ensureSuiteContext(options: { document: JsonLdDoc; addSuiteContext: boolean }) { + if (_includesCompatibleContext({ document: options.document })) { + return + } + + super.ensureSuiteContext({ document: options.document, addSuiteContext: options.addSuiteContext }) + } + + /** + * Checks whether a given proof exists in the document. + * + * @override + * + * @param {object} options - Options hashmap. + * @param {object} options.proof - A proof. + * @param {object} options.document - A JSON-LD document. + * @param {object} options.purpose - A jsonld-signatures ProofPurpose + * instance (e.g. AssertionProofPurpose, AuthenticationProofPurpose, etc). + * @param {Function} options.documentLoader - A secure document loader (it is + * recommended to use one that provides static known documents, instead of + * fetching from the web) for returning contexts, controller documents, + * keys, and other relevant URLs needed for the proof. + * + * @returns {Promise} Whether a match for the proof was found. + */ + public async matchProof(options: { + proof: Proof + document: VerificationMethod + // eslint-disable-next-line @typescript-eslint/no-explicit-any + purpose: any + documentLoader?: DocumentLoader + }) { + if (!_includesCompatibleContext({ document: options.document })) { + return false + } + return super.matchProof({ + proof: options.proof, + document: options.document, + purpose: options.purpose, + documentLoader: options.documentLoader, + }) + } +} + +function _includesCompatibleContext(options: { document: JsonLdDoc }) { + // Handle the unfortunate Ed25519Signature2018 / credentials/v1 collision + const hasEd2018 = _includesContext({ + document: options.document, + contextUrl: ED25519_SUITE_CONTEXT_URL_2018, + }) + const hasEd2020 = _includesContext({ + document: options.document, + contextUrl: ED25519_SUITE_CONTEXT_URL_2020, + }) + const hasCred = _includesContext({ document: options.document, contextUrl: CREDENTIALS_CONTEXT_V1_URL }) + const hasSecV2 = _includesContext({ document: options.document, contextUrl: SECURITY_CONTEXT_URL }) + + // TODO: the console.warn statements below should probably be replaced with logging statements. However, this would currently require injection and I'm not sure we want to do that. + if (hasEd2018 && hasCred) { + // Warn if both are present + // console.warn('Warning: The ed25519-2018/v1 and credentials/v1 ' + 'contexts are incompatible.') + // console.warn('For VCs using Ed25519Signature2018 suite,' + ' using the credentials/v1 context is sufficient.') + return false + } + + if (hasEd2018 && hasSecV2) { + // Warn if both are present + // console.warn('Warning: The ed25519-2018/v1 and security/v2 ' + 'contexts are incompatible.') + // console.warn('For VCs using Ed25519Signature2018 suite,' + ' using the security/v2 context is sufficient.') + return false + } + + // Either one by itself is fine, for this suite + return hasEd2018 || hasEd2020 || hasCred || hasSecV2 +} + +function _isEd2018Key(verificationMethod: JsonLdDoc) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - .hasValue is not part of the public API + return jsonld.hasValue(verificationMethod, 'type', 'Ed25519VerificationKey2018') +} + +function _includesEd2018Context(document: JsonLdDoc) { + return _includesContext({ document, contextUrl: ED25519_SUITE_CONTEXT_URL_2018 }) +} + +function _isEd2020Key(verificationMethod: JsonLdDoc) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - .hasValue is not part of the public API + return jsonld.hasValue(verificationMethod, 'type', 'Ed25519VerificationKey2020') +} + +function _includesEd2020Context(document: JsonLdDoc) { + return _includesContext({ document, contextUrl: ED25519_SUITE_CONTEXT_URL_2020 }) +} diff --git a/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/constants.ts b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/constants.ts new file mode 100644 index 0000000000..881a7ea3b1 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/constants.ts @@ -0,0 +1,2 @@ +export const ED25519_SUITE_CONTEXT_URL_2018 = 'https://w3id.org/security/suites/ed25519-2018/v1' +export const ED25519_SUITE_CONTEXT_URL_2020 = 'https://w3id.org/security/suites/ed25519-2020/v1' diff --git a/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/context.ts b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/context.ts new file mode 100644 index 0000000000..1f2c6af92c --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/context.ts @@ -0,0 +1,99 @@ +import { ED25519_SUITE_CONTEXT_URL_2018 } from './constants' + +export const context = { + '@context': { + id: '@id', + type: '@type', + '@protected': true, + + proof: { + '@id': 'https://w3id.org/security#proof', + '@type': '@id', + '@container': '@graph', + }, + Ed25519VerificationKey2018: { + '@id': 'https://w3id.org/security#Ed25519VerificationKey2018', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + revoked: { + '@id': 'https://w3id.org/security#revoked', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + publicKeyBase58: { + '@id': 'https://w3id.org/security#publicKeyBase58', + }, + }, + }, + Ed25519Signature2018: { + '@id': 'https://w3id.org/security#Ed25519Signature2018', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + jws: { + '@id': 'https://w3id.org/security#jws', + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + }, +} + +const ed25519Signature2018Context = new Map() +ed25519Signature2018Context.set(ED25519_SUITE_CONTEXT_URL_2018, context) + +export { ed25519Signature2018Context } diff --git a/packages/core/src/modules/vc/data-integrity/signature-suites/index.ts b/packages/core/src/modules/vc/data-integrity/signature-suites/index.ts new file mode 100644 index 0000000000..7eecb7ef25 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/signature-suites/index.ts @@ -0,0 +1,2 @@ +export * from './ed25519/Ed25519Signature2018' +export * from './JwsLinkedDataSignature' diff --git a/packages/core/src/modules/vc/index.ts b/packages/core/src/modules/vc/index.ts new file mode 100644 index 0000000000..fb59e32f95 --- /dev/null +++ b/packages/core/src/modules/vc/index.ts @@ -0,0 +1,11 @@ +export * from './W3cCredentialService' +export * from './W3cCredentialsModuleConfig' +export * from './W3cCredentialServiceOptions' +export * from './repository' +export * from './W3cCredentialsModule' +export * from './W3cCredentialsApi' +export * from './models' +export * from './data-integrity' +export * from './jwt-vc' +export * from './constants' +export { w3cDate } from './util' diff --git a/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts b/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts new file mode 100644 index 0000000000..3f4e442b59 --- /dev/null +++ b/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts @@ -0,0 +1,533 @@ +import type { AgentContext } from '../../../agent/context' +import type { VerifyJwsResult } from '../../../crypto/JwsService' +import type { DidPurpose, VerificationMethod } from '../../dids' +import type { + W3cJwtSignCredentialOptions, + W3cJwtSignPresentationOptions, + W3cJwtVerifyCredentialOptions, + W3cJwtVerifyPresentationOptions, +} from '../W3cCredentialServiceOptions' +import type { SingleValidationResult, W3cVerifyCredentialResult, W3cVerifyPresentationResult } from '../models' + +import { JwsService } from '../../../crypto' +import { getJwkFromKey, getJwkClassFromJwaSignatureAlgorithm } from '../../../crypto/jose/jwk' +import { CredoError } from '../../../error' +import { injectable } from '../../../plugins' +import { asArray, isDid, MessageValidator } from '../../../utils' +import { getKeyDidMappingByKeyType, DidResolverService, getKeyFromVerificationMethod } from '../../dids' +import { W3cJsonLdVerifiableCredential } from '../data-integrity' + +import { W3cJwtVerifiableCredential } from './W3cJwtVerifiableCredential' +import { W3cJwtVerifiablePresentation } from './W3cJwtVerifiablePresentation' +import { getJwtPayloadFromCredential } from './credentialTransformer' +import { getJwtPayloadFromPresentation } from './presentationTransformer' + +/** + * Supports signing and verification of credentials according to the [Verifiable Credential Data Model](https://www.w3.org/TR/vc-data-model) + * using [Json Web Tokens](https://www.w3.org/TR/vc-data-model/#json-web-token). + */ +@injectable() +export class W3cJwtCredentialService { + private jwsService: JwsService + + public constructor(jwsService: JwsService) { + this.jwsService = jwsService + } + + /** + * Signs a credential + */ + public async signCredential( + agentContext: AgentContext, + options: W3cJwtSignCredentialOptions + ): Promise { + // Validate the instance + MessageValidator.validateSync(options.credential) + + // Get the JWT payload for the JWT based on the JWT Encoding rules form the VC-DATA-MODEL + // https://www.w3.org/TR/vc-data-model/#jwt-encoding + const jwtPayload = getJwtPayloadFromCredential(options.credential) + + if (!isDid(options.verificationMethod)) { + throw new CredoError(`Only did identifiers are supported as verification method`) + } + + const verificationMethod = await this.resolveVerificationMethod(agentContext, options.verificationMethod, [ + 'assertionMethod', + ]) + const key = getKeyFromVerificationMethod(verificationMethod) + + const jwt = await this.jwsService.createJwsCompact(agentContext, { + payload: jwtPayload, + key, + protectedHeaderOptions: { + typ: 'JWT', + alg: options.alg, + kid: options.verificationMethod, + }, + }) + + // TODO: this re-parses and validates the credential in the JWT, which is not necessary. + // We should somehow create an instance of W3cJwtVerifiableCredential directly from the JWT + const jwtVc = W3cJwtVerifiableCredential.fromSerializedJwt(jwt) + + return jwtVc + } + + /** + * Verifies the signature(s) of a credential + * + * @param credential the credential to be verified + * @returns the verification result + */ + public async verifyCredential( + agentContext: AgentContext, + options: W3cJwtVerifyCredentialOptions + ): Promise { + // NOTE: this is mostly from the JSON-LD service that adds this option. Once we support + // the same granular validation results, we can remove this and the user could just check + // which of the validations failed. Supporting for consistency with the JSON-LD service for now. + const verifyCredentialStatus = options.verifyCredentialStatus ?? true + + const validationResults: W3cVerifyCredentialResult = { + isValid: false, + validations: {}, + } + + try { + let credential: W3cJwtVerifiableCredential + try { + // If instance is provided as input, we want to validate the credential (otherwise it's done in the fromSerializedJwt method below) + if (options.credential instanceof W3cJwtVerifiableCredential) { + MessageValidator.validateSync(options.credential.credential) + } + + credential = + options.credential instanceof W3cJwtVerifiableCredential + ? options.credential + : W3cJwtVerifiableCredential.fromSerializedJwt(options.credential) + + // Verify the JWT payload (verifies whether it's not expired, etc...) + credential.jwt.payload.validate() + + validationResults.validations.dataModel = { + isValid: true, + } + } catch (error) { + validationResults.validations.dataModel = { + isValid: false, + error, + } + + return validationResults + } + + const issuerVerificationMethod = await this.getVerificationMethodForJwtCredential(agentContext, { + credential, + purpose: ['assertionMethod'], + }) + const issuerPublicKey = getKeyFromVerificationMethod(issuerVerificationMethod) + const issuerPublicJwk = getJwkFromKey(issuerPublicKey) + + let signatureResult: VerifyJwsResult | undefined = undefined + try { + // Verify the JWS signature + signatureResult = await this.jwsService.verifyJws(agentContext, { + jws: credential.jwt.serializedJwt, + // We have pre-fetched the key based on the issuer/signer of the credential + jwkResolver: () => issuerPublicJwk, + }) + + if (!signatureResult.isValid) { + validationResults.validations.signature = { + isValid: false, + error: new CredoError('Invalid JWS signature'), + } + } else { + validationResults.validations.signature = { + isValid: true, + } + } + } catch (error) { + validationResults.validations.signature = { + isValid: false, + error, + } + } + + // Validate whether the credential is signed with the 'issuer' id + // NOTE: this uses the verificationMethod.controller. We may want to use the verificationMethod.id? + if (credential.issuerId !== issuerVerificationMethod.controller) { + validationResults.validations.issuerIsSigner = { + isValid: false, + error: new CredoError( + `Credential is signed using verification method ${issuerVerificationMethod.id}, while the issuer of the credential is '${credential.issuerId}'` + ), + } + } else { + validationResults.validations.issuerIsSigner = { + isValid: true, + } + } + + // Validate whether the `issuer` of the credential is also the signer + const issuerIsSigner = signatureResult?.signerKeys.some( + (signerKey) => signerKey.fingerprint === issuerPublicKey.fingerprint + ) + if (!issuerIsSigner) { + validationResults.validations.issuerIsSigner = { + isValid: false, + error: new CredoError('Credential is not signed by the issuer of the credential'), + } + } else { + validationResults.validations.issuerIsSigner = { + isValid: true, + } + } + + // Validate credentialStatus + if (verifyCredentialStatus && !credential.credentialStatus) { + validationResults.validations.credentialStatus = { + isValid: true, + } + } else if (verifyCredentialStatus && credential.credentialStatus) { + validationResults.validations.credentialStatus = { + isValid: false, + error: new CredoError('Verifying credential status is not supported for JWT VCs'), + } + } + + validationResults.isValid = Object.values(validationResults.validations).every((v) => v.isValid) + + return validationResults + } catch (error) { + validationResults.error = error + return validationResults + } + } + + /** + * Signs a presentation including the credentials it includes + * + * @param presentation the presentation to be signed + * @returns the signed presentation + */ + public async signPresentation( + agentContext: AgentContext, + options: W3cJwtSignPresentationOptions + ): Promise { + // Validate the instance + MessageValidator.validateSync(options.presentation) + + // Get the JWT payload for the JWT based on the JWT Encoding rules form the VC-DATA-MODEL + // https://www.w3.org/TR/vc-data-model/#jwt-encoding + const jwtPayload = getJwtPayloadFromPresentation(options.presentation) + + // Set the nonce so it's included in the signature + jwtPayload.additionalClaims.nonce = options.challenge + jwtPayload.aud = options.domain + + const verificationMethod = await this.resolveVerificationMethod(agentContext, options.verificationMethod, [ + 'authentication', + ]) + + const jwt = await this.jwsService.createJwsCompact(agentContext, { + payload: jwtPayload, + key: getKeyFromVerificationMethod(verificationMethod), + protectedHeaderOptions: { + typ: 'JWT', + alg: options.alg, + kid: options.verificationMethod, + }, + }) + + // TODO: this re-parses and validates the presentation in the JWT, which is not necessary. + // We should somehow create an instance of W3cJwtVerifiablePresentation directly from the JWT + const jwtVp = W3cJwtVerifiablePresentation.fromSerializedJwt(jwt) + + return jwtVp + } + + /** + * Verifies a presentation including the credentials it includes + * + * @param presentation the presentation to be verified + * @returns the verification result + */ + public async verifyPresentation( + agentContext: AgentContext, + options: W3cJwtVerifyPresentationOptions + ): Promise { + const validationResults: W3cVerifyPresentationResult = { + isValid: false, + validations: {}, + } + + try { + let presentation: W3cJwtVerifiablePresentation + try { + // If instance is provided as input, we want to validate the presentation + if (options.presentation instanceof W3cJwtVerifiablePresentation) { + MessageValidator.validateSync(options.presentation.presentation) + } + + presentation = + options.presentation instanceof W3cJwtVerifiablePresentation + ? options.presentation + : W3cJwtVerifiablePresentation.fromSerializedJwt(options.presentation) + + // Verify the JWT payload (verifies whether it's not expired, etc...) + presentation.jwt.payload.validate() + + // Make sure challenge matches nonce + if (options.challenge !== presentation.jwt.payload.additionalClaims.nonce) { + throw new CredoError(`JWT payload 'nonce' does not match challenge '${options.challenge}'`) + } + + const audArray = asArray(presentation.jwt.payload.aud) + if (options.domain && !audArray.includes(options.domain)) { + throw new CredoError(`JWT payload 'aud' does not include domain '${options.domain}'`) + } + + validationResults.validations.dataModel = { + isValid: true, + } + } catch (error) { + validationResults.validations.dataModel = { + isValid: false, + error, + } + + return validationResults + } + + const proverVerificationMethod = await this.getVerificationMethodForJwtCredential(agentContext, { + credential: presentation, + purpose: ['authentication'], + }) + const proverPublicKey = getKeyFromVerificationMethod(proverVerificationMethod) + const proverPublicJwk = getJwkFromKey(proverPublicKey) + + let signatureResult: VerifyJwsResult | undefined = undefined + try { + // Verify the JWS signature + signatureResult = await this.jwsService.verifyJws(agentContext, { + jws: presentation.jwt.serializedJwt, + // We have pre-fetched the key based on the singer/holder of the presentation + jwkResolver: () => proverPublicJwk, + }) + + if (!signatureResult.isValid) { + validationResults.validations.presentationSignature = { + isValid: false, + error: new CredoError('Invalid JWS signature on presentation'), + } + } else { + validationResults.validations.presentationSignature = { + isValid: true, + } + } + } catch (error) { + validationResults.validations.presentationSignature = { + isValid: false, + error, + } + } + + // Validate whether the presentation is signed with the 'holder' id + // NOTE: this uses the verificationMethod.controller. We may want to use the verificationMethod.id? + if (presentation.holderId && proverVerificationMethod.controller !== presentation.holderId) { + validationResults.validations.holderIsSigner = { + isValid: false, + error: new CredoError( + `Presentation is signed using verification method ${proverVerificationMethod.id}, while the holder of the presentation is '${presentation.holderId}'` + ), + } + } else { + // If no holderId is present, this validation passes by default as there can't be + // a mismatch between the 'holder' property and the signer of the presentation. + validationResults.validations.holderIsSigner = { + isValid: true, + } + } + + // To keep things simple, we only support JWT VCs in JWT VPs for now + const credentials = asArray(presentation.presentation.verifiableCredential) + + // Verify all credentials in parallel, and await the result + validationResults.validations.credentials = await Promise.all( + credentials.map(async (credential) => { + if (credential instanceof W3cJsonLdVerifiableCredential) { + return { + isValid: false, + error: new CredoError( + 'Credential is of format ldp_vc. presentations in jwp_vp format can only contain credentials in jwt_vc format' + ), + validations: {}, + } + } + + const credentialResult = await this.verifyCredential(agentContext, { + credential, + verifyCredentialStatus: options.verifyCredentialStatus, + }) + + let credentialSubjectAuthentication: SingleValidationResult + + // Check whether any of the credentialSubjectIds for each credential is the same as the controller of the verificationMethod + // This authenticates the presentation creator controls one of the credentialSubject ids. + // NOTE: this doesn't take into account the case where the credentialSubject is no the holder. In the + // future we can add support for other flows, but for now this is the most common use case. + // TODO: should this be handled on a higher level? I don't really see it being handled in the jsonld lib + // or in the did-jwt-vc lib (it seems they don't even verify the credentials itself), but we probably need some + // more experience on the use cases before we loosen the restrictions (as it means we need to handle it on a higher layer). + const credentialSubjectIds = credential.credentialSubjectIds + const presentationAuthenticatesCredentialSubject = credentialSubjectIds.some( + (subjectId) => proverVerificationMethod.controller === subjectId + ) + + if (credentialSubjectIds.length > 0 && !presentationAuthenticatesCredentialSubject) { + credentialSubjectAuthentication = { + isValid: false, + error: new CredoError( + 'Credential has one or more credentialSubject ids, but presentation does not authenticate credential subject' + ), + } + } else { + credentialSubjectAuthentication = { + isValid: true, + } + } + + return { + ...credentialResult, + isValid: credentialResult.isValid && credentialSubjectAuthentication.isValid, + validations: { + ...credentialResult.validations, + credentialSubjectAuthentication, + }, + } + }) + ) + + // Deeply nested check whether all validations have passed + validationResults.isValid = Object.values(validationResults.validations).every((v) => + Array.isArray(v) ? v.every((vv) => vv.isValid) : v.isValid + ) + + return validationResults + } catch (error) { + validationResults.error = error + return validationResults + } + } + + private async resolveVerificationMethod( + agentContext: AgentContext, + verificationMethod: string, + allowsPurposes?: DidPurpose[] + ): Promise { + const didResolver = agentContext.dependencyManager.resolve(DidResolverService) + const didDocument = await didResolver.resolveDidDocument(agentContext, verificationMethod) + + return didDocument.dereferenceKey(verificationMethod, allowsPurposes) + } + + /** + * This method tries to find the verification method associated with the JWT credential or presentation. + * This verification method can then be used to verify the credential or presentation signature. + * + * The following methods are used to extract the verification method: + * - verification method is resolved based on the `kid` in the protected header + * - either as absolute reference (e.g. `did:example:123#key-1`) + * - or as relative reference to the `iss` of the JWT (e.g. `iss` is `did:example:123` and `kid` is `#key-1`) + * - the did document is resolved based on the `iss` field, after which the verification method is extracted based on the `alg` + * used to sign the JWT and the specified `purpose`. Only a single verification method may be present, and in all other cases, + * an error is thrown. + * + * The signer (`iss`) of the JWT is verified against the `controller` of the verificationMethod resolved in the did + * document. This means if the `iss` of a credential is `did:example:123` and the controller of the verificationMethod + * is `did:example:456`, an error is thrown to prevent the JWT from successfully being verified. + * + * In addition the JWT must conform to one of the following rules: + * - MUST be a credential and have an `iss` field and MAY have an absolute or relative `kid` + * - MUST not be a credential AND ONE of the following: + * - have an `iss` field and MAY have an absolute or relative `kid` + * - does not have an `iss` field and MUST have an absolute `kid` + */ + private async getVerificationMethodForJwtCredential( + agentContext: AgentContext, + options: { + credential: W3cJwtVerifiableCredential | W3cJwtVerifiablePresentation + purpose?: DidPurpose[] + } + ) { + const { credential, purpose } = options + const kid = credential.jwt.header.kid + + const didResolver = agentContext.dependencyManager.resolve(DidResolverService) + + // The signerId is the `holder` of the presentation or the `issuer` of the credential + // For a credential only the `iss` COULD be enough to resolve the signer key (see method comments) + const signerId = credential.jwt.payload.iss + + let verificationMethod: VerificationMethod + + // If the kid starts with # we assume it is a relative did url, and we resolve it based on the `iss` and the `kid` + if (kid?.startsWith('#')) { + if (!signerId) { + throw new CredoError(`JWT 'kid' MUST be absolute when when no 'iss' is present in JWT payload`) + } + + const didDocument = await didResolver.resolveDidDocument(agentContext, signerId) + verificationMethod = didDocument.dereferenceKey(`${signerId}${kid}`, purpose) + } + // this is a full did url (todo check if it contains a #) + else if (kid && isDid(kid)) { + const didDocument = await didResolver.resolveDidDocument(agentContext, kid) + + verificationMethod = didDocument.dereferenceKey(kid, purpose) + + if (signerId && didDocument.id !== signerId) { + throw new CredoError(`kid '${kid}' does not match id of signer (holder/issuer) '${signerId}'`) + } + } else { + if (!signerId) { + throw new CredoError(`JWT 'iss' MUST be present in payload when no 'kid' is specified`) + } + + // Find the verificationMethod in the did document based on the alg and proofPurpose + const jwkClass = getJwkClassFromJwaSignatureAlgorithm(credential.jwt.header.alg) + if (!jwkClass) throw new CredoError(`Unsupported JWT alg '${credential.jwt.header.alg}'`) + + const { supportedVerificationMethodTypes } = getKeyDidMappingByKeyType(jwkClass.keyType) + + const didDocument = await didResolver.resolveDidDocument(agentContext, signerId) + const verificationMethods = + didDocument.assertionMethod + ?.map((v) => (typeof v === 'string' ? didDocument.dereferenceVerificationMethod(v) : v)) + .filter((v) => supportedVerificationMethodTypes.includes(v.type)) ?? [] + + if (verificationMethods.length === 0) { + throw new CredoError( + `No verification methods found for signer '${signerId}' and key type '${jwkClass.keyType}' for alg '${credential.jwt.header.alg}'. Unable to determine which public key is associated with the credential.` + ) + } else if (verificationMethods.length > 1) { + throw new CredoError( + `Multiple verification methods found for signer '${signerId}' and key type '${jwkClass.keyType}' for alg '${credential.jwt.header.alg}'. Unable to determine which public key is associated with the credential.` + ) + } + + verificationMethod = verificationMethods[0] + } + + // Verify the controller of the verificationMethod matches the signer of the credential + if (signerId && verificationMethod.controller !== signerId) { + throw new CredoError( + `Verification method controller '${verificationMethod.controller}' does not match the signer '${signerId}'` + ) + } + + return verificationMethod + } +} diff --git a/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiableCredential.ts b/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiableCredential.ts new file mode 100644 index 0000000000..869f00121e --- /dev/null +++ b/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiableCredential.ts @@ -0,0 +1,126 @@ +import type { W3cCredential } from '../models/credential/W3cCredential' +import type { W3cJsonCredential } from '../models/credential/W3cJsonCredential' + +import { Jwt } from '../../../crypto/jose/jwt/Jwt' +import { JsonTransformer } from '../../../utils' +import { ClaimFormat } from '../models/ClaimFormat' + +import { getCredentialFromJwtPayload } from './credentialTransformer' + +export interface W3cJwtVerifiableCredentialOptions { + jwt: Jwt +} + +export class W3cJwtVerifiableCredential { + public readonly jwt: Jwt + private _credential: W3cCredential + + public constructor(options: W3cJwtVerifiableCredentialOptions) { + this.jwt = options.jwt + + this._credential = getCredentialFromJwtPayload(this.jwt.payload) + } + + public static fromSerializedJwt(serializedJwt: string) { + const jwt = Jwt.fromSerializedJwt(serializedJwt) + + return new W3cJwtVerifiableCredential({ + jwt, + }) + } + + /** + * Get the W3cCredential from the JWT payload. This does not include the JWT wrapper, + * and thus is not suitable for sharing. If you need a JWT, use the `serializedJwt` property. + * + * All properties and getters from the `W3cCredential` interface are implemented as getters + * on the `W3cJwtVerifiableCredential` class itself, so you can also use this directly + * instead of accessing the inner `credential` property. + */ + public get credential(): W3cCredential { + return this._credential + } + + public get serializedJwt(): string { + return this.jwt.serializedJwt + } + + // + // Below all properties from the `W3cCredential` interface are implemented as getters + // this is to make the interface compatible with the W3cJsonLdVerifiableCredential interface + // which makes using the different classes interchangeably from a user point of view. + // This is 'easier' than extending the W3cCredential class as it means we have to create the + // instance based on JSON, but also add custom properties. + // + + public get context() { + return this.credential.context + } + + public get id() { + return this.credential.id + } + + public get type() { + return this.credential.type + } + + public get issuer() { + return this.credential.issuer + } + + public get issuanceDate() { + return this.credential.issuanceDate + } + + public get expirationDate() { + return this.credential.expirationDate + } + + public get credentialSubject() { + return this.credential.credentialSubject + } + + public get credentialSchema() { + return this.credential.credentialSchema + } + + public get credentialStatus() { + return this.credential.credentialStatus + } + + public get issuerId() { + return this.credential.issuerId + } + + public get credentialSchemaIds() { + return this.credential.credentialSchemaIds + } + + public get credentialSubjectIds() { + return this.credential.credentialSubjectIds + } + + public get contexts() { + return this.credential.contexts + } + + /** + * The {@link ClaimFormat} of the credential. For JWT credentials this is always `jwt_vc`. + */ + public get claimFormat(): ClaimFormat.JwtVc { + return ClaimFormat.JwtVc + } + + /** + * Get the encoded variant of the W3C Verifiable Credential. For JWT credentials this is + * a JWT string. + */ + public get encoded() { + return this.serializedJwt + } + + public get jsonCredential(): W3cJsonCredential { + return JsonTransformer.toJSON(this.credential) as W3cJsonCredential + } +} diff --git a/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiablePresentation.ts b/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiablePresentation.ts new file mode 100644 index 0000000000..68a1923e5d --- /dev/null +++ b/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiablePresentation.ts @@ -0,0 +1,97 @@ +import type { W3cPresentation } from '../models' + +import { Jwt } from '../../../crypto/jose/jwt/Jwt' +import { CredoError } from '../../../error' +import { ClaimFormat } from '../models' + +import { getPresentationFromJwtPayload } from './presentationTransformer' + +export interface W3cJwtVerifiablePresentationOptions { + jwt: Jwt +} + +export class W3cJwtVerifiablePresentation { + public readonly jwt: Jwt + private _presentation: W3cPresentation + + public constructor(options: W3cJwtVerifiablePresentationOptions) { + this.jwt = options.jwt + + this._presentation = getPresentationFromJwtPayload(this.jwt.payload) + } + + public static fromSerializedJwt(serializedJwt: string) { + const jwt = Jwt.fromSerializedJwt(serializedJwt) + + if (!jwt.payload.additionalClaims.nonce) { + throw new CredoError(`JWT payload does not contain required claim 'nonce'`) + } + + return new W3cJwtVerifiablePresentation({ + jwt, + }) + } + + /** + * Get the W3cPresentation from the JWT payload. This does not include the JWT wrapper, + * and thus is not suitable for sharing. If you need a JWT, use the `serializedJwt` property. + * + * All properties and getters from the `W3cPresentation` interface are implemented as getters + * on the `W3cJwtVerifiablePresentation` class itself, so you can also use this directly + * instead of accessing the inner `presentation` property. + */ + public get presentation(): W3cPresentation { + return this._presentation + } + + public get serializedJwt(): string { + return this.jwt.serializedJwt + } + + // + // Below all properties from the `W3cPresentation` interface are implemented as getters + // this is to make the interface compatible with the W3cJsonLdVerifiablePresentation interface + // which makes using the different classes interchangeably from a user point of view. + // This is 'easier' than extending the W3cPresentation class as it means we have to create the + // instance based on JSON, but also add custom properties. + // + + public get context() { + return this.presentation.context + } + + public get id() { + return this.presentation.id + } + + public get type() { + return this.presentation.type + } + + public get holder() { + return this.presentation.holder + } + + public get verifiableCredential() { + return this.presentation.verifiableCredential + } + + public get holderId() { + return this.presentation.holderId + } + + /** + * The {@link ClaimFormat} of the presentation. For JWT presentations this is always `jwt_vp`. + */ + public get claimFormat(): ClaimFormat.JwtVp { + return ClaimFormat.JwtVp + } + + /** + * Get the encoded variant of the W3C Verifiable Presentation. For JWT presentations this is + * a JWT string. + */ + public get encoded() { + return this.serializedJwt + } +} diff --git a/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts b/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts new file mode 100644 index 0000000000..72eb5c71f1 --- /dev/null +++ b/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts @@ -0,0 +1,399 @@ +import { InMemoryWallet } from '../../../../../../../tests/InMemoryWallet' +import { getAgentConfig, getAgentContext, testLogger } from '../../../../../tests' +import { InjectionSymbols } from '../../../../constants' +import { JwsService, KeyType } from '../../../../crypto' +import { JwaSignatureAlgorithm } from '../../../../crypto/jose/jwa' +import { getJwkFromKey } from '../../../../crypto/jose/jwk' +import { CredoError, ClassValidationError } from '../../../../error' +import { JsonTransformer } from '../../../../utils' +import { DidJwk, DidKey, DidRepository, DidsModuleConfig } from '../../../dids' +import { CREDENTIALS_CONTEXT_V1_URL } from '../../constants' +import { ClaimFormat, W3cCredential, W3cPresentation } from '../../models' +import { W3cJwtCredentialService } from '../W3cJwtCredentialService' +import { W3cJwtVerifiableCredential } from '../W3cJwtVerifiableCredential' + +import { + CredoEs256DidJwkJwtVc, + CredoEs256DidJwkJwtVcIssuerSeed, + CredoEs256DidJwkJwtVcSubjectSeed, + CredoEs256DidKeyJwtVp, + Ed256DidJwkJwtVcUnsigned, +} from './fixtures/credo-jwt-vc' +import { didIonJwtVcPresentationProfileJwtVc } from './fixtures/jwt-vc-presentation-profile' +import { didKeyTransmuteJwtVc, didKeyTransmuteJwtVp } from './fixtures/transmute-verifiable-data' + +const config = getAgentConfig('W3cJwtCredentialService') +const wallet = new InMemoryWallet() +const agentContext = getAgentContext({ + wallet, + registerInstances: [ + [InjectionSymbols.Logger, testLogger], + [DidsModuleConfig, new DidsModuleConfig()], + [DidRepository, {} as unknown as DidRepository], + ], + agentConfig: config, +}) + +const jwsService = new JwsService() +const w3cJwtCredentialService = new W3cJwtCredentialService(jwsService) + +// Runs in Node 18 because of usage of Askar +describe('W3cJwtCredentialService', () => { + let issuerDidJwk: DidJwk + let holderDidKey: DidKey + + beforeAll(async () => { + await wallet.createAndOpen(config.walletConfig) + + const issuerKey = await agentContext.wallet.createKey({ + keyType: KeyType.P256, + seed: CredoEs256DidJwkJwtVcIssuerSeed, + }) + issuerDidJwk = DidJwk.fromJwk(getJwkFromKey(issuerKey)) + + const holderKey = await agentContext.wallet.createKey({ + keyType: KeyType.Ed25519, + seed: CredoEs256DidJwkJwtVcSubjectSeed, + }) + holderDidKey = new DidKey(holderKey) + }) + + describe('signCredential', () => { + test('signs an ES256 JWT vc', async () => { + const credential = JsonTransformer.fromJSON(Ed256DidJwkJwtVcUnsigned, W3cCredential) + + const vcJwt = await w3cJwtCredentialService.signCredential(agentContext, { + alg: JwaSignatureAlgorithm.ES256, + format: ClaimFormat.JwtVc, + verificationMethod: issuerDidJwk.verificationMethodId, + credential, + }) + + expect(vcJwt.serializedJwt).toEqual(CredoEs256DidJwkJwtVc) + }) + + test('throws when invalid credential is passed', async () => { + const credentialJson = { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential'], + issuer: + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InpRT293SUMxZ1dKdGRkZEI1R0F0NGxhdTZMdDhJaHk3NzFpQWZhbS0xcGMiLCJ5IjoiY2pEXzdvM2dkUTF2Z2lReTNfc01HczdXcndDTVU5RlFZaW1BM0h4bk1sdyJ9', + issuanceDate: '2023-01-25T16:58:06.292Z', + credentialSubject: { + id: 'did:key:z6MkqgkLrRyLg6bqk27djwbbaQWgaSYgFVCKq9YKxZbNkpVv', + }, + } + + // Throw when verificationMethod is not a did + await expect( + w3cJwtCredentialService.signCredential(agentContext, { + verificationMethod: 'hello', + alg: JwaSignatureAlgorithm.ES256, + credential: JsonTransformer.fromJSON(credentialJson, W3cCredential), + format: ClaimFormat.JwtVc, + }) + ).rejects.toThrowError('Only did identifiers are supported as verification method') + + // Throw when not according to data model + await expect( + w3cJwtCredentialService.signCredential(agentContext, { + verificationMethod: issuerDidJwk.verificationMethodId, + alg: JwaSignatureAlgorithm.ES256, + credential: JsonTransformer.fromJSON({ ...credentialJson, issuanceDate: undefined }, W3cCredential, { + validate: false, + }), + format: ClaimFormat.JwtVc, + }) + ).rejects.toThrowError( + 'property issuanceDate has failed the following constraints: issuanceDate must be RFC 3339 date' + ) + + // Throw when verificationMethod id does not exist in did document + await expect( + w3cJwtCredentialService.signCredential(agentContext, { + verificationMethod: issuerDidJwk.verificationMethodId + 'extra', + alg: JwaSignatureAlgorithm.ES256, + credential: JsonTransformer.fromJSON(credentialJson, W3cCredential), + format: ClaimFormat.JwtVc, + }) + ).rejects.toThrowError( + `Unable to locate verification method with id 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InpRT293SUMxZ1dKdGRkZEI1R0F0NGxhdTZMdDhJaHk3NzFpQWZhbS0xcGMiLCJ5IjoiY2pEXzdvM2dkUTF2Z2lReTNfc01HczdXcndDTVU5RlFZaW1BM0h4bk1sdyJ9#0extra' in purposes assertionMethod` + ) + }) + }) + + describe('verifyCredential', () => { + // Fails because the `jti` is not an uri (and the `vc.id` MUST be an uri according to vc data model) + test.skip('verifies a vc from the vc-jwt-presentation-profile', async () => { + const result = await w3cJwtCredentialService.verifyCredential(agentContext, { + credential: didIonJwtVcPresentationProfileJwtVc, + verifyCredentialStatus: false, + }) + + expect(result).toMatchObject({ + verified: true, + }) + }) + + test('verifies an ES256 JWT vc signed by Credo', async () => { + const result = await w3cJwtCredentialService.verifyCredential(agentContext, { + credential: CredoEs256DidJwkJwtVc, + }) + + expect(result).toEqual({ + isValid: true, + validations: { + // credential has no credentialStatus, so always valid + credentialStatus: { + isValid: true, + }, + // This both validates whether the credential matches the + // data model, as well as whether the credential is expired etc.. + dataModel: { + isValid: true, + }, + // This validates whether the signature is valid + signature: { + isValid: true, + }, + // This validates whether the issuer is also the signer of the credential + issuerIsSigner: { + isValid: true, + }, + }, + }) + }) + + test('verifies an EdDSA JWT vc from the transmute vc.js library', async () => { + const result = await w3cJwtCredentialService.verifyCredential(agentContext, { + credential: didKeyTransmuteJwtVc, + }) + + expect(result).toEqual({ + isValid: true, + validations: { + // credential has no credentialStatus, so always valid + credentialStatus: { + isValid: true, + }, + // This both validates whether the credential matches the + // data model, as well as whether the credential is expired etc.. + dataModel: { + isValid: true, + }, + // This validates whether the signature is valid + signature: { + isValid: true, + }, + // This validates whether the issuer is also the signer of the credential + issuerIsSigner: { + isValid: true, + }, + }, + }) + }) + + test('returns invalid result when credential is not according to data model', async () => { + const jwtVc = W3cJwtVerifiableCredential.fromSerializedJwt(CredoEs256DidJwkJwtVc) + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete jwtVc.credential.issuer + + const result = await w3cJwtCredentialService.verifyCredential(agentContext, { + credential: jwtVc, + verifyCredentialStatus: false, + }) + + expect(result).toEqual({ + isValid: false, + validations: { + dataModel: { + isValid: false, + error: expect.any(ClassValidationError), + }, + }, + }) + + expect(result.validations.dataModel?.error?.message).toContain('Failed to validate class') + }) + + test('returns invalid result when credential is expired', async () => { + const jwtVc = W3cJwtVerifiableCredential.fromSerializedJwt(CredoEs256DidJwkJwtVc) + + jwtVc.jwt.payload.exp = new Date('2020-01-01').getTime() / 1000 + + const result = await w3cJwtCredentialService.verifyCredential(agentContext, { + credential: jwtVc, + verifyCredentialStatus: false, + }) + + expect(result).toEqual({ + isValid: false, + validations: { + dataModel: { + isValid: false, + error: expect.any(CredoError), + }, + }, + }) + + expect(result.validations.dataModel?.error?.message).toContain('JWT expired at 1577836800') + }) + + test('returns invalid result when signature is not valid', async () => { + const jwtVc = W3cJwtVerifiableCredential.fromSerializedJwt(CredoEs256DidJwkJwtVc + 'a') + + const result = await w3cJwtCredentialService.verifyCredential(agentContext, { + credential: jwtVc, + }) + + expect(result).toEqual({ + isValid: false, + validations: { + dataModel: { + isValid: true, + }, + signature: { + isValid: false, + error: expect.any(CredoError), + }, + issuerIsSigner: { + isValid: false, + error: expect.any(CredoError), + }, + credentialStatus: { + isValid: true, + }, + }, + }) + + expect(result.validations.signature?.error?.message).toContain('Invalid JWS signature') + }) + }) + + describe('signPresentation', () => { + test('signs an ES256 JWT vp', async () => { + // Create a new instance of the credential from the serialized JWT + const parsedJwtVc = W3cJwtVerifiableCredential.fromSerializedJwt(CredoEs256DidJwkJwtVc) + + const presentation = new W3cPresentation({ + context: [CREDENTIALS_CONTEXT_V1_URL], + type: ['VerifiablePresentation'], + verifiableCredential: [parsedJwtVc], + id: 'urn:21ff21f1-3cf9-4fa3-88b4-a045efbb1b5f', + holder: holderDidKey.did, + }) + + const signedJwtVp = await w3cJwtCredentialService.signPresentation(agentContext, { + presentation, + alg: JwaSignatureAlgorithm.EdDSA, + challenge: 'daf942ad-816f-45ee-a9fc-facd08e5abca', + domain: 'example.com', + format: ClaimFormat.JwtVp, + verificationMethod: `${holderDidKey.did}#${holderDidKey.key.fingerprint}`, + }) + + expect(signedJwtVp.serializedJwt).toEqual(CredoEs256DidKeyJwtVp) + }) + }) + + describe('verifyPresentation', () => { + test('verifies an ES256 JWT vp signed by Credo', async () => { + const result = await w3cJwtCredentialService.verifyPresentation(agentContext, { + presentation: CredoEs256DidKeyJwtVp, + challenge: 'daf942ad-816f-45ee-a9fc-facd08e5abca', + domain: 'example.com', + }) + + expect(result).toEqual({ + isValid: true, + validations: { + dataModel: { + isValid: true, + }, + presentationSignature: { + isValid: true, + }, + holderIsSigner: { + isValid: true, + }, + credentials: [ + { + isValid: true, + validations: { + dataModel: { + isValid: true, + }, + signature: { + isValid: true, + }, + issuerIsSigner: { + isValid: true, + }, + credentialStatus: { + isValid: true, + }, + credentialSubjectAuthentication: { + isValid: true, + }, + }, + }, + ], + }, + }) + }) + + // NOTE: this test doesn't fully succeed because the VP from the transmute + // library doesn't authenticate the credentialSubject.id in the credential + // in the VP. For now, all VPs must authenticate the credentialSubject, if + // the credential has a credential subject id (so it's not a bearer credential) + test('verifies an EdDSA JWT vp from the transmute vc.js library', async () => { + const result = await w3cJwtCredentialService.verifyPresentation(agentContext, { + presentation: didKeyTransmuteJwtVp, + challenge: '123', + domain: 'example.com', + }) + + expect(result).toEqual({ + isValid: false, + validations: { + dataModel: { + isValid: true, + }, + presentationSignature: { + isValid: true, + }, + holderIsSigner: { + isValid: true, + }, + credentials: [ + { + isValid: false, + validations: { + dataModel: { + isValid: true, + }, + signature: { + isValid: true, + }, + issuerIsSigner: { + isValid: true, + }, + credentialStatus: { + isValid: true, + }, + credentialSubjectAuthentication: { + isValid: false, + error: new CredoError( + 'Credential has one or more credentialSubject ids, but presentation does not authenticate credential subject' + ), + }, + }, + }, + ], + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/vc/jwt-vc/__tests__/credentialTransformer.test.ts b/packages/core/src/modules/vc/jwt-vc/__tests__/credentialTransformer.test.ts new file mode 100644 index 0000000000..7932fdc6fa --- /dev/null +++ b/packages/core/src/modules/vc/jwt-vc/__tests__/credentialTransformer.test.ts @@ -0,0 +1,267 @@ +import { JwtPayload } from '../../../../crypto/jose/jwt' +import { JsonTransformer } from '../../../../utils' +import { W3cCredential } from '../../models' +import { getCredentialFromJwtPayload, getJwtPayloadFromCredential } from '../credentialTransformer' + +describe('credentialTransformer', () => { + describe('getJwtPayloadFromCredential', () => { + test('extracts jwt payload from credential', () => { + const credential = new W3cCredential({ + type: ['VerifiableCredential'], + credentialSubject: { + id: 'https://example.com', + }, + issuanceDate: new Date('2020-01-01').toISOString(), + issuer: 'did:example:123', + id: 'urn:123', + }) + + const jwtPayload = getJwtPayloadFromCredential(credential) + + expect(jwtPayload.toJson()).toEqual({ + vc: { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential'], + credentialSubject: {}, + expirationDate: undefined, + }, + nbf: expect.any(Number), + iss: 'did:example:123', + jti: 'urn:123', + sub: 'https://example.com', + aud: undefined, + exp: undefined, + iat: undefined, + }) + }) + }) + + describe('getCredentialFromJwtPayload', () => { + test('extracts credential from jwt payload', () => { + const vc: Record = { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential'], + credentialSubject: {}, + } + + const jwtPayload = new JwtPayload({ + iss: 'urn:iss', + nbf: 1262373804, + exp: 1262373804, + sub: 'did:example:123', + jti: 'urn:jti', + additionalClaims: { + vc, + }, + }) + + const credential = JsonTransformer.toJSON(getCredentialFromJwtPayload(jwtPayload)) + + expect(credential).toEqual({ + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential'], + id: 'urn:jti', + issuer: 'urn:iss', + credentialSubject: { + id: 'did:example:123', + }, + issuanceDate: '2010-01-01T19:23:24Z', // 1262373804 + expirationDate: '2010-01-01T19:23:24Z', // 1262373804 + }) + }) + + test(`throw error if jwt payload does not contain 'vc' property or it is not an object`, () => { + const jwtPayload = new JwtPayload({}) + + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError("JWT does not contain a valid 'vc' claim") + + jwtPayload.additionalClaims.vc = 'invalid' + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError("JWT does not contain a valid 'vc' claim") + }) + + test(`throw error if jwt payload does not contain 'nbf' or 'iss' property`, () => { + const jwtPayload = new JwtPayload({ + additionalClaims: { + vc: {}, + }, + }) + + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError( + "JWT does not contain valid 'nbf' and 'iss' claims" + ) + + jwtPayload.nbf = 100 + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError( + "JWT does not contain valid 'nbf' and 'iss' claims" + ) + + jwtPayload.nbf = undefined + jwtPayload.iss = 'iss' + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError( + "JWT does not contain valid 'nbf' and 'iss' claims" + ) + }) + + test(`throw error if jwt vc credentialSubject does not have a single credentialSubject`, () => { + const vc: Record = {} + const jwtPayload = new JwtPayload({ + iss: 'iss', + nbf: 100, + additionalClaims: { + vc, + }, + }) + + // no credentialSubject at all + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError( + 'JWT VC does not have a valid credential subject' + ) + + // Array but no entry + vc.credentialSubject = [] + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError( + 'JWT VCs must have exactly one credential subject' + ) + + // Array with entry, but not an object + vc.credentialSubject = [10] + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError( + 'JWT VCs must have a credential subject of type object' + ) + + // entry, but not an object + vc.credentialSubject = 10 + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError( + 'JWT VC does not have a valid credential subject' + ) + + jwtPayload.nbf = undefined + jwtPayload.iss = 'iss' + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError( + "JWT does not contain valid 'nbf' and 'iss' claims" + ) + }) + + test(`throw error if jwt vc has an id and it does not match the jti`, () => { + const vc: Record = { + credentialSubject: {}, + id: '13', + } + const jwtPayload = new JwtPayload({ + iss: 'iss', + nbf: 100, + jti: '12', + additionalClaims: { + vc, + }, + }) + + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError('JWT jti and vc.id do not match') + }) + + test(`throw error if jwt vc has an issuer id and it does not match the iss`, () => { + const vc: Record = { + credentialSubject: {}, + issuer: '123', + } + const jwtPayload = new JwtPayload({ + iss: 'iss', + nbf: 100, + additionalClaims: { + vc, + }, + }) + + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError('JWT iss and vc.issuer(.id) do not match') + + // nested issuer object + vc.issuer = { id: '123' } + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError('JWT iss and vc.issuer(.id) do not match') + }) + + test(`throw error if jwt vc has an issuanceDate and it does not match the nbf`, () => { + const vc: Record = { + credentialSubject: {}, + issuanceDate: '2010-01-01T19:23:24Z', // 1262373804 + } + const jwtPayload = new JwtPayload({ + iss: 'iss', + nbf: 1577833200, + additionalClaims: { + vc, + }, + }) + + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError('JWT nbf and vc.issuanceDate do not match') + + vc.issuanceDate = 10 + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError('JWT vc.issuanceDate must be a string') + }) + + test(`throw error if jwt vc has an expirationDate and it does not match the exp`, () => { + const vc: Record = { + credentialSubject: {}, + expirationDate: '2010-01-01T19:23:24Z', // 1262373804 + } + const jwtPayload = new JwtPayload({ + iss: 'iss', + nbf: 1577833200, + exp: 1577833200, + additionalClaims: { + vc, + }, + }) + + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError('JWT exp and vc.expirationDate do not match') + + vc.expirationDate = 10 + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError('JWT vc.expirationDate must be a string') + }) + + test(`throw error if jwt vc has a credentialSubject.id and it does not match the sub`, () => { + const vc: Record = {} + const jwtPayload = new JwtPayload({ + iss: 'iss', + nbf: 1577833200, + exp: 1577833200, + sub: 'did:example:123', + additionalClaims: { + vc, + }, + }) + + vc.credentialSubject = { id: 'did:example:456' } + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError( + 'JWT sub and vc.credentialSubject.id do not match' + ) + + vc.credentialSubject = [{ id: 'did:example:456' }] + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError( + 'JWT sub and vc.credentialSubject.id do not match' + ) + }) + + test(`throw validation error if vc is not a valid w3c vc`, () => { + const vc: Record = { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential2'], + credentialSubject: {}, + } + + const jwtPayload = new JwtPayload({ + iss: 'urn:iss', + nbf: 1577833200, + exp: 1577833200, + sub: 'did:example:123', + jti: 'urn:jti', + additionalClaims: { + vc, + }, + }) + + expect(() => getCredentialFromJwtPayload(jwtPayload)).toThrowError( + 'property type has failed the following constraints: type must be an array of strings which includes "VerifiableCredential"' + ) + }) + }) +}) diff --git a/packages/core/src/modules/vc/jwt-vc/__tests__/fixtures/credo-jwt-vc.ts b/packages/core/src/modules/vc/jwt-vc/__tests__/fixtures/credo-jwt-vc.ts new file mode 100644 index 0000000000..e931321eac --- /dev/null +++ b/packages/core/src/modules/vc/jwt-vc/__tests__/fixtures/credo-jwt-vc.ts @@ -0,0 +1,49 @@ +import { TypedArrayEncoder } from '../../../../../utils' + +export const Ed256DidJwkJwtVcUnsigned = { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://purl.imsglobal.org/spec/ob/v3p0/context.json'], + type: ['VerifiableCredential', 'VerifiableCredentialExtension', 'OpenBadgeCredential'], + issuer: { + id: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InpRT293SUMxZ1dKdGRkZEI1R0F0NGxhdTZMdDhJaHk3NzFpQWZhbS0xcGMiLCJ5IjoiY2pEXzdvM2dkUTF2Z2lReTNfc01HczdXcndDTVU5RlFZaW1BM0h4bk1sdyJ9', + name: 'Jobs for the Future (JFF)', + iconUrl: 'https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png', + image: 'https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png', + }, + name: 'JFF x vc-edu PlugFest 2', + description: "MATTR's submission for JFF Plugfest 2", + credentialBranding: { + backgroundColor: '#464c49', + }, + issuanceDate: '2023-01-25T16:58:06.292Z', + credentialSubject: { + id: 'did:key:z6MkqgkLrRyLg6bqk27djwbbaQWgaSYgFVCKq9YKxZbNkpVv', + type: ['AchievementSubject'], + achievement: { + id: 'urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922', + name: 'JFF x vc-edu PlugFest 2 Interoperability', + type: ['Achievement'], + image: { + id: 'https://w3c-ccg.github.io/vc-ed/plugfest-2-2022/images/JFF-VC-EDU-PLUGFEST2-badge-image.png', + type: 'Image', + }, + criteria: { + type: 'Criteria', + narrative: + 'Solutions providers earned this badge by demonstrating interoperability between multiple providers based on the OBv3 candidate final standard, with some additional required fields. Credential issuers earning this badge successfully issued a credential into at least two wallets. Wallet implementers earning this badge successfully displayed credentials issued by at least two different credential issuers.', + }, + description: + 'This credential solution supports the use of OBv3 and w3c Verifiable Credentials and is interoperable with at least two other solutions. This was demonstrated successfully during JFF x vc-edu PlugFest 2.', + }, + }, +} + +export const CredoEs256DidJwkJwtVc = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpGUXlJc0ltTnlkaUk2SWxBdE1qVTJJaXdpZUNJNklucFJUMjkzU1VNeFoxZEtkR1JrWkVJMVIwRjBOR3hoZFRaTWREaEphSGszTnpGcFFXWmhiUzB4Y0dNaUxDSjVJam9pWTJwRVh6ZHZNMmRrVVRGMloybFJlVE5mYzAxSGN6ZFhjbmREVFZVNVJsRlphVzFCTTBoNGJrMXNkeUo5IzAifQ.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9jb250ZXh0Lmpzb24iXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlZlcmlmaWFibGVDcmVkZW50aWFsRXh0ZW5zaW9uIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOnsibmFtZSI6IkpvYnMgZm9yIHRoZSBGdXR1cmUgKEpGRikiLCJpY29uVXJsIjoiaHR0cHM6Ly93M2MtY2NnLmdpdGh1Yi5pby92Yy1lZC9wbHVnZmVzdC0xLTIwMjIvaW1hZ2VzL0pGRl9Mb2dvTG9ja3VwLnBuZyIsImltYWdlIjoiaHR0cHM6Ly93M2MtY2NnLmdpdGh1Yi5pby92Yy1lZC9wbHVnZmVzdC0xLTIwMjIvaW1hZ2VzL0pGRl9Mb2dvTG9ja3VwLnBuZyJ9LCJuYW1lIjoiSkZGIHggdmMtZWR1IFBsdWdGZXN0IDIiLCJkZXNjcmlwdGlvbiI6Ik1BVFRSJ3Mgc3VibWlzc2lvbiBmb3IgSkZGIFBsdWdmZXN0IDIiLCJjcmVkZW50aWFsQnJhbmRpbmciOnsiYmFja2dyb3VuZENvbG9yIjoiIzQ2NGM0OSJ9LCJjcmVkZW50aWFsU3ViamVjdCI6eyJ0eXBlIjpbIkFjaGlldmVtZW50U3ViamVjdCJdLCJhY2hpZXZlbWVudCI6eyJpZCI6InVybjp1dWlkOmJkNmQ5MzE2LWY3YWUtNDA3My1hMWU1LTJmN2Y1YmQyMjkyMiIsIm5hbWUiOiJKRkYgeCB2Yy1lZHUgUGx1Z0Zlc3QgMiBJbnRlcm9wZXJhYmlsaXR5IiwidHlwZSI6WyJBY2hpZXZlbWVudCJdLCJpbWFnZSI6eyJpZCI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMi0yMDIyL2ltYWdlcy9KRkYtVkMtRURVLVBMVUdGRVNUMi1iYWRnZS1pbWFnZS5wbmciLCJ0eXBlIjoiSW1hZ2UifSwiY3JpdGVyaWEiOnsidHlwZSI6IkNyaXRlcmlhIiwibmFycmF0aXZlIjoiU29sdXRpb25zIHByb3ZpZGVycyBlYXJuZWQgdGhpcyBiYWRnZSBieSBkZW1vbnN0cmF0aW5nIGludGVyb3BlcmFiaWxpdHkgYmV0d2VlbiBtdWx0aXBsZSBwcm92aWRlcnMgYmFzZWQgb24gdGhlIE9CdjMgY2FuZGlkYXRlIGZpbmFsIHN0YW5kYXJkLCB3aXRoIHNvbWUgYWRkaXRpb25hbCByZXF1aXJlZCBmaWVsZHMuIENyZWRlbnRpYWwgaXNzdWVycyBlYXJuaW5nIHRoaXMgYmFkZ2Ugc3VjY2Vzc2Z1bGx5IGlzc3VlZCBhIGNyZWRlbnRpYWwgaW50byBhdCBsZWFzdCB0d28gd2FsbGV0cy4gIFdhbGxldCBpbXBsZW1lbnRlcnMgZWFybmluZyB0aGlzIGJhZGdlIHN1Y2Nlc3NmdWxseSBkaXNwbGF5ZWQgY3JlZGVudGlhbHMgaXNzdWVkIGJ5IGF0IGxlYXN0IHR3byBkaWZmZXJlbnQgY3JlZGVudGlhbCBpc3N1ZXJzLiJ9LCJkZXNjcmlwdGlvbiI6IlRoaXMgY3JlZGVudGlhbCBzb2x1dGlvbiBzdXBwb3J0cyB0aGUgdXNlIG9mIE9CdjMgYW5kIHczYyBWZXJpZmlhYmxlIENyZWRlbnRpYWxzIGFuZCBpcyBpbnRlcm9wZXJhYmxlIHdpdGggYXQgbGVhc3QgdHdvIG90aGVyIHNvbHV0aW9ucy4gIFRoaXMgd2FzIGRlbW9uc3RyYXRlZCBzdWNjZXNzZnVsbHkgZHVyaW5nIEpGRiB4IHZjLWVkdSBQbHVnRmVzdCAyLiJ9fX0sImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpGUXlJc0ltTnlkaUk2SWxBdE1qVTJJaXdpZUNJNklucFJUMjkzU1VNeFoxZEtkR1JrWkVJMVIwRjBOR3hoZFRaTWREaEphSGszTnpGcFFXWmhiUzB4Y0dNaUxDSjVJam9pWTJwRVh6ZHZNMmRrVVRGMloybFJlVE5mYzAxSGN6ZFhjbmREVFZVNVJsRlphVzFCTTBoNGJrMXNkeUo5Iiwic3ViIjoiZGlkOmtleTp6Nk1rcWdrTHJSeUxnNmJxazI3ZGp3YmJhUVdnYVNZZ0ZWQ0txOVlLeFpiTmtwVnYiLCJuYmYiOjE2NzQ2NjU4ODZ9.anABxv424eMpp0xgbTx6aZvZxblkSThq-XbgixhWegFCVz2Q-EtRUiGJuOUjmql5TttTZ_YgtN9PgozOfuTZtg' + +export const CredoEs256DidJwkJwtVcIssuerSeed = TypedArrayEncoder.fromString( + '00000000000000000000000000000My100000000000000000000000000000My1' +) +export const CredoEs256DidJwkJwtVcSubjectSeed = TypedArrayEncoder.fromString('00000000000000000000000000000My1') + +export const CredoEs256DidKeyJwtVp = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3Fna0xyUnlMZzZicWsyN2Rqd2JiYVFXZ2FTWWdGVkNLcTlZS3haYk5rcFZ2I3o2TWtxZ2tMclJ5TGc2YnFrMjdkandiYmFRV2dhU1lnRlZDS3E5WUt4WmJOa3BWdiJ9.eyJ2cCI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iXSwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlsiZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKRlV6STFOaUlzSW10cFpDSTZJbVJwWkRwcWQyczZaWGxLY21SSWEybFBhVXBHVVhsSmMwbHRUbmxrYVVrMlNXeEJkRTFxVlRKSmFYZHBaVU5KTmtsdWNGSlVNamt6VTFWTmVGb3haRXRrUjFKcldrVkpNVkl3UmpCT1IzaG9aRlJhVFdSRWFFcGhTR3N6VG5wR2NGRlhXbWhpVXpCNFkwZE5hVXhEU2pWSmFtOXBXVEp3UlZoNlpIWk5NbVJyVlZSR01sb3liRkpsVkU1bVl6QXhTR042WkZoamJtUkVWRlpWTlZKc1JscGhWekZDVFRCb05HSnJNWE5rZVVvNUl6QWlmUS5leUoyWXlJNmV5SkFZMjl1ZEdWNGRDSTZXeUpvZEhSd2N6b3ZMM2QzZHk1M015NXZjbWN2TWpBeE9DOWpjbVZrWlc1MGFXRnNjeTkyTVNJc0ltaDBkSEJ6T2k4dmNIVnliQzVwYlhObmJHOWlZV3d1YjNKbkwzTndaV012YjJJdmRqTndNQzlqYjI1MFpYaDBMbXB6YjI0aVhTd2lkSGx3WlNJNld5SldaWEpwWm1saFlteGxRM0psWkdWdWRHbGhiQ0lzSWxabGNtbG1hV0ZpYkdWRGNtVmtaVzUwYVdGc1JYaDBaVzV6YVc5dUlpd2lUM0JsYmtKaFpHZGxRM0psWkdWdWRHbGhiQ0pkTENKcGMzTjFaWElpT25zaWJtRnRaU0k2SWtwdlluTWdabTl5SUhSb1pTQkdkWFIxY21VZ0tFcEdSaWtpTENKcFkyOXVWWEpzSWpvaWFIUjBjSE02THk5M00yTXRZMk5uTG1kcGRHaDFZaTVwYnk5Mll5MWxaQzl3YkhWblptVnpkQzB4TFRJd01qSXZhVzFoWjJWekwwcEdSbDlNYjJkdlRHOWphM1Z3TG5CdVp5SXNJbWx0WVdkbElqb2lhSFIwY0hNNkx5OTNNMk10WTJObkxtZHBkR2gxWWk1cGJ5OTJZeTFsWkM5d2JIVm5abVZ6ZEMweExUSXdNakl2YVcxaFoyVnpMMHBHUmw5TWIyZHZURzlqYTNWd0xuQnVaeUo5TENKdVlXMWxJam9pU2taR0lIZ2dkbU10WldSMUlGQnNkV2RHWlhOMElESWlMQ0prWlhOamNtbHdkR2x2YmlJNklrMUJWRlJTSjNNZ2MzVmliV2x6YzJsdmJpQm1iM0lnU2taR0lGQnNkV2RtWlhOMElESWlMQ0pqY21Wa1pXNTBhV0ZzUW5KaGJtUnBibWNpT25zaVltRmphMmR5YjNWdVpFTnZiRzl5SWpvaUl6UTJOR00wT1NKOUxDSmpjbVZrWlc1MGFXRnNVM1ZpYW1WamRDSTZleUowZVhCbElqcGJJa0ZqYUdsbGRtVnRaVzUwVTNWaWFtVmpkQ0pkTENKaFkyaHBaWFpsYldWdWRDSTZleUpwWkNJNkluVnlianAxZFdsa09tSmtObVE1TXpFMkxXWTNZV1V0TkRBM015MWhNV1UxTFRKbU4yWTFZbVF5TWpreU1pSXNJbTVoYldVaU9pSktSa1lnZUNCMll5MWxaSFVnVUd4MVowWmxjM1FnTWlCSmJuUmxjbTl3WlhKaFltbHNhWFI1SWl3aWRIbHdaU0k2V3lKQlkyaHBaWFpsYldWdWRDSmRMQ0pwYldGblpTSTZleUpwWkNJNkltaDBkSEJ6T2k4dmR6TmpMV05qWnk1bmFYUm9kV0l1YVc4dmRtTXRaV1F2Y0d4MVoyWmxjM1F0TWkweU1ESXlMMmx0WVdkbGN5OUtSa1l0VmtNdFJVUlZMVkJNVlVkR1JWTlVNaTFpWVdSblpTMXBiV0ZuWlM1d2JtY2lMQ0owZVhCbElqb2lTVzFoWjJVaWZTd2lZM0pwZEdWeWFXRWlPbnNpZEhsd1pTSTZJa055YVhSbGNtbGhJaXdpYm1GeWNtRjBhWFpsSWpvaVUyOXNkWFJwYjI1eklIQnliM1pwWkdWeWN5QmxZWEp1WldRZ2RHaHBjeUJpWVdSblpTQmllU0JrWlcxdmJuTjBjbUYwYVc1bklHbHVkR1Z5YjNCbGNtRmlhV3hwZEhrZ1ltVjBkMlZsYmlCdGRXeDBhWEJzWlNCd2NtOTJhV1JsY25NZ1ltRnpaV1FnYjI0Z2RHaGxJRTlDZGpNZ1kyRnVaR2xrWVhSbElHWnBibUZzSUhOMFlXNWtZWEprTENCM2FYUm9JSE52YldVZ1lXUmthWFJwYjI1aGJDQnlaWEYxYVhKbFpDQm1hV1ZzWkhNdUlFTnlaV1JsYm5ScFlXd2dhWE56ZFdWeWN5QmxZWEp1YVc1bklIUm9hWE1nWW1Ga1oyVWdjM1ZqWTJWemMyWjFiR3g1SUdsemMzVmxaQ0JoSUdOeVpXUmxiblJwWVd3Z2FXNTBieUJoZENCc1pXRnpkQ0IwZDI4Z2QyRnNiR1YwY3k0Z0lGZGhiR3hsZENCcGJYQnNaVzFsYm5SbGNuTWdaV0Z5Ym1sdVp5QjBhR2x6SUdKaFpHZGxJSE4xWTJObGMzTm1kV3hzZVNCa2FYTndiR0Y1WldRZ1kzSmxaR1Z1ZEdsaGJITWdhWE56ZFdWa0lHSjVJR0YwSUd4bFlYTjBJSFIzYnlCa2FXWm1aWEpsYm5RZ1kzSmxaR1Z1ZEdsaGJDQnBjM04xWlhKekxpSjlMQ0prWlhOamNtbHdkR2x2YmlJNklsUm9hWE1nWTNKbFpHVnVkR2xoYkNCemIyeDFkR2x2YmlCemRYQndiM0owY3lCMGFHVWdkWE5sSUc5bUlFOUNkak1nWVc1a0lIY3pZeUJXWlhKcFptbGhZbXhsSUVOeVpXUmxiblJwWVd4eklHRnVaQ0JwY3lCcGJuUmxjbTl3WlhKaFlteGxJSGRwZEdnZ1lYUWdiR1ZoYzNRZ2RIZHZJRzkwYUdWeUlITnZiSFYwYVc5dWN5NGdJRlJvYVhNZ2QyRnpJR1JsYlc5dWMzUnlZWFJsWkNCemRXTmpaWE56Wm5Wc2JIa2daSFZ5YVc1bklFcEdSaUI0SUhaakxXVmtkU0JRYkhWblJtVnpkQ0F5TGlKOWZYMHNJbWx6Y3lJNkltUnBaRHBxZDJzNlpYbEtjbVJJYTJsUGFVcEdVWGxKYzBsdFRubGthVWsyU1d4QmRFMXFWVEpKYVhkcFpVTkpOa2x1Y0ZKVU1qa3pVMVZOZUZveFpFdGtSMUpyV2tWSk1WSXdSakJPUjNob1pGUmFUV1JFYUVwaFNHc3pUbnBHY0ZGWFdtaGlVekI0WTBkTmFVeERTalZKYW05cFdUSndSVmg2WkhaTk1tUnJWVlJHTWxveWJGSmxWRTVtWXpBeFNHTjZaRmhqYm1SRVZGWlZOVkpzUmxwaFZ6RkNUVEJvTkdKck1YTmtlVW81SWl3aWMzVmlJam9pWkdsa09tdGxlVHA2TmsxcmNXZHJUSEpTZVV4bk5tSnhhekkzWkdwM1ltSmhVVmRuWVZOWlowWldRMHR4T1ZsTGVGcGlUbXR3Vm5ZaUxDSnVZbVlpT2pFMk56UTJOalU0T0RaOS5hbkFCeHY0MjRlTXBwMHhnYlR4NmFadlp4YmxrU1RocS1YYmdpeGhXZWdGQ1Z6MlEtRXRSVWlHSnVPVWptcWw1VHR0VFpfWWd0TjlQZ296T2Z1VFp0ZyJdfSwibm9uY2UiOiJkYWY5NDJhZC04MTZmLTQ1ZWUtYTlmYy1mYWNkMDhlNWFiY2EiLCJpc3MiOiJkaWQ6a2V5Ono2TWtxZ2tMclJ5TGc2YnFrMjdkandiYmFRV2dhU1lnRlZDS3E5WUt4WmJOa3BWdiIsImF1ZCI6ImV4YW1wbGUuY29tIiwianRpIjoidXJuOjIxZmYyMWYxLTNjZjktNGZhMy04OGI0LWEwNDVlZmJiMWI1ZiJ9.ar3YGkn333XW8_624RfW2DlA2XuLNJAUk9OrSAvS6RtoqVVzH_TWklvCq1BT-Mot3j56cERx748qWyKhDAm1Dw' diff --git a/packages/core/src/modules/vc/jwt-vc/__tests__/fixtures/jwt-vc-presentation-profile.ts b/packages/core/src/modules/vc/jwt-vc/__tests__/fixtures/jwt-vc-presentation-profile.ts new file mode 100644 index 0000000000..6018d3c8fa --- /dev/null +++ b/packages/core/src/modules/vc/jwt-vc/__tests__/fixtures/jwt-vc-presentation-profile.ts @@ -0,0 +1,2 @@ +export const didIonJwtVcPresentationProfileJwtVc = + 'eyJraWQiOiJkaWQ6aW9uOkVpQkFBOTlUQWV6eEtSYzJ3dXVCbnI0enpHc1MyWWNzT0E0SVBRVjBLWTY0WGc6ZXlKa1pXeDBZU0k2ZXlKd1lYUmphR1Z6SWpwYmV5SmhZM1JwYjI0aU9pSnlaWEJzWVdObElpd2laRzlqZFcxbGJuUWlPbnNpY0hWaWJHbGpTMlY1Y3lJNlczc2lhV1FpT2lKclpYa3RNU0lzSW5CMVlteHBZMHRsZVVwM2F5STZleUpqY25ZaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpZUNJNklrZG5Xa2RVWnpobFEyRTNiRll5T0UxTU9VcFViVUpWZG1zM1JGbENZbVpTUzFkTWFIYzJOVXB2TVhNaUxDSnJhV1FpT2lKclpYa3RNU0o5TENKd2RYSndiM05sY3lJNld5SmhkWFJvWlc1MGFXTmhkR2x2YmlKZExDSjBlWEJsSWpvaVNuTnZibGRsWWt0bGVUSXdNakFpZlYxOWZWMHNJblZ3WkdGMFpVTnZiVzFwZEcxbGJuUWlPaUpGYVVSS1YwWjJXVUo1UXpkMmF6QTJNWEF6ZEhZd2QyOVdTVGs1TVRGUVRHZ3dVVnA0Y1dwWk0yWTRNVkZSSW4wc0luTjFabVpwZUVSaGRHRWlPbnNpWkdWc2RHRklZWE5vSWpvaVJXbEJYMVJ2VmxOQlpEQlRSV3hPVTJWclExazFVRFZIWjAxS1F5MU1UVnBGWTJaU1YyWnFaR05hWVhKRlFTSXNJbkpsWTI5MlpYSjVRMjl0YldsMGJXVnVkQ0k2SWtWcFJETjBaVFY0ZUZsaWVtSm9kMHBZZEVVd1oydFpWM1ozTWxaMlZGQjRNVTlsYTBSVGNYZHVaelJUV21jaWZYMCNrZXktMSIsInR5cCI6IkpXVCIsImFsZyI6IkVkRFNBIn0.eyJzdWIiOiJkaWQ6aW9uOkVpQWVNNk5vOWtkcG9zNl9laEJVRGg0UklOWTRVU0RNaC1RZFdrc21zSTNXa0E6ZXlKa1pXeDBZU0k2ZXlKd1lYUmphR1Z6SWpwYmV5SmhZM1JwYjI0aU9pSnlaWEJzWVdObElpd2laRzlqZFcxbGJuUWlPbnNpY0hWaWJHbGpTMlY1Y3lJNlczc2lhV1FpT2lKclpYa3RNU0lzSW5CMVlteHBZMHRsZVVwM2F5STZleUpqY25ZaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpZUNJNkluY3dOazlXTjJVMmJsUjFjblEyUnpsV2NGWlllRWwzV1c1NWFtWjFjSGhsUjNsTFFsTXRZbXh4ZG1jaUxDSnJhV1FpT2lKclpYa3RNU0o5TENKd2RYSndiM05sY3lJNld5SmhkWFJvWlc1MGFXTmhkR2x2YmlKZExDSjBlWEJsSWpvaVNuTnZibGRsWWt0bGVUSXdNakFpZlYxOWZWMHNJblZ3WkdGMFpVTnZiVzFwZEcxbGJuUWlPaUpGYVVGU05HUlZRbXhxTldOR2EzZE1ka3BUV1VZelZFeGpMVjgxTVdoRFgyeFphR3hYWmt4V1oyOXNlVFJSSW4wc0luTjFabVpwZUVSaGRHRWlPbnNpWkdWc2RHRklZWE5vSWpvaVJXbEVjVkp5V1U1ZlYzSlRha0ZRZG5sRllsSlFSVms0V1ZoUFJtTnZUMFJUWkV4VVRXSXRNMkZLVkVsR1FTSXNJbkpsWTI5MlpYSjVRMjl0YldsMGJXVnVkQ0k2SWtWcFFVd3lNRmRZYWtwUVFXNTRXV2RRWTFVNVJWOVBPRTFPZEhOcFFrMDBRa3RwYVZOd1QzWkZUV3BWT1VFaWZYMCIsIm5iZiI6MTY3NDc3MjA2MywiaXNzIjoiZGlkOmlvbjpFaUJBQTk5VEFlenhLUmMyd3V1Qm5yNHp6R3NTMlljc09BNElQUVYwS1k2NFhnOmV5SmtaV3gwWVNJNmV5SndZWFJqYUdWeklqcGJleUpoWTNScGIyNGlPaUp5WlhCc1lXTmxJaXdpWkc5amRXMWxiblFpT25zaWNIVmliR2xqUzJWNWN5STZXM3NpYVdRaU9pSnJaWGt0TVNJc0luQjFZbXhwWTB0bGVVcDNheUk2ZXlKamNuWWlPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aWVDSTZJa2RuV2tkVVp6aGxRMkUzYkZZeU9FMU1PVXBVYlVKVmRtczNSRmxDWW1aU1MxZE1hSGMyTlVwdk1YTWlMQ0pyYVdRaU9pSnJaWGt0TVNKOUxDSndkWEp3YjNObGN5STZXeUpoZFhSb1pXNTBhV05oZEdsdmJpSmRMQ0owZVhCbElqb2lTbk52YmxkbFlrdGxlVEl3TWpBaWZWMTlmVjBzSW5Wd1pHRjBaVU52YlcxcGRHMWxiblFpT2lKRmFVUktWMFoyV1VKNVF6ZDJhekEyTVhBemRIWXdkMjlXU1RrNU1URlFUR2d3VVZwNGNXcFpNMlk0TVZGUkluMHNJbk4xWm1acGVFUmhkR0VpT25zaVpHVnNkR0ZJWVhOb0lqb2lSV2xCWDFSdlZsTkJaREJUUld4T1UyVnJRMWsxVURWSFowMUtReTFNVFZwRlkyWlNWMlpxWkdOYVlYSkZRU0lzSW5KbFkyOTJaWEo1UTI5dGJXbDBiV1Z1ZENJNklrVnBSRE4wWlRWNGVGbGllbUpvZDBwWWRFVXdaMnRaVjNaM01sWjJWRkI0TVU5bGEwUlRjWGR1WnpSVFdtY2lmWDAiLCJpYXQiOjE2NzQ3NzIwNjMsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOlwvXC93d3cudzMub3JnXC8yMDE4XC9jcmVkZW50aWFsc1wvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlZlcmlmaWVkRW1wbG95ZWUiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiZGlzcGxheU5hbWUiOiJQYXQgU21pdGgiLCJnaXZlbk5hbWUiOiJQYXQiLCJqb2JUaXRsZSI6IldvcmtlciIsInN1cm5hbWUiOiJTbWl0aCIsInByZWZlcnJlZExhbmd1YWdlIjoiZW4tVVMiLCJtYWlsIjoicGF0LnNtaXRoQGV4YW1wbGUuY29tIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczpcL1wvZXhhbXBsZS5jb21cL2FwaVwvYXN0YXR1c2xpc3RcL2RpZDppb246RWlCQUE5OVRBZXp4S1JjMnd1dUJucjR6ekdzUzJZY3NPQTRJUFFWMEtZNjRYZ1wvMSMwIiwidHlwZSI6IlJldm9jYXRpb25MaXN0MjAyMVN0YXR1cyIsInN0YXR1c0xpc3RJbmRleCI6IjAiLCJzdGF0dXNMaXN0Q3JlZGVudGlhbCI6Imh0dHBzOlwvXC9leGFtcGxlLmNvbVwvYXBpXC9hc3RhdHVzbGlzdFwvZGlkOmlvbjpFaUJBQTk5VEFlenhLUmMyd3V1Qm5yNHp6R3NTMlljc09BNElQUVYwS1k2NFhnXC8xIn19LCJqdGkiOiJiODA1MmY5Yy00ZjhjLTQzMzAtYmJjMS00MDMzYjhlZTVkNmIifQ.VEiKCr3RVScUMF81FxgrGCldYxKIJc4ucLX3z0xakml_GOxnnvwko3C6Qqj7JMUI9K7vQUUMVjI81KxktYt0AQ' diff --git a/packages/core/src/modules/vc/jwt-vc/__tests__/fixtures/transmute-verifiable-data.ts b/packages/core/src/modules/vc/jwt-vc/__tests__/fixtures/transmute-verifiable-data.ts new file mode 100644 index 0000000000..bf5cb0ab68 --- /dev/null +++ b/packages/core/src/modules/vc/jwt-vc/__tests__/fixtures/transmute-verifiable-data.ts @@ -0,0 +1,15 @@ +// + +/** + * JWT VC from the Transmute verifiable data package + * @see https://github.com/transmute-industries/verifiable-data/blob/e89a530073862f31e7f1d2904c53179625c2cd14/packages/vc.js/src/vc-jwt/__tests__/aud-and-nonce/__fixtures__/presentation.json#LL5C6-L5C772 + */ +export const didKeyTransmuteJwtVc = + 'eyJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtva3JzVm84RGJHRHNuTUFqbm9IaEpvdE1iRFppSGZ2eE00ajY1ZDhwclhVciIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImlzc3VlciI6eyJpZCI6ImRpZDprZXk6ejZNa29rcnNWbzhEYkdEc25NQWpub0hoSm90TWJEWmlIZnZ4TTRqNjVkOHByWFVyIn0sImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMTk6MjM6MjRaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEifX0sImp0aSI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8zNzMyIiwibmJmIjoxMjYyMzczODA0fQ.suzrfmzM07yiiibK0vOdP9Q0dARA7XVNRUa9DSbH519EWrUDgzsq6SiIG9yyBt39yaqsZc1-8byyuMrPziyWBg' + +/** + * JWT VP from the Transmute verifiable data package + * @see https://github.com/transmute-industries/verifiable-data/blob/e89a530073862f31e7f1d2904c53179625c2cd14/packages/vc.js/src/vc-jwt/__tests__/EdDSA.transmute/__fixtures__/verifiablePresentation.json + */ +export const didKeyTransmuteJwtVp = + 'eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa29rcnNWbzhEYkdEc25NQWpub0hoSm90TWJEWmlIZnZ4TTRqNjVkOHByWFVyI3o2TWtva3JzVm84RGJHRHNuTUFqbm9IaEpvdE1iRFppSGZ2eE00ajY1ZDhwclhVciJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtva3JzVm84RGJHRHNuTUFqbm9IaEpvdE1iRFppSGZ2eE00ajY1ZDhwclhVciIsInN1YiI6ImRpZDprZXk6ejZNa29rcnNWbzhEYkdEc25NQWpub0hoSm90TWJEWmlIZnZ4TTRqNjVkOHByWFVyIiwidnAiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIl0sInZlcmlmaWFibGVDcmVkZW50aWFsIjpbImV5SmhiR2NpT2lKRlpFUlRRU0o5LmV5SnBjM01pT2lKa2FXUTZhMlY1T25vMlRXdHZhM0p6Vm04NFJHSkhSSE51VFVGcWJtOUlhRXB2ZEUxaVJGcHBTR1oyZUUwMGFqWTFaRGh3Y2xoVmNpSXNJbk4xWWlJNkltUnBaRHBsZUdGdGNHeGxPbVZpWm1WaU1XWTNNVEpsWW1NMlpqRmpNamMyWlRFeVpXTXlNU0lzSW5aaklqcDdJa0JqYjI1MFpYaDBJanBiSW1oMGRIQnpPaTh2ZDNkM0xuY3pMbTl5Wnk4eU1ERTRMMk55WldSbGJuUnBZV3h6TDNZeElsMHNJbWxrSWpvaWFIUjBjRG92TDJWNFlXMXdiR1V1WldSMUwyTnlaV1JsYm5ScFlXeHpMek0zTXpJaUxDSjBlWEJsSWpwYklsWmxjbWxtYVdGaWJHVkRjbVZrWlc1MGFXRnNJbDBzSW1semMzVmxjaUk2ZXlKcFpDSTZJbVJwWkRwclpYazZlalpOYTI5cmNuTldiemhFWWtkRWMyNU5RV3B1YjBob1NtOTBUV0pFV21sSVpuWjRUVFJxTmpWa09IQnlXRlZ5SW4wc0ltbHpjM1ZoYm1ObFJHRjBaU0k2SWpJd01UQXRNREV0TURGVU1UazZNak02TWpSYUlpd2lZM0psWkdWdWRHbGhiRk4xWW1wbFkzUWlPbnNpYVdRaU9pSmthV1E2WlhoaGJYQnNaVHBsWW1abFlqRm1OekV5WldKak5tWXhZekkzTm1VeE1tVmpNakVpZlgwc0ltcDBhU0k2SW1oMGRIQTZMeTlsZUdGdGNHeGxMbVZrZFM5amNtVmtaVzUwYVdGc2N5OHpOek15SWl3aWJtSm1Jam94TWpZeU16Y3pPREEwZlEuc3V6cmZtek0wN3lpaWliSzB2T2RQOVEwZEFSQTdYVk5SVWE5RFNiSDUxOUVXclVEZ3pzcTZTaUlHOXl5QnQzOXlhcXNaYzEtOGJ5eXVNclB6aXlXQmciXSwiaG9sZGVyIjp7ImlkIjoiZGlkOmtleTp6Nk1rb2tyc1ZvOERiR0Rzbk1Bam5vSGhKb3RNYkRaaUhmdnhNNGo2NWQ4cHJYVXIifX0sImF1ZCI6ImV4YW1wbGUuY29tIiwibm9uY2UiOiIxMjMifQ.kYN5KGZNiDuorA9UutExMl06q8fK-QI4zuaWa9-s1gL8jrMfl6q0atkDLyVoqTxdky_oBBmrGYy7FhtnSrDtBQ' diff --git a/packages/core/src/modules/vc/jwt-vc/__tests__/presentationTransformer.test.ts b/packages/core/src/modules/vc/jwt-vc/__tests__/presentationTransformer.test.ts new file mode 100644 index 0000000000..88c1bedf82 --- /dev/null +++ b/packages/core/src/modules/vc/jwt-vc/__tests__/presentationTransformer.test.ts @@ -0,0 +1,127 @@ +import { JwtPayload } from '../../../../crypto/jose/jwt' +import { JsonTransformer } from '../../../../utils' +import { W3cPresentation } from '../../models' +import { W3cJwtVerifiableCredential } from '../W3cJwtVerifiableCredential' +import { getJwtPayloadFromPresentation, getPresentationFromJwtPayload } from '../presentationTransformer' + +import { CredoEs256DidJwkJwtVc } from './fixtures/credo-jwt-vc' + +describe('presentationTransformer', () => { + describe('getJwtPayloadFromPresentation', () => { + test('extracts jwt payload from presentation', () => { + const presentation = new W3cPresentation({ + id: 'urn:123', + holder: 'did:example:123', + verifiableCredential: [W3cJwtVerifiableCredential.fromSerializedJwt(CredoEs256DidJwkJwtVc)], + }) + + const jwtPayload = getJwtPayloadFromPresentation(presentation) + + expect(jwtPayload.toJson()).toEqual({ + vp: { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [CredoEs256DidJwkJwtVc], + }, + iss: 'did:example:123', + jti: 'urn:123', + sub: undefined, + aud: undefined, + exp: undefined, + iat: undefined, + }) + }) + }) + + describe('getPresentationFromJwtPayload', () => { + test('extracts presentation from jwt payload', () => { + const vp: Record = { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [CredoEs256DidJwkJwtVc], + id: 'urn:123', + holder: 'did:example:123', + } + + const jwtPayload = new JwtPayload({ + iss: 'did:example:123', + nbf: undefined, + exp: undefined, + sub: undefined, + jti: 'urn:123', + additionalClaims: { + vp, + }, + }) + + const presentation = JsonTransformer.toJSON(getPresentationFromJwtPayload(jwtPayload)) + + expect(presentation).toEqual({ + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + id: 'urn:123', + holder: 'did:example:123', + verifiableCredential: [CredoEs256DidJwkJwtVc], + }) + }) + + test(`throw error if jwt payload does not contain 'vp' property or it is not an object`, () => { + const jwtPayload = new JwtPayload({}) + + expect(() => getPresentationFromJwtPayload(jwtPayload)).toThrowError("JWT does not contain a valid 'vp' claim") + + jwtPayload.additionalClaims.vp = 'invalid' + expect(() => getPresentationFromJwtPayload(jwtPayload)).toThrowError("JWT does not contain a valid 'vp' claim") + }) + + test(`throw error if jwt vp has an id and it does not match the jti`, () => { + const vp: Record = { + id: '13', + } + const jwtPayload = new JwtPayload({ + jti: '12', + additionalClaims: { + vp, + }, + }) + + expect(() => getPresentationFromJwtPayload(jwtPayload)).toThrowError('JWT jti and vp.id do not match') + }) + + test(`throw error if jwt vp has an holder id and it does not match the iss`, () => { + const vp: Record = { + holder: '123', + } + const jwtPayload = new JwtPayload({ + iss: 'iss', + additionalClaims: { + vp, + }, + }) + + expect(() => getPresentationFromJwtPayload(jwtPayload)).toThrowError('JWT iss and vp.holder(.id) do not match') + + // nested holder object + vp.holder = { id: '123' } + expect(() => getPresentationFromJwtPayload(jwtPayload)).toThrowError('JWT iss and vp.holder(.id) do not match') + }) + + test(`throw validation error if vp is not a valid w3c vp`, () => { + const vp: Record = { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation2'], + verifiableCredential: [CredoEs256DidJwkJwtVc], + } + + const jwtPayload = new JwtPayload({ + additionalClaims: { + vp, + }, + }) + + expect(() => getPresentationFromJwtPayload(jwtPayload)).toThrowError( + 'property type has failed the following constraints: type must be an array of strings which includes "VerifiablePresentation"' + ) + }) + }) +}) diff --git a/packages/core/src/modules/vc/jwt-vc/credentialTransformer.ts b/packages/core/src/modules/vc/jwt-vc/credentialTransformer.ts new file mode 100644 index 0000000000..5a13b15619 --- /dev/null +++ b/packages/core/src/modules/vc/jwt-vc/credentialTransformer.ts @@ -0,0 +1,164 @@ +import type { JwtPayloadOptions } from '../../../crypto/jose/jwt' +import type { W3cJsonCredential } from '../models/credential/W3cJsonCredential' + +import { isObject } from 'class-validator' + +import { JwtPayload } from '../../../crypto/jose/jwt' +import { CredoError } from '../../../error' +import { JsonTransformer, isJsonObject } from '../../../utils' +import { W3cCredential } from '../models/credential/W3cCredential' +import { w3cDate } from '../util' + +export function getJwtPayloadFromCredential(credential: W3cCredential) { + const vc = JsonTransformer.toJSON(credential) as Partial + + const payloadOptions: JwtPayloadOptions = { + additionalClaims: { + vc, + }, + } + + // Extract `nbf` and remove issuance date from vc + const issuanceDate = Date.parse(credential.issuanceDate) + if (isNaN(issuanceDate)) { + throw new CredoError('JWT VCs must have a valid issuance date') + } + payloadOptions.nbf = Math.floor(issuanceDate / 1000) + delete vc.issuanceDate + + // Extract `exp` and remove expiration date from vc + if (credential.expirationDate) { + const expirationDate = Date.parse(credential.expirationDate) + if (!isNaN(expirationDate)) { + payloadOptions.exp = Math.floor(expirationDate / 1000) + delete vc.expirationDate + } + } + + // Extract `iss` and remove issuer id from vc + payloadOptions.iss = credential.issuerId + if (typeof vc.issuer === 'string') { + delete vc.issuer + } else if (typeof vc.issuer === 'object') { + delete vc.issuer.id + if (Object.keys(vc.issuer).length === 0) { + delete vc.issuer + } + } + + // Extract `jti` and remove id from vc + if (credential.id) { + payloadOptions.jti = credential.id + delete vc.id + } + + if (Array.isArray(credential.credentialSubject) && credential.credentialSubject.length !== 1) { + throw new CredoError('JWT VCs must have exactly one credential subject') + } + + // Extract `sub` and remove credential subject id from vc + const [credentialSubjectId] = credential.credentialSubjectIds + if (credentialSubjectId) { + payloadOptions.sub = credentialSubjectId + + if (Array.isArray(vc.credentialSubject)) { + delete vc.credentialSubject[0].id + } else { + delete vc.credentialSubject?.id + } + } + + return new JwtPayload(payloadOptions) +} + +export function getCredentialFromJwtPayload(jwtPayload: JwtPayload) { + if (!('vc' in jwtPayload.additionalClaims) || !isJsonObject(jwtPayload.additionalClaims.vc)) { + throw new CredoError("JWT does not contain a valid 'vc' claim") + } + + const jwtVc = jwtPayload.additionalClaims.vc + + if (!jwtPayload.nbf || !jwtPayload.iss) { + throw new CredoError("JWT does not contain valid 'nbf' and 'iss' claims") + } + + if (Array.isArray(jwtVc.credentialSubject) && jwtVc.credentialSubject.length !== 1) { + throw new CredoError('JWT VCs must have exactly one credential subject') + } + + if (Array.isArray(jwtVc.credentialSubject) && !isObject(jwtVc.credentialSubject[0])) { + throw new CredoError('JWT VCs must have a credential subject of type object') + } + + const credentialSubject = Array.isArray(jwtVc.credentialSubject) + ? jwtVc.credentialSubject[0] + : jwtVc.credentialSubject + if (!isJsonObject(credentialSubject)) { + throw new CredoError('JWT VC does not have a valid credential subject') + } + const subjectWithId = jwtPayload.sub ? { ...credentialSubject, id: jwtPayload.sub } : credentialSubject + + // Validate vc.id and jti + if (jwtVc.id && jwtPayload.jti !== jwtVc.id) { + throw new CredoError('JWT jti and vc.id do not match') + } + + // Validate vc.issuer and iss + if ( + (typeof jwtVc.issuer === 'string' && jwtPayload.iss !== jwtVc.issuer) || + (isJsonObject(jwtVc.issuer) && jwtVc.issuer.id && jwtPayload.iss !== jwtVc.issuer.id) + ) { + throw new CredoError('JWT iss and vc.issuer(.id) do not match') + } + + // Validate vc.issuanceDate and nbf + if (jwtVc.issuanceDate) { + if (typeof jwtVc.issuanceDate !== 'string') { + throw new CredoError('JWT vc.issuanceDate must be a string') + } + + const issuanceDate = Date.parse(jwtVc.issuanceDate) / 1000 + if (jwtPayload.nbf !== issuanceDate) { + throw new CredoError('JWT nbf and vc.issuanceDate do not match') + } + } + + // Validate vc.expirationDate and exp + if (jwtVc.expirationDate) { + if (typeof jwtVc.expirationDate !== 'string') { + throw new CredoError('JWT vc.expirationDate must be a string') + } + + const expirationDate = Date.parse(jwtVc.expirationDate) / 1000 + if (expirationDate !== jwtPayload.exp) { + throw new CredoError('JWT exp and vc.expirationDate do not match') + } + } + + // Validate vc.credentialSubject.id and sub + if ( + (isJsonObject(jwtVc.credentialSubject) && + jwtVc.credentialSubject.id && + jwtPayload.sub !== jwtVc.credentialSubject.id) || + (Array.isArray(jwtVc.credentialSubject) && + isJsonObject(jwtVc.credentialSubject[0]) && + jwtVc.credentialSubject[0].id && + jwtPayload.sub !== jwtVc.credentialSubject[0].id) + ) { + throw new CredoError('JWT sub and vc.credentialSubject.id do not match') + } + + // Create a verifiable credential structure that is compatible with the VC data model + const dataModelVc = { + ...jwtVc, + issuanceDate: w3cDate(jwtPayload.nbf * 1000), + expirationDate: jwtPayload.exp ? w3cDate(jwtPayload.exp * 1000) : undefined, + issuer: typeof jwtVc.issuer === 'object' ? { ...jwtVc.issuer, id: jwtPayload.iss } : jwtPayload.iss, + id: jwtPayload.jti, + credentialSubject: Array.isArray(jwtVc.credentialSubject) ? [subjectWithId] : subjectWithId, + } + + const vcInstance = JsonTransformer.fromJSON(dataModelVc, W3cCredential) + + return vcInstance +} diff --git a/packages/core/src/modules/vc/jwt-vc/index.ts b/packages/core/src/modules/vc/jwt-vc/index.ts new file mode 100644 index 0000000000..89f624da26 --- /dev/null +++ b/packages/core/src/modules/vc/jwt-vc/index.ts @@ -0,0 +1,3 @@ +export * from './W3cJwtCredentialService' +export * from './W3cJwtVerifiableCredential' +export * from './W3cJwtVerifiablePresentation' diff --git a/packages/core/src/modules/vc/jwt-vc/presentationTransformer.ts b/packages/core/src/modules/vc/jwt-vc/presentationTransformer.ts new file mode 100644 index 0000000000..9b70b3ecf4 --- /dev/null +++ b/packages/core/src/modules/vc/jwt-vc/presentationTransformer.ts @@ -0,0 +1,70 @@ +import type { JwtPayloadOptions } from '../../../crypto/jose/jwt' +import type { W3cJsonPresentation } from '../models/presentation/W3cJsonPresentation' + +import { JwtPayload } from '../../../crypto/jose/jwt' +import { CredoError } from '../../../error' +import { JsonTransformer, isJsonObject } from '../../../utils' +import { W3cPresentation } from '../models/presentation/W3cPresentation' + +export function getJwtPayloadFromPresentation(presentation: W3cPresentation) { + const vp = JsonTransformer.toJSON(presentation) as Partial + + const payloadOptions: JwtPayloadOptions = { + additionalClaims: { + vp, + }, + } + + // Extract `iss` and remove holder id from vp + if (presentation.holderId) { + payloadOptions.iss = presentation.holderId + + if (typeof vp.holder === 'string') { + delete vp.holder + } else if (typeof vp.holder === 'object') { + delete vp.holder.id + if (Object.keys(vp.holder).length === 0) { + delete vp.holder + } + } + } + + // Extract `jti` and remove id from vp + if (presentation.id) { + payloadOptions.jti = presentation.id + delete vp.id + } + + return new JwtPayload(payloadOptions) +} + +export function getPresentationFromJwtPayload(jwtPayload: JwtPayload) { + if (!('vp' in jwtPayload.additionalClaims) || !isJsonObject(jwtPayload.additionalClaims.vp)) { + throw new CredoError("JWT does not contain a valid 'vp' claim") + } + + const jwtVp = jwtPayload.additionalClaims.vp + + // Validate vp.id and jti + if (jwtVp.id && jwtPayload.jti !== jwtVp.id) { + throw new CredoError('JWT jti and vp.id do not match') + } + + // Validate vp.holder and iss + if ( + (typeof jwtVp.holder === 'string' && jwtPayload.iss !== jwtVp.holder) || + (isJsonObject(jwtVp.holder) && jwtVp.holder.id && jwtPayload.iss !== jwtVp.holder.id) + ) { + throw new CredoError('JWT iss and vp.holder(.id) do not match') + } + + const dataModelVp = { + ...jwtVp, + id: jwtPayload.jti, + holder: jwtPayload.iss, + } + + const vpInstance = JsonTransformer.fromJSON(dataModelVp, W3cPresentation) + + return vpInstance +} diff --git a/packages/core/src/modules/vc/models/ClaimFormat.ts b/packages/core/src/modules/vc/models/ClaimFormat.ts new file mode 100644 index 0000000000..47e1b48c52 --- /dev/null +++ b/packages/core/src/modules/vc/models/ClaimFormat.ts @@ -0,0 +1,16 @@ +/** + * Defines the claim format based on the formats registered in + * [DIF Claim Format Registry](https://identity.foundation/claim-format-registry/). + */ +export enum ClaimFormat { + Jwt = 'jwt', + JwtVc = 'jwt_vc', + JwtVp = 'jwt_vp', + Ldp = 'ldp', + LdpVc = 'ldp_vc', + LdpVp = 'ldp_vp', + Di = 'di', + DiVc = 'di_vc', + DiVp = 'di_vp', + SdJwtVc = 'vc+sd-jwt', +} diff --git a/packages/core/src/modules/vc/models/W3cVerifyResult.ts b/packages/core/src/modules/vc/models/W3cVerifyResult.ts new file mode 100644 index 0000000000..d89ba2ae97 --- /dev/null +++ b/packages/core/src/modules/vc/models/W3cVerifyResult.ts @@ -0,0 +1,121 @@ +export type W3cVerifyPresentationResult = W3cVerifyResult +export type W3cVerifyCredentialResult = W3cVerifyResult + +export type SingleValidationResult = { isValid: boolean; error?: Error } + +interface W3cVerifyResult { + /** + * Whether the verification as a whole is valid. This means that + * all validations inside the validations object should have passed. + */ + isValid: boolean + + /** + * Validations that have been performed + */ + validations: Partial + + /** + * Error that was caught during verification not related to + * any of the specific validations that are performed + */ + error?: Error +} + +interface W3cCredentialValidations { + /** + * Validation that validates whether the credential conforms + * to the data model and is currently valid (not expired or + * issued in the future). + */ + dataModel: SingleValidationResult + + /** + * Whether the signature of the credential is valid + */ + signature: SingleValidationResult + + /** + * Whether the credential status is still valid, meaning + * that is hasn't been revoked yet. + */ + credentialStatus: SingleValidationResult + + /** + * Whether the 'issuer' of the credential is also the + * signer of the credential proof + */ + issuerIsSigner: SingleValidationResult + + /** + * NOTE: this validation is currently only present for ldp_vc credentials. + * When this validation is present, ALL OTHER validations will be skipped. + * + * Whether the presentation is valid according to the [vc.js](https://github.com/digitalbazaar/vc) + * library. As the library handles all validations, it is not possible to include the other + * validation items separately. In the future the vc.js library will be replaced to provide a similar + * validation result for all credential formats. + */ + vcJs: SingleValidationResult +} + +interface W3cPresentationValidations { + /** + * Validation that validates whether the presentation conforms + * to the data model. + */ + dataModel: SingleValidationResult + + /** + * Whether the signature of the presentation is valid + */ + presentationSignature: SingleValidationResult + + /** + * Validation results of the credentials inside the presentation. + * The order matches the order of credentials in the presentation. + * + * This object extends the credential verification result with the exception that + * a new `credentialSubjectAuthentication` has been added. + */ + credentials: W3cVerifyResult< + W3cCredentialValidations & { + /** + * Whether the credential subject authentication is valid. Note this only + * takes into account credentialSubject authentication, and not cases where + * the holder of a credential may be different from the credential subject. + * + * The credentialSubject authentication is deemed valid in the following cases: + * - The credential has no credential subject identifiers. In this case the + * credential is seen as a bearer credential and thus authentication is not needed. + * - The credential has AT LEAST one credential subject id, and the presentation + * is signed by at least one of the credential subject ids. + */ + credentialSubjectAuthentication: SingleValidationResult + } + >[] + + /** + * Whether the presentation is signed by the 'holder' of the + * presentation. + * + * NOTE: this check will return the value `true` for `isValid` + * when the `holder` property is not set on the presentation. + * as the goal of this validation is to assert whether the + * 'holder' property that is used in the presentation is valid. + * If the property is not present, the validation can be seen as + * successful + */ + holderIsSigner: SingleValidationResult + + /** + * NOTE: this validation is currently only present for ldp_vp presentations. + * When this validation is present, ALL OTHER validations will be skipped. + * + * Whether the presentation is valid according to the [vc.js](https://github.com/digitalbazaar/vc) + * library. As the library handles all validations, it is not possible to include the other + * validation items separately. In the future the vc.js library will be replaced to provide a similar + * validation result for all credential formats. + */ + vcJs: SingleValidationResult +} diff --git a/packages/core/src/modules/vc/models/credential/W3cCredential.ts b/packages/core/src/modules/vc/models/credential/W3cCredential.ts new file mode 100644 index 0000000000..e62cd75ad0 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cCredential.ts @@ -0,0 +1,148 @@ +import type { W3cCredentialSubjectOptions } from './W3cCredentialSubject' +import type { W3cIssuerOptions } from './W3cIssuer' +import type { JsonObject } from '../../../../types' +import type { ValidationOptions } from 'class-validator' + +import { Expose, Type } from 'class-transformer' +import { buildMessage, IsInstance, IsOptional, IsRFC3339, ValidateBy, ValidateNested } from 'class-validator' + +import { asArray, JsonTransformer, mapSingleOrArray } from '../../../../utils' +import { SingleOrArray } from '../../../../utils/type' +import { IsInstanceOrArrayOfInstances, IsUri } from '../../../../utils/validators' +import { CREDENTIALS_CONTEXT_V1_URL, VERIFIABLE_CREDENTIAL_TYPE } from '../../constants' +import { IsCredentialJsonLdContext } from '../../validators' + +import { W3cCredentialSchema } from './W3cCredentialSchema' +import { W3cCredentialStatus } from './W3cCredentialStatus' +import { IsW3cCredentialSubject, W3cCredentialSubject, W3cCredentialSubjectTransformer } from './W3cCredentialSubject' +import { IsW3cIssuer, W3cIssuer, W3cIssuerTransformer } from './W3cIssuer' + +export interface W3cCredentialOptions { + context?: Array + id?: string + type: Array + issuer: string | W3cIssuerOptions + issuanceDate: string + expirationDate?: string + credentialSubject: SingleOrArray + credentialStatus?: W3cCredentialStatus +} + +export class W3cCredential { + public constructor(options: W3cCredentialOptions) { + if (options) { + this.context = options.context ?? [CREDENTIALS_CONTEXT_V1_URL] + this.id = options.id + this.type = options.type || ['VerifiableCredential'] + this.issuer = + typeof options.issuer === 'string' || options.issuer instanceof W3cIssuer + ? options.issuer + : new W3cIssuer(options.issuer) + this.issuanceDate = options.issuanceDate + this.expirationDate = options.expirationDate + this.credentialSubject = mapSingleOrArray(options.credentialSubject, (subject) => + subject instanceof W3cCredentialSubject ? subject : new W3cCredentialSubject(subject) + ) + + if (options.credentialStatus) { + this.credentialStatus = + options.credentialStatus instanceof W3cCredentialStatus + ? options.credentialStatus + : new W3cCredentialStatus(options.credentialStatus) + } + } + } + + @Expose({ name: '@context' }) + @IsCredentialJsonLdContext() + public context!: Array + + @IsOptional() + @IsUri() + public id?: string + + @IsCredentialType() + public type!: Array + + @W3cIssuerTransformer() + @IsW3cIssuer() + public issuer!: string | W3cIssuer + + @IsRFC3339() + public issuanceDate!: string + + @IsRFC3339() + @IsOptional() + public expirationDate?: string + + @IsW3cCredentialSubject({ each: true }) + @W3cCredentialSubjectTransformer() + public credentialSubject!: SingleOrArray + + @IsOptional() + @Type(() => W3cCredentialSchema) + @ValidateNested({ each: true }) + @IsInstanceOrArrayOfInstances({ classType: W3cCredentialSchema, allowEmptyArray: true }) + public credentialSchema?: SingleOrArray + + @IsOptional() + @Type(() => W3cCredentialStatus) + @ValidateNested({ each: true }) + @IsInstance(W3cCredentialStatus) + public credentialStatus?: W3cCredentialStatus + + public get issuerId(): string { + return this.issuer instanceof W3cIssuer ? this.issuer.id : this.issuer + } + + public get credentialSchemaIds(): string[] { + if (!this.credentialSchema) return [] + + if (Array.isArray(this.credentialSchema)) { + return this.credentialSchema.map((credentialSchema) => credentialSchema.id) + } + + return [this.credentialSchema.id] + } + + public get credentialSubjectIds(): string[] { + if (Array.isArray(this.credentialSubject)) { + return this.credentialSubject + .map((credentialSubject) => credentialSubject.id) + .filter((v): v is string => v !== undefined) + } + + return this.credentialSubject.id ? [this.credentialSubject.id] : [] + } + + public get contexts(): Array { + return asArray(this.context) + } + + public static fromJson(json: Record) { + return JsonTransformer.fromJSON(json, W3cCredential) + } +} + +// Custom validator + +export function IsCredentialType(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'IsVerifiableCredentialType', + validator: { + validate: (value): boolean => { + if (Array.isArray(value)) { + return value.includes(VERIFIABLE_CREDENTIAL_TYPE) + } + return false + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be an array of strings which includes "VerifiableCredential"', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/modules/vc/models/credential/W3cCredentialSchema.ts b/packages/core/src/modules/vc/models/credential/W3cCredentialSchema.ts new file mode 100644 index 0000000000..e37b9f3180 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cCredentialSchema.ts @@ -0,0 +1,23 @@ +import { IsString } from 'class-validator' + +import { IsUri } from '../../../../utils/validators' + +export interface W3cCredentialSchemaOptions { + id: string + type: string +} + +export class W3cCredentialSchema { + public constructor(options: W3cCredentialSchemaOptions) { + if (options) { + this.id = options.id + this.type = options.type + } + } + + @IsUri() + public id!: string + + @IsString() + public type!: string +} diff --git a/packages/core/src/modules/vc/models/credential/W3cCredentialStatus.ts b/packages/core/src/modules/vc/models/credential/W3cCredentialStatus.ts new file mode 100644 index 0000000000..cf1de83151 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cCredentialStatus.ts @@ -0,0 +1,23 @@ +import { IsString } from 'class-validator' + +import { IsUri } from '../../../../utils/validators' + +export interface W3cCredentialStatusOptions { + id: string + type: string +} + +export class W3cCredentialStatus { + public constructor(options: W3cCredentialStatusOptions) { + if (options) { + this.id = options.id + this.type = options.type + } + } + + @IsUri() + public id!: string + + @IsString() + public type!: string +} diff --git a/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts b/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts new file mode 100644 index 0000000000..6c44458591 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts @@ -0,0 +1,82 @@ +import type { ValidationOptions } from 'class-validator' + +import { Transform, TransformationType } from 'class-transformer' +import { IsOptional, ValidateBy, buildMessage, isInstance } from 'class-validator' + +import { CredoError } from '../../../../error' + +/** + * @see https://www.w3.org/TR/vc-data-model/#credential-subject + */ + +export interface W3cCredentialSubjectOptions { + id?: string + // note claims must not contain an id field + claims?: Record +} + +export class W3cCredentialSubject { + public constructor(options: W3cCredentialSubjectOptions) { + if (options) { + this.id = options.id + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, ...claims } = options.claims ?? {} + this.claims = Object.keys(claims).length > 0 ? claims : undefined + } + } + + @IsOptional() + public id?: string + + @IsOptional() + public claims?: Record +} + +export function W3cCredentialSubjectTransformer() { + return Transform(({ value, type }: { value: W3cCredentialSubjectOptions; type: TransformationType }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + const vToClass = (v: unknown) => { + if (!v || typeof v !== 'object') throw new CredoError('Invalid credential subject') + if (isInstance(v, W3cCredentialSubject)) return v + const { id, ...claims } = v as Record + if (id !== undefined && typeof id !== 'string') throw new CredoError('Invalid credential subject id') + return new W3cCredentialSubject({ id, claims }) + } + + if (Array.isArray(value) && value.length === 0) { + throw new CredoError('At least one credential subject is required') + } + + return Array.isArray(value) ? value.map(vToClass) : vToClass(value) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + const vToJson = (v: unknown) => { + if (v instanceof W3cCredentialSubject) return v.id ? { ...v.claims, id: v.id } : { ...v.claims } + return v + } + + return Array.isArray(value) ? value.map(vToJson) : vToJson(value) + } + // PLAIN_TO_PLAIN + return value + }) +} + +export function IsW3cCredentialSubject(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'IsW3cCredentialSubject', + validator: { + validate: (value): boolean => { + return isInstance(value, W3cCredentialSubject) + }, + defaultMessage: buildMessage( + (eachPrefix) => + eachPrefix + '$property must be an object or an array of objects with an optional id property', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/modules/vc/models/credential/W3cIssuer.ts b/packages/core/src/modules/vc/models/credential/W3cIssuer.ts new file mode 100644 index 0000000000..611f16d0b3 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cIssuer.ts @@ -0,0 +1,68 @@ +import type { ValidationOptions } from 'class-validator' + +import { Transform, TransformationType, plainToInstance, instanceToPlain } from 'class-transformer' +import { buildMessage, isInstance, isString, ValidateBy } from 'class-validator' + +import { IsUri, isUri } from '../../../../utils/validators' + +/** + * TODO: check how to support arbitrary data in class + * @see https://www.w3.org/TR/vc-data-model/#credential-subject + */ + +export interface W3cIssuerOptions { + id: string +} + +export class W3cIssuer { + public constructor(options: W3cIssuerOptions) { + if (options) { + this.id = options.id + } + } + + @IsUri() + public id!: string +} + +// Custom transformers + +export function W3cIssuerTransformer() { + return Transform(({ value, type }: { value: string | W3cIssuerOptions; type: TransformationType }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + if (isString(value)) return value + return plainToInstance(W3cIssuer, value) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + if (isString(value)) return value + return instanceToPlain(value) + } + // PLAIN_TO_PLAIN + return value + }) +} + +// Custom validators + +export function IsW3cIssuer(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'IsW3cIssuer', + validator: { + validate: (value): boolean => { + if (typeof value === 'string') { + return isUri(value) + } + if (isInstance(value, W3cIssuer)) { + return isUri(value.id) + } + return false + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be an URI or an object with an id property which is an URI', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/modules/vc/models/credential/W3cJsonCredential.ts b/packages/core/src/modules/vc/models/credential/W3cJsonCredential.ts new file mode 100644 index 0000000000..fa43911d43 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cJsonCredential.ts @@ -0,0 +1,13 @@ +import type { JsonObject } from '../../../../types' +import type { SingleOrArray } from '../../../../utils' + +export interface W3cJsonCredential { + '@context': Array + id?: string + type: Array + issuer: string | { id?: string } + issuanceDate: string + expirationDate?: string + credentialSubject: SingleOrArray + [key: string]: unknown +} diff --git a/packages/core/src/modules/vc/models/credential/W3cVerifiableCredential.ts b/packages/core/src/modules/vc/models/credential/W3cVerifiableCredential.ts new file mode 100644 index 0000000000..8517276653 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cVerifiableCredential.ts @@ -0,0 +1,45 @@ +import type { SingleOrArray } from '../../../../utils' +import type { ClaimFormat } from '../ClaimFormat' + +import { Transform, TransformationType } from 'class-transformer' +import { ValidationError } from 'class-validator' + +import { CredoError, ClassValidationError } from '../../../../error' +import { JsonTransformer } from '../../../../utils' +import { W3cJsonLdVerifiableCredential } from '../../data-integrity/models/W3cJsonLdVerifiableCredential' +import { W3cJwtVerifiableCredential } from '../../jwt-vc/W3cJwtVerifiableCredential' + +const getCredential = (v: unknown) => { + try { + return typeof v === 'string' + ? W3cJwtVerifiableCredential.fromSerializedJwt(v) + : // Validation is done separately + JsonTransformer.fromJSON(v, W3cJsonLdVerifiableCredential, { validate: false }) + } catch (error) { + if (error instanceof ValidationError || error instanceof ClassValidationError) throw error + throw new CredoError(`value '${v}' is not a valid W3cJwtVerifiableCredential. ${error.message}`) + } +} + +const getEncoded = (v: unknown) => + v instanceof W3cJwtVerifiableCredential ? v.serializedJwt : JsonTransformer.toJSON(v) + +export function W3cVerifiableCredentialTransformer() { + return Transform(({ value, type }: { value: SingleOrArray; type: TransformationType }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + return Array.isArray(value) ? value.map(getCredential) : getCredential(value) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + if (Array.isArray(value)) return value.map(getEncoded) + return getEncoded(value) + } + // PLAIN_TO_PLAIN + return value + }) +} + +export type W3cVerifiableCredential = + Format extends ClaimFormat.JwtVc + ? W3cJwtVerifiableCredential + : Format extends ClaimFormat.LdpVc + ? W3cJsonLdVerifiableCredential + : W3cJsonLdVerifiableCredential | W3cJwtVerifiableCredential diff --git a/packages/core/src/modules/vc/models/credential/__tests__/W3cCredential.test.ts b/packages/core/src/modules/vc/models/credential/__tests__/W3cCredential.test.ts new file mode 100644 index 0000000000..1b66e22e8d --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/__tests__/W3cCredential.test.ts @@ -0,0 +1,223 @@ +import { JsonTransformer } from '../../../../../utils' +import { W3cCredential } from '../W3cCredential' + +const validCredential = { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'], + id: 'http://example.edu/credentials/1872', + type: ['VerifiableCredential', 'AlumniCredential'], + issuer: 'https://example.edu/issuers/14', + issuanceDate: '2010-01-01T19:23:24Z', + credentialSubject: { + id: 'did:example:ebfeb1f712ebc6f1c276e12ec21', + alumniOf: { + id: 'did:example:c276e12ec21ebfeb1f712ebc6f1', + name: [ + { + value: 'Example University', + lang: 'en', + }, + ], + }, + }, + credentialSchema: { + id: 'https://example.org/examples/degree.json', + type: 'JsonSchemaValidator2018', + }, +} + +describe('W3cCredential', () => { + test('throws an error when verifiable credential context is missing or not the first entry', () => { + expect(() => JsonTransformer.fromJSON({ ...validCredential, '@context': [] }, W3cCredential)).toThrowError( + /context must be an array of strings or objects, where the first item is the verifiable credential context URL./ + ) + + expect(() => + JsonTransformer.fromJSON( + { + ...validCredential, + '@context': ['https://www.w3.org/2018/credentials/examples/v1', 'https://www.w3.org/2018/credentials/v1'], + }, + W3cCredential + ) + ).toThrowError( + /context must be an array of strings or objects, where the first item is the verifiable credential context URL./ + ) + + expect(() => + JsonTransformer.fromJSON({ ...validCredential, '@context': { some: 'property' } }, W3cCredential) + ).toThrowError( + /context must be an array of strings or objects, where the first item is the verifiable credential context URL./ + ) + }) + + test('throws an error when id is present and it is not an uri', () => { + expect(() => + JsonTransformer.fromJSON({ ...validCredential, id: 'f8c7d9c9-3f9f-4d1d-9c0d-5b3b5d7b8f5c' }, W3cCredential) + ).toThrowError(/id must be an URI/) + + expect(() => JsonTransformer.fromJSON({ ...validCredential, id: 10 }, W3cCredential)).toThrowError( + /id must be an URI/ + ) + }) + + test('throws an error when type is not an array of string or does not include VerifiableCredential', () => { + expect(() => JsonTransformer.fromJSON({ ...validCredential, type: [] }, W3cCredential)).toThrowError( + /type must be an array of strings which includes "VerifiableCredential"/ + ) + + expect(() => JsonTransformer.fromJSON({ ...validCredential, type: ['AnotherType'] }, W3cCredential)).toThrowError( + /type must be an array of strings which includes "VerifiableCredential"/ + ) + + expect(() => JsonTransformer.fromJSON({ ...validCredential, type: { some: 'prop' } }, W3cCredential)).toThrowError( + /type must be an array of strings which includes "VerifiableCredential"/ + ) + }) + + test('throws an error when issuer is not a valid uri or object with id', () => { + expect(() => + JsonTransformer.fromJSON({ ...validCredential, issuer: 'f8c7d9c9-3f9f-4d1d-9c0d-5b3b5d7b8f5c' }, W3cCredential) + ).toThrowError(/issuer must be an URI or an object with an id property which is an URI/) + + expect(() => + JsonTransformer.fromJSON( + { ...validCredential, issuer: { id: 'f8c7d9c9-3f9f-4d1d-9c0d-5b3b5d7b8f5c' } }, + W3cCredential + ) + ).toThrowError(/issuer must be an URI or an object with an id property which is an URI/) + + expect(() => JsonTransformer.fromJSON({ ...validCredential, issuer: 10 }, W3cCredential)).toThrowError( + /issuer must be an URI or an object with an id property which is an URI/ + ) + + // Valid cases + expect(() => + JsonTransformer.fromJSON({ ...validCredential, issuer: { id: 'urn:uri' } }, W3cCredential) + ).not.toThrowError() + expect(() => JsonTransformer.fromJSON({ ...validCredential, issuer: 'uri:uri' }, W3cCredential)).not.toThrowError() + }) + + test('throws an error when issuanceDate is not a valid date', () => { + expect(() => JsonTransformer.fromJSON({ ...validCredential, issuanceDate: '2020' }, W3cCredential)).toThrowError( + /property issuanceDate has failed the following constraints: issuanceDate must be RFC 3339 date/ + ) + }) + + test('throws an error when expirationDate is present and it is not a valid date', () => { + expect(() => JsonTransformer.fromJSON({ ...validCredential, expirationDate: '2020' }, W3cCredential)).toThrowError( + /property expirationDate has failed the following constraints: expirationDate must be RFC 3339 date/ + ) + }) + + test('throws an error when credentialSchema is present and it is not a valid credentialSchema object/array', () => { + expect(() => JsonTransformer.fromJSON({ ...validCredential, credentialSchema: {} }, W3cCredential)).toThrowError( + /property credentialSchema\./ + ) + + expect(() => JsonTransformer.fromJSON({ ...validCredential, credentialSchema: [{}] }, W3cCredential)).toThrowError( + /property credentialSchema\[0\]\./ + ) + + expect(() => + JsonTransformer.fromJSON( + { ...validCredential, credentialSchema: [{ id: 'some-random-value', type: 'valid' }] }, + W3cCredential + ) + ).toThrowError(/property credentialSchema\[0\]\.id has failed the following constraints/) + + expect(() => + JsonTransformer.fromJSON( + { ...validCredential, credentialSchema: [validCredential.credentialSchema, validCredential.credentialSchema] }, + W3cCredential + ) + ).not.toThrowError() + + expect(() => + JsonTransformer.fromJSON({ ...validCredential, credentialSchema: [] }, W3cCredential) + ).not.toThrowError() + }) + + test('throws an error when credentialSubject is present and it is not a valid credentialSubject object/array', () => { + expect(() => JsonTransformer.fromJSON({ ...validCredential, credentialSubject: [] }, W3cCredential)).toThrow() + + expect(() => + JsonTransformer.fromJSON( + { + ...validCredential, + credentialSubject: [ + { + id: 'some-random-value', + }, + ], + }, + W3cCredential + ) + ).not.toThrow() + + expect(() => + JsonTransformer.fromJSON( + { + ...validCredential, + credentialSubject: { id: 'urn:uri' }, + }, + W3cCredential + ) + ).not.toThrowError() + + expect(() => + JsonTransformer.fromJSON( + { + ...validCredential, + credentialSubject: { id: 'urn:uri', claims: { some: 'value', someOther: { nested: 'value' } } }, + }, + W3cCredential + ) + ).not.toThrowError() + + expect(() => + JsonTransformer.fromJSON( + [ + { + ...validCredential, + credentialSubject: { id: 'urn:uri', claims: { some: 'value1', someOther: { nested: 'value1' } } }, + }, + ], + W3cCredential + ) + ).not.toThrowError() + + expect(() => + JsonTransformer.fromJSON( + [ + { + ...validCredential, + credentialSubject: { id: 'urn:uri', claims: { some: 'value2', someOther: { nested: 'value2' } } }, + }, + ], + W3cCredential + ) + ).not.toThrowError() + + expect(() => + JsonTransformer.fromJSON( + { + ...validCredential, + credentialSubject: [ + { + id: 'urn:uri', + }, + ], + }, + W3cCredential + ) + ).not.toThrowError() + }) + + it('should transform from JSON to a class instance and back', () => { + const credential = JsonTransformer.fromJSON(validCredential, W3cCredential) + expect(credential).toBeInstanceOf(W3cCredential) + + const transformedJson = JsonTransformer.toJSON(credential) + expect(transformedJson).toEqual(validCredential) + }) +}) diff --git a/packages/core/src/modules/vc/models/credential/index.ts b/packages/core/src/modules/vc/models/credential/index.ts new file mode 100644 index 0000000000..d0118e7e07 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/index.ts @@ -0,0 +1,5 @@ +export * from './W3cCredentialSchema' +export * from './W3cCredentialSubject' +export * from './W3cIssuer' +export * from './W3cCredential' +export * from './W3cVerifiableCredential' diff --git a/packages/core/src/modules/vc/models/index.ts b/packages/core/src/modules/vc/models/index.ts new file mode 100644 index 0000000000..88225ac387 --- /dev/null +++ b/packages/core/src/modules/vc/models/index.ts @@ -0,0 +1,4 @@ +export * from './credential' +export * from './presentation' +export * from './W3cVerifyResult' +export * from './ClaimFormat' diff --git a/packages/core/src/modules/vc/models/presentation/W3cHolder.ts b/packages/core/src/modules/vc/models/presentation/W3cHolder.ts new file mode 100644 index 0000000000..842f8c3119 --- /dev/null +++ b/packages/core/src/modules/vc/models/presentation/W3cHolder.ts @@ -0,0 +1,65 @@ +import type { ValidationOptions } from 'class-validator' + +import { Transform, TransformationType, plainToInstance, instanceToPlain } from 'class-transformer' +import { buildMessage, isInstance, isString, ValidateBy } from 'class-validator' + +import { IsUri, isUri } from '../../../../utils/validators' + +/** + * TODO: check how to support arbitrary data in class + */ +export interface W3cHolderOptions { + id: string +} + +export class W3cHolder { + public constructor(options: W3cHolderOptions) { + if (options) { + this.id = options.id + } + } + + @IsUri() + public id!: string +} + +// Custom transformers +export function W3cHolderTransformer() { + return Transform(({ value, type }: { value: string | W3cHolderOptions; type: TransformationType }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + if (isString(value)) return value + return plainToInstance(W3cHolder, value) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + if (isString(value)) return value + return instanceToPlain(value) + } + // PLAIN_TO_PLAIN + return value + }) +} + +// Custom validators + +export function IsW3cHolder(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'IsW3cHolder', + validator: { + validate: (value): boolean => { + if (typeof value === 'string') { + return isUri(value) + } + if (isInstance(value, W3cHolder)) { + return isUri(value.id) + } + return false + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be an URI or an object with an id property which is an URI', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts new file mode 100644 index 0000000000..a47b3e90dc --- /dev/null +++ b/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts @@ -0,0 +1,13 @@ +import type { JsonObject } from '../../../../types' +import type { DifPresentationExchangeSubmission } from '../../../dif-presentation-exchange' +import type { W3cJsonCredential } from '../credential/W3cJsonCredential' + +export interface W3cJsonPresentation { + '@context': Array + id?: string + type: Array + holder: string | { id?: string } + verifiableCredential: Array + presentation_submission?: DifPresentationExchangeSubmission + [key: string]: unknown +} diff --git a/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts new file mode 100644 index 0000000000..cf37fc434b --- /dev/null +++ b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts @@ -0,0 +1,96 @@ +import type { W3cHolderOptions } from './W3cHolder' +import type { W3cJsonPresentation } from './W3cJsonPresentation' +import type { JsonObject } from '../../../../types' +import type { W3cVerifiableCredential } from '../credential/W3cVerifiableCredential' +import type { ValidationOptions } from 'class-validator' + +import { Expose } from 'class-transformer' +import { ValidateNested, buildMessage, IsOptional, ValidateBy } from 'class-validator' + +import { JsonTransformer } from '../../../../utils' +import { SingleOrArray } from '../../../../utils/type' +import { IsUri, IsInstanceOrArrayOfInstances } from '../../../../utils/validators' +import { CREDENTIALS_CONTEXT_V1_URL, VERIFIABLE_PRESENTATION_TYPE } from '../../constants' +import { W3cJsonLdVerifiableCredential } from '../../data-integrity/models/W3cJsonLdVerifiableCredential' +import { W3cJwtVerifiableCredential } from '../../jwt-vc/W3cJwtVerifiableCredential' +import { IsCredentialJsonLdContext } from '../../validators' +import { W3cVerifiableCredentialTransformer } from '../credential/W3cVerifiableCredential' + +import { IsW3cHolder, W3cHolder, W3cHolderTransformer } from './W3cHolder' + +export interface W3cPresentationOptions { + id?: string + context?: Array + type?: Array + verifiableCredential: SingleOrArray + holder?: string | W3cHolderOptions +} + +export class W3cPresentation { + public constructor(options: W3cPresentationOptions) { + if (options) { + this.id = options.id + this.context = options.context ?? [CREDENTIALS_CONTEXT_V1_URL] + this.type = options.type ?? [VERIFIABLE_PRESENTATION_TYPE] + this.verifiableCredential = options.verifiableCredential + + if (options.holder) { + this.holder = typeof options.holder === 'string' ? options.holder : new W3cHolder(options.holder) + } + } + } + + @Expose({ name: '@context' }) + @IsCredentialJsonLdContext() + public context!: Array + + @IsOptional() + @IsUri() + public id?: string + + @IsVerifiablePresentationType() + public type!: Array + + @W3cHolderTransformer() + @IsW3cHolder() + @IsOptional() + public holder?: string | W3cHolder + + @W3cVerifiableCredentialTransformer() + @IsInstanceOrArrayOfInstances({ classType: [W3cJsonLdVerifiableCredential, W3cJwtVerifiableCredential] }) + @ValidateNested({ each: true }) + public verifiableCredential!: SingleOrArray + + public get holderId(): string | null { + if (!this.holder) return null + + return this.holder instanceof W3cHolder ? this.holder.id : this.holder + } + + public toJSON() { + return JsonTransformer.toJSON(this) as W3cJsonPresentation + } +} + +// Custom validators + +export function IsVerifiablePresentationType(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'IsVerifiablePresentationType', + validator: { + validate: (value): boolean => { + if (Array.isArray(value)) { + return value.includes(VERIFIABLE_PRESENTATION_TYPE) + } + return false + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be an array of strings which includes "VerifiablePresentation"', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/modules/vc/models/presentation/W3cVerifiablePresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cVerifiablePresentation.ts new file mode 100644 index 0000000000..8ce1304a19 --- /dev/null +++ b/packages/core/src/modules/vc/models/presentation/W3cVerifiablePresentation.ts @@ -0,0 +1,10 @@ +import type { W3cJsonLdVerifiablePresentation } from '../../data-integrity' +import type { W3cJwtVerifiablePresentation } from '../../jwt-vc' +import type { ClaimFormat } from '../ClaimFormat' + +export type W3cVerifiablePresentation = + Format extends ClaimFormat.JwtVp + ? W3cJwtVerifiablePresentation + : Format extends ClaimFormat.LdpVp + ? W3cJsonLdVerifiablePresentation + : W3cJsonLdVerifiablePresentation | W3cJwtVerifiablePresentation diff --git a/packages/core/src/modules/vc/models/presentation/__tests__/W3cPresentation.test.ts b/packages/core/src/modules/vc/models/presentation/__tests__/W3cPresentation.test.ts new file mode 100644 index 0000000000..5a39075bd9 --- /dev/null +++ b/packages/core/src/modules/vc/models/presentation/__tests__/W3cPresentation.test.ts @@ -0,0 +1,146 @@ +import { JsonTransformer } from '../../../../../utils' +import { W3cPresentation } from '../W3cPresentation' + +const jsonLdCredential = { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'], + id: 'http://example.edu/credentials/1872', + type: ['VerifiableCredential', 'AlumniCredential'], + issuer: 'https://example.edu/issuers/14', + issuanceDate: '2010-01-01T19:23:24Z', + credentialSubject: { + id: 'did:example:ebfeb1f712ebc6f1c276e12ec21', + alumniOf: { + id: 'did:example:c276e12ec21ebfeb1f712ebc6f1', + name: [ + { + value: 'Example University', + lang: 'en', + }, + ], + }, + }, + proof: { + type: 'RsaSignature2018', + verificationMethod: 'did:example:123#5', + created: '2017-06-18T21:19:10Z', + proofPurpose: 'assertionMethod', + }, +} + +const jwtCredential = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOmFiZmUxM2Y3MTIxMjA0MzFjMjc2ZTEyZWNhYiNrZXlzLTEifQ.eyJzdWIiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsImlzcyI6Imh0dHBzOi8vZXhhbXBsZS5jb20va2V5cy9mb28uandrIiwibmJmIjoxNTQxNDkzNzI0LCJpYXQiOjE1NDE0OTM3MjQsImV4cCI6MTU3MzAyOTcyMywibm9uY2UiOiI2NjAhNjM0NUZTZXIiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJVbml2ZXJzaXR5RGVncmVlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IjxzcGFuIGxhbmc9J2ZyLUNBJz5CYWNjYWxhdXLDqWF0IGVuIG11c2lxdWVzIG51bcOpcmlxdWVzPC9zcGFuPiJ9fX19.KLJo5GAyBND3LDTn9H7FQokEsUEi8jKwXhGvoN3JtRa51xrNDgXDb0cq1UTYB-rK4Ft9YVmR1NI_ZOF8oGc_7wAp8PHbF2HaWodQIoOBxxT-4WNqAxft7ET6lkH-4S6Ux3rSGAmczMohEEf8eCeN-jC8WekdPl6zKZQj0YPB1rx6X0-xlFBs7cl6Wt8rfBP_tZ9YgVWrQmUWypSioc0MUyiphmyEbLZagTyPlUyflGlEdqrZAv6eSe6RtxJy6M1-lD7a5HTzanYTWBPAUHDZGyGKXdJw-W_x0IWChBzI8t3kpG253fg6V3tPgHeKXE94fz_QpYfg--7kLsyBAfQGbg' + +const validPresentation = { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiablePresentation'], + id: 'http://example.edu/credentials/1872', + verifiableCredential: [jsonLdCredential, jwtCredential], +} + +describe('W3cPresentation', () => { + test('throws an error when verifiable credential context is missing or not the first entry', () => { + expect(() => JsonTransformer.fromJSON({ ...validPresentation, '@context': [] }, W3cPresentation)).toThrowError( + /context must be an array of strings or objects, where the first item is the verifiable credential context URL./ + ) + + expect(() => + JsonTransformer.fromJSON( + { + ...validPresentation, + '@context': ['https://www.w3.org/2018/credentials/examples/v1', 'https://www.w3.org/2018/credentials/v1'], + }, + W3cPresentation + ) + ).toThrowError( + /context must be an array of strings or objects, where the first item is the verifiable credential context URL./ + ) + + expect(() => + JsonTransformer.fromJSON({ ...validPresentation, '@context': { some: 'property' } }, W3cPresentation) + ).toThrowError( + /context must be an array of strings or objects, where the first item is the verifiable credential context URL./ + ) + }) + + test('throws an error when id is present and it is not an uri', () => { + expect(() => + JsonTransformer.fromJSON({ ...validPresentation, id: 'f8c7d9c9-3f9f-4d1d-9c0d-5b3b5d7b8f5c' }, W3cPresentation) + ).toThrowError(/id must be an URI/) + + expect(() => JsonTransformer.fromJSON({ ...validPresentation, id: 10 }, W3cPresentation)).toThrowError( + /id must be an URI/ + ) + }) + + test('throws an error when type is not an array of string or does not include VerifiablePresentation', () => { + expect(() => JsonTransformer.fromJSON({ ...validPresentation, type: [] }, W3cPresentation)).toThrowError( + /type must be an array of strings which includes "VerifiablePresentation"/ + ) + + expect(() => + JsonTransformer.fromJSON({ ...validPresentation, type: ['AnotherType'] }, W3cPresentation) + ).toThrowError(/type must be an array of strings which includes "VerifiablePresentation"/) + + expect(() => + JsonTransformer.fromJSON({ ...validPresentation, type: { some: 'prop' } }, W3cPresentation) + ).toThrowError(/type must be an array of strings which includes "VerifiablePresentation"/) + }) + + test('throws an error when holder is present and it is not an uri', () => { + expect(() => + JsonTransformer.fromJSON( + { ...validPresentation, holder: 'f8c7d9c9-3f9f-4d1d-9c0d-5b3b5d7b8f5c' }, + W3cPresentation + ) + ).toThrowError(/holder must be an URI/) + + expect(() => JsonTransformer.fromJSON({ ...validPresentation, holder: 10 }, W3cPresentation)).toThrowError( + /holder must be an URI/ + ) + }) + + test('throws an error when verifiableCredential is not a credential or an array of credentials', () => { + expect(() => + JsonTransformer.fromJSON({ ...validPresentation, verifiableCredential: undefined }, W3cPresentation) + ).toThrowError( + /verifiableCredential value must be an instance of, or an array of instances containing W3cJsonLdVerifiableCredential, W3cJwtVerifiableCredential/ + ) + + expect(() => + JsonTransformer.fromJSON({ ...validPresentation, verifiableCredential: [] }, W3cPresentation) + ).toThrowError( + /verifiableCredential value must be an instance of, or an array of instances containing W3cJsonLdVerifiableCredential, W3cJwtVerifiableCredential/ + ) + + expect(() => + JsonTransformer.fromJSON({ ...validPresentation, verifiableCredential: [{ random: 'prop' }] }, W3cPresentation) + ).toThrowError(/property verifiableCredential\[0\]\./) + + expect(() => + JsonTransformer.fromJSON({ ...validPresentation, verifiableCredential: ['ey.incorrect.jwt'] }, W3cPresentation) + ).toThrowError(/value 'ey.incorrect.jwt' is not a valid W3cJwtVerifiableCredential. Invalid JWT./) + + // Deeply nested property missing + expect(() => + JsonTransformer.fromJSON( + { + ...validPresentation, + verifiableCredential: [ + { ...jsonLdCredential, proof: { ...jsonLdCredential.proof, verificationMethod: undefined } }, + ], + }, + W3cPresentation + ) + ).toThrowError( + /property verifiableCredential\[0\]\.proof\.verificationMethod has failed the following constraints: verificationMethod must be a string/ + ) + }) + + it('should transform from JSON to a class instance and back', () => { + const presentation = JsonTransformer.fromJSON(validPresentation, W3cPresentation) + expect(presentation).toBeInstanceOf(W3cPresentation) + + const transformedJson = JsonTransformer.toJSON(presentation) + expect(transformedJson).toEqual(validPresentation) + }) +}) diff --git a/packages/core/src/modules/vc/models/presentation/index.ts b/packages/core/src/modules/vc/models/presentation/index.ts new file mode 100644 index 0000000000..c9c056c1c4 --- /dev/null +++ b/packages/core/src/modules/vc/models/presentation/index.ts @@ -0,0 +1,2 @@ +export * from './W3cPresentation' +export * from './W3cVerifiablePresentation' diff --git a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts new file mode 100644 index 0000000000..3f0b485fec --- /dev/null +++ b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts @@ -0,0 +1,96 @@ +import type { TagsBase } from '../../../storage/BaseRecord' +import type { Constructable } from '../../../utils/mixins' + +import { BaseRecord } from '../../../storage/BaseRecord' +import { JsonTransformer } from '../../../utils' +import { uuid } from '../../../utils/uuid' +import { ClaimFormat, W3cVerifiableCredential, W3cVerifiableCredentialTransformer } from '../models' + +export interface W3cCredentialRecordOptions { + id?: string + createdAt?: Date + credential: W3cVerifiableCredential + tags: CustomW3cCredentialTags +} + +export type CustomW3cCredentialTags = TagsBase & { + /** + * Expanded types are used for JSON-LD credentials to allow for filtering on the expanded type. + */ + expandedTypes?: Array +} + +export type DefaultW3cCredentialTags = { + issuerId: string + subjectIds: Array + schemaIds: Array + contexts: Array + givenId?: string + + // Can be any of the values for claimFormat + claimFormat: W3cVerifiableCredential['claimFormat'] + + proofTypes?: Array + cryptosuites?: Array + types: Array + algs?: Array +} + +export class W3cCredentialRecord extends BaseRecord { + public static readonly type = 'W3cCredentialRecord' + public readonly type = W3cCredentialRecord.type + + @W3cVerifiableCredentialTransformer() + public credential!: W3cVerifiableCredential + + public constructor(props: W3cCredentialRecordOptions) { + super() + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags + this.credential = props.credential + } + } + + public getTags() { + // Contexts are usually strings, but can sometimes be objects. We're unable to use objects as tags, + // so we filter out the objects before setting the tags. + const stringContexts = this.credential.contexts.filter((ctx): ctx is string => typeof ctx === 'string') + + const tags: DefaultW3cCredentialTags = { + ...this._tags, + issuerId: this.credential.issuerId, + subjectIds: this.credential.credentialSubjectIds, + schemaIds: this.credential.credentialSchemaIds, + contexts: stringContexts, + givenId: this.credential.id, + claimFormat: this.credential.claimFormat, + types: this.credential.type, + } + + // Proof types is used for ldp_vc credentials + if (this.credential.claimFormat === ClaimFormat.LdpVc) { + tags.proofTypes = this.credential.proofTypes + tags.cryptosuites = this.credential.dataIntegrityCryptosuites + } + + // Algs is used for jwt_vc credentials + else if (this.credential.claimFormat === ClaimFormat.JwtVc) { + tags.algs = [this.credential.jwt.header.alg] + } + + return tags + } + + /** + * This overwrites the default clone method for records + * as the W3cRecord has issues with the default clone method + * due to how W3cJwtVerifiableCredential is implemented. This is + * a temporary way to make sure the clone still works, but ideally + * we find an alternative. + */ + public clone(): this { + return JsonTransformer.fromJSON(JsonTransformer.toJSON(this), this.constructor as Constructable) + } +} diff --git a/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts b/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts new file mode 100644 index 0000000000..6f711e6ed2 --- /dev/null +++ b/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts @@ -0,0 +1,17 @@ +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { W3cCredentialRecord } from './W3cCredentialRecord' + +@injectable() +export class W3cCredentialRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(W3cCredentialRecord, storageService, eventEmitter) + } +} diff --git a/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts new file mode 100644 index 0000000000..89b7fb89d9 --- /dev/null +++ b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts @@ -0,0 +1,38 @@ +import { getAnonCredsTagsFromRecord } from '../../../../../../anoncreds/src/utils/w3cAnonCredsUtils' +import { JsonTransformer } from '../../../../utils' +import { Ed25519Signature2018Fixtures } from '../../data-integrity/__tests__/fixtures' +import { W3cJsonLdVerifiableCredential } from '../../data-integrity/models' +import { W3cCredentialRecord } from '../W3cCredentialRecord' + +describe('W3cCredentialRecord', () => { + describe('getTags', () => { + it('should return default tags (w3c credential)', () => { + const credential = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ) + + const w3cCredentialRecord = new W3cCredentialRecord({ + credential, + tags: { + expandedTypes: ['https://expanded.tag#1'], + }, + }) + + expect(w3cCredentialRecord.getTags()).toEqual({ + claimFormat: 'ldp_vc', + issuerId: credential.issuerId, + subjectIds: credential.credentialSubjectIds, + cryptosuites: [], + schemaIds: credential.credentialSchemaIds, + contexts: credential.contexts, + proofTypes: credential.proofTypes, + givenId: credential.id, + expandedTypes: ['https://expanded.tag#1'], + types: ['VerifiableCredential', 'UniversityDegreeCredential'], + }) + + expect(getAnonCredsTagsFromRecord(w3cCredentialRecord)).toBeUndefined() + }) + }) +}) diff --git a/packages/core/src/modules/vc/repository/index.ts b/packages/core/src/modules/vc/repository/index.ts new file mode 100644 index 0000000000..64aae1fdcb --- /dev/null +++ b/packages/core/src/modules/vc/repository/index.ts @@ -0,0 +1,2 @@ +export * from './W3cCredentialRecord' +export * from './W3cCredentialRepository' diff --git a/packages/core/src/modules/vc/util.ts b/packages/core/src/modules/vc/util.ts new file mode 100644 index 0000000000..ac78458d87 --- /dev/null +++ b/packages/core/src/modules/vc/util.ts @@ -0,0 +1,14 @@ +/** + * Formats an input date to w3c standard date format + * @param date {number|string} Optional if not defined current date is returned + * + * @returns {string} date in a standard format as a string + */ +export const w3cDate = (date?: number | string): string => { + let result = new Date() + if (typeof date === 'number' || typeof date === 'string') { + result = new Date(date) + } + const str = result.toISOString() + return str.substr(0, str.length - 5) + 'Z' +} diff --git a/packages/core/src/modules/vc/validators.ts b/packages/core/src/modules/vc/validators.ts new file mode 100644 index 0000000000..fbb873e923 --- /dev/null +++ b/packages/core/src/modules/vc/validators.ts @@ -0,0 +1,32 @@ +import type { ValidationOptions } from 'class-validator' + +import { buildMessage, isString, isURL, ValidateBy } from 'class-validator' + +import { isJsonObject } from '../../utils' + +import { CREDENTIALS_CONTEXT_V1_URL } from './constants' + +export function IsCredentialJsonLdContext(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'IsCredentialJsonLdContext', + validator: { + validate: (value): boolean => { + if (!Array.isArray(value)) return false + + // First item must be the verifiable credential context + if (value[0] !== CREDENTIALS_CONTEXT_V1_URL) return false + + return value.every((v) => (isString(v) && isURL(v)) || isJsonObject(v)) + }, + defaultMessage: buildMessage( + (eachPrefix) => + eachPrefix + + '$property must be an array of strings or objects, where the first item is the verifiable credential context URL.', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/plugins/DependencyManager.ts b/packages/core/src/plugins/DependencyManager.ts new file mode 100644 index 0000000000..3487ec443e --- /dev/null +++ b/packages/core/src/plugins/DependencyManager.ts @@ -0,0 +1,123 @@ +import type { ModulesMap } from '../agent/AgentModules' +import type { MessageHandler } from '../agent/MessageHandler' +import type { MessageHandlerMiddleware } from '../agent/MessageHandlerMiddleware' +import type { Constructor } from '../utils/mixins' +import type { DependencyContainer } from 'tsyringe' + +import { container as rootContainer, InjectionToken, Lifecycle } from 'tsyringe' + +import { FeatureRegistry } from '../agent/FeatureRegistry' +import { MessageHandlerRegistry } from '../agent/MessageHandlerRegistry' +import { CredoError } from '../error' + +export { InjectionToken } + +export class DependencyManager { + public readonly container: DependencyContainer + public readonly registeredModules: ModulesMap + + public constructor( + container: DependencyContainer = rootContainer.createChildContainer(), + registeredModules: ModulesMap = {} + ) { + this.container = container + this.registeredModules = registeredModules + } + + public registerModules(modules: ModulesMap) { + const featureRegistry = this.resolve(FeatureRegistry) + + for (const [moduleKey, module] of Object.entries(modules)) { + if (this.registeredModules[moduleKey]) { + throw new CredoError( + `Module with key ${moduleKey} has already been registered. Only a single module can be registered with the same key.` + ) + } + + this.registeredModules[moduleKey] = module + if (module.api) { + this.registerContextScoped(module.api) + } + module.register(this, featureRegistry) + } + } + + public registerMessageHandlers(messageHandlers: MessageHandler[]) { + const messageHandlerRegistry = this.resolve(MessageHandlerRegistry) + + for (const messageHandler of messageHandlers) { + messageHandlerRegistry.registerMessageHandler(messageHandler) + } + } + + public registerMessageHandlerMiddleware(messageHandlerMiddleware: MessageHandlerMiddleware) { + const messageHandlerRegistry = this.resolve(MessageHandlerRegistry) + + messageHandlerRegistry.registerMessageHandlerMiddleware(messageHandlerMiddleware) + } + + public get fallbackMessageHandler() { + const messageHandlerRegistry = this.resolve(MessageHandlerRegistry) + + return messageHandlerRegistry.fallbackMessageHandler + } + + public get messageHandlerMiddlewares() { + const messageHandlerRegistry = this.resolve(MessageHandlerRegistry) + + return messageHandlerRegistry.messageHandlerMiddlewares + } + + /** + * Sets the fallback message handler, the message handler that will be called if no handler + * is registered for an incoming message type. + */ + public setFallbackMessageHandler(fallbackMessageHandler: MessageHandler['handle']) { + const messageHandlerRegistry = this.resolve(MessageHandlerRegistry) + + messageHandlerRegistry.setFallbackMessageHandler(fallbackMessageHandler) + } + + public registerSingleton(from: InjectionToken, to: InjectionToken): void + public registerSingleton(token: Constructor): void + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public registerSingleton(fromOrToken: InjectionToken | Constructor, to?: any) { + this.container.registerSingleton(fromOrToken, to) + } + + public resolve(token: InjectionToken): T { + return this.container.resolve(token) + } + + public registerInstance(token: InjectionToken, instance: T) { + this.container.registerInstance(token, instance) + } + + public isRegistered(token: InjectionToken): boolean { + return this.container.isRegistered(token) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public registerContextScoped(token: Constructor): void + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public registerContextScoped(token: InjectionToken, provider: Constructor): void + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public registerContextScoped(token: any, provider?: any) { + if (provider) this.container.register(token, provider, { lifecycle: Lifecycle.ContainerScoped }) + else this.container.register(token, token, { lifecycle: Lifecycle.ContainerScoped }) + } + + /** + * Dispose the dependency manager. Calls `.dispose()` on all instances that implement the `Disposable` interface and have + * been constructed by the `DependencyManager`. This means all instances registered using `registerInstance` won't have the + * dispose method called. + */ + public async dispose() { + await this.container.dispose() + } + + public createChild() { + return new DependencyManager(this.container.createChildContainer(), this.registeredModules) + } +} diff --git a/packages/core/src/plugins/Module.ts b/packages/core/src/plugins/Module.ts new file mode 100644 index 0000000000..183a47c7f8 --- /dev/null +++ b/packages/core/src/plugins/Module.ts @@ -0,0 +1,20 @@ +import type { DependencyManager } from './DependencyManager' +import type { AgentContext } from '../agent' +import type { FeatureRegistry } from '../agent/FeatureRegistry' +import type { Update } from '../storage/migration/updates' +import type { Constructor } from '../utils/mixins' + +export interface Module { + api?: Constructor + register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void + initialize?(agentContext: AgentContext): Promise + + /** + * List of updates that should be executed when the framework version is updated. + */ + updates?: Update[] +} + +export interface ApiModule extends Module { + api: Constructor +} diff --git a/packages/core/src/plugins/__tests__/DependencyManager.test.ts b/packages/core/src/plugins/__tests__/DependencyManager.test.ts new file mode 100644 index 0000000000..780bbaba67 --- /dev/null +++ b/packages/core/src/plugins/__tests__/DependencyManager.test.ts @@ -0,0 +1,154 @@ +import type { Module } from '../Module' +import type { DependencyContainer } from 'tsyringe' + +import { container as rootContainer, injectable, Lifecycle } from 'tsyringe' + +import { FeatureRegistry } from '../../agent/FeatureRegistry' +import { DependencyManager } from '../DependencyManager' + +class Instance { + public random = Math.random() +} +const instance = new Instance() + +describe('DependencyManager', () => { + let container: DependencyContainer + let dependencyManager: DependencyManager + + beforeEach(() => { + container = rootContainer.createChildContainer() + dependencyManager = new DependencyManager(container) + }) + + afterEach(() => { + jest.resetAllMocks() + container.reset() + }) + + describe('registerModules', () => { + it('calls the register method for all module plugins', () => { + @injectable() + class Module1 implements Module { + public register = jest.fn() + } + + @injectable() + class Module2 implements Module { + public register = jest.fn() + } + + const module1 = new Module1() + const module2 = new Module2() + + const featureRegistry = container.resolve(FeatureRegistry) + + dependencyManager.registerModules({ module1, module2 }) + expect(module1.register).toHaveBeenCalledTimes(1) + expect(module1.register).toHaveBeenLastCalledWith(dependencyManager, featureRegistry) + + expect(module2.register).toHaveBeenCalledTimes(1) + expect(module2.register).toHaveBeenLastCalledWith(dependencyManager, featureRegistry) + + expect(dependencyManager.registeredModules).toMatchObject({ + module1, + module2, + }) + }) + }) + + describe('registerSingleton', () => { + it('calls registerSingleton on the container', () => { + class Singleton {} + + const registerSingletonSpy = jest.spyOn(container, 'registerSingleton') + dependencyManager.registerSingleton(Singleton) + + expect(registerSingletonSpy).toHaveBeenLastCalledWith(Singleton, undefined) + + dependencyManager.registerSingleton(Singleton, 'Singleton') + + expect(registerSingletonSpy).toHaveBeenLastCalledWith(Singleton, 'Singleton') + }) + }) + + describe('resolve', () => { + it('calls resolve on the container', () => { + // FIXME: somehow this doesn't work if we don't create a child container + const child = container.createChildContainer() + const dependencyManager = new DependencyManager(child) + child.registerInstance(Instance, instance) + + const resolveSpy = jest.spyOn(child, 'resolve') + expect(dependencyManager.resolve(Instance)).toBe(instance) + + expect(resolveSpy).toHaveBeenCalledWith(Instance) + }) + }) + + describe('isRegistered', () => { + it('calls isRegistered on the container', () => { + class Singleton {} + + const isRegisteredSpy = jest.spyOn(container, 'isRegistered') + + expect(dependencyManager.isRegistered(Singleton)).toBe(false) + + expect(isRegisteredSpy).toHaveBeenCalledTimes(1) + }) + }) + + describe('registerInstance', () => { + it('calls registerInstance on the container', () => { + class Instance {} + const instance = new Instance() + + const registerInstanceSpy = jest.spyOn(container, 'registerInstance') + + dependencyManager.registerInstance(Instance, instance) + + expect(registerInstanceSpy).toHaveBeenCalledWith(Instance, instance) + }) + }) + + describe('registerContextScoped', () => { + it('calls register on the container with Lifecycle.ContainerScoped', () => { + class SomeService {} + + const registerSpy = jest.spyOn(container, 'register') + + dependencyManager.registerContextScoped(SomeService) + expect(registerSpy).toHaveBeenCalledWith(SomeService, SomeService, { lifecycle: Lifecycle.ContainerScoped }) + registerSpy.mockClear() + + dependencyManager.registerContextScoped('SomeService', SomeService) + expect(registerSpy).toHaveBeenCalledWith('SomeService', SomeService, { lifecycle: Lifecycle.ContainerScoped }) + }) + }) + + describe('createChild', () => { + it('calls createChildContainer on the container', () => { + const createChildSpy = jest.spyOn(container, 'createChildContainer') + + const childDependencyManager = dependencyManager.createChild() + expect(createChildSpy).toHaveBeenCalledTimes(1) + expect(childDependencyManager.container).toBe(createChildSpy.mock.results[0].value) + }) + + it('inherits the registeredModules from the parent dependency manager', () => { + const module = { + register: jest.fn(), + } + + dependencyManager.registerModules({ + module1: module, + module2: module, + }) + + const childDependencyManager = dependencyManager.createChild() + expect(childDependencyManager.registeredModules).toMatchObject({ + module1: module, + module2: module, + }) + }) + }) +}) diff --git a/packages/core/src/plugins/index.ts b/packages/core/src/plugins/index.ts new file mode 100644 index 0000000000..bf419032f6 --- /dev/null +++ b/packages/core/src/plugins/index.ts @@ -0,0 +1,4 @@ +export * from './DependencyManager' +export * from './Module' +export * from './utils' +export { inject, injectable, Disposable, injectAll } from 'tsyringe' diff --git a/packages/core/src/plugins/utils.ts b/packages/core/src/plugins/utils.ts new file mode 100644 index 0000000000..34ad85a7b9 --- /dev/null +++ b/packages/core/src/plugins/utils.ts @@ -0,0 +1,34 @@ +import type { ApiModule, Module } from './Module' +import type { AgentContext } from '../agent' + +export function getRegisteredModuleByInstance( + agentContext: AgentContext, + moduleType: { new (...args: unknown[]): M } +): M | undefined { + const module = Object.values(agentContext.dependencyManager.registeredModules).find( + (module): module is M => module instanceof moduleType + ) + + return module +} + +export function getRegisteredModuleByName( + agentContext: AgentContext, + constructorName: string +): M | undefined { + const module = Object.values(agentContext.dependencyManager.registeredModules).find( + (module): module is M => module.constructor.name === constructorName + ) + + return module +} + +export function getApiForModuleByName( + agentContext: AgentContext, + constructorName: string +): InstanceType | undefined { + const module = getRegisteredModuleByName(agentContext, constructorName) + if (!module || !module.api) return undefined + + return agentContext.dependencyManager.resolve(module.api) as InstanceType +} diff --git a/packages/core/src/storage/BaseRecord.ts b/packages/core/src/storage/BaseRecord.ts new file mode 100644 index 0000000000..7f26952200 --- /dev/null +++ b/packages/core/src/storage/BaseRecord.ts @@ -0,0 +1,107 @@ +import { Exclude } from 'class-transformer' + +import { JsonTransformer } from '../utils/JsonTransformer' +import { DateTransformer, MetadataTransformer } from '../utils/transformers' + +import { Metadata } from './Metadata' + +export type TagValue = string | boolean | undefined | Array | null +export type TagsBase = { + [key: string]: TagValue + [key: number]: never +} + +export type Tags = CustomTags & DefaultTags + +export type RecordTags = ReturnType + +// The BaseRecord requires a DefaultTags and CustomTags type, but we want to be +// able to use the BaseRecord without specifying these types. If we don't specify +// these types, the default TagsBase will be used, but this is not compatible +// with records that have specified a custom type. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type BaseRecordAny = BaseRecord + +export abstract class BaseRecord< + DefaultTags extends TagsBase = TagsBase, + CustomTags extends TagsBase = TagsBase, + // We want an empty object, as Record will make typescript + // not infer the types correctly + // eslint-disable-next-line @typescript-eslint/ban-types + MetadataValues = {} +> { + protected _tags: CustomTags = {} as CustomTags + + public id!: string + + @DateTransformer() + public createdAt!: Date + + @DateTransformer() + public updatedAt?: Date + + @Exclude() + public readonly type = BaseRecord.type + public static readonly type: string = 'BaseRecord' + + /** @inheritdoc {Metadata#Metadata} */ + @MetadataTransformer() + public metadata: Metadata = new Metadata({}) + + /** + * Get all tags. This is includes custom and default tags + * @returns tags object + */ + public abstract getTags(): Tags + + /** + * Set the value for a tag + * @param name name of the tag + * @param value value of the tag + */ + public setTag(name: keyof CustomTags, value: CustomTags[keyof CustomTags]) { + this._tags[name] = value + } + + /** + * Get the value for a tag + * @param name name of the tag + * @returns The tag value, or undefined if not found + */ + public getTag(name: keyof CustomTags | keyof DefaultTags) { + return this.getTags()[name] + } + + /** + * Set custom tags. This will merge the tags object with passed in tag properties + * + * @param tags the tags to set + */ + public setTags(tags: Partial) { + this._tags = { + ...this._tags, + ...tags, + } + } + + /** + * Replace tags. This will replace the whole tags object. + * Default tags will still be overridden when retrieving tags + * + * @param tags the tags to set + */ + public replaceTags(tags: CustomTags & Partial) { + this._tags = tags + } + + public toJSON(): Record { + return JsonTransformer.toJSON(this) + } + + /** + * Clones the record. + */ + public clone() { + return JsonTransformer.clone(this) + } +} diff --git a/packages/core/src/storage/FileSystem.ts b/packages/core/src/storage/FileSystem.ts new file mode 100644 index 0000000000..9a5710835e --- /dev/null +++ b/packages/core/src/storage/FileSystem.ts @@ -0,0 +1,19 @@ +import type { Buffer } from '../utils/buffer' + +export interface DownloadToFileOptions { + verifyHash?: { algorithm: 'sha256'; hash: Buffer } +} + +export interface FileSystem { + readonly dataPath: string + readonly cachePath: string + readonly tempPath: string + + exists(path: string): Promise + createDirectory(path: string): Promise + copyFile(sourcePath: string, destinationPath: string): Promise + write(path: string, data: string): Promise + read(path: string): Promise + delete(path: string): Promise + downloadToFile(url: string, path: string, options?: DownloadToFileOptions): Promise +} diff --git a/packages/core/src/storage/Metadata.ts b/packages/core/src/storage/Metadata.ts new file mode 100644 index 0000000000..c635c1c2c5 --- /dev/null +++ b/packages/core/src/storage/Metadata.ts @@ -0,0 +1,90 @@ +// Any is used to prevent frustrating TS errors if we just want to store arbitrary json data +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type MetadataValue = Record + +export type MetadataBase = { + [key: string]: MetadataValue +} + +/** + * Metadata access class to get, set (create and update), add (append to a record) and delete metadata on any record. + * + * set will override the previous value if it already exists + * + * note: To add persistence to these records, you have to update the record in the correct repository + * + * @example + * + * ```ts + * connectionRecord.metadata.set('foo', { bar: 'baz' }) connectionRepository.update(connectionRecord) + * ``` + */ +export class Metadata { + public readonly data: MetadataBase + + public constructor(data: MetadataBase) { + this.data = data + } + + /** + * Gets the value by key in the metadata + * + * Any extension of the `BaseRecord` can implement their own typed metadata + * + * @param key the key to retrieve the metadata by + * @returns the value saved in the key value pair + * @returns null when the key could not be found + */ + public get( + key: Key + ): (Key extends keyof MetadataTypes ? MetadataTypes[Key] : Value) | null { + return (this.data[key] as Key extends keyof MetadataTypes ? MetadataTypes[Key] : Value) ?? null + } + + /** + * Will set, or override, a key-value pair on the metadata + * + * @param key the key to set the metadata by + * @param value the value to set in the metadata + */ + public set( + key: Key, + value: Key extends keyof MetadataTypes ? MetadataTypes[Key] : Value + ): void { + this.data[key] = value as MetadataValue + } + + /** + * Adds a record to a metadata key + * + * @param key the key to add the metadata at + * @param value the value to add in the metadata + */ + public add( + key: Key, + value: Partial + ): void { + this.data[key] = { + ...this.data[key], + ...value, + } + } + + /** + * Retrieves all the metadata for a record + * + * @returns all the metadata that exists on the record + */ + public get keys(): string[] { + return Object.keys(this.data) + } + + /** + * Will delete the key value pair in the metadata + * + * @param key the key to delete the data by + */ + public delete(key: Key): void { + delete this.data[key] + } +} diff --git a/packages/core/src/storage/Repository.ts b/packages/core/src/storage/Repository.ts new file mode 100644 index 0000000000..f97dde1c2d --- /dev/null +++ b/packages/core/src/storage/Repository.ts @@ -0,0 +1,152 @@ +import type { BaseRecord } from './BaseRecord' +import type { RecordSavedEvent, RecordUpdatedEvent, RecordDeletedEvent } from './RepositoryEvents' +import type { BaseRecordConstructor, Query, QueryOptions, StorageService } from './StorageService' +import type { AgentContext } from '../agent' +import type { EventEmitter } from '../agent/EventEmitter' + +import { RecordDuplicateError, RecordNotFoundError } from '../error' + +import { RepositoryEventTypes } from './RepositoryEvents' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class Repository> { + private storageService: StorageService + private recordClass: BaseRecordConstructor + private eventEmitter: EventEmitter + + public constructor( + recordClass: BaseRecordConstructor, + storageService: StorageService, + eventEmitter: EventEmitter + ) { + this.storageService = storageService + this.recordClass = recordClass + this.eventEmitter = eventEmitter + } + + /** @inheritDoc {StorageService#save} */ + public async save(agentContext: AgentContext, record: T): Promise { + await this.storageService.save(agentContext, record) + + this.eventEmitter.emit>(agentContext, { + type: RepositoryEventTypes.RecordSaved, + payload: { + // Record in event should be static + record: record.clone(), + }, + }) + } + + /** @inheritDoc {StorageService#update} */ + public async update(agentContext: AgentContext, record: T): Promise { + await this.storageService.update(agentContext, record) + + this.eventEmitter.emit>(agentContext, { + type: RepositoryEventTypes.RecordUpdated, + payload: { + // Record in event should be static + record: record.clone(), + }, + }) + } + + /** @inheritDoc {StorageService#delete} */ + public async delete(agentContext: AgentContext, record: T): Promise { + await this.storageService.delete(agentContext, record) + + this.eventEmitter.emit>(agentContext, { + type: RepositoryEventTypes.RecordDeleted, + payload: { + // Record in event should be static + record: record.clone(), + }, + }) + } + + /** + * Delete record by id. Throws {RecordNotFoundError} if no record is found + * @param id the id of the record to delete + * @returns + */ + public async deleteById(agentContext: AgentContext, id: string): Promise { + await this.storageService.deleteById(agentContext, this.recordClass, id) + + this.eventEmitter.emit>(agentContext, { + type: RepositoryEventTypes.RecordDeleted, + payload: { + record: { id, type: this.recordClass.type }, + }, + }) + } + + /** @inheritDoc {StorageService#getById} */ + public async getById(agentContext: AgentContext, id: string): Promise { + return this.storageService.getById(agentContext, this.recordClass, id) + } + + /** + * Find record by id. Returns null if no record is found + * @param id the id of the record to retrieve + * @returns + */ + public async findById(agentContext: AgentContext, id: string): Promise { + try { + return await this.storageService.getById(agentContext, this.recordClass, id) + } catch (error) { + if (error instanceof RecordNotFoundError) return null + + throw error + } + } + + /** @inheritDoc {StorageService#getAll} */ + public async getAll(agentContext: AgentContext): Promise { + return this.storageService.getAll(agentContext, this.recordClass) + } + + /** @inheritDoc {StorageService#findByQuery} */ + public async findByQuery(agentContext: AgentContext, query: Query, queryOptions?: QueryOptions): Promise { + return this.storageService.findByQuery(agentContext, this.recordClass, query, queryOptions) + } + + /** + * Find a single record by query. Returns null if not found. + * @param query the query + * @returns the record, or null if not found + * @throws {RecordDuplicateError} if multiple records are found for the given query + */ + public async findSingleByQuery(agentContext: AgentContext, query: Query): Promise { + const records = await this.findByQuery(agentContext, query) + + if (records.length > 1) { + throw new RecordDuplicateError(`Multiple records found for given query '${JSON.stringify(query)}'`, { + recordType: this.recordClass.type, + }) + } + + if (records.length < 1) { + return null + } + + return records[0] + } + + /** + * Find a single record by query. Throws if not found + * @param query the query + * @returns the record + * @throws {RecordDuplicateError} if multiple records are found for the given query + * @throws {RecordNotFoundError} if no record is found for the given query + */ + public async getSingleByQuery(agentContext: AgentContext, query: Query): Promise { + const record = await this.findSingleByQuery(agentContext, query) + + if (!record) { + throw new RecordNotFoundError(`No record found for given query '${JSON.stringify(query)}'`, { + recordType: this.recordClass.type, + }) + } + + return record + } +} diff --git a/packages/core/src/storage/RepositoryEvents.ts b/packages/core/src/storage/RepositoryEvents.ts new file mode 100644 index 0000000000..ac6524eb01 --- /dev/null +++ b/packages/core/src/storage/RepositoryEvents.ts @@ -0,0 +1,32 @@ +import type { BaseRecord } from './BaseRecord' +import type { BaseEvent } from '../agent/Events' + +export enum RepositoryEventTypes { + RecordSaved = 'RecordSaved', + RecordUpdated = 'RecordUpdated', + RecordDeleted = 'RecordDeleted', +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface RecordSavedEvent> extends BaseEvent { + type: typeof RepositoryEventTypes.RecordSaved + payload: { + record: T + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface RecordUpdatedEvent> extends BaseEvent { + type: typeof RepositoryEventTypes.RecordUpdated + payload: { + record: T + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface RecordDeletedEvent> extends BaseEvent { + type: typeof RepositoryEventTypes.RecordDeleted + payload: { + record: T | { id: string; type: string } + } +} diff --git a/packages/core/src/storage/StorageService.ts b/packages/core/src/storage/StorageService.ts new file mode 100644 index 0000000000..79b66fb92b --- /dev/null +++ b/packages/core/src/storage/StorageService.ts @@ -0,0 +1,96 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { BaseRecord, TagsBase } from './BaseRecord' +import type { AgentContext } from '../agent' +import type { Constructor } from '../utils/mixins' + +// https://stackoverflow.com/questions/51954558/how-can-i-remove-a-wider-type-from-a-union-type-without-removing-its-subtypes-in/51955852#51955852 +export type SimpleQuery> = T extends BaseRecord + ? DefaultTags extends TagsBase + ? Partial> & TagsBase + : CustomTags extends TagsBase + ? Partial> & TagsBase + : Partial & TagsBase + : Partial> & TagsBase + +interface AdvancedQuery> { + $and?: Query[] + $or?: Query[] + $not?: Query +} + +export type QueryOptions = { + limit?: number + offset?: number +} + +export type Query> = AdvancedQuery | SimpleQuery + +export interface BaseRecordConstructor extends Constructor { + type: string +} + +export interface StorageService> { + /** + * Save record in storage + * + * @param record the record to store + * @throws {RecordDuplicateError} if a record with this id already exists + */ + save(agentContext: AgentContext, record: T): Promise + + /** + * Update record in storage + * + * @param record the record to update + * @throws {RecordNotFoundError} if a record with this id and type does not exist + */ + update(agentContext: AgentContext, record: T): Promise + + /** + * Delete record from storage + * + * @param record the record to delete + * @throws {RecordNotFoundError} if a record with this id and type does not exist + */ + delete(agentContext: AgentContext, record: T): Promise + + /** + * Delete record by id. + * + * @param recordClass the record class to delete the record for + * @param id the id of the record to delete from storage + * @throws {RecordNotFoundError} if a record with this id and type does not exist + */ + deleteById(agentContext: AgentContext, recordClass: BaseRecordConstructor, id: string): Promise + + /** + * Get record by id. + * + * @param recordClass the record class to get the record for + * @param id the id of the record to retrieve from storage + * @throws {RecordNotFoundError} if a record with this id and type does not exist + */ + getById(agentContext: AgentContext, recordClass: BaseRecordConstructor, id: string): Promise + + /** + * Get all records by specified record class. + * + * @param recordClass the record class to get records for + */ + getAll(agentContext: AgentContext, recordClass: BaseRecordConstructor): Promise + + /** + * Find all records by specified record class and query. + * + * @param recordClass the record class to find records for + * @param query the query to use for finding records + * @param queryOptions optional parameters to customize the query execution (e.g., limit, offset) + * + */ + findByQuery( + agentContext: AgentContext, + recordClass: BaseRecordConstructor, + query: Query, + queryOptions?: QueryOptions + ): Promise +} diff --git a/packages/core/src/storage/__tests__/DidCommMessageRecord.test.ts b/packages/core/src/storage/__tests__/DidCommMessageRecord.test.ts new file mode 100644 index 0000000000..30198e7f00 --- /dev/null +++ b/packages/core/src/storage/__tests__/DidCommMessageRecord.test.ts @@ -0,0 +1,55 @@ +import { ConnectionInvitationMessage } from '../../modules/connections' +import { DidCommMessageRecord, DidCommMessageRole } from '../didcomm' + +describe('DidCommMessageRecord', () => { + it('correctly computes message type tags', () => { + const didCommMessage = { + '@id': '7eb74118-7f91-4ba9-9960-c709b036aa86', + '@type': 'https://didcomm.org/test-protocol/1.0/send-test', + some: { other: 'property' }, + '~thread': { + thid: 'ea24e14a-4fc4-40f4-85a0-f6fcf02bfc1c', + }, + } + + const didCommeMessageRecord = new DidCommMessageRecord({ + message: didCommMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0', + }) + + expect(didCommeMessageRecord.getTags()).toEqual({ + role: DidCommMessageRole.Receiver, + associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0', + + // Computed properties based on message id and type + threadId: 'ea24e14a-4fc4-40f4-85a0-f6fcf02bfc1c', + protocolName: 'test-protocol', + messageName: 'send-test', + protocolMajorVersion: '1', + protocolMinorVersion: '0', + messageType: 'https://didcomm.org/test-protocol/1.0/send-test', + messageId: '7eb74118-7f91-4ba9-9960-c709b036aa86', + }) + }) + + it('correctly returns a message class instance', () => { + const invitationJson = { + '@type': 'https://didcomm.org/connections/1.0/invitation', + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'], + serviceEndpoint: 'https://example.com', + label: 'test', + } + + const didCommeMessageRecord = new DidCommMessageRecord({ + message: invitationJson, + role: DidCommMessageRole.Receiver, + associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0', + }) + + const invitation = didCommeMessageRecord.getMessageInstance(ConnectionInvitationMessage) + + expect(invitation).toBeInstanceOf(ConnectionInvitationMessage) + }) +}) diff --git a/packages/core/src/storage/__tests__/DidCommMessageRepository.test.ts b/packages/core/src/storage/__tests__/DidCommMessageRepository.test.ts new file mode 100644 index 0000000000..804b3f7f0d --- /dev/null +++ b/packages/core/src/storage/__tests__/DidCommMessageRepository.test.ts @@ -0,0 +1,179 @@ +import type { StorageService } from '../StorageService' + +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../tests/helpers' +import { EventEmitter } from '../../agent/EventEmitter' +import { ConnectionInvitationMessage } from '../../modules/connections' +import { JsonTransformer } from '../../utils/JsonTransformer' +import { DidCommMessageRecord, DidCommMessageRepository, DidCommMessageRole } from '../didcomm' + +jest.mock('../../../../../tests/InMemoryStorageService') + +const StorageMock = InMemoryStorageService as unknown as jest.Mock> + +const invitationJson = { + '@type': 'https://didcomm.org/connections/1.0/invitation', + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'], + serviceEndpoint: 'https://example.com', + label: 'test', +} + +const config = getAgentConfig('DidCommMessageRepository') +const agentContext = getAgentContext() + +describe('DidCommMessageRepository', () => { + let repository: DidCommMessageRepository + let storageMock: StorageService + let eventEmitter: EventEmitter + + beforeEach(async () => { + storageMock = new StorageMock() + eventEmitter = new EventEmitter(config.agentDependencies, new Subject()) + repository = new DidCommMessageRepository(storageMock, eventEmitter) + }) + + const getRecord = ({ id }: { id?: string } = {}) => { + return new DidCommMessageRecord({ + id, + message: invitationJson, + role: DidCommMessageRole.Receiver, + associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0', + }) + } + + describe('getAgentMessage()', () => { + it('should get the record using the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record])) + + const invitation = await repository.findAgentMessage(agentContext, { + messageClass: ConnectionInvitationMessage, + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + + expect(storageMock.findByQuery).toBeCalledWith( + agentContext, + DidCommMessageRecord, + { + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + messageName: 'invitation', + protocolName: 'connections', + protocolMajorVersion: '1', + }, + undefined + ) + expect(invitation).toBeInstanceOf(ConnectionInvitationMessage) + }) + }) + describe('findAgentMessage()', () => { + it('should get the record using the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record])) + + const invitation = await repository.findAgentMessage(agentContext, { + messageClass: ConnectionInvitationMessage, + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + + expect(storageMock.findByQuery).toBeCalledWith( + agentContext, + DidCommMessageRecord, + { + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + messageName: 'invitation', + protocolName: 'connections', + protocolMajorVersion: '1', + }, + undefined + ) + expect(invitation).toBeInstanceOf(ConnectionInvitationMessage) + }) + + it("should return null because the record doesn't exist", async () => { + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([])) + + const invitation = await repository.findAgentMessage(agentContext, { + messageClass: ConnectionInvitationMessage, + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + + expect(storageMock.findByQuery).toBeCalledWith( + agentContext, + DidCommMessageRecord, + { + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + messageName: 'invitation', + protocolName: 'connections', + protocolMajorVersion: '1', + }, + undefined + ) + expect(invitation).toBeNull() + }) + }) + + describe('saveAgentMessage()', () => { + it('should transform and save the agent message', async () => { + await repository.saveAgentMessage(agentContext, { + role: DidCommMessageRole.Receiver, + agentMessage: JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage), + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + + expect(storageMock.save).toBeCalledWith( + agentContext, + expect.objectContaining({ + role: DidCommMessageRole.Receiver, + message: invitationJson, + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + ) + }) + }) + + describe('saveOrUpdateAgentMessage()', () => { + it('should transform and save the agent message', async () => { + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([])) + await repository.saveOrUpdateAgentMessage(agentContext, { + role: DidCommMessageRole.Receiver, + agentMessage: JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage), + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + + expect(storageMock.save).toBeCalledWith( + agentContext, + expect.objectContaining({ + role: DidCommMessageRole.Receiver, + message: invitationJson, + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + ) + }) + + it('should transform and update the agent message', async () => { + const record = getRecord({ id: 'test-id' }) + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record])) + await repository.saveOrUpdateAgentMessage(agentContext, { + role: DidCommMessageRole.Receiver, + agentMessage: JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage), + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + + expect(storageMock.findByQuery).toBeCalledWith( + agentContext, + DidCommMessageRecord, + { + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + messageName: 'invitation', + protocolName: 'connections', + protocolMajorVersion: '1', + }, + undefined + ) + expect(storageMock.update).toBeCalledWith(agentContext, record) + }) + }) +}) diff --git a/packages/core/src/storage/__tests__/Metadata.test.ts b/packages/core/src/storage/__tests__/Metadata.test.ts new file mode 100644 index 0000000000..847f6be318 --- /dev/null +++ b/packages/core/src/storage/__tests__/Metadata.test.ts @@ -0,0 +1,64 @@ +import { TestRecord } from './TestRecord' + +describe('Metadata', () => { + let testRecord: TestRecord + + beforeEach(() => { + testRecord = new TestRecord() + }) + + test('set() as create', () => { + testRecord.metadata.set('bar', { aries: { framework: 'javascript' } }) + + expect(testRecord.toJSON()).toMatchObject({ + metadata: { bar: { aries: { framework: 'javascript' } } }, + }) + }) + + test('set() as update ', () => { + testRecord.metadata.set('bar', { baz: 'abc' }) + expect(testRecord.toJSON()).toMatchObject({ + metadata: { bar: { baz: 'abc' } }, + }) + + testRecord.metadata.set('bar', { baz: 'foo' }) + expect(testRecord.toJSON()).toMatchObject({ + metadata: { bar: { baz: 'foo' } }, + }) + }) + + test('add() ', () => { + testRecord.metadata.set('sample', { foo: 'bar' }) + testRecord.metadata.add('sample', { baz: 'foo' }) + + expect(testRecord.toJSON()).toMatchObject({ + metadata: { sample: { foo: 'bar', baz: 'foo' } }, + }) + }) + + test('get()', () => { + testRecord.metadata.set('bar', { baz: 'foo' }) + const record = testRecord.metadata.get<{ baz: 'foo' }>('bar') + + expect(record).toMatchObject({ baz: 'foo' }) + }) + + test('delete()', () => { + testRecord.metadata.set('bar', { baz: 'foo' }) + testRecord.metadata.delete('bar') + + expect(testRecord.toJSON()).toMatchObject({ + metadata: {}, + }) + }) + + test('keys()', () => { + testRecord.metadata.set('bar', { baz: 'foo' }) + testRecord.metadata.set('bazz', { blub: 'foo' }) + testRecord.metadata.set('test', { abc: { def: 'hij' } }) + + const keys = testRecord.metadata.keys + + expect(keys).toMatchObject(['bar', 'bazz', 'test']) + }) +}) diff --git a/packages/core/src/storage/__tests__/Repository.test.ts b/packages/core/src/storage/__tests__/Repository.test.ts new file mode 100644 index 0000000000..c99a1022cc --- /dev/null +++ b/packages/core/src/storage/__tests__/Repository.test.ts @@ -0,0 +1,309 @@ +import type { AgentContext } from '../../agent' +import type { TagsBase } from '../BaseRecord' +import type { RecordDeletedEvent, RecordSavedEvent, RecordUpdatedEvent } from '../RepositoryEvents' +import type { StorageService } from '../StorageService' + +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../tests/helpers' +import { EventEmitter } from '../../agent/EventEmitter' +import { CredoError, RecordDuplicateError, RecordNotFoundError } from '../../error' +import { Repository } from '../Repository' +import { RepositoryEventTypes } from '../RepositoryEvents' + +import { TestRecord } from './TestRecord' + +jest.mock('../../../../../tests/InMemoryStorageService') + +const StorageMock = InMemoryStorageService as unknown as jest.Mock> + +const config = getAgentConfig('Repository') + +describe('Repository', () => { + let repository: Repository + let storageMock: StorageService + let agentContext: AgentContext + let eventEmitter: EventEmitter + + beforeEach(async () => { + storageMock = new StorageMock() + eventEmitter = new EventEmitter(config.agentDependencies, new Subject()) + repository = new Repository(TestRecord, storageMock, eventEmitter) + agentContext = getAgentContext() + }) + + const getRecord = ({ id, tags }: { id?: string; tags?: TagsBase } = {}) => { + return new TestRecord({ + id, + foo: 'bar', + tags: tags ?? { myTag: 'foobar' }, + }) + } + + describe('save()', () => { + it('should save the record using the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + await repository.save(agentContext, record) + + expect(storageMock.save).toBeCalledWith(agentContext, record) + }) + + it(`should emit saved event`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on>(RepositoryEventTypes.RecordSaved, eventListenerMock) + + // given + const record = getRecord({ id: 'test-id' }) + + // when + await repository.save(agentContext, record) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'RecordSaved', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + record: expect.objectContaining({ + id: 'test-id', + }), + }, + }) + }) + }) + + describe('update()', () => { + it('should update the record using the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + await repository.update(agentContext, record) + + expect(storageMock.update).toBeCalledWith(agentContext, record) + }) + + it(`should emit updated event`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on>(RepositoryEventTypes.RecordUpdated, eventListenerMock) + + // given + const record = getRecord({ id: 'test-id' }) + + // when + await repository.update(agentContext, record) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'RecordUpdated', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + record: expect.objectContaining({ + id: 'test-id', + }), + }, + }) + }) + }) + + describe('delete()', () => { + it('should delete the record using the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + await repository.delete(agentContext, record) + + expect(storageMock.delete).toBeCalledWith(agentContext, record) + }) + + it(`should emit deleted event`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on>(RepositoryEventTypes.RecordDeleted, eventListenerMock) + + // given + const record = getRecord({ id: 'test-id' }) + + // when + await repository.delete(agentContext, record) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'RecordDeleted', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + record: expect.objectContaining({ + id: 'test-id', + }), + }, + }) + }) + }) + + describe('deleteById()', () => { + it('should delete the record by record id', async () => { + await repository.deleteById(agentContext, 'test-id') + + expect(storageMock.deleteById).toBeCalledWith(agentContext, TestRecord, 'test-id') + }) + + it(`should emit deleted event`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on>(RepositoryEventTypes.RecordDeleted, eventListenerMock) + + const record = getRecord({ id: 'test-id' }) + + await repository.deleteById(agentContext, record.id) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'RecordDeleted', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + record: expect.objectContaining({ + id: record.id, + type: record.type, + }), + }, + }) + }) + }) + + describe('getById()', () => { + it('should get the record using the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + mockFunction(storageMock.getById).mockReturnValue(Promise.resolve(record)) + + const returnValue = await repository.getById(agentContext, 'test-id') + + expect(storageMock.getById).toBeCalledWith(agentContext, TestRecord, 'test-id') + expect(returnValue).toBe(record) + }) + }) + + describe('findById()', () => { + it('should get the record using the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + mockFunction(storageMock.getById).mockReturnValue(Promise.resolve(record)) + + const returnValue = await repository.findById(agentContext, 'test-id') + + expect(storageMock.getById).toBeCalledWith(agentContext, TestRecord, 'test-id') + expect(returnValue).toBe(record) + }) + + it('should return null if the storage service throws RecordNotFoundError', async () => { + mockFunction(storageMock.getById).mockReturnValue( + Promise.reject(new RecordNotFoundError('Not found', { recordType: TestRecord.type })) + ) + + const returnValue = await repository.findById(agentContext, 'test-id') + + expect(storageMock.getById).toBeCalledWith(agentContext, TestRecord, 'test-id') + expect(returnValue).toBeNull() + }) + + it('should return null if the storage service throws an error that is not RecordNotFoundError', async () => { + mockFunction(storageMock.getById).mockReturnValue(Promise.reject(new CredoError('Not found'))) + + expect(repository.findById(agentContext, 'test-id')).rejects.toThrowError(CredoError) + expect(storageMock.getById).toBeCalledWith(agentContext, TestRecord, 'test-id') + }) + }) + + describe('getAll()', () => { + it('should get the records using the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + const record2 = getRecord({ id: 'test-id2' }) + mockFunction(storageMock.getAll).mockReturnValue(Promise.resolve([record, record2])) + + const returnValue = await repository.getAll(agentContext) + + expect(storageMock.getAll).toBeCalledWith(agentContext, TestRecord) + expect(returnValue).toEqual(expect.arrayContaining([record, record2])) + }) + }) + + describe('findByQuery()', () => { + it('should get the records using the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + const record2 = getRecord({ id: 'test-id2' }) + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record, record2])) + + const returnValue = await repository.findByQuery(agentContext, { something: 'interesting' }, { limit: 10 }) + + expect(storageMock.findByQuery).toBeCalledWith( + agentContext, + TestRecord, + { something: 'interesting' }, + { limit: 10 } + ) + expect(returnValue).toEqual(expect.arrayContaining([record, record2])) + }) + }) + + describe('findSingleByQuery()', () => { + it('should get the record using the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record])) + + const returnValue = await repository.findSingleByQuery(agentContext, { something: 'interesting' }) + + expect(storageMock.findByQuery).toBeCalledWith(agentContext, TestRecord, { something: 'interesting' }, undefined) + expect(returnValue).toBe(record) + }) + + it('should return null if the no records are returned by the storage service', async () => { + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([])) + + const returnValue = await repository.findSingleByQuery(agentContext, { something: 'interesting' }) + + expect(storageMock.findByQuery).toBeCalledWith(agentContext, TestRecord, { something: 'interesting' }, undefined) + expect(returnValue).toBeNull() + }) + + it('should throw RecordDuplicateError if more than one record is returned by the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + const record2 = getRecord({ id: 'test-id2' }) + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record, record2])) + + expect(repository.findSingleByQuery(agentContext, { something: 'interesting' })).rejects.toThrowError( + RecordDuplicateError + ) + expect(storageMock.findByQuery).toBeCalledWith(agentContext, TestRecord, { something: 'interesting' }, undefined) + }) + }) + + describe('getSingleByQuery()', () => { + it('should get the record using the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record])) + + const returnValue = await repository.getSingleByQuery(agentContext, { something: 'interesting' }) + + expect(storageMock.findByQuery).toBeCalledWith(agentContext, TestRecord, { something: 'interesting' }, undefined) + expect(returnValue).toBe(record) + }) + + it('should throw RecordNotFoundError if no records are returned by the storage service', async () => { + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([])) + + expect(repository.getSingleByQuery(agentContext, { something: 'interesting' })).rejects.toThrowError( + RecordNotFoundError + ) + expect(storageMock.findByQuery).toBeCalledWith(agentContext, TestRecord, { something: 'interesting' }, undefined) + }) + + it('should throw RecordDuplicateError if more than one record is returned by the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + const record2 = getRecord({ id: 'test-id2' }) + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record, record2])) + + expect(repository.getSingleByQuery(agentContext, { something: 'interesting' })).rejects.toThrowError( + RecordDuplicateError + ) + expect(storageMock.findByQuery).toBeCalledWith(agentContext, TestRecord, { something: 'interesting' }, undefined) + }) + }) +}) diff --git a/packages/core/src/storage/__tests__/TestRecord.ts b/packages/core/src/storage/__tests__/TestRecord.ts new file mode 100644 index 0000000000..9872d3d405 --- /dev/null +++ b/packages/core/src/storage/__tests__/TestRecord.ts @@ -0,0 +1,31 @@ +import type { TagsBase } from '../BaseRecord' + +import { uuid } from '../../utils/uuid' +import { BaseRecord } from '../BaseRecord' + +export interface TestRecordProps { + id?: string + createdAt?: Date + tags?: TagsBase + foo: string +} + +export class TestRecord extends BaseRecord { + public foo!: string + + public constructor(props?: TestRecordProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + + this.foo = props.foo + this._tags = props.tags ?? {} + } + } + + public getTags(): TagsBase { + return this._tags + } +} diff --git a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts new file mode 100644 index 0000000000..28d5d05949 --- /dev/null +++ b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts @@ -0,0 +1,101 @@ +import type { DidCommMessageRole } from './DidCommMessageRole' +import type { ConstructableAgentMessage } from '../../agent/AgentMessage' +import type { PlaintextMessage } from '../../types' + +import { CredoError } from '../../error' +import { JsonTransformer } from '../../utils/JsonTransformer' +import { canHandleMessageType, parseMessageType } from '../../utils/messageType' +import { isJsonObject } from '../../utils/type' +import { uuid } from '../../utils/uuid' +import { BaseRecord } from '../BaseRecord' + +export type DefaultDidCommMessageTags = { + role: DidCommMessageRole + associatedRecordId?: string + + // Computed + protocolName: string + messageName: string + protocolMajorVersion: string + protocolMinorVersion: string + messageType: string + messageId: string + threadId: string +} + +export interface DidCommMessageRecordProps { + role: DidCommMessageRole + message: PlaintextMessage + id?: string + createdAt?: Date + associatedRecordId?: string +} + +export class DidCommMessageRecord extends BaseRecord { + public message!: PlaintextMessage + public role!: DidCommMessageRole + + /** + * The id of the record that is associated with this message record. + * + * E.g. if the connection record wants to store an invitation message + * the associatedRecordId will be the id of the connection record. + */ + public associatedRecordId?: string + + public static readonly type = 'DidCommMessageRecord' + public readonly type = DidCommMessageRecord.type + + public constructor(props: DidCommMessageRecordProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.associatedRecordId = props.associatedRecordId + this.role = props.role + this.message = props.message + } + } + + public getTags() { + const messageId = this.message['@id'] as string + const messageType = this.message['@type'] as string + + const { protocolName, protocolMajorVersion, protocolMinorVersion, messageName } = parseMessageType(messageType) + + const thread = this.message['~thread'] + let threadId = messageId + + if (isJsonObject(thread) && typeof thread.thid === 'string') { + threadId = thread.thid + } + + return { + ...this._tags, + role: this.role, + associatedRecordId: this.associatedRecordId, + + // Computed properties based on message id and type + threadId, + protocolName, + messageName, + protocolMajorVersion: protocolMajorVersion.toString(), + protocolMinorVersion: protocolMinorVersion.toString(), + messageType, + messageId, + } + } + + public getMessageInstance( + messageClass: MessageClass + ): InstanceType { + const messageType = parseMessageType(this.message['@type'] as string) + + if (!canHandleMessageType(messageClass, messageType)) { + throw new CredoError('Provided message class type does not match type of stored message') + } + + return JsonTransformer.fromJSON(this.message, messageClass) as InstanceType + } +} diff --git a/packages/core/src/storage/didcomm/DidCommMessageRepository.ts b/packages/core/src/storage/didcomm/DidCommMessageRepository.ts new file mode 100644 index 0000000000..a13a70f4fd --- /dev/null +++ b/packages/core/src/storage/didcomm/DidCommMessageRepository.ts @@ -0,0 +1,96 @@ +import type { DidCommMessageRole } from './DidCommMessageRole' +import type { AgentContext } from '../../agent' +import type { AgentMessage, ConstructableAgentMessage } from '../../agent/AgentMessage' + +import { EventEmitter } from '../../agent/EventEmitter' +import { InjectionSymbols } from '../../constants' +import { inject, injectable } from '../../plugins' +import { parseMessageType } from '../../utils/messageType' +import { Repository } from '../Repository' +import { StorageService } from '../StorageService' + +import { DidCommMessageRecord } from './DidCommMessageRecord' + +@injectable() +export class DidCommMessageRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(DidCommMessageRecord, storageService, eventEmitter) + } + + public async saveAgentMessage( + agentContext: AgentContext, + { role, agentMessage, associatedRecordId }: SaveAgentMessageOptions + ) { + const didCommMessageRecord = new DidCommMessageRecord({ + message: agentMessage.toJSON(), + role, + associatedRecordId, + }) + + await this.save(agentContext, didCommMessageRecord) + } + + public async saveOrUpdateAgentMessage(agentContext: AgentContext, options: SaveAgentMessageOptions) { + const { messageName, protocolName, protocolMajorVersion } = parseMessageType(options.agentMessage.type) + + const record = await this.findSingleByQuery(agentContext, { + associatedRecordId: options.associatedRecordId, + messageName: messageName, + protocolName: protocolName, + protocolMajorVersion: String(protocolMajorVersion), + }) + + if (record) { + record.message = options.agentMessage.toJSON() + record.role = options.role + await this.update(agentContext, record) + return + } + + await this.saveAgentMessage(agentContext, options) + } + + public async getAgentMessage( + agentContext: AgentContext, + { associatedRecordId, messageClass, role }: GetAgentMessageOptions + ): Promise> { + const record = await this.getSingleByQuery(agentContext, { + associatedRecordId, + messageName: messageClass.type.messageName, + protocolName: messageClass.type.protocolName, + protocolMajorVersion: String(messageClass.type.protocolMajorVersion), + role, + }) + + return record.getMessageInstance(messageClass) + } + public async findAgentMessage( + agentContext: AgentContext, + { associatedRecordId, messageClass, role }: GetAgentMessageOptions + ): Promise | null> { + const record = await this.findSingleByQuery(agentContext, { + associatedRecordId, + messageName: messageClass.type.messageName, + protocolName: messageClass.type.protocolName, + protocolMajorVersion: String(messageClass.type.protocolMajorVersion), + role, + }) + + return record?.getMessageInstance(messageClass) ?? null + } +} + +export interface SaveAgentMessageOptions { + role: DidCommMessageRole + agentMessage: AgentMessage + associatedRecordId: string +} + +export interface GetAgentMessageOptions { + associatedRecordId: string + messageClass: MessageClass + role?: DidCommMessageRole +} diff --git a/packages/core/src/storage/didcomm/DidCommMessageRole.ts b/packages/core/src/storage/didcomm/DidCommMessageRole.ts new file mode 100644 index 0000000000..0404647f76 --- /dev/null +++ b/packages/core/src/storage/didcomm/DidCommMessageRole.ts @@ -0,0 +1,4 @@ +export enum DidCommMessageRole { + Sender = 'sender', + Receiver = 'receiver', +} diff --git a/packages/core/src/storage/didcomm/index.ts b/packages/core/src/storage/didcomm/index.ts new file mode 100644 index 0000000000..a658508c7b --- /dev/null +++ b/packages/core/src/storage/didcomm/index.ts @@ -0,0 +1,3 @@ +export * from './DidCommMessageRecord' +export * from './DidCommMessageRepository' +export * from './DidCommMessageRole' diff --git a/packages/core/src/storage/index.ts b/packages/core/src/storage/index.ts new file mode 100644 index 0000000000..deb5cb0901 --- /dev/null +++ b/packages/core/src/storage/index.ts @@ -0,0 +1,2 @@ +export * from './didcomm' +export * from './migration' diff --git a/packages/core/src/storage/migration/StorageUpdateService.ts b/packages/core/src/storage/migration/StorageUpdateService.ts new file mode 100644 index 0000000000..62860244ae --- /dev/null +++ b/packages/core/src/storage/migration/StorageUpdateService.ts @@ -0,0 +1,85 @@ +import type { UpdateToVersion } from './updates' +import type { AgentContext } from '../../agent' +import type { VersionString } from '../../utils/version' + +import { InjectionSymbols } from '../../constants' +import { Logger } from '../../logger' +import { injectable, inject } from '../../plugins' + +import { isStorageUpToDate } from './isUpToDate' +import { StorageVersionRecord } from './repository/StorageVersionRecord' +import { StorageVersionRepository } from './repository/StorageVersionRepository' +import { INITIAL_STORAGE_VERSION } from './updates' + +@injectable() +export class StorageUpdateService { + private static STORAGE_VERSION_RECORD_ID = 'STORAGE_VERSION_RECORD_ID' + + private logger: Logger + private storageVersionRepository: StorageVersionRepository + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + storageVersionRepository: StorageVersionRepository + ) { + this.logger = logger + this.storageVersionRepository = storageVersionRepository + } + + public async isUpToDate(agentContext: AgentContext, updateToVersion?: UpdateToVersion) { + const currentStorageVersion = await this.getCurrentStorageVersion(agentContext) + return isStorageUpToDate(currentStorageVersion, updateToVersion) + } + + public async getCurrentStorageVersion(agentContext: AgentContext): Promise { + const storageVersionRecord = await this.getStorageVersionRecord(agentContext) + + return storageVersionRecord.storageVersion + } + + public async setCurrentStorageVersion(agentContext: AgentContext, storageVersion: VersionString) { + this.logger.debug(`Setting current agent storage version to ${storageVersion}`) + const storageVersionRecord = await this.storageVersionRepository.findById( + agentContext, + StorageUpdateService.STORAGE_VERSION_RECORD_ID + ) + + if (!storageVersionRecord) { + this.logger.trace('Storage upgrade record does not exist yet. Creating.') + await this.storageVersionRepository.save( + agentContext, + new StorageVersionRecord({ + id: StorageUpdateService.STORAGE_VERSION_RECORD_ID, + storageVersion, + }) + ) + } else { + this.logger.trace('Storage upgrade record already exists. Updating.') + storageVersionRecord.storageVersion = storageVersion + await this.storageVersionRepository.update(agentContext, storageVersionRecord) + } + } + + /** + * Retrieve the update record, creating it if it doesn't exist already. + * + * The storageVersion will be set to the INITIAL_STORAGE_VERSION if it doesn't exist yet, + * as we can assume the wallet was created before the update record existed + */ + public async getStorageVersionRecord(agentContext: AgentContext) { + let storageVersionRecord = await this.storageVersionRepository.findById( + agentContext, + StorageUpdateService.STORAGE_VERSION_RECORD_ID + ) + + if (!storageVersionRecord) { + storageVersionRecord = new StorageVersionRecord({ + id: StorageUpdateService.STORAGE_VERSION_RECORD_ID, + storageVersion: INITIAL_STORAGE_VERSION, + }) + await this.storageVersionRepository.save(agentContext, storageVersionRecord) + } + + return storageVersionRecord + } +} diff --git a/packages/core/src/storage/migration/UpdateAssistant.ts b/packages/core/src/storage/migration/UpdateAssistant.ts new file mode 100644 index 0000000000..b1df93c427 --- /dev/null +++ b/packages/core/src/storage/migration/UpdateAssistant.ts @@ -0,0 +1,296 @@ +import type { Update, UpdateConfig, UpdateToVersion } from './updates' +import type { BaseAgent } from '../../agent/BaseAgent' +import type { Module } from '../../plugins' +import type { FileSystem } from '../FileSystem' + +import { InjectionSymbols } from '../../constants' +import { CredoError } from '../../error' +import { isFirstVersionEqualToSecond, isFirstVersionHigherThanSecond, parseVersionString } from '../../utils/version' +import { WalletExportPathExistsError, WalletExportUnsupportedError } from '../../wallet/error' +import { WalletError } from '../../wallet/error/WalletError' + +import { StorageUpdateService } from './StorageUpdateService' +import { StorageUpdateError } from './error/StorageUpdateError' +import { DEFAULT_UPDATE_CONFIG, CURRENT_FRAMEWORK_STORAGE_VERSION, supportedUpdates } from './updates' + +export interface UpdateAssistantUpdateOptions { + updateToVersion?: UpdateToVersion + backupBeforeStorageUpdate?: boolean +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class UpdateAssistant = BaseAgent> { + private agent: Agent + private storageUpdateService: StorageUpdateService + private updateConfig: UpdateConfig + private fileSystem: FileSystem + + public constructor(agent: Agent, updateConfig: UpdateConfig = DEFAULT_UPDATE_CONFIG) { + this.agent = agent + this.updateConfig = updateConfig + + this.storageUpdateService = this.agent.dependencyManager.resolve(StorageUpdateService) + this.fileSystem = this.agent.dependencyManager.resolve(InjectionSymbols.FileSystem) + } + + public async initialize() { + if (this.agent.isInitialized) { + throw new CredoError("Can't initialize UpdateAssistant after agent is initialized") + } + + // Initialize the wallet if not already done + if (!this.agent.wallet.isInitialized && this.agent.config.walletConfig) { + await this.agent.wallet.initialize(this.agent.config.walletConfig) + } else if (!this.agent.wallet.isInitialized) { + throw new WalletError( + 'Wallet config has not been set on the agent config. ' + + 'Make sure to initialize the wallet yourself before initializing the update assistant, ' + + 'or provide the required wallet configuration in the agent constructor' + ) + } + } + + public async isUpToDate(updateToVersion?: UpdateToVersion) { + return this.storageUpdateService.isUpToDate(this.agent.context, updateToVersion) + } + + public async getCurrentAgentStorageVersion() { + return this.storageUpdateService.getCurrentStorageVersion(this.agent.context) + } + + public static get frameworkStorageVersion() { + return CURRENT_FRAMEWORK_STORAGE_VERSION + } + + public async getNeededUpdates(toVersion?: UpdateToVersion) { + const currentStorageVersion = parseVersionString( + await this.storageUpdateService.getCurrentStorageVersion(this.agent.context) + ) + + const parsedToVersion = toVersion ? parseVersionString(toVersion) : undefined + + // If the current storage version is higher or equal to the toVersion, we can't update, so return empty array + if ( + parsedToVersion && + (isFirstVersionHigherThanSecond(currentStorageVersion, parsedToVersion) || + isFirstVersionEqualToSecond(currentStorageVersion, parsedToVersion)) + ) { + return [] + } + + // Filter updates. We don't want older updates we already applied + // or aren't needed because the wallet was created after the update script was made + const neededUpdates = supportedUpdates.filter((update) => { + const updateToVersion = parseVersionString(update.toVersion) + + // If the update toVersion is higher than the wanted toVersion, we skip the update + if (parsedToVersion && isFirstVersionHigherThanSecond(updateToVersion, parsedToVersion)) { + return false + } + + // if an update toVersion is higher than currentStorageVersion we want to to include the update + return isFirstVersionHigherThanSecond(updateToVersion, currentStorageVersion) + }) + + // The current storage version is too old to update + if ( + neededUpdates.length > 0 && + isFirstVersionHigherThanSecond(parseVersionString(neededUpdates[0].fromVersion), currentStorageVersion) + ) { + throw new CredoError( + `First fromVersion is higher than current storage version. You need to use an older version of the framework to update to at least version ${neededUpdates[0].fromVersion}` + ) + } + + const lastUpdateToVersion = neededUpdates.length > 0 ? neededUpdates[neededUpdates.length - 1].toVersion : undefined + if (toVersion && lastUpdateToVersion && lastUpdateToVersion !== toVersion) { + throw new CredoError( + `No update found for toVersion ${toVersion}. Make sure the toVersion is a valid version you can update to` + ) + } + + return neededUpdates + } + + public async update(options?: UpdateAssistantUpdateOptions) { + const updateIdentifier = Date.now().toString() + const updateToVersion = options?.updateToVersion + + // By default do a backup first (should be explicitly disabled in case the wallet backend does not support export) + const createBackup = options?.backupBeforeStorageUpdate ?? true + + try { + this.agent.config.logger.info(`Starting update of agent storage with updateIdentifier ${updateIdentifier}`) + const neededUpdates = await this.getNeededUpdates(updateToVersion) + + const currentStorageVersion = parseVersionString( + await this.storageUpdateService.getCurrentStorageVersion(this.agent.context) + ) + const parsedToVersion = updateToVersion ? parseVersionString(updateToVersion) : undefined + + // If the current storage version is higher or equal to the toVersion, we can't update. + if ( + parsedToVersion && + (isFirstVersionHigherThanSecond(currentStorageVersion, parsedToVersion) || + isFirstVersionEqualToSecond(currentStorageVersion, parsedToVersion)) + ) { + throw new StorageUpdateError( + `Can't update to version ${updateToVersion} because it is lower or equal to the current agent storage version ${currentStorageVersion[0]}.${currentStorageVersion[1]}}` + ) + } + + if (neededUpdates.length === 0) { + this.agent.config.logger.info('No update needed. Agent storage is up to date.') + return + } + + const fromVersion = neededUpdates[0].fromVersion + const toVersion = neededUpdates[neededUpdates.length - 1].toVersion + + this.agent.config.logger.info( + `Starting update process. Total of ${neededUpdates.length} update(s) will be applied to update the agent storage from version ${fromVersion} to version ${toVersion}` + ) + + // Create backup in case migration goes wrong + if (createBackup) { + await this.createBackup(updateIdentifier) + } + + try { + for (const update of neededUpdates) { + const registeredModules = Object.values(this.agent.dependencyManager.registeredModules) + const modulesWithUpdate: Array<{ module: Module; update: Update }> = [] + + // Filter modules that have an update script for the current update + for (const registeredModule of registeredModules) { + const moduleUpdate = registeredModule.updates?.find( + (module) => module.fromVersion === update.fromVersion && module.toVersion === update.toVersion + ) + + if (moduleUpdate) { + modulesWithUpdate.push({ + module: registeredModule, + update: moduleUpdate, + }) + } + } + + this.agent.config.logger.info( + `Starting update of agent storage from version ${update.fromVersion} to version ${update.toVersion}. Found ${modulesWithUpdate.length} extension module(s) with update scripts` + ) + await update.doUpdate(this.agent, this.updateConfig) + + this.agent.config.logger.info( + `Finished update of core agent storage from version ${update.fromVersion} to version ${update.toVersion}. Starting update of extension modules` + ) + + for (const moduleWithUpdate of modulesWithUpdate) { + this.agent.config.logger.info( + `Starting update of extension module ${moduleWithUpdate.module.constructor.name} from version ${moduleWithUpdate.update.fromVersion} to version ${moduleWithUpdate.update.toVersion}` + ) + await moduleWithUpdate.update.doUpdate(this.agent, this.updateConfig) + this.agent.config.logger.info( + `Finished update of extension module ${moduleWithUpdate.module.constructor.name} from version ${moduleWithUpdate.update.fromVersion} to version ${moduleWithUpdate.update.toVersion}` + ) + } + + // Update the framework version in storage + await this.storageUpdateService.setCurrentStorageVersion(this.agent.context, update.toVersion) + this.agent.config.logger.info( + `Successfully updated agent storage from version ${update.fromVersion} to version ${update.toVersion}` + ) + } + if (createBackup) { + // Delete backup file, as it is not needed anymore + await this.fileSystem.delete(this.getBackupPath(updateIdentifier)) + } + } catch (error) { + this.agent.config.logger.fatal('An error occurred while updating the wallet.', { + error, + }) + + if (createBackup) { + this.agent.config.logger.debug('Restoring backup.') + // In the case of an error we want to restore the backup + await this.restoreBackup(updateIdentifier) + + // Delete backup file, as wallet was already restored (backup-error file will persist though) + await this.fileSystem.delete(this.getBackupPath(updateIdentifier)) + } + + throw error + } + } catch (error) { + // Backup already exists at path + if (error instanceof WalletExportPathExistsError) { + const backupPath = this.getBackupPath(updateIdentifier) + const errorMessage = `Error updating storage with updateIdentifier ${updateIdentifier} because the backup at path ${backupPath} already exists` + this.agent.config.logger.fatal(errorMessage, { + error, + updateIdentifier, + backupPath, + }) + throw new StorageUpdateError(errorMessage, { cause: error }) + } + // Wallet backend does not support export + if (error instanceof WalletExportUnsupportedError) { + const errorMessage = `Error updating storage with updateIdentifier ${updateIdentifier} because the wallet backend does not support exporting. + Make sure to do a manual backup of your wallet and disable 'backupBeforeStorageUpdate' before proceeding.` + this.agent.config.logger.fatal(errorMessage, { + error, + updateIdentifier, + }) + throw new StorageUpdateError(errorMessage, { cause: error }) + } + + this.agent.config.logger.error(`Error updating storage (updateIdentifier: ${updateIdentifier})`, { + cause: error, + }) + + throw new StorageUpdateError(`Error updating storage (updateIdentifier: ${updateIdentifier}): ${error.message}`, { + cause: error, + }) + } + + return updateIdentifier + } + + private getBackupPath(backupIdentifier: string) { + return `${this.fileSystem.dataPath}/migration/backup/${backupIdentifier}` + } + + private async createBackup(backupIdentifier: string) { + const backupPath = this.getBackupPath(backupIdentifier) + + const walletKey = this.agent.wallet.walletConfig?.key + if (!walletKey) { + throw new CredoError("Could not extract wallet key from wallet module. Can't create backup") + } + + await this.agent.wallet.export({ key: walletKey, path: backupPath }) + this.agent.config.logger.info('Created backup of the wallet', { + backupPath, + }) + } + + private async restoreBackup(backupIdentifier: string) { + const backupPath = this.getBackupPath(backupIdentifier) + + const walletConfig = this.agent.wallet.walletConfig + if (!walletConfig) { + throw new CredoError('Could not extract wallet config from wallet module. Cannot restore backup') + } + + // Export and delete current wallet + await this.agent.wallet.export({ key: walletConfig.key, path: `${backupPath}-error` }) + await this.agent.wallet.delete() + + // Import backup + await this.agent.wallet.import(walletConfig, { key: walletConfig.key, path: backupPath }) + await this.agent.wallet.initialize(walletConfig) + + this.agent.config.logger.info(`Successfully restored wallet from backup ${backupIdentifier}`, { + backupPath, + }) + } +} diff --git a/packages/core/src/storage/migration/__tests__/0.1.test.ts b/packages/core/src/storage/migration/__tests__/0.1.test.ts new file mode 100644 index 0000000000..d7b7481263 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/0.1.test.ts @@ -0,0 +1,283 @@ +import type { V0_1ToV0_2UpdateConfig } from '../updates/0.1-0.2' + +import { readFileSync } from 'fs' +import path from 'path' + +import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' +import { RegisteredAskarTestWallet } from '../../../../../askar/tests/helpers' +import { Agent } from '../../../../src' +import { agentDependencies as dependencies } from '../../../../tests/helpers' +import { InjectionSymbols } from '../../../constants' +import { DependencyManager } from '../../../plugins' +import * as uuid from '../../../utils/uuid' +import { UpdateAssistant } from '../UpdateAssistant' + +const backupDate = new Date('2022-01-21T22:50:20.522Z') +jest.useFakeTimers().setSystemTime(backupDate) + +const walletConfig = { + id: `Wallet: 0.1 Update`, + key: `Key: 0.1 Update`, +} + +const mediationRoleUpdateStrategies: V0_1ToV0_2UpdateConfig['mediationRoleUpdateStrategy'][] = [ + 'allMediator', + 'allRecipient', + 'doNotChange', + 'recipientIfEndpoint', +] + +describe('UpdateAssistant | v0.1 - v0.2', () => { + it(`should correctly update the role in the mediation record`, async () => { + const aliceMediationRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/alice-4-mediators-0.1.json'), + 'utf8' + ) + + for (const mediationRoleUpdateStrategy of mediationRoleUpdateStrategies) { + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + + const agent = new Agent( + { + config: { label: 'Test Agent', walletConfig }, + dependencies, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy, + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceMediationRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([ + { + fromVersion: '0.1', + toVersion: '0.2', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update({ updateToVersion: '0.2' }) + + expect(await updateAssistant.isUpToDate('0.2')).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([]) + + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot( + mediationRoleUpdateStrategy + ) + + await agent.shutdown() + await agent.wallet.delete() + } + }) + + it(`should correctly update credential records and create didcomm records`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + let uuidCounter = 1 + const uuidSpy = jest.spyOn(uuid, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const aliceCredentialRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/alice-4-credentials-0.1.json'), + 'utf8' + ) + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + + const agent = new Agent( + { + config: { label: 'Test Agent', walletConfig }, + dependencies, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceCredentialRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.isUpToDate('0.2')).toBe(false) + expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([ + { + fromVersion: '0.1', + toVersion: '0.2', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update({ updateToVersion: '0.2' }) + + expect(await updateAssistant.isUpToDate('0.2')).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([]) + + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + uuidSpy.mockReset() + }) + + it(`should correctly update the credential records and create didcomm records with auto update`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + let uuidCounter = 1 + const uuidSpy = jest.spyOn(uuid, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const aliceCredentialRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/alice-4-credentials-0.1.json'), + 'utf8' + ) + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + + const agent = new Agent( + { + config: { label: 'Test Agent', walletConfig, autoUpdateStorageOnStartup: true }, + dependencies, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceCredentialRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.isUpToDate('0.2')).toBe(false) + expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([ + { + fromVersion: '0.1', + toVersion: '0.2', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update({ updateToVersion: '0.2' }) + + expect(await updateAssistant.isUpToDate('0.2')).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([]) + + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + uuidSpy.mockReset() + }) + + it(`should correctly update the connection record and create the did and oob records`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + let uuidCounter = 1 + const uuidSpy = jest.spyOn(uuid, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const aliceConnectionRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/alice-8-connections-0.1.json'), + 'utf8' + ) + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + + const agent = new Agent( + { + config: { + label: 'Test Agent', + walletConfig, + autoUpdateStorageOnStartup: true, + }, + dependencies, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceConnectionRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.isUpToDate('0.2')).toBe(false) + expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([ + { + fromVersion: '0.1', + toVersion: '0.2', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update({ updateToVersion: '0.2' }) + + expect(await updateAssistant.isUpToDate('0.2')).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([]) + + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + uuidSpy.mockReset() + }) +}) diff --git a/packages/core/src/storage/migration/__tests__/0.2.test.ts b/packages/core/src/storage/migration/__tests__/0.2.test.ts new file mode 100644 index 0000000000..bbb00eb2a8 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/0.2.test.ts @@ -0,0 +1,195 @@ +import { readFileSync } from 'fs' +import path from 'path' + +import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' +import { RegisteredAskarTestWallet } from '../../../../../askar/tests/helpers' +import { Agent, MediatorRoutingRecord } from '../../../../src' +import { agentDependencies } from '../../../../tests/helpers' +import { InjectionSymbols } from '../../../constants' +import { DependencyManager } from '../../../plugins' +import * as uuid from '../../../utils/uuid' +import { UpdateAssistant } from '../UpdateAssistant' + +const backupDate = new Date('2023-01-21T22:50:20.522Z') +jest.useFakeTimers().setSystemTime(backupDate) + +const walletConfig = { + id: `Wallet: 0.2 Update`, + key: `Key: 0.2 Update`, +} + +describe('UpdateAssistant | v0.2 - v0.3.1', () => { + it(`should correctly update proof records and create didcomm records`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + let uuidCounter = 1 + const uuidSpy = jest.spyOn(uuid, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const aliceCredentialRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/alice-4-proofs-0.2.json'), + 'utf8' + ) + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + + const agent = new Agent( + { + config: { + label: 'Test Agent', + walletConfig, + }, + dependencies: agentDependencies, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceCredentialRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.isUpToDate('0.3.1')).toBe(false) + expect(await updateAssistant.getNeededUpdates('0.3.1')).toEqual([ + { + fromVersion: '0.2', + toVersion: '0.3', + doUpdate: expect.any(Function), + }, + { + fromVersion: '0.3', + toVersion: '0.3.1', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update({ updateToVersion: '0.3.1' }) + + expect(await updateAssistant.isUpToDate('0.3.1')).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.3.1')).toEqual([]) + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + uuidSpy.mockReset() + }) + + it(`should correctly update the proofs records and create didcomm records with auto update`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + let uuidCounter = 1 + const uuidSpy = jest.spyOn(uuid, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const aliceCredentialRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/alice-4-proofs-0.2.json'), + 'utf8' + ) + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + + const agent = new Agent( + { + config: { + label: 'Test Agent', + walletConfig, + autoUpdateStorageOnStartup: true, + }, + dependencies: agentDependencies, + }, + dependencyManager + ) + + // We need to manually initialize the wallet as we're using the in memory wallet service + // When we call agent.initialize() it will create the wallet and store the current framework + // version in the in memory storage service. We need to manually set the records between initializing + // the wallet and calling agent.initialize() + await agent.wallet.initialize(walletConfig) + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceCredentialRecordsString), + creationDate: new Date(), + }, + } + + await agent.initialize() + await storageService.deleteById(agent.context, MediatorRoutingRecord, 'MEDIATOR_ROUTING_RECORD') + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + uuidSpy.mockReset() + }) + + it(`should correctly update the did records`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + let uuidCounter = 1 + const uuidSpy = jest.spyOn(uuid, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const aliceDidRecordsString = readFileSync(path.join(__dirname, '__fixtures__/alice-8-dids-0.2.json'), 'utf8') + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + + const agent = new Agent( + { + config: { + label: 'Test Agent', + walletConfig, + autoUpdateStorageOnStartup: true, + }, + dependencies: agentDependencies, + }, + dependencyManager + ) + + // We need to manually initialize the wallet as we're using the in memory wallet service + // When we call agent.initialize() it will create the wallet and store the current framework + // version in the in memory storage service. We need to manually set the records between initializing + // the wallet and calling agent.initialize() + await agent.wallet.initialize(walletConfig) + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceDidRecordsString), + creationDate: new Date(), + }, + } + + await agent.initialize() + + await storageService.deleteById(agent.context, MediatorRoutingRecord, 'MEDIATOR_ROUTING_RECORD') + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + uuidSpy.mockReset() + }) +}) diff --git a/packages/core/src/storage/migration/__tests__/0.3.test.ts b/packages/core/src/storage/migration/__tests__/0.3.test.ts new file mode 100644 index 0000000000..a40c1f5ffe --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/0.3.test.ts @@ -0,0 +1,153 @@ +import { readFileSync } from 'fs' +import path from 'path' + +import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' +import { RegisteredAskarTestWallet } from '../../../../../askar/tests/helpers' +import { agentDependencies } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { InjectionSymbols } from '../../../constants' +import { DependencyManager } from '../../../plugins' +import * as uuid from '../../../utils/uuid' +import { UpdateAssistant } from '../UpdateAssistant' + +const backupDate = new Date('2023-03-18T22:50:20.522Z') +jest.useFakeTimers().setSystemTime(backupDate) + +const walletConfig = { + id: `Wallet: 0.4 Update`, + key: `Key: 0.4 Update`, +} + +describe('UpdateAssistant | v0.3.1 - v0.4', () => { + it(`should correctly update the did records and remove cache records`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + let uuidCounter = 1 + const uuidSpy = jest.spyOn(uuid, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const aliceDidRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/alice-2-sov-dids-one-cache-record-0.3.json'), + 'utf8' + ) + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + + const agent = new Agent( + { + config: { + label: 'Test Agent', + walletConfig, + }, + dependencies: agentDependencies, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceDidRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.isUpToDate('0.4')).toBe(false) + expect(await updateAssistant.getNeededUpdates('0.4')).toEqual([ + { + fromVersion: '0.3.1', + toVersion: '0.4', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update({ updateToVersion: '0.4' }) + + expect(await updateAssistant.isUpToDate('0.4')).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.4')).toEqual([]) + + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + uuidSpy.mockReset() + }) + + it(`should correctly update 'claimFormat' tag to w3c records`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + let uuidCounter = 1 + const uuidSpy = jest.spyOn(uuid, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const aliceW3cCredentialRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/alice-2-w3c-credential-records-0.3.json'), + 'utf8' + ) + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + + const agent = new Agent( + { + config: { + label: 'Test Agent', + walletConfig, + }, + dependencies: agentDependencies, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceW3cCredentialRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.isUpToDate('0.4')).toBe(false) + expect(await updateAssistant.getNeededUpdates('0.4')).toEqual([ + { + fromVersion: '0.3.1', + toVersion: '0.4', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update({ updateToVersion: '0.4' }) + + expect(await updateAssistant.isUpToDate('0.4')).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.4')).toEqual([]) + + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + uuidSpy.mockReset() + }) +}) diff --git a/packages/core/src/storage/migration/__tests__/0.4.test.ts b/packages/core/src/storage/migration/__tests__/0.4.test.ts new file mode 100644 index 0000000000..943e3f2c87 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/0.4.test.ts @@ -0,0 +1,223 @@ +import { readFileSync } from 'fs' +import path from 'path' + +import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' +import { RegisteredAskarTestWallet } from '../../../../../askar/tests/helpers' +import { agentDependencies } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { InjectionSymbols } from '../../../constants' +import { W3cCredentialsModule } from '../../../modules/vc' +import { customDocumentLoader } from '../../../modules/vc/data-integrity/__tests__/documentLoader' +import { DependencyManager } from '../../../plugins' +import * as uuid from '../../../utils/uuid' +import { UpdateAssistant } from '../UpdateAssistant' + +const backupDate = new Date('2024-02-05T22:50:20.522Z') +jest.useFakeTimers().setSystemTime(backupDate) + +const walletConfig = { + id: `Wallet: 0.5 Update`, + key: `Key: 0.5 Update`, +} + +describe('UpdateAssistant | v0.4 - v0.5', () => { + it(`should correctly add 'type' tag to w3c records`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + let uuidCounter = 1 + const uuidSpy = jest.spyOn(uuid, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const aliceW3cCredentialRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/alice-2-w3c-credential-records-0.4.json'), + 'utf8' + ) + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + + const agent = new Agent( + { + config: { + label: 'Test Agent', + walletConfig, + }, + dependencies: agentDependencies, + modules: { + w3cCredentials: new W3cCredentialsModule({ + documentLoader: customDocumentLoader, + }), + }, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceW3cCredentialRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.isUpToDate('0.5')).toBe(false) + expect(await updateAssistant.getNeededUpdates('0.5')).toEqual([ + { + fromVersion: '0.4', + toVersion: '0.5', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update() + + expect(await updateAssistant.isUpToDate()).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.5')).toEqual([]) + + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + uuidSpy.mockReset() + }) + + it(`should correctly add role to credential exchange records`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + // let uuidCounter = 1 + // const uuidSpy = jest.spyOn(uuid, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const aliceW3cCredentialRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/2-credentials-0.4.json'), + 'utf8' + ) + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + + const agent = new Agent( + { + config: { + label: 'Test Agent', + walletConfig, + }, + dependencies: agentDependencies, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceW3cCredentialRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.isUpToDate('0.5')).toBe(false) + expect(await updateAssistant.getNeededUpdates('0.5')).toEqual([ + { + fromVersion: '0.4', + toVersion: '0.5', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update() + + expect(await updateAssistant.isUpToDate()).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.5')).toEqual([]) + + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + // uuidSpy.mockReset() + }) + + it(`should correctly add role to proof exchange records`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + // let uuidCounter = 1 + // const uuidSpy = jest.spyOn(uuid, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const aliceW3cCredentialRecordsString = readFileSync(path.join(__dirname, '__fixtures__/2-proofs-0.4.json'), 'utf8') + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + + const agent = new Agent( + { + config: { + label: 'Test Agent', + walletConfig, + }, + dependencies: agentDependencies, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceW3cCredentialRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.isUpToDate('0.5')).toBe(false) + expect(await updateAssistant.getNeededUpdates('0.5')).toEqual([ + { + fromVersion: '0.4', + toVersion: '0.5', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update() + + expect(await updateAssistant.isUpToDate()).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.5')).toEqual([]) + + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + // uuidSpy.mockReset() + }) +}) diff --git a/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts b/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts new file mode 100644 index 0000000000..eac4d95755 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts @@ -0,0 +1,84 @@ +import type { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' +import type { BaseRecord } from '../../BaseRecord' + +import { getInMemoryAgentOptions } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { InjectionSymbols } from '../../../constants' +import { UpdateAssistant } from '../UpdateAssistant' +import { CURRENT_FRAMEWORK_STORAGE_VERSION } from '../updates' + +const agentOptions = getInMemoryAgentOptions('UpdateAssistant', {}) + +describe('UpdateAssistant', () => { + let updateAssistant: UpdateAssistant + let agent: Agent + let storageService: InMemoryStorageService + + beforeEach(async () => { + agent = new Agent(agentOptions) + + updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'allMediator', + }, + }) + + storageService = agent.dependencyManager.resolve(InjectionSymbols.StorageService) + + await updateAssistant.initialize() + }) + + afterEach(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + describe('upgrade()', () => { + it('should not upgrade records when upgrading after a new wallet is created', async () => { + const beforeStorage = JSON.stringify(storageService.contextCorrelationIdToRecords) + await updateAssistant.update() + + // We parse and stringify so the dates are equal (both string) + expect(JSON.parse(beforeStorage)).toEqual( + JSON.parse(JSON.stringify(storageService.contextCorrelationIdToRecords)) + ) + }) + }) + + describe('isUpToDate()', () => { + it('should return true when a new wallet is created', async () => { + expect(await updateAssistant.isUpToDate()).toBe(true) + }) + }) + + describe('isUpToDate()', () => { + it('should return true when a new wallet is created', async () => { + expect(await updateAssistant.isUpToDate()).toBe(true) + }) + + it('should return true for a lower version than current storage', async () => { + expect(await updateAssistant.isUpToDate('0.2')).toBe(true) + }) + + it('should return true for current agent storage version', async () => { + expect(await updateAssistant.isUpToDate('0.3')).toBe(true) + }) + + it('should return false for a higher version than current storage', async () => { + // @ts-expect-error isUpToDate only allows existing versions to be passed, 100.100 is not a valid version (yet) + expect(await updateAssistant.isUpToDate('100.100')).toBe(false) + }) + }) + + describe('UpdateAssistant.frameworkStorageVersion', () => { + it(`should return ${CURRENT_FRAMEWORK_STORAGE_VERSION}`, async () => { + expect(UpdateAssistant.frameworkStorageVersion).toBe(CURRENT_FRAMEWORK_STORAGE_VERSION) + }) + }) + + describe('getCurrentAgentStorageVersion()', () => { + it(`should return ${CURRENT_FRAMEWORK_STORAGE_VERSION} when a new wallet is created`, async () => { + expect(await updateAssistant.getCurrentAgentStorageVersion()).toBe(CURRENT_FRAMEWORK_STORAGE_VERSION) + }) + }) +}) diff --git a/packages/core/src/storage/migration/__tests__/__fixtures__/2-credentials-0.4.json b/packages/core/src/storage/migration/__tests__/__fixtures__/2-credentials-0.4.json new file mode 100644 index 0000000000..d5d36e57e5 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__fixtures__/2-credentials-0.4.json @@ -0,0 +1,451 @@ +{ + "STORAGE_VERSION_RECORD_ID": { + "value": { + "metadata": {}, + "id": "STORAGE_VERSION_RECORD_ID", + "createdAt": "2023-03-18T18:35:02.888Z", + "storageVersion": "0.4", + "updatedAt": "2023-03-18T18:35:02.888Z" + }, + "id": "STORAGE_VERSION_RECORD_ID", + "type": "StorageVersionRecord", + "tags": {} + }, + "8df00782-5090-434e-8f34-96d5e484658a": { + "value": { + "metadata": { + "_anoncreds/credentialRequest": { + "link_secret_blinding_data": { + "v_prime": "13120265176299908185898873509373450263031037373213459182327318255420442817142729461299281465699119807362298565594454759288247538050340146656288128953997075333312454192263427271050503267075194365317557114968037548461894334655900837672256313231303463277753218922239512751838553222838565650935887880786124571846070357367162926133614147020173182949018785393466888036067039449038012778965503244623829426114742052867541393595950207886123033595140571273680755807414104278569809550597205459544801806826874048588059800699107031186607221972879349194293825608495858147307683980675532601970303518939133216185993672407675581367508904617978807526724974743", + "vr_prime": null + }, + "nonce": "533240365625577424098040", + "link_secret_name": "ae343b1d-e2af-4706-9aae-2010a7f2c882" + }, + "_anoncreds/credential": { + "credentialDefinitionId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/CLAIM_DEF/400832/test", + "schemaId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/SCHEMA/test0.1599221872308001/1.0" + } + }, + "credentials": [ + { + "credentialRecordType": "anoncreds", + "credentialRecordId": "c5775c27-93d1-46e0-bb00-65e408a58d97" + } + ], + "id": "8df00782-5090-434e-8f34-96d5e484658a", + "createdAt": "2024-02-28T09:36:11.555Z", + "state": "done", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + "parentThreadId": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349", + "protocolVersion": "v2", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "test", + "value": "test" + } + ], + "updatedAt": "2024-02-28T09:36:43.744Z" + }, + "tags": { + "credentialIds": ["c5775c27-93d1-46e0-bb00-65e408a58d97"], + "parentThreadId": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349", + "state": "done", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286" + }, + "id": "8df00782-5090-434e-8f34-96d5e484658a", + "type": "CredentialRecord" + }, + "e04ca938-64e1-441e-b5d9-4525befe4686": { + "value": { + "metadata": { + "_anoncreds/credential": { + "schemaId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/SCHEMA/test0.1599221872308001/1.0", + "credentialDefinitionId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/CLAIM_DEF/400832/test" + } + }, + "credentials": [], + "id": "e04ca938-64e1-441e-b5d9-4525befe4686", + "createdAt": "2024-02-28T09:36:04.294Z", + "state": "done", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + "protocolVersion": "v2", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "test", + "value": "test" + } + ], + "updatedAt": "2024-02-28T09:36:52.778Z" + }, + "tags": { + "state": "done", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + "credentialIds": [] + }, + "id": "e04ca938-64e1-441e-b5d9-4525befe4686", + "type": "CredentialRecord" + }, + "c30fcbff-c5c5-4676-89ee-9c68e2c39d1c": { + "value": { + "metadata": {}, + "id": "c30fcbff-c5c5-4676-89ee-9c68e2c39d1c", + "createdAt": "2024-02-28T09:36:08.529Z", + "associatedRecordId": "e04ca938-64e1-441e-b5d9-4525befe4686", + "role": "sender", + "message": { + "@type": "https://didcomm.org/issue-credential/2.0/offer-credential", + "@id": "a1ec787c-8df6-4efe-9280-6b4407db026e", + "formats": [ + { + "attach_id": "d6883b94-7a6b-4fe7-9ddc-98b23c46c478", + "format": "anoncreds/credential@v1.0" + } + ], + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/2.0/credential-preview", + "attributes": [ + { + "name": "test", + "value": "test" + } + ] + }, + "offers~attach": [ + { + "@id": "d6883b94-7a6b-4fe7-9ddc-98b23c46c478", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvU0NIRU1BL3Rlc3QwLjE1OTkyMjE4NzIzMDgwMDEvMS4wIiwiY3JlZF9kZWZfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvQ0xBSU1fREVGLzQwMDgzMi90ZXN0Iiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiIxMDk0MzkxMjM2ODU1Nzg0MTgzNDIyMDY2NjI2MzE2OTQ4ODQ1ODMwNzA0MjY0MjUzNDUyMTkwNTAxMTM2ODIyNTcwNTY0MTIzMzY4NDAiLCJ4el9jYXAiOiIyMTk4OTc3Nzk0MzA1NjU5NzMwNDk2MDU0MjM4NDE4OTYxODUzMDEyMTgzODgxODQxNjk4NDUwMTY2ODU5MTA1MDg1MDY0MzIzNzcyMjE3MDUxMDQwODYwNDcwODY0NTM1MDI5Mzk2NTY4Njc0ODkyNzg4Nzg5MTIzNTU3MzE2MTkwNjAyNjczMTczODM0NDUwNjcxMjA2ODA2MjYxOTg2Mzc3NjE1NTc3MzU4MjI0MTM4NDc1OTExNjgyOTQ2MTAzOTkzODg5MTIxODMyNjExNTg4NDc0NzUwMjIxNTQ3MjcwNDAyMjUwMTIyMjc5ODcwNDQ4MDA3OTgwMjQ0NDA5OTc5NDgyNDg1MjU2MDk3OTY4ODQyNDg3ODM2MzMyNTA4MjA0OTIwNjM3ODE0NDMyNDczMjg0NDc0MzQzNzQ3Njg0MjI2MTMxMjAwNjQyMDI3NjQ2NjMwMzkzMDE4MTk1NTAzOTQ3MjkxNjA0Nzg5MjM5MTY3ODUwNjA5Mzc3MTE4NjA3NjUwMzE0NTYyOTk3MDc2NjQ3MDUzNzcxNzgxMjAwMTAxNjExOTc2MTI4ODY5OTE0NTM0NzQ5MDc0MDc3NzcyOTUzMjkzNjMwMzMyMDc5MTk4MzAxNjMwNTY3MjQ3Mjc0OTY5MTM5ODI2Nzc2NTM2NzEwMTgxMjQ3MDY2NDE4OTY1NTQyNDY5MjMyMDkxMDYwNjI4Njc0MTM4OTgwMDcwODE0Njg1OTMyMjg0MzIyMjMzMDQ3NjQ2NTkxODc3NjkyODgyMTM5ODQ4MzgxNjQxMTg1ODE1ODAxMDg0OTM5NTk2NzMwMTYxMjA1MDg2MzMzNzgwNjI5OTEyMTc1NDA0ODQ2MDk5MTI5MjY0NjM3ODA2MjQ3MzE2NzU2NTg3NDI5MjEwNjkzNDM5NTQyMjEzNzI2IiwieHJfY2FwIjpbWyJtYXN0ZXJfc2VjcmV0IiwiMTA4NjM5NjM3NDg4MzU1Nzc0MDI5ODE3OTk0NDkxOTE1MjIyMzc2ODk2NzQwMjM0MDk3MzI0NjcwNzU3NDQ1NjQ0OTAxNTk2NjUwMjk1MDc3NjM0Nzk0Mzc5MzMwODUyNzUxNjQwODk5NjYwMDUwOTY5MDUzNDYyMjcyMzQ5MTEzNjYxOTg3NzMxNDQ3NDMzNTIzNTc0OTc5NjYyMzE0MTAxMTI3Njc5MTAwNzcwMjY0NDMxMTIxNjYzMzMyMTQ2MTU4NzM3MzU0NTk0MDM1NTM1MjI5Mzc4MjU3NzUyOTAwMzA3Mjg3NjQ4NzcwOTU4NTg5ODg1NzA3NDEyOTgzNzYwNTE2ODk0NzkzMTE3NTQ5Nzc3Njg1NTg0MjQ3MzQ1ODMxNzk2MjUzMzQ1MDk1NzIyODU4NjM4MjAxMjgxMjIzNDYyOTg2NjE2MjYzNzIyMDk1MjMxMjg0MTgzMzM3ODYwODQ2Njc5Njg5ODM2MTM4NzAxNjE4MzI1MDAyNTM5NjczNzM4NjUwMTMxNzMzODIzNDk0Mjk5MzQzNDQxNjc5MzM1MTQ5NTIwODQ4Mzg4Njk5MTk5ODA1NTk4MTAzNTk0NTk4OTE0OTkyOTI2MDMzNTA0MzAxNTY5MjMwODYyMzc3NTg5ODg2OTYyMDIxOTIxODM3ODI3MDYyNTI0MzIzMTg3OTcwNjg0MzIxNzY0MzcwMzc4NDc0Mjc4MzA4NzMzOTY4ODgzNTMyNzM2MTE1NTQ3MzA0MzU4ODgyMzc1Njc0MTQwMjEzNzc1OTE1OTU3NzU5MTM3NjUwMjY0Njg3OTUzMTk4NTE5OTY0MDcyMzEwNDY3OTA2Njk0OTQxMzM4NDAzNDg4NTYyMjgxMDE5MTQzMDk5MTE0NjM3MTY1OTI2MzQxOTY4NTk3MjE4MjU3OTU5OCJdLFsidGVzdCIsIjcwMDE5MTUyMzc5MTExNTQzNzgwOTgxNTYyMDU5OTYzMDYzMzg4ODg5NDg1NjkxOTM5MzM3NzgxNjkwNTU3NjA5MDA4MTA2NjY5NzAwNjA3MzI1OTAyNjQyMzA4NTIzMDc5MjA3NjEwNTU1MjQ0NTY0MjkwNzc5ODA5Mzg5ODEzNTI0OTc5MzE5MTg4NDI0NzIwNzUxMjQwMzQ3NTY0NTQ3MDY2NDE1NTE3MzU5NjUxODU1NzU3MzY4NDcxOTM5OTk3NjY1MTk5NTE4OTQ2ODMzMDY1MTMyNjYxMDI4Nzc5ODg1NDQ5ODMwMzg1MTA3MzUxOTgxMDM1NTAzMzM1MDg0NjgxMDQ4MzE0NjQzMDQ4NzIwMzQxMzk0MjI5NTEyNDcyNDY0NjUwNDI3NDA4NTI5ODkwMzg1ODc1MzkzMjA0ODExMTUwODgxNDA4NzY3NTMzNjI1MjU4MDUwNDc2NzU2NDIyMzk5NjMxNjA5NTU2MjI0NjQ1Mjg5NDM0Mjk3NDkwMzg0MzYwNDM0Mzg4MDU1NDAxODgyNDIxNDU1OTI0NjQxMTUwNjQ1NTkzNzUwMjQ2MTI4NTYzNzMzMzgzMzQ2MTYyNjYzNjE5MTYyMzIxMDM3MDgzNTcxNzc0MzQ5MzYwMTcxNzkwNzUzNzYyMDI3NTczMDc0MDI1NTgyOTQyODk4MzMwMTY3NDY0MTExNjA1MTMxMjk5MDE2MjQ5MzU2MDY5MjM3OTAzNjAyNjgwMjMwNzgzMjg0MDIwOTMxMjY3MDkzNTE1MzU3MjQ1MDEwOTI0MTI2MTIyNjUwNTM1MDI5MjIxMzY2NzA5NTI2NjY1Mjc5NzIyMDg0NjI0MzQwOTkyODQ4NDU0OTgxNTExOTIwNDE4NzM2NTE1NjIxMTgxNjkxMzcyNDE5ODU2OTg1NDkzMSJdXX0sIm5vbmNlIjoiOTczNDk4MTg3NTMwNzE2MjQ3MDE5NzU0In0=" + } + } + ], + "~thread": { + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286" + } + }, + "updatedAt": "2024-02-28T09:36:08.529Z" + }, + "tags": { + "associatedRecordId": "e04ca938-64e1-441e-b5d9-4525befe4686", + "messageId": "a1ec787c-8df6-4efe-9280-6b4407db026e", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/offer-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286" + }, + "id": "c30fcbff-c5c5-4676-89ee-9c68e2c39d1c", + "type": "DidCommMessageRecord" + }, + "de893e30-a2f3-458e-b8ec-aaca324c0806": { + "value": { + "metadata": {}, + "id": "de893e30-a2f3-458e-b8ec-aaca324c0806", + "createdAt": "2024-02-28T09:36:31.458Z", + "associatedRecordId": "e04ca938-64e1-441e-b5d9-4525befe4686", + "role": "sender", + "message": { + "@type": "https://didcomm.org/issue-credential/2.0/issue-credential", + "@id": "44bead94-eb8a-46c8-abb7-f55e99fb8e28", + "formats": [ + { + "attach_id": "e5c0d797-67f3-4d1d-a3e1-336786820213", + "format": "anoncreds/credential@v1.0" + } + ], + "credentials~attach": [ + { + "@id": "e5c0d797-67f3-4d1d-a3e1-336786820213", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvU0NIRU1BL3Rlc3QwLjE1OTkyMjE4NzIzMDgwMDEvMS4wIiwiY3JlZF9kZWZfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvQ0xBSU1fREVGLzQwMDgzMi90ZXN0IiwicmV2X3JlZ19pZCI6bnVsbCwidmFsdWVzIjp7InRlc3QiOnsicmF3IjoidGVzdCIsImVuY29kZWQiOiI3MjE1NTkzOTQ4Njg0Njg0OTUwOTc1OTM2OTczMzI2NjQ4Njk4MjgyMTc5NTgxMDQ0ODI0NTQyMzE2ODk1NzM5MDYwNzY0NDM2MzI3MiJ9fSwic2lnbmF0dXJlIjp7InBfY3JlZGVudGlhbCI6eyJtXzIiOiI4NTY3NjYyMzQ4NDI3NzYyNDY4MjQ0NDgyMDQ0NDcwMjQ2NzQ5OTM1NzkxMDI3NDE2NTAxMzQxODM3OTEyNTEzNDA3NTMyNTU0MzcyOSIsImEiOiIyOTA3ODU4MzAyMzY0NDcyMzQxNzI1Njk2Mzk1MTgzNDc2OTQyOTU0NDg5MzU2MTMxMjYwODA0MTk5OTAzOTcyNTc3OTA0NTQ3ODk1NzY2NTYxMDUzOTM0NTY2OTI0OTAzMzY0Mzg0NTg3MTMxMDYzODIwNTM1OTgyNzkxMzM0NTUzOTg5MzYwNDc4NDE3MjIyMTE3Nzg3NDE1MTg2NzMyMzc4OTc4MzM1MjA2OTg1ODExNDY3NzA2MjM4MjMyOTIwMTk0MTMzNjExNDY0MDA5NDQ0ODk4MjQ5MjU0ODYyMTc0NTQxODE0MDI4NzUyMzM1ODQ1NTc5Njk4NTQ1MjI5MjYwMzgxNDA1MDYxMzAyMjQ5OTIwNjM1MDQ4MTk2NjQ1MDk0OTE0Nzc5ODYwNzI0ODM2NzM5MzAyMjQwMDg3MTM4OTQ0MTk4NTQ4MzI1MTk5NTY2NjExMDA2ODQzMTM1NjEwMjQyMDA2OTQ1MDIzMjcxMjM3MDQxMDA3MTAzODUyOTIzNzMzODU5MDAyMjI1NTY3NTQ1MTE0ODM2MjYwMDcyNzU4NDk5NDY2NTE3NzI4NTg1MzA4MTc5OTY4MzYyMzY3NDI5MTYwMzY0MDMxNDczMzY2NjQ2ODkxODY2NjE3NTEwMzYyMjgzMTYwNDQwMjIxNTMyMjI4OTUwOTU5NDE1Nzg3NTIxMjQwNjU4NDU1ODk3MzY4MjMyNzEyNzUzMjgwMzM3MzQyNDU3NTIzMDk0NjcyMTUyMTk2ODY4MDA3NTYwODE3ODU4NjE2NTU2NTE2MjkzMjY0MzM3ODIzNjQyODQwMzMzMSIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxMDM5MjcwODI5MzQzMDYyMDQxODk0NTM5MTQ4NTQ1Njg2MzMiLCJ2IjoiMTAxMTMyNDY1OTQ5MTk5Njg2OTQyNDM3NDkxNTA1MzM0MjEyNTE5MDMyMTI1MTYxNzYyODA0NTA5NDQzNDM1NjQ0MzExMDk1MDU5Mzg4NDE4OTE2MjUyNDc2NjczNjQzNzk0MTIxMjk0MDU0MDUzNDg0NzcwOTU2MTAwMDI0MzM1NjgzNDc4NTY1MDkxNTk2MTEzMzU3NTg4MTg3NTY5MjY4MjUwNjM1NjA3NzAzMTQ4NzMxNTE5MDg5NjQwMDAwNDQzNjA1MzM5OTQ4MzM1MTc4Nzg5ODcxNDEwMTYyOTA1NTA2OTM0MDc0OTQxMjE4NjY4NjA1ODgwMTc3MjEyOTQ4NjYxNzc5MTI0MzI4NDA1MjgzMzIwNjU2NDQzMDg2ODk1MDY0NDQ3NTMwMjI4NTgzODE1OTg3ODk0NzYxMzcwMjg0ODExMjIwNTY2NTgxNzM2NzQwNzY1NzUyNzIyMTE3MDEwODEzODkyMTE5MDE1NzAyNDI1Njk3OTg4NzQwNjIzMDA4NDQzOTY4MTQ5NDAwNjk3ODI2NzMzNjg5NzU0ODYwNjk4NzIwMjkzMjI3ODU3NjU3NzM0MTc5ODEyOTQ2MDkwNTU0ODE3MDcyNjQ4NDgwOTA4MTg4NTI4NDY4MjAzMzM2MDEyMzY2OTUyNjUyODgwNDY5NjUwMTEzODU1NjQ4OTc0Mzk0NzU4NjM5NjUwMjM0NzY0Mzk5OTQ5NjMyNzk3NTYwMzc4NDU2NDIzNjc4NDM1NDIzMDUwMzg4MDU0NDEwMzg3NjIyMDEzMTYxMDc2OTEwOTQ3MjI3Mzk3NDQxMTgzMDA0NDM3MTI0NDU1Nzc0NTI1NzIwMDcxMjg4MjA5NDY2OTcwNDQwNDk3MTY1MTE1MTQ1OTc3NDM5MjkxNDI3MjgyNzI2MTAxMzAwNTg0NTU3MjYzNzMzNDY0NzA3NzA0NTk0NTQxODgzNjE0MTA3MzIwNDIxNDM3MjMxNzY5MzM2NDcxNTE3NjgyNzg1NDk3OTA1MzAzODM4ODk0ODM2NjE3NjU0MTc3Mzk3MDEwOTQ1NDI5ODU0NjM1NzAyODgwNDA3NjkyOTAxNjQzNTEifSwicl9jcmVkZW50aWFsIjpudWxsfSwic2lnbmF0dXJlX2NvcnJlY3RuZXNzX3Byb29mIjp7InNlIjoiMjE0NzgwNDA1MTAyNzUxNzA2MTIzMzc5MTYwODIxMTUzMjkwMDU5MjQ3MjkxNjAxMzg3Mjg1MDA3MzE5NjY2ODk3Njg4NjQ4NzYzNTAzMTQxMjc1ODIwNjUyMjIwNzAzNTg0NTMwOTEzMjY1NTc0NzYxMzI2NDA1MTI5MTUxNjQzOTM0NjkxNjI0MDAyNDE1NTU0ODY0NjUwMzEzNTIxMjczMDk2MTc4NjMwOTY2NDI2ODU0NzE1Nzk3MzYyNzk3NTM0ODM3OTE4NDUzOTQxMjAwNTIwNTI4NTA1Nzk3NjEwOTcwNTk3Njc2Mzc2NDE2MjA2MzcyNDYyNzU5NjcyNTE2NTYyNDE5Mzk0NDk5OTk3Mjg5MzQzOTg0MDE3MjM5OTg1MjA3OTg4OTYxNzc5NDU2NTAzODg3Njk2MzA3MjE3NzczNDI2MjMxMDU0MTc1NzYzNzgzMDA4MDIxMzgyMDU5MDY1OTU3MjI2NDg3OTkzOTk3MjI5OTg3NTgzNDU1ODE5NTI4MTA4Nzk2MTIxNzA2MjY0MTc4ODI1MDM5NTA2MzkzODk3MTc5NDk5Mjc5OTUzODYwODY1OTUzNzA3NjQyNTkxMDY0ODIyMjg4ODg1NDE5MjMyMTc1NTYwMzIwMDczNDgzNTg0Mzc3NDUxMDMxNTA2NDQ0NTcwNzQ1MTEyNTYxNjIxNzMzNjY1NjE2MzU0NjUxMzE0MjI3OTgzNjEyODM2NjkwMzgwNDg4MDY4NDk2MTkzMzc4OTUxMjY2NDI0Nzg2MzIxODY1NjYyOTMyNTM1OTc4MjY4NDgxMzQ0MzQ4NDQ5NzkiLCJjIjoiNzE1NzQ0NjIzMTA1OTU5NjM4NDg4ODk3ODU3NTYwNzIzOTYxMDE5NjE5MjIwNTc2NTkyMTgyMjc4NDk3Mjk4NjE4MzQzNTU4MDAyMDEifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=" + } + } + ], + "~thread": { + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286", + "pthid": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349" + }, + "~please_ack": { + "on": ["RECEIPT"] + }, + "~service": { + "recipientKeys": ["xaRkB1mi5rQxihB5T2pAyx3m54fuT65nq9C1mjVNdZy"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:2001" + } + }, + "updatedAt": "2024-02-28T09:36:32.927Z" + }, + "tags": { + "associatedRecordId": "e04ca938-64e1-441e-b5d9-4525befe4686", + "messageId": "44bead94-eb8a-46c8-abb7-f55e99fb8e28", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/issue-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286" + }, + "id": "de893e30-a2f3-458e-b8ec-aaca324c0806", + "type": "DidCommMessageRecord" + }, + "90d4edf7-c408-48a0-a711-2502f3c649ac": { + "value": { + "metadata": {}, + "id": "90d4edf7-c408-48a0-a711-2502f3c649ac", + "createdAt": "2024-02-28T09:36:26.745Z", + "associatedRecordId": "e04ca938-64e1-441e-b5d9-4525befe4686", + "role": "receiver", + "message": { + "@type": "https://didcomm.org/issue-credential/2.0/request-credential", + "@id": "0f3c0bb1-737e-4e9c-9f6a-85337222941e", + "formats": [ + { + "attach_id": "a81bbf5e-e766-44b8-8ba4-4a92819473b9", + "format": "anoncreds/credential-request@v1.0" + } + ], + "requests~attach": [ + { + "@id": "a81bbf5e-e766-44b8-8ba4-4a92819473b9", + "mime-type": "application/json", + "data": { + "base64": "eyJlbnRyb3B5IjoiNDYwNDIxODQ1MzE0ODU4NDI3ODE2OTAxIiwiY3JlZF9kZWZfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvQ0xBSU1fREVGLzQwMDgzMi90ZXN0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiODYwNjAyODA1NjM1NTgzNTUwNTc0MTAyOTkxNTExOTA3NTMxNjQ1NTE5MTEwMTI0MTUyMzA0MjMzNTMzMTA4NDcxMDc2NDc5MzA3NzMwMTI3NzQ1NDE1NTE1ODUwOTA1ODAxNjg1ODc0NDM2MDE5NjU2NTg0NjU0ODc0NTEyNjMwMzEwMjc2MzIzNzY2MzE2ODk2NTM3MDk4MzAxNTg1NzE5NTU1MjQ2NDc4NjY5OTI2NDcwMzc1MTgzMzUwOTMwNjU2ODgwMTY1MjcyMjU4NDY2MzcyOTAxMjA5MjkwMTMyNTEwNTgzNTA3NjAyNTc5MTg5NDc4OTU1OTMzNzA0OTc2NzA3OTI3NTg2MjU5NjQ4MjQ2NDQxMTEyOTYyOTczMDA3NDkyNzUzMTY1Nzc3MzQwNzQ0MTA2MzcxOTAyMzc0OTc4NzQ4MDI0NTgxODc2MjEwNTU0NDI5NDcyMTc0NzgzNTI5NTY2MjcxMTEyOTgyNjkxNTgwMTgxMDI4ODc5NjgxNjQxMDg4MDQwODY3OTAxODcxNTY4NjY3NzEzNzE1Njc2MzM4NTcwNDE1NTUwNTIyMzAzMjAyNDYxMTg4NDIwMjUyNDA2ODUyODUzOTczODUyOTY1ODI3NjUyMjEzMTA1NzM4OTE1NzIyNzQ4MTg4NTc0MDIxMzE3OTEwMDY5NDQyMDM3MDQyMzI5NzczNTQ4MzEzNTQyNDAxMzcyMzMzMTMyODQ1NzkzMzUwMDQ2MDk3Nzg4NjA4MTA2NTA5OTUxNDE1NDkzNjkxNjAxNjExMTIwNzE1MzM0NjExMTM3MjY0NDM0NjUiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI4MzE2NjQxOTY2MDcyMzc1MzAxNzYxOTMxMzY2OTMxMjEyNjA3NzUyMTk5MDI1Nzc5MDYyMDUwMjA5NDI4OTkzMzg2NTQ0Mzc3NjY5MCIsInZfZGFzaF9jYXAiOiIxMDkxMTY1NDc5NzEyMTM3ODgxNzIxMjQ3NjYzNjE2ODU5MDE5Njg1MTg2MDY5OTI4NjIyNjE1NDg2MzM0MTQzNzYzMzU0ODMxNTc2NDMyNDQxMjEyODQ4MTYyNjI5Mzg4ODc0NTQyMTYyNTMwNTU5MjMxNTU0NzIwNTMyNTc3ODY0MDQ1NjMwNzA4MjkyOTkzODQzMzU0ODE3NjEyMzQ3OTQ1MzU3NzM0NjMyNTc0NTA3ODM2MjAwODAxMjY2NDc3Mzc5MDUwMTMzNjQ1MzE3MTM2Nzg1MzM4OTU5NDg0MjUzNTEyNTEzOTcyNDM3MDM2MjcyNDU3Mzk4ODE4NjExODYyMjE1OTA3NDQzOTAwNDE2ODMyMDI3Nzk3NDE5MTQxNTk5MTY0MTY1MTA2NzA0MjgyMjg1NjcyOTEwMTMwMzc4NDA5NjExNzI3NjA3MjIxODA3ODg0MjI3OTgyNjM1MjY2NjI2NTEzOTg1OTIxNTgyMjM4NzU4NTM0MjkxMTU5MDgxMTk0NjUwNjg5Mjk0NzM5MTA1MTQwNTQzNjQ0NDUzMzg1NzU4ODY2NDg2MzgyNDY1MjA0MjQ0NTYyNTkxNzI5ODQzNDI1Mjc4MDAzMTk1MTk1MzU4MzgxODAxNzA5NTI5MzEwOTU4NDk5OTg1MDcwNjk0NzA2MjcwNjUyOTQ2MzU0MDg1MjYyOTMxNzI0NzI5MTk2MjY3MDczMDAyODk4NzQxMzc3ODEzMjgzNzAzNjU4MDkwNDE3ODcxMjE5NzQ1NzIwNTc1OTE4MTU5MzMzMDczMTA4OTE3Mjk5MzA2MzE0NTk4ODc5NDQ4NjQ1NjA4NDM2NTYzOTU5OTA2ODY2NzcwMjQ3MzIwMTkwMDQ0MTQxNjc2MDAyNTI5Njg2MTA1Nzc1NjI0MTU1NTkwMjMxMjMyNjQyNTY0MDE4NTM1ODQxNzU4MDYxMzk2IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxODk2Nzk5NDIxNjYxODE0MjQxMTcxMDU0NTcxOTYwMDc5MzM1MDUyOTU1NTM4MDM2OTExNzk1NjUxMzgyOTc5NTkxMTA0MTM3MDU3NTA1NTI1ODYyNjY5MTc1NjAxNDE3MDM3NzAyMTYyNzY4ODQ4MTc5MzA5MTU3MDAzMTI3NzI0NDc5NjQwNDQzNjcyOTQzNTg1MjkwMTk3Mjg3MTM3NjA3MDEzOTY1MzUzNjI0MzU4NSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI1MzMyNDAzNjU2MjU1Nzc0MjQwOTgwNDAifQ==" + } + } + ], + "~thread": { + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286", + "pthid": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349" + }, + "~service": { + "recipientKeys": ["Ab2MHChPWKG1f7xSMp8tKJFZj8qdTAnsQvN4nJ5vuZdA"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001" + }, + "~transport": { + "return_route": "all" + } + }, + "updatedAt": "2024-02-28T09:36:26.745Z" + }, + "tags": { + "associatedRecordId": "e04ca938-64e1-441e-b5d9-4525befe4686", + "messageId": "0f3c0bb1-737e-4e9c-9f6a-85337222941e", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/request-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286" + }, + "id": "90d4edf7-c408-48a0-a711-2502f3c649ac", + "type": "DidCommMessageRecord" + }, + "f189e88c-4743-460a-bccc-443f2a692b98": { + "value": { + "metadata": {}, + "id": "f189e88c-4743-460a-bccc-443f2a692b98", + "createdAt": "2024-02-28T09:36:11.829Z", + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "role": "receiver", + "message": { + "@type": "https://didcomm.org/issue-credential/2.0/offer-credential", + "@id": "a1ec787c-8df6-4efe-9280-6b4407db026e", + "formats": [ + { + "attach_id": "d6883b94-7a6b-4fe7-9ddc-98b23c46c478", + "format": "anoncreds/credential@v1.0" + } + ], + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/2.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "test", + "value": "test" + } + ] + }, + "offers~attach": [ + { + "@id": "d6883b94-7a6b-4fe7-9ddc-98b23c46c478", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvU0NIRU1BL3Rlc3QwLjE1OTkyMjE4NzIzMDgwMDEvMS4wIiwiY3JlZF9kZWZfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvQ0xBSU1fREVGLzQwMDgzMi90ZXN0Iiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiIxMDk0MzkxMjM2ODU1Nzg0MTgzNDIyMDY2NjI2MzE2OTQ4ODQ1ODMwNzA0MjY0MjUzNDUyMTkwNTAxMTM2ODIyNTcwNTY0MTIzMzY4NDAiLCJ4el9jYXAiOiIyMTk4OTc3Nzk0MzA1NjU5NzMwNDk2MDU0MjM4NDE4OTYxODUzMDEyMTgzODgxODQxNjk4NDUwMTY2ODU5MTA1MDg1MDY0MzIzNzcyMjE3MDUxMDQwODYwNDcwODY0NTM1MDI5Mzk2NTY4Njc0ODkyNzg4Nzg5MTIzNTU3MzE2MTkwNjAyNjczMTczODM0NDUwNjcxMjA2ODA2MjYxOTg2Mzc3NjE1NTc3MzU4MjI0MTM4NDc1OTExNjgyOTQ2MTAzOTkzODg5MTIxODMyNjExNTg4NDc0NzUwMjIxNTQ3MjcwNDAyMjUwMTIyMjc5ODcwNDQ4MDA3OTgwMjQ0NDA5OTc5NDgyNDg1MjU2MDk3OTY4ODQyNDg3ODM2MzMyNTA4MjA0OTIwNjM3ODE0NDMyNDczMjg0NDc0MzQzNzQ3Njg0MjI2MTMxMjAwNjQyMDI3NjQ2NjMwMzkzMDE4MTk1NTAzOTQ3MjkxNjA0Nzg5MjM5MTY3ODUwNjA5Mzc3MTE4NjA3NjUwMzE0NTYyOTk3MDc2NjQ3MDUzNzcxNzgxMjAwMTAxNjExOTc2MTI4ODY5OTE0NTM0NzQ5MDc0MDc3NzcyOTUzMjkzNjMwMzMyMDc5MTk4MzAxNjMwNTY3MjQ3Mjc0OTY5MTM5ODI2Nzc2NTM2NzEwMTgxMjQ3MDY2NDE4OTY1NTQyNDY5MjMyMDkxMDYwNjI4Njc0MTM4OTgwMDcwODE0Njg1OTMyMjg0MzIyMjMzMDQ3NjQ2NTkxODc3NjkyODgyMTM5ODQ4MzgxNjQxMTg1ODE1ODAxMDg0OTM5NTk2NzMwMTYxMjA1MDg2MzMzNzgwNjI5OTEyMTc1NDA0ODQ2MDk5MTI5MjY0NjM3ODA2MjQ3MzE2NzU2NTg3NDI5MjEwNjkzNDM5NTQyMjEzNzI2IiwieHJfY2FwIjpbWyJtYXN0ZXJfc2VjcmV0IiwiMTA4NjM5NjM3NDg4MzU1Nzc0MDI5ODE3OTk0NDkxOTE1MjIyMzc2ODk2NzQwMjM0MDk3MzI0NjcwNzU3NDQ1NjQ0OTAxNTk2NjUwMjk1MDc3NjM0Nzk0Mzc5MzMwODUyNzUxNjQwODk5NjYwMDUwOTY5MDUzNDYyMjcyMzQ5MTEzNjYxOTg3NzMxNDQ3NDMzNTIzNTc0OTc5NjYyMzE0MTAxMTI3Njc5MTAwNzcwMjY0NDMxMTIxNjYzMzMyMTQ2MTU4NzM3MzU0NTk0MDM1NTM1MjI5Mzc4MjU3NzUyOTAwMzA3Mjg3NjQ4NzcwOTU4NTg5ODg1NzA3NDEyOTgzNzYwNTE2ODk0NzkzMTE3NTQ5Nzc3Njg1NTg0MjQ3MzQ1ODMxNzk2MjUzMzQ1MDk1NzIyODU4NjM4MjAxMjgxMjIzNDYyOTg2NjE2MjYzNzIyMDk1MjMxMjg0MTgzMzM3ODYwODQ2Njc5Njg5ODM2MTM4NzAxNjE4MzI1MDAyNTM5NjczNzM4NjUwMTMxNzMzODIzNDk0Mjk5MzQzNDQxNjc5MzM1MTQ5NTIwODQ4Mzg4Njk5MTk5ODA1NTk4MTAzNTk0NTk4OTE0OTkyOTI2MDMzNTA0MzAxNTY5MjMwODYyMzc3NTg5ODg2OTYyMDIxOTIxODM3ODI3MDYyNTI0MzIzMTg3OTcwNjg0MzIxNzY0MzcwMzc4NDc0Mjc4MzA4NzMzOTY4ODgzNTMyNzM2MTE1NTQ3MzA0MzU4ODgyMzc1Njc0MTQwMjEzNzc1OTE1OTU3NzU5MTM3NjUwMjY0Njg3OTUzMTk4NTE5OTY0MDcyMzEwNDY3OTA2Njk0OTQxMzM4NDAzNDg4NTYyMjgxMDE5MTQzMDk5MTE0NjM3MTY1OTI2MzQxOTY4NTk3MjE4MjU3OTU5OCJdLFsidGVzdCIsIjcwMDE5MTUyMzc5MTExNTQzNzgwOTgxNTYyMDU5OTYzMDYzMzg4ODg5NDg1NjkxOTM5MzM3NzgxNjkwNTU3NjA5MDA4MTA2NjY5NzAwNjA3MzI1OTAyNjQyMzA4NTIzMDc5MjA3NjEwNTU1MjQ0NTY0MjkwNzc5ODA5Mzg5ODEzNTI0OTc5MzE5MTg4NDI0NzIwNzUxMjQwMzQ3NTY0NTQ3MDY2NDE1NTE3MzU5NjUxODU1NzU3MzY4NDcxOTM5OTk3NjY1MTk5NTE4OTQ2ODMzMDY1MTMyNjYxMDI4Nzc5ODg1NDQ5ODMwMzg1MTA3MzUxOTgxMDM1NTAzMzM1MDg0NjgxMDQ4MzE0NjQzMDQ4NzIwMzQxMzk0MjI5NTEyNDcyNDY0NjUwNDI3NDA4NTI5ODkwMzg1ODc1MzkzMjA0ODExMTUwODgxNDA4NzY3NTMzNjI1MjU4MDUwNDc2NzU2NDIyMzk5NjMxNjA5NTU2MjI0NjQ1Mjg5NDM0Mjk3NDkwMzg0MzYwNDM0Mzg4MDU1NDAxODgyNDIxNDU1OTI0NjQxMTUwNjQ1NTkzNzUwMjQ2MTI4NTYzNzMzMzgzMzQ2MTYyNjYzNjE5MTYyMzIxMDM3MDgzNTcxNzc0MzQ5MzYwMTcxNzkwNzUzNzYyMDI3NTczMDc0MDI1NTgyOTQyODk4MzMwMTY3NDY0MTExNjA1MTMxMjk5MDE2MjQ5MzU2MDY5MjM3OTAzNjAyNjgwMjMwNzgzMjg0MDIwOTMxMjY3MDkzNTE1MzU3MjQ1MDEwOTI0MTI2MTIyNjUwNTM1MDI5MjIxMzY2NzA5NTI2NjY1Mjc5NzIyMDg0NjI0MzQwOTkyODQ4NDU0OTgxNTExOTIwNDE4NzM2NTE1NjIxMTgxNjkxMzcyNDE5ODU2OTg1NDkzMSJdXX0sIm5vbmNlIjoiOTczNDk4MTg3NTMwNzE2MjQ3MDE5NzU0In0=" + } + } + ], + "~thread": { + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286", + "pthid": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349" + } + }, + "updatedAt": "2024-02-28T09:36:11.829Z" + }, + "tags": { + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "messageId": "a1ec787c-8df6-4efe-9280-6b4407db026e", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/offer-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286" + }, + "id": "f189e88c-4743-460a-bccc-443f2a692b98", + "type": "DidCommMessageRecord" + }, + "b576366d-7fa2-4ede-b83c-3d29e6e31a78": { + "value": { + "metadata": {}, + "id": "b576366d-7fa2-4ede-b83c-3d29e6e31a78", + "createdAt": "2024-02-28T09:36:43.506Z", + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "role": "receiver", + "message": { + "@type": "https://didcomm.org/issue-credential/2.0/issue-credential", + "@id": "44bead94-eb8a-46c8-abb7-f55e99fb8e28", + "formats": [ + { + "attach_id": "e5c0d797-67f3-4d1d-a3e1-336786820213", + "format": "anoncreds/credential@v1.0" + } + ], + "credentials~attach": [ + { + "@id": "e5c0d797-67f3-4d1d-a3e1-336786820213", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvU0NIRU1BL3Rlc3QwLjE1OTkyMjE4NzIzMDgwMDEvMS4wIiwiY3JlZF9kZWZfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvQ0xBSU1fREVGLzQwMDgzMi90ZXN0IiwicmV2X3JlZ19pZCI6bnVsbCwidmFsdWVzIjp7InRlc3QiOnsicmF3IjoidGVzdCIsImVuY29kZWQiOiI3MjE1NTkzOTQ4Njg0Njg0OTUwOTc1OTM2OTczMzI2NjQ4Njk4MjgyMTc5NTgxMDQ0ODI0NTQyMzE2ODk1NzM5MDYwNzY0NDM2MzI3MiJ9fSwic2lnbmF0dXJlIjp7InBfY3JlZGVudGlhbCI6eyJtXzIiOiI4NTY3NjYyMzQ4NDI3NzYyNDY4MjQ0NDgyMDQ0NDcwMjQ2NzQ5OTM1NzkxMDI3NDE2NTAxMzQxODM3OTEyNTEzNDA3NTMyNTU0MzcyOSIsImEiOiIyOTA3ODU4MzAyMzY0NDcyMzQxNzI1Njk2Mzk1MTgzNDc2OTQyOTU0NDg5MzU2MTMxMjYwODA0MTk5OTAzOTcyNTc3OTA0NTQ3ODk1NzY2NTYxMDUzOTM0NTY2OTI0OTAzMzY0Mzg0NTg3MTMxMDYzODIwNTM1OTgyNzkxMzM0NTUzOTg5MzYwNDc4NDE3MjIyMTE3Nzg3NDE1MTg2NzMyMzc4OTc4MzM1MjA2OTg1ODExNDY3NzA2MjM4MjMyOTIwMTk0MTMzNjExNDY0MDA5NDQ0ODk4MjQ5MjU0ODYyMTc0NTQxODE0MDI4NzUyMzM1ODQ1NTc5Njk4NTQ1MjI5MjYwMzgxNDA1MDYxMzAyMjQ5OTIwNjM1MDQ4MTk2NjQ1MDk0OTE0Nzc5ODYwNzI0ODM2NzM5MzAyMjQwMDg3MTM4OTQ0MTk4NTQ4MzI1MTk5NTY2NjExMDA2ODQzMTM1NjEwMjQyMDA2OTQ1MDIzMjcxMjM3MDQxMDA3MTAzODUyOTIzNzMzODU5MDAyMjI1NTY3NTQ1MTE0ODM2MjYwMDcyNzU4NDk5NDY2NTE3NzI4NTg1MzA4MTc5OTY4MzYyMzY3NDI5MTYwMzY0MDMxNDczMzY2NjQ2ODkxODY2NjE3NTEwMzYyMjgzMTYwNDQwMjIxNTMyMjI4OTUwOTU5NDE1Nzg3NTIxMjQwNjU4NDU1ODk3MzY4MjMyNzEyNzUzMjgwMzM3MzQyNDU3NTIzMDk0NjcyMTUyMTk2ODY4MDA3NTYwODE3ODU4NjE2NTU2NTE2MjkzMjY0MzM3ODIzNjQyODQwMzMzMSIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxMDM5MjcwODI5MzQzMDYyMDQxODk0NTM5MTQ4NTQ1Njg2MzMiLCJ2IjoiMTAxMTMyNDY1OTQ5MTk5Njg2OTQyNDM3NDkxNTA1MzM0MjEyNTE5MDMyMTI1MTYxNzYyODA0NTA5NDQzNDM1NjQ0MzExMDk1MDU5Mzg4NDE4OTE2MjUyNDc2NjczNjQzNzk0MTIxMjk0MDU0MDUzNDg0NzcwOTU2MTAwMDI0MzM1NjgzNDc4NTY1MDkxNTk2MTEzMzU3NTg4MTg3NTY5MjY4MjUwNjM1NjA3NzAzMTQ4NzMxNTE5MDg5NjQwMDAwNDQzNjA1MzM5OTQ4MzM1MTc4Nzg5ODcxNDEwMTYyOTA1NTA2OTM0MDc0OTQxMjE4NjY4NjA1ODgwMTc3MjEyOTQ4NjYxNzc5MTI0MzI4NDA1MjgzMzIwNjU2NDQzMDg2ODk1MDY0NDQ3NTMwMjI4NTgzODE1OTg3ODk0NzYxMzcwMjg0ODExMjIwNTY2NTgxNzM2NzQwNzY1NzUyNzIyMTE3MDEwODEzODkyMTE5MDE1NzAyNDI1Njk3OTg4NzQwNjIzMDA4NDQzOTY4MTQ5NDAwNjk3ODI2NzMzNjg5NzU0ODYwNjk4NzIwMjkzMjI3ODU3NjU3NzM0MTc5ODEyOTQ2MDkwNTU0ODE3MDcyNjQ4NDgwOTA4MTg4NTI4NDY4MjAzMzM2MDEyMzY2OTUyNjUyODgwNDY5NjUwMTEzODU1NjQ4OTc0Mzk0NzU4NjM5NjUwMjM0NzY0Mzk5OTQ5NjMyNzk3NTYwMzc4NDU2NDIzNjc4NDM1NDIzMDUwMzg4MDU0NDEwMzg3NjIyMDEzMTYxMDc2OTEwOTQ3MjI3Mzk3NDQxMTgzMDA0NDM3MTI0NDU1Nzc0NTI1NzIwMDcxMjg4MjA5NDY2OTcwNDQwNDk3MTY1MTE1MTQ1OTc3NDM5MjkxNDI3MjgyNzI2MTAxMzAwNTg0NTU3MjYzNzMzNDY0NzA3NzA0NTk0NTQxODgzNjE0MTA3MzIwNDIxNDM3MjMxNzY5MzM2NDcxNTE3NjgyNzg1NDk3OTA1MzAzODM4ODk0ODM2NjE3NjU0MTc3Mzk3MDEwOTQ1NDI5ODU0NjM1NzAyODgwNDA3NjkyOTAxNjQzNTEifSwicl9jcmVkZW50aWFsIjpudWxsfSwic2lnbmF0dXJlX2NvcnJlY3RuZXNzX3Byb29mIjp7InNlIjoiMjE0NzgwNDA1MTAyNzUxNzA2MTIzMzc5MTYwODIxMTUzMjkwMDU5MjQ3MjkxNjAxMzg3Mjg1MDA3MzE5NjY2ODk3Njg4NjQ4NzYzNTAzMTQxMjc1ODIwNjUyMjIwNzAzNTg0NTMwOTEzMjY1NTc0NzYxMzI2NDA1MTI5MTUxNjQzOTM0NjkxNjI0MDAyNDE1NTU0ODY0NjUwMzEzNTIxMjczMDk2MTc4NjMwOTY2NDI2ODU0NzE1Nzk3MzYyNzk3NTM0ODM3OTE4NDUzOTQxMjAwNTIwNTI4NTA1Nzk3NjEwOTcwNTk3Njc2Mzc2NDE2MjA2MzcyNDYyNzU5NjcyNTE2NTYyNDE5Mzk0NDk5OTk3Mjg5MzQzOTg0MDE3MjM5OTg1MjA3OTg4OTYxNzc5NDU2NTAzODg3Njk2MzA3MjE3NzczNDI2MjMxMDU0MTc1NzYzNzgzMDA4MDIxMzgyMDU5MDY1OTU3MjI2NDg3OTkzOTk3MjI5OTg3NTgzNDU1ODE5NTI4MTA4Nzk2MTIxNzA2MjY0MTc4ODI1MDM5NTA2MzkzODk3MTc5NDk5Mjc5OTUzODYwODY1OTUzNzA3NjQyNTkxMDY0ODIyMjg4ODg1NDE5MjMyMTc1NTYwMzIwMDczNDgzNTg0Mzc3NDUxMDMxNTA2NDQ0NTcwNzQ1MTEyNTYxNjIxNzMzNjY1NjE2MzU0NjUxMzE0MjI3OTgzNjEyODM2NjkwMzgwNDg4MDY4NDk2MTkzMzc4OTUxMjY2NDI0Nzg2MzIxODY1NjYyOTMyNTM1OTc4MjY4NDgxMzQ0MzQ4NDQ5NzkiLCJjIjoiNzE1NzQ0NjIzMTA1OTU5NjM4NDg4ODk3ODU3NTYwNzIzOTYxMDE5NjE5MjIwNTc2NTkyMTgyMjc4NDk3Mjk4NjE4MzQzNTU4MDAyMDEifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=" + } + } + ], + "~thread": { + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286", + "pthid": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349" + }, + "~please_ack": { + "on": ["RECEIPT"] + }, + "~service": { + "recipientKeys": ["xaRkB1mi5rQxihB5T2pAyx3m54fuT65nq9C1mjVNdZy"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:2001" + } + }, + "updatedAt": "2024-02-28T09:36:43.506Z" + }, + "tags": { + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "messageId": "44bead94-eb8a-46c8-abb7-f55e99fb8e28", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/issue-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286" + }, + "id": "b576366d-7fa2-4ede-b83c-3d29e6e31a78", + "type": "DidCommMessageRecord" + }, + "9a9b7488-2205-4e98-9aae-2b5e2b56a988": { + "value": { + "metadata": {}, + "id": "9a9b7488-2205-4e98-9aae-2b5e2b56a988", + "createdAt": "2024-02-28T09:36:19.670Z", + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "role": "sender", + "message": { + "@type": "https://didcomm.org/issue-credential/2.0/request-credential", + "@id": "0f3c0bb1-737e-4e9c-9f6a-85337222941e", + "formats": [ + { + "attach_id": "a81bbf5e-e766-44b8-8ba4-4a92819473b9", + "format": "anoncreds/credential-request@v1.0" + } + ], + "requests~attach": [ + { + "@id": "a81bbf5e-e766-44b8-8ba4-4a92819473b9", + "mime-type": "application/json", + "data": { + "base64": "eyJlbnRyb3B5IjoiNDYwNDIxODQ1MzE0ODU4NDI3ODE2OTAxIiwiY3JlZF9kZWZfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvQ0xBSU1fREVGLzQwMDgzMi90ZXN0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiODYwNjAyODA1NjM1NTgzNTUwNTc0MTAyOTkxNTExOTA3NTMxNjQ1NTE5MTEwMTI0MTUyMzA0MjMzNTMzMTA4NDcxMDc2NDc5MzA3NzMwMTI3NzQ1NDE1NTE1ODUwOTA1ODAxNjg1ODc0NDM2MDE5NjU2NTg0NjU0ODc0NTEyNjMwMzEwMjc2MzIzNzY2MzE2ODk2NTM3MDk4MzAxNTg1NzE5NTU1MjQ2NDc4NjY5OTI2NDcwMzc1MTgzMzUwOTMwNjU2ODgwMTY1MjcyMjU4NDY2MzcyOTAxMjA5MjkwMTMyNTEwNTgzNTA3NjAyNTc5MTg5NDc4OTU1OTMzNzA0OTc2NzA3OTI3NTg2MjU5NjQ4MjQ2NDQxMTEyOTYyOTczMDA3NDkyNzUzMTY1Nzc3MzQwNzQ0MTA2MzcxOTAyMzc0OTc4NzQ4MDI0NTgxODc2MjEwNTU0NDI5NDcyMTc0NzgzNTI5NTY2MjcxMTEyOTgyNjkxNTgwMTgxMDI4ODc5NjgxNjQxMDg4MDQwODY3OTAxODcxNTY4NjY3NzEzNzE1Njc2MzM4NTcwNDE1NTUwNTIyMzAzMjAyNDYxMTg4NDIwMjUyNDA2ODUyODUzOTczODUyOTY1ODI3NjUyMjEzMTA1NzM4OTE1NzIyNzQ4MTg4NTc0MDIxMzE3OTEwMDY5NDQyMDM3MDQyMzI5NzczNTQ4MzEzNTQyNDAxMzcyMzMzMTMyODQ1NzkzMzUwMDQ2MDk3Nzg4NjA4MTA2NTA5OTUxNDE1NDkzNjkxNjAxNjExMTIwNzE1MzM0NjExMTM3MjY0NDM0NjUiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI4MzE2NjQxOTY2MDcyMzc1MzAxNzYxOTMxMzY2OTMxMjEyNjA3NzUyMTk5MDI1Nzc5MDYyMDUwMjA5NDI4OTkzMzg2NTQ0Mzc3NjY5MCIsInZfZGFzaF9jYXAiOiIxMDkxMTY1NDc5NzEyMTM3ODgxNzIxMjQ3NjYzNjE2ODU5MDE5Njg1MTg2MDY5OTI4NjIyNjE1NDg2MzM0MTQzNzYzMzU0ODMxNTc2NDMyNDQxMjEyODQ4MTYyNjI5Mzg4ODc0NTQyMTYyNTMwNTU5MjMxNTU0NzIwNTMyNTc3ODY0MDQ1NjMwNzA4MjkyOTkzODQzMzU0ODE3NjEyMzQ3OTQ1MzU3NzM0NjMyNTc0NTA3ODM2MjAwODAxMjY2NDc3Mzc5MDUwMTMzNjQ1MzE3MTM2Nzg1MzM4OTU5NDg0MjUzNTEyNTEzOTcyNDM3MDM2MjcyNDU3Mzk4ODE4NjExODYyMjE1OTA3NDQzOTAwNDE2ODMyMDI3Nzk3NDE5MTQxNTk5MTY0MTY1MTA2NzA0MjgyMjg1NjcyOTEwMTMwMzc4NDA5NjExNzI3NjA3MjIxODA3ODg0MjI3OTgyNjM1MjY2NjI2NTEzOTg1OTIxNTgyMjM4NzU4NTM0MjkxMTU5MDgxMTk0NjUwNjg5Mjk0NzM5MTA1MTQwNTQzNjQ0NDUzMzg1NzU4ODY2NDg2MzgyNDY1MjA0MjQ0NTYyNTkxNzI5ODQzNDI1Mjc4MDAzMTk1MTk1MzU4MzgxODAxNzA5NTI5MzEwOTU4NDk5OTg1MDcwNjk0NzA2MjcwNjUyOTQ2MzU0MDg1MjYyOTMxNzI0NzI5MTk2MjY3MDczMDAyODk4NzQxMzc3ODEzMjgzNzAzNjU4MDkwNDE3ODcxMjE5NzQ1NzIwNTc1OTE4MTU5MzMzMDczMTA4OTE3Mjk5MzA2MzE0NTk4ODc5NDQ4NjQ1NjA4NDM2NTYzOTU5OTA2ODY2NzcwMjQ3MzIwMTkwMDQ0MTQxNjc2MDAyNTI5Njg2MTA1Nzc1NjI0MTU1NTkwMjMxMjMyNjQyNTY0MDE4NTM1ODQxNzU4MDYxMzk2IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxODk2Nzk5NDIxNjYxODE0MjQxMTcxMDU0NTcxOTYwMDc5MzM1MDUyOTU1NTM4MDM2OTExNzk1NjUxMzgyOTc5NTkxMTA0MTM3MDU3NTA1NTI1ODYyNjY5MTc1NjAxNDE3MDM3NzAyMTYyNzY4ODQ4MTc5MzA5MTU3MDAzMTI3NzI0NDc5NjQwNDQzNjcyOTQzNTg1MjkwMTk3Mjg3MTM3NjA3MDEzOTY1MzUzNjI0MzU4NSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI1MzMyNDAzNjU2MjU1Nzc0MjQwOTgwNDAifQ==" + } + } + ], + "~thread": { + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286", + "pthid": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349" + }, + "~service": { + "recipientKeys": ["Ab2MHChPWKG1f7xSMp8tKJFZj8qdTAnsQvN4nJ5vuZdA"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001" + } + }, + "updatedAt": "2024-02-28T09:36:21.638Z" + }, + "tags": { + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "messageId": "0f3c0bb1-737e-4e9c-9f6a-85337222941e", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/request-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286" + }, + "id": "9a9b7488-2205-4e98-9aae-2b5e2b56a988", + "type": "DidCommMessageRecord" + }, + "6bab4ff7-45a1-4d66-98a4-ae81efd7f460": { + "value": { + "metadata": {}, + "id": "6bab4ff7-45a1-4d66-98a4-ae81efd7f460", + "createdAt": "2024-02-28T09:36:46.755Z", + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "role": "sender", + "message": { + "@type": "https://didcomm.org/issue-credential/2.0/ack", + "@id": "6a49a044-b5cb-4d91-9a6f-18d808656cc8", + "status": "OK", + "~thread": { + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286", + "pthid": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349" + }, + "~service": { + "recipientKeys": ["Ab2MHChPWKG1f7xSMp8tKJFZj8qdTAnsQvN4nJ5vuZdA"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001" + } + }, + "updatedAt": "2024-02-28T09:36:46.755Z" + }, + "tags": { + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "messageId": "6a49a044-b5cb-4d91-9a6f-18d808656cc8", + "messageName": "ack", + "messageType": "https://didcomm.org/issue-credential/2.0/ack", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286" + }, + "id": "6bab4ff7-45a1-4d66-98a4-ae81efd7f460", + "type": "DidCommMessageRecord" + } +} diff --git a/packages/core/src/storage/migration/__tests__/__fixtures__/2-proofs-0.4.json b/packages/core/src/storage/migration/__tests__/__fixtures__/2-proofs-0.4.json new file mode 100644 index 0000000000..c7737b17fb --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__fixtures__/2-proofs-0.4.json @@ -0,0 +1,296 @@ +{ + "STORAGE_VERSION_RECORD_ID": { + "value": { + "metadata": {}, + "id": "STORAGE_VERSION_RECORD_ID", + "createdAt": "2023-03-18T18:35:02.888Z", + "storageVersion": "0.4", + "updatedAt": "2023-03-18T18:35:02.888Z" + }, + "id": "STORAGE_VERSION_RECORD_ID", + "type": "StorageVersionRecord", + "tags": {} + }, + "6cea02c6-8a02-480d-be63-598e4dd7287a": { + "value": { + "metadata": {}, + "id": "6cea02c6-8a02-480d-be63-598e4dd7287a", + "createdAt": "2024-02-28T09:36:53.850Z", + "protocolVersion": "v2", + "state": "done", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + "parentThreadId": "2b2e1468-d90f-43d8-a30c-6a494b9f4420", + "updatedAt": "2024-02-28T09:37:20.180Z" + }, + "tags": { + "parentThreadId": "2b2e1468-d90f-43d8-a30c-6a494b9f4420", + "state": "done", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68" + }, + "id": "6cea02c6-8a02-480d-be63-598e4dd7287a", + "type": "ProofRecord" + }, + "a190fdb4-161d-41ea-bfe6-219c5cf63b59": { + "value": { + "metadata": {}, + "id": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "createdAt": "2024-02-28T09:36:43.889Z", + "protocolVersion": "v2", + "state": "done", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + "updatedAt": "2024-02-28T09:37:14.269Z", + "isVerified": true + }, + "tags": { + "state": "done", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68" + }, + "id": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "type": "ProofRecord" + }, + "3f3351dd-7b56-4288-8ed7-b9c46f33718e": { + "value": { + "metadata": {}, + "id": "3f3351dd-7b56-4288-8ed7-b9c46f33718e", + "createdAt": "2024-02-28T09:36:44.806Z", + "associatedRecordId": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "role": "sender", + "message": { + "@type": "https://didcomm.org/present-proof/2.0/request-presentation", + "will_confirm": true, + "present_multiple": false, + "formats": [ + { + "attach_id": "f8e75054-35ab-4f7a-83f8-0f6638a865b0", + "format": "anoncreds/proof-request@v1.0" + } + ], + "request_presentations~attach": [ + { + "@id": "f8e75054-35ab-4f7a-83f8-0f6638a865b0", + "mime-type": "application/json", + "data": { + "base64": "eyJuYW1lIjoidGVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjExMTUyNTQ2NTQwNjU3ODkyNDY4OTA3MDUiLCJyZXF1ZXN0ZWRfYXR0cmlidXRlcyI6eyJ0ZXN0Ijp7Im5hbWUiOiJ0ZXN0IiwicmVzdHJpY3Rpb25zIjpbeyJjcmVkX2RlZl9pZCI6ImRpZDppbmR5OmJjb3ZyaW46dGVzdDo2TEhxZFVlV0RXc0w5NHpSYzFVTEV4L2Fub25jcmVkcy92MC9DTEFJTV9ERUYvNDAwODMyL3Rlc3QifV19fSwicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOnt9fQ==" + } + } + ], + "@id": "9e1568e0-7c3e-4112-b76e-869ea9d12f06", + "~thread": { + "thid": "a2f120ce-ff0f-498a-a092-97225e5d5f68" + } + }, + "updatedAt": "2024-02-28T09:36:44.806Z" + }, + "tags": { + "associatedRecordId": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "messageId": "9e1568e0-7c3e-4112-b76e-869ea9d12f06", + "messageName": "request-presentation", + "messageType": "https://didcomm.org/present-proof/2.0/request-presentation", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "present-proof", + "role": "sender", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68" + }, + "id": "3f3351dd-7b56-4288-8ed7-b9c46f33718e", + "type": "DidCommMessageRecord" + }, + "cee2243c-d02d-4e1a-b97c-6befcb768ced": { + "value": { + "metadata": {}, + "id": "cee2243c-d02d-4e1a-b97c-6befcb768ced", + "createdAt": "2024-02-28T09:37:15.884Z", + "associatedRecordId": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "role": "sender", + "message": { + "@type": "https://didcomm.org/present-proof/2.0/ack", + "@id": "ae6f67d0-21b6-4b12-b039-7b9dbd530d30", + "status": "OK", + "~thread": { + "thid": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + "pthid": "2b2e1468-d90f-43d8-a30c-6a494b9f4420" + }, + "~service": { + "recipientKeys": ["GTzoFyznwPBdprbgXcZ7LJyzMia921w8v1ykcCvud5hX"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:2001" + } + }, + "updatedAt": "2024-02-28T09:37:15.884Z" + }, + "tags": { + "associatedRecordId": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "messageId": "ae6f67d0-21b6-4b12-b039-7b9dbd530d30", + "messageName": "ack", + "messageType": "https://didcomm.org/present-proof/2.0/ack", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "present-proof", + "role": "sender", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68" + }, + "id": "cee2243c-d02d-4e1a-b97c-6befcb768ced", + "type": "DidCommMessageRecord" + }, + "76ad893a-f582-4bd6-be47-27e88a7ebc53": { + "value": { + "metadata": {}, + "id": "76ad893a-f582-4bd6-be47-27e88a7ebc53", + "createdAt": "2024-02-28T09:37:13.397Z", + "associatedRecordId": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "role": "receiver", + "message": { + "@type": "https://didcomm.org/present-proof/2.0/presentation", + "last_presentation": true, + "formats": [ + { + "attach_id": "21f8cb0f-084c-4741-8815-026914a92ce1", + "format": "anoncreds/proof@v1.0" + } + ], + "presentations~attach": [ + { + "@id": "21f8cb0f-084c-4741-8815-026914a92ce1", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsidGVzdCI6IjcyMTU1OTM5NDg2ODQ2ODQ5NTA5NzU5MzY5NzMzMjY2NDg2OTgyODIxNzk1ODEwNDQ4MjQ1NDIzMTY4OTU3MzkwNjA3NjQ0MzYzMjcyIn0sImFfcHJpbWUiOiIyNDc2MzE0OTMxMzE1OTU5ODY1MzU1MjgzMjM1MjkxMDc4MDM0NjAzMjcxMjQzMTEwOTQ3OTM0NDU3MzAyMTUwMjY5NDIzODkyMDYzODk2OTkzNzgxMDQwMDQ2NzI1MzIyNjE5Nzg1MzU2MTgxMjU0NDAxOTIzMzk4NjYyNzMyNjI3MjU3MzA5MDgxMjA3NjY2ODI5NDc0MDY5MDA2NjY5NDk3MTI4NzI1ODA4NjExOTAwMzQ0MTIxOTUwODc0NjYzMTEwNjE0NTc2ODc2NjQ2NTM2NTcxOTkwMjM5MDg4NTIwNjAyODY1NzM5MDM5MTAxMjI3NDY3Mzk5NzE3ODc3MDc4ODA0NzM5NDYyOTkzMjg5OTc4NjM2NDQxNzg2MTE2OTM4MzkzMzIzMTMwNDQwNDgzNDI5NjY1ODczNTE2NTA4ODA2MzA5MTMzODMxMjkxMzM5NDg1NDIyMzg1MDE1MzY3Nzc1NDc3ODkwNjQzMzg0MTYyODk5MDA1NzA0MjMxNzM1MTg2ODE5OTEwNTc2MDU0NjIzODkzOTk0MDkyNTA4MjU4ODgzODQ0MjkzMTkxNzY2ODQyNzg1ODA4NzY3MDQ1NDE3MTQ3NDc3NDQwNTk4MTk2NjM5OTA0NTQxNjE2MDY2MTYyMjYxNzE5MTUyMTYzNDk5ODEzNDc3NjM2MzE2NDgwNzQxMzI3OTg3OTk2NTk5MTc2OTc1MzQzNzUzOTgyOTMyMzA2NTA4MzQ3ODMxMjM2NjA1ODc2NTkwMDI3NDI5MDY0ODg0NzU5NTc4OTY2MjY5NzcxODQxNDUzMTI1NjYzMjgxOCIsImUiOiIxMzI4MDA2MjE3OTg5NjIzMjUyNTQ2MzQwNDE2MTAxMTcwODY0NTM4NDQwNzEzNzczNDE4NjA5ODU2NDE3MjI2MTg1NTQxMzE3NTk4MTQ0NzQ2Nzg1MDQ2NDcxMjA2MTUzNzY2NjU3NjQwNzEyNDc1ODY3NjMwOTE4NTIwMzY4MTE1MDQ3NTEzNDQiLCJ2IjoiNTgyMDc2NDI4NzI3NjQwNjY3ODA5ODkxNTI4MDIyMzQ0NjYwMDU4Njg1MTkxOTUwODIyMTExMzgwMjU1MDcxMTAwNzE3NzM5MzE0MTQxMzkxMTYxOTU5NTg2NDk0ODE2Njg5OTg2NTc0NDk1NzExOTI4OTEzODcwNjEwMDE1NjE3NTYxOTI5NjQzMjY5NjM2ODEzNTk4NjgwMTAyMDQ4NDYzNjk1NjgxMDIzNDI5MTM2NDI2NDgwNDEzNzI2MzEyNzMxMTczODE2NjI3MjExNzEyMTQzNTg4OTgyMDM5NTk5Mjg0NTgzNDI1MjE3MjI1OTg0NjcxNjYxODcwNzY4NzMxNjYyODE2MjMzOTUzMDg2MzYyMDA5NDA0OTQ3NTQxOTY5OTAwNzcxODA1NzI0NTUwNTQ1MTczOTg5MDI2NjAwNzk3ODkwMzc1MDE5OTQ4MjM3OTA2NjY1MTA5MTY0NDIxNDQ1MTEwMTQ0ODYyNzEzNjY1NjA0MTkwMzY1NTQzNjM3ODY4NTc1MjI1OTA2MTMyNjk5MTc0NzcxMjMzNDkzOTUzODc5MTQwMjQwNDIxMzY5NTA3MDU3MTgwMjA5NTM1NzEyODI1NzI2NDkwODkxMDE2OTUyMDkzMzc5MTMyMjI3MzMzNjg2OTEyMDE3MTEwMDM5ODcwNTc3MTMwNTA4ODQ2MDM2Mzc0MzYxMzE1MTQ3ODgwODA4NzkyNjQ0MzU2Mjg4NzgwNjQ4OTUyNDQxNzkyMDU0NDY4ODU2NjY1ODg0MTg1MzYyNDM4Mjk1NTcxNjAyMjk5MTA5MjA4ODg5NTU2NzE3ODc5NzI4NDUzMjk5NDI0MTQwMzc1NTU1OTQwNjUzOTAxOTIzMDE5NjQyNjU4MzI0NDU0MzU0MDk5MjU4ODczODY2NjY2MDg2MTYwODA5NTExNDYwOTQzMTAzNzEwMDk5MTU3MzExMDM3OTAwODIwOTI1ODYwNTYyMzE4NDc0MDc3NTQ3NTg2MTUxODg0NzEwOTU2Mzc0MzUxNDI1MTQzMjA2MjI3ODk0MzE5NjM2MDc4MDQyODU5NTMyOTU3MTk0MzYwNDYzNTIyODg5MDM1NjUyODAyNjk5MjUyNDkzMDcwNjA0ODgwMjY0NzA4MDg1MDAwNzczODUzOTQyMzg0ODQ2MDY5NTYwMTYzMjE0MzUwODM0MTcwODU4OTU3OTUzOTc5NDg3MTk3NDY0NjY0MDU1IiwibSI6eyJtYXN0ZXJfc2VjcmV0IjoiMzg0ODM0OTE0MjE1Mjg5MjAyNTM1MTM4ODg1MzgzNjAwNzAyNjMyNDQzODE4NjkzMzA2OTYzMjM4ODMwMDE1ODgwMzU2MDQ2NjMwMjMwNTc3MDU3NTQ2MjY0MzkwNzk2NjQxODg5NjMzMTkwMzUzMTcxOTUzNjAwMDAxOTM2NDE1NzM5NDcxODExNjQyMDQ2Mzk0MjEyODk3OTE3Nzg4NDQ4MzMxNTU0NjA5MTA1ODU3NSJ9LCJtMiI6Ijc3OTk1NzIwMTQ1NDU5ODMxODM2MjI3NTA2NTM4NDQxNjUxMDI3NDA0NDM5MTcyMDQwMDc5ODc3NDUyMDI2NDI5NTgxNjMyNDYxNjg5NzIzMzkwMjU4NDc5NDY5MTg4Mjk4NzgwMTgyMDkzNTMxMDc3NzkzNjEzODUwMjAwNzc1NjIzMDcxMjk4NTI5ODExOTk3OTkyMTEzODUxODM5MjcyMjExMTQ5NDEzNTY3OTEyMzIifSwiZ2VfcHJvb2ZzIjpbXX0sIm5vbl9yZXZvY19wcm9vZiI6bnVsbH1dLCJhZ2dyZWdhdGVkX3Byb29mIjp7ImNfaGFzaCI6IjQ1NTgyNDIzNzE0NzIyNTkwODQ4NDgxOTM4NTYxNDU3MTcwNjI4NzAzODk4OTI1NTU2OTE0Mjc3NDU5MTU4OTM3Nzc0NDA3NDgxNzIyIiwiY19saXN0IjpbWzE5Niw0MSwxMTYsMzIsNDIsMTgsMjUsMTA3LDE1MCwyMzcsMTMsNjksMTIyLDEyOCwyMDcsNDYsMjQsMjE2LDYxLDg4LDE1OSwyMjMsMTQyLDE2MiwxMDksMTMwLDIyOSwyNDQsNjIsMTEzLDUsMTIxLDU5LDE1NiwyMTEsMTUwLDEsOTMsNTUsMTI4LDI0MywyMzgsODAsMTgxLDEyMSwyMjcsNzgsNzQsMzAsODIsMTQ0LDIwNSwxNzIsODEsMjQxLDE3NSwxNCwxNjIsNzMsMTk0LDY0LDE3NSwyMzEsMTM3LDI0OSwyMzcsMjMyLDE1MiwxOTMsMTAzLDEyNiwyNTMsMTI1LDE4MSwyMzUsMjMzLDEwOSwxNzQsMTcwLDE2OSw0MiwyMzEsMTYsMjI0LDIwNiwxNywxMzMsODEsNDYsMzUsNzAsMjE1LDM3LDI0MCwxNTUsMTQzLDE2MCw2OCwxOTksNzgsODgsMTksMTE0LDEwNywxMDYsMTUzLDE0MywxNjcsNDUsMTE2LDE2Myw1Myw2MSwxMTAsMTg5LDIzMCwxNTUsMjM1LDIzMywxNSwyNDEsNzUsMTM4LDEwNyw3MCwyLDk1LDE4NSwxMzEsMTI5LDEwMSwyMzksMTk1LDY3LDE4NCwzOSwxMDMsNDcsMTYwLDEzMSwyMTUsMjQ3LDIwNywyMywxMDYsMTkwLDE0NCwwLDEwLDM3LDEyNSw5MSwxMTQsMTI0LDEyNSwxNDgsMTU0LDE1NSwxNzUsMjI1LDI1MywxMzgsMjA5LDE2OCw3NywxOTYsNDIsMTgwLDEzMywxMTEsMzgsMTUzLDQzLDE1OSwxOTUsOTMsODAsNDMsMTMzLDg3LDE5NCwyNDcsMzgsNDAsMjE1LDE3OSwxOSwyMSwxMTcsNywxNDAsMjE3LDQwLDM5LDY2LDE3LDkzLDM3LDE1Miw5MCwyNDQsMzcsMTQ4LDk3LDE3NCw1OCw2NCwxNjYsNzAsOTAsMzYsNTUsMTEzLDIxMCwxODQsMjE0LDk0LDQ1LDEyNCwyNDEsMjM5LDE0OSwzOSwzMyw4OCwxNSwxNSwyNDksNTcsMjQ0LDUyLDIzMSwxOTUsNzUsNjUsNjUsMjUyLDEyMiwzMywyNTUsMjQ0LDE4MiwyLDExOSwyMjAsNDIsNzIsNzQsMTU3LDE1OCwyMTMsMTEyLDg3LDExLDE5NywyNDJdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsidGVzdCI6eyJzdWJfcHJvb2ZfaW5kZXgiOjAsInJhdyI6InRlc3QiLCJlbmNvZGVkIjoiNzIxNTU5Mzk0ODY4NDY4NDk1MDk3NTkzNjk3MzMyNjY0ODY5ODI4MjE3OTU4MTA0NDgyNDU0MjMxNjg5NTczOTA2MDc2NDQzNjMyNzIifX0sInNlbGZfYXR0ZXN0ZWRfYXR0cnMiOnt9LCJ1bnJldmVhbGVkX2F0dHJzIjp7fSwicHJlZGljYXRlcyI6e319LCJpZGVudGlmaWVycyI6W3sic2NoZW1hX2lkIjoiZGlkOmluZHk6YmNvdnJpbjp0ZXN0OjZMSHFkVWVXRFdzTDk0elJjMVVMRXgvYW5vbmNyZWRzL3YwL1NDSEVNQS90ZXN0MC4xNTk5MjIxODcyMzA4MDAxLzEuMCIsImNyZWRfZGVmX2lkIjoiZGlkOmluZHk6YmNvdnJpbjp0ZXN0OjZMSHFkVWVXRFdzTDk0elJjMVVMRXgvYW5vbmNyZWRzL3YwL0NMQUlNX0RFRi80MDA4MzIvdGVzdCIsInJldl9yZWdfaWQiOm51bGwsInRpbWVzdGFtcCI6bnVsbH1dfQ==" + } + } + ], + "@id": "b62d95a7-076c-4e38-ba1f-9d0d8d51677d", + "~thread": { + "thid": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + "pthid": "2b2e1468-d90f-43d8-a30c-6a494b9f4420" + }, + "~please_ack": { + "on": ["RECEIPT"] + }, + "~service": { + "recipientKeys": ["4jXwJs8iWhNoQWoNhugUuFAKHo6Lodr983s6gHDtHNSX"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001" + }, + "~transport": { + "return_route": "all" + } + }, + "updatedAt": "2024-02-28T09:37:13.397Z" + }, + "tags": { + "associatedRecordId": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "messageId": "b62d95a7-076c-4e38-ba1f-9d0d8d51677d", + "messageName": "presentation", + "messageType": "https://didcomm.org/present-proof/2.0/presentation", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "present-proof", + "role": "receiver", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68" + }, + "id": "76ad893a-f582-4bd6-be47-27e88a7ebc53", + "type": "DidCommMessageRecord" + }, + "423586ec-1f01-458c-bc83-7080c7c90173": { + "value": { + "metadata": {}, + "id": "423586ec-1f01-458c-bc83-7080c7c90173", + "createdAt": "2024-02-28T09:37:01.712Z", + "associatedRecordId": "6cea02c6-8a02-480d-be63-598e4dd7287a", + "role": "sender", + "message": { + "@type": "https://didcomm.org/present-proof/2.0/presentation", + "last_presentation": true, + "formats": [ + { + "attach_id": "21f8cb0f-084c-4741-8815-026914a92ce1", + "format": "anoncreds/proof@v1.0" + } + ], + "presentations~attach": [ + { + "@id": "21f8cb0f-084c-4741-8815-026914a92ce1", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsidGVzdCI6IjcyMTU1OTM5NDg2ODQ2ODQ5NTA5NzU5MzY5NzMzMjY2NDg2OTgyODIxNzk1ODEwNDQ4MjQ1NDIzMTY4OTU3MzkwNjA3NjQ0MzYzMjcyIn0sImFfcHJpbWUiOiIyNDc2MzE0OTMxMzE1OTU5ODY1MzU1MjgzMjM1MjkxMDc4MDM0NjAzMjcxMjQzMTEwOTQ3OTM0NDU3MzAyMTUwMjY5NDIzODkyMDYzODk2OTkzNzgxMDQwMDQ2NzI1MzIyNjE5Nzg1MzU2MTgxMjU0NDAxOTIzMzk4NjYyNzMyNjI3MjU3MzA5MDgxMjA3NjY2ODI5NDc0MDY5MDA2NjY5NDk3MTI4NzI1ODA4NjExOTAwMzQ0MTIxOTUwODc0NjYzMTEwNjE0NTc2ODc2NjQ2NTM2NTcxOTkwMjM5MDg4NTIwNjAyODY1NzM5MDM5MTAxMjI3NDY3Mzk5NzE3ODc3MDc4ODA0NzM5NDYyOTkzMjg5OTc4NjM2NDQxNzg2MTE2OTM4MzkzMzIzMTMwNDQwNDgzNDI5NjY1ODczNTE2NTA4ODA2MzA5MTMzODMxMjkxMzM5NDg1NDIyMzg1MDE1MzY3Nzc1NDc3ODkwNjQzMzg0MTYyODk5MDA1NzA0MjMxNzM1MTg2ODE5OTEwNTc2MDU0NjIzODkzOTk0MDkyNTA4MjU4ODgzODQ0MjkzMTkxNzY2ODQyNzg1ODA4NzY3MDQ1NDE3MTQ3NDc3NDQwNTk4MTk2NjM5OTA0NTQxNjE2MDY2MTYyMjYxNzE5MTUyMTYzNDk5ODEzNDc3NjM2MzE2NDgwNzQxMzI3OTg3OTk2NTk5MTc2OTc1MzQzNzUzOTgyOTMyMzA2NTA4MzQ3ODMxMjM2NjA1ODc2NTkwMDI3NDI5MDY0ODg0NzU5NTc4OTY2MjY5NzcxODQxNDUzMTI1NjYzMjgxOCIsImUiOiIxMzI4MDA2MjE3OTg5NjIzMjUyNTQ2MzQwNDE2MTAxMTcwODY0NTM4NDQwNzEzNzczNDE4NjA5ODU2NDE3MjI2MTg1NTQxMzE3NTk4MTQ0NzQ2Nzg1MDQ2NDcxMjA2MTUzNzY2NjU3NjQwNzEyNDc1ODY3NjMwOTE4NTIwMzY4MTE1MDQ3NTEzNDQiLCJ2IjoiNTgyMDc2NDI4NzI3NjQwNjY3ODA5ODkxNTI4MDIyMzQ0NjYwMDU4Njg1MTkxOTUwODIyMTExMzgwMjU1MDcxMTAwNzE3NzM5MzE0MTQxMzkxMTYxOTU5NTg2NDk0ODE2Njg5OTg2NTc0NDk1NzExOTI4OTEzODcwNjEwMDE1NjE3NTYxOTI5NjQzMjY5NjM2ODEzNTk4NjgwMTAyMDQ4NDYzNjk1NjgxMDIzNDI5MTM2NDI2NDgwNDEzNzI2MzEyNzMxMTczODE2NjI3MjExNzEyMTQzNTg4OTgyMDM5NTk5Mjg0NTgzNDI1MjE3MjI1OTg0NjcxNjYxODcwNzY4NzMxNjYyODE2MjMzOTUzMDg2MzYyMDA5NDA0OTQ3NTQxOTY5OTAwNzcxODA1NzI0NTUwNTQ1MTczOTg5MDI2NjAwNzk3ODkwMzc1MDE5OTQ4MjM3OTA2NjY1MTA5MTY0NDIxNDQ1MTEwMTQ0ODYyNzEzNjY1NjA0MTkwMzY1NTQzNjM3ODY4NTc1MjI1OTA2MTMyNjk5MTc0NzcxMjMzNDkzOTUzODc5MTQwMjQwNDIxMzY5NTA3MDU3MTgwMjA5NTM1NzEyODI1NzI2NDkwODkxMDE2OTUyMDkzMzc5MTMyMjI3MzMzNjg2OTEyMDE3MTEwMDM5ODcwNTc3MTMwNTA4ODQ2MDM2Mzc0MzYxMzE1MTQ3ODgwODA4NzkyNjQ0MzU2Mjg4NzgwNjQ4OTUyNDQxNzkyMDU0NDY4ODU2NjY1ODg0MTg1MzYyNDM4Mjk1NTcxNjAyMjk5MTA5MjA4ODg5NTU2NzE3ODc5NzI4NDUzMjk5NDI0MTQwMzc1NTU1OTQwNjUzOTAxOTIzMDE5NjQyNjU4MzI0NDU0MzU0MDk5MjU4ODczODY2NjY2MDg2MTYwODA5NTExNDYwOTQzMTAzNzEwMDk5MTU3MzExMDM3OTAwODIwOTI1ODYwNTYyMzE4NDc0MDc3NTQ3NTg2MTUxODg0NzEwOTU2Mzc0MzUxNDI1MTQzMjA2MjI3ODk0MzE5NjM2MDc4MDQyODU5NTMyOTU3MTk0MzYwNDYzNTIyODg5MDM1NjUyODAyNjk5MjUyNDkzMDcwNjA0ODgwMjY0NzA4MDg1MDAwNzczODUzOTQyMzg0ODQ2MDY5NTYwMTYzMjE0MzUwODM0MTcwODU4OTU3OTUzOTc5NDg3MTk3NDY0NjY0MDU1IiwibSI6eyJtYXN0ZXJfc2VjcmV0IjoiMzg0ODM0OTE0MjE1Mjg5MjAyNTM1MTM4ODg1MzgzNjAwNzAyNjMyNDQzODE4NjkzMzA2OTYzMjM4ODMwMDE1ODgwMzU2MDQ2NjMwMjMwNTc3MDU3NTQ2MjY0MzkwNzk2NjQxODg5NjMzMTkwMzUzMTcxOTUzNjAwMDAxOTM2NDE1NzM5NDcxODExNjQyMDQ2Mzk0MjEyODk3OTE3Nzg4NDQ4MzMxNTU0NjA5MTA1ODU3NSJ9LCJtMiI6Ijc3OTk1NzIwMTQ1NDU5ODMxODM2MjI3NTA2NTM4NDQxNjUxMDI3NDA0NDM5MTcyMDQwMDc5ODc3NDUyMDI2NDI5NTgxNjMyNDYxNjg5NzIzMzkwMjU4NDc5NDY5MTg4Mjk4NzgwMTgyMDkzNTMxMDc3NzkzNjEzODUwMjAwNzc1NjIzMDcxMjk4NTI5ODExOTk3OTkyMTEzODUxODM5MjcyMjExMTQ5NDEzNTY3OTEyMzIifSwiZ2VfcHJvb2ZzIjpbXX0sIm5vbl9yZXZvY19wcm9vZiI6bnVsbH1dLCJhZ2dyZWdhdGVkX3Byb29mIjp7ImNfaGFzaCI6IjQ1NTgyNDIzNzE0NzIyNTkwODQ4NDgxOTM4NTYxNDU3MTcwNjI4NzAzODk4OTI1NTU2OTE0Mjc3NDU5MTU4OTM3Nzc0NDA3NDgxNzIyIiwiY19saXN0IjpbWzE5Niw0MSwxMTYsMzIsNDIsMTgsMjUsMTA3LDE1MCwyMzcsMTMsNjksMTIyLDEyOCwyMDcsNDYsMjQsMjE2LDYxLDg4LDE1OSwyMjMsMTQyLDE2MiwxMDksMTMwLDIyOSwyNDQsNjIsMTEzLDUsMTIxLDU5LDE1NiwyMTEsMTUwLDEsOTMsNTUsMTI4LDI0MywyMzgsODAsMTgxLDEyMSwyMjcsNzgsNzQsMzAsODIsMTQ0LDIwNSwxNzIsODEsMjQxLDE3NSwxNCwxNjIsNzMsMTk0LDY0LDE3NSwyMzEsMTM3LDI0OSwyMzcsMjMyLDE1MiwxOTMsMTAzLDEyNiwyNTMsMTI1LDE4MSwyMzUsMjMzLDEwOSwxNzQsMTcwLDE2OSw0MiwyMzEsMTYsMjI0LDIwNiwxNywxMzMsODEsNDYsMzUsNzAsMjE1LDM3LDI0MCwxNTUsMTQzLDE2MCw2OCwxOTksNzgsODgsMTksMTE0LDEwNywxMDYsMTUzLDE0MywxNjcsNDUsMTE2LDE2Myw1Myw2MSwxMTAsMTg5LDIzMCwxNTUsMjM1LDIzMywxNSwyNDEsNzUsMTM4LDEwNyw3MCwyLDk1LDE4NSwxMzEsMTI5LDEwMSwyMzksMTk1LDY3LDE4NCwzOSwxMDMsNDcsMTYwLDEzMSwyMTUsMjQ3LDIwNywyMywxMDYsMTkwLDE0NCwwLDEwLDM3LDEyNSw5MSwxMTQsMTI0LDEyNSwxNDgsMTU0LDE1NSwxNzUsMjI1LDI1MywxMzgsMjA5LDE2OCw3NywxOTYsNDIsMTgwLDEzMywxMTEsMzgsMTUzLDQzLDE1OSwxOTUsOTMsODAsNDMsMTMzLDg3LDE5NCwyNDcsMzgsNDAsMjE1LDE3OSwxOSwyMSwxMTcsNywxNDAsMjE3LDQwLDM5LDY2LDE3LDkzLDM3LDE1Miw5MCwyNDQsMzcsMTQ4LDk3LDE3NCw1OCw2NCwxNjYsNzAsOTAsMzYsNTUsMTEzLDIxMCwxODQsMjE0LDk0LDQ1LDEyNCwyNDEsMjM5LDE0OSwzOSwzMyw4OCwxNSwxNSwyNDksNTcsMjQ0LDUyLDIzMSwxOTUsNzUsNjUsNjUsMjUyLDEyMiwzMywyNTUsMjQ0LDE4MiwyLDExOSwyMjAsNDIsNzIsNzQsMTU3LDE1OCwyMTMsMTEyLDg3LDExLDE5NywyNDJdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsidGVzdCI6eyJzdWJfcHJvb2ZfaW5kZXgiOjAsInJhdyI6InRlc3QiLCJlbmNvZGVkIjoiNzIxNTU5Mzk0ODY4NDY4NDk1MDk3NTkzNjk3MzMyNjY0ODY5ODI4MjE3OTU4MTA0NDgyNDU0MjMxNjg5NTczOTA2MDc2NDQzNjMyNzIifX0sInNlbGZfYXR0ZXN0ZWRfYXR0cnMiOnt9LCJ1bnJldmVhbGVkX2F0dHJzIjp7fSwicHJlZGljYXRlcyI6e319LCJpZGVudGlmaWVycyI6W3sic2NoZW1hX2lkIjoiZGlkOmluZHk6YmNvdnJpbjp0ZXN0OjZMSHFkVWVXRFdzTDk0elJjMVVMRXgvYW5vbmNyZWRzL3YwL1NDSEVNQS90ZXN0MC4xNTk5MjIxODcyMzA4MDAxLzEuMCIsImNyZWRfZGVmX2lkIjoiZGlkOmluZHk6YmNvdnJpbjp0ZXN0OjZMSHFkVWVXRFdzTDk0elJjMVVMRXgvYW5vbmNyZWRzL3YwL0NMQUlNX0RFRi80MDA4MzIvdGVzdCIsInJldl9yZWdfaWQiOm51bGwsInRpbWVzdGFtcCI6bnVsbH1dfQ==" + } + } + ], + "@id": "b62d95a7-076c-4e38-ba1f-9d0d8d51677d", + "~thread": { + "thid": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + "pthid": "2b2e1468-d90f-43d8-a30c-6a494b9f4420" + }, + "~please_ack": { + "on": ["RECEIPT"] + }, + "~service": { + "recipientKeys": ["4jXwJs8iWhNoQWoNhugUuFAKHo6Lodr983s6gHDtHNSX"], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001" + } + }, + "updatedAt": "2024-02-28T09:37:03.716Z" + }, + "tags": { + "associatedRecordId": "6cea02c6-8a02-480d-be63-598e4dd7287a", + "messageId": "b62d95a7-076c-4e38-ba1f-9d0d8d51677d", + "messageName": "presentation", + "messageType": "https://didcomm.org/present-proof/2.0/presentation", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "present-proof", + "role": "sender", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68" + }, + "id": "423586ec-1f01-458c-bc83-7080c7c90173", + "type": "DidCommMessageRecord" + }, + "9bf15c4a-8876-4878-85f6-a33eca8e8842": { + "value": { + "metadata": {}, + "id": "9bf15c4a-8876-4878-85f6-a33eca8e8842", + "createdAt": "2024-02-28T09:36:54.124Z", + "associatedRecordId": "6cea02c6-8a02-480d-be63-598e4dd7287a", + "role": "receiver", + "message": { + "@type": "https://didcomm.org/present-proof/2.0/request-presentation", + "will_confirm": true, + "present_multiple": false, + "formats": [ + { + "attach_id": "f8e75054-35ab-4f7a-83f8-0f6638a865b0", + "format": "anoncreds/proof-request@v1.0" + } + ], + "request_presentations~attach": [ + { + "@id": "f8e75054-35ab-4f7a-83f8-0f6638a865b0", + "mime-type": "application/json", + "data": { + "base64": "eyJuYW1lIjoidGVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjExMTUyNTQ2NTQwNjU3ODkyNDY4OTA3MDUiLCJyZXF1ZXN0ZWRfYXR0cmlidXRlcyI6eyJ0ZXN0Ijp7Im5hbWUiOiJ0ZXN0IiwicmVzdHJpY3Rpb25zIjpbeyJjcmVkX2RlZl9pZCI6ImRpZDppbmR5OmJjb3ZyaW46dGVzdDo2TEhxZFVlV0RXc0w5NHpSYzFVTEV4L2Fub25jcmVkcy92MC9DTEFJTV9ERUYvNDAwODMyL3Rlc3QifV19fSwicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOnt9fQ==" + } + } + ], + "@id": "9e1568e0-7c3e-4112-b76e-869ea9d12f06", + "~thread": { + "thid": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + "pthid": "2b2e1468-d90f-43d8-a30c-6a494b9f4420" + } + }, + "updatedAt": "2024-02-28T09:36:54.124Z" + }, + "tags": { + "associatedRecordId": "6cea02c6-8a02-480d-be63-598e4dd7287a", + "messageId": "9e1568e0-7c3e-4112-b76e-869ea9d12f06", + "messageName": "request-presentation", + "messageType": "https://didcomm.org/present-proof/2.0/request-presentation", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "present-proof", + "role": "receiver", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68" + }, + "id": "9bf15c4a-8876-4878-85f6-a33eca8e8842", + "type": "DidCommMessageRecord" + } +} diff --git a/packages/core/src/storage/migration/__tests__/__fixtures__/alice-2-sov-dids-one-cache-record-0.3.json b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-2-sov-dids-one-cache-record-0.3.json new file mode 100644 index 0000000000..1edebe9e11 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-2-sov-dids-one-cache-record-0.3.json @@ -0,0 +1,87 @@ +{ + "STORAGE_VERSION_RECORD_ID": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "STORAGE_VERSION_RECORD_ID", + "createdAt": "2023-03-18T18:35:02.888Z", + "storageVersion": "0.3.1", + "updatedAt": "2023-03-18T18:35:02.888Z" + }, + "id": "STORAGE_VERSION_RECORD_ID", + "type": "StorageVersionRecord", + "tags": {} + }, + "DID_POOL_CACHE": { + "value": { + "metadata": {}, + "id": "DID_POOL_CACHE", + "createdAt": "2023-03-18T18:53:44.165Z", + "entries": [ + { + "key": "A4CYPASJYRZRt98YWrac3H", + "value": { + "nymResponse": { + "did": "A4CYPASJYRZRt98YWrac3H", + "verkey": "5wFaN9wUdLipt6rFjhep9pp2aanJKqe5MywkEAHqhRNS", + "role": "101" + }, + "poolId": "bcovrin:test2" + } + } + ], + "updatedAt": "2023-03-18T18:53:44.166Z" + }, + "id": "DID_POOL_CACHE", + "type": "CacheRecord", + "tags": {} + }, + "8168612b-73d1-4917-9a61-84e8102988f0": { + "value": { + "_tags": { + "recipientKeyFingerprints": [], + "qualifiedIndyDid": "did:indy:bcovrin:test:8DFqUo6UtQLLZETE7Gm29k" + }, + "metadata": {}, + "id": "8168612b-73d1-4917-9a61-84e8102988f0", + "did": "did:sov:8DFqUo6UtQLLZETE7Gm29k", + "role": "created", + "createdAt": "2023-03-18T18:35:04.191Z", + "updatedAt": "2023-03-18T18:35:04.191Z" + }, + "id": "8168612b-73d1-4917-9a61-84e8102988f0", + "type": "DidRecord", + "tags": { + "recipientKeyFingerprints": [], + "qualifiedIndyDid": "did:indy:bcovrin:test:8DFqUo6UtQLLZETE7Gm29k", + "role": "created", + "method": "sov", + "did": "did:sov:8DFqUo6UtQLLZETE7Gm29k", + "methodSpecificIdentifier": "8DFqUo6UtQLLZETE7Gm29k" + } + }, + "4993c740-5cd9-4c79-a7d8-23d1266d31be": { + "value": { + "_tags": { + "recipientKeyFingerprints": [], + "qualifiedIndyDid": "did:indy:bcovrin:test:Pow4pdnPgTS7JAXvWkoF2c" + }, + "metadata": {}, + "id": "4993c740-5cd9-4c79-a7d8-23d1266d31be", + "did": "did:sov:Pow4pdnPgTS7JAXvWkoF2c", + "role": "created", + "createdAt": "2023-03-18T18:35:07.208Z", + "updatedAt": "2023-03-18T18:35:07.208Z" + }, + "id": "4993c740-5cd9-4c79-a7d8-23d1266d31be", + "type": "DidRecord", + "tags": { + "recipientKeyFingerprints": [], + "qualifiedIndyDid": "did:indy:bcovrin:test:Pow4pdnPgTS7JAXvWkoF2c", + "role": "created", + "method": "sov", + "did": "did:sov:Pow4pdnPgTS7JAXvWkoF2c", + "methodSpecificIdentifier": "Pow4pdnPgTS7JAXvWkoF2c" + } + } +} diff --git a/packages/core/src/storage/migration/__tests__/__fixtures__/alice-2-w3c-credential-records-0.3.json b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-2-w3c-credential-records-0.3.json new file mode 100644 index 0000000000..b24d9f6ff9 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-2-w3c-credential-records-0.3.json @@ -0,0 +1,70 @@ +{ + "STORAGE_VERSION_RECORD_ID": { + "value": { + "metadata": {}, + "id": "STORAGE_VERSION_RECORD_ID", + "createdAt": "2023-03-18T18:35:02.888Z", + "storageVersion": "0.3.1", + "updatedAt": "2023-03-18T18:35:02.888Z" + }, + "id": "STORAGE_VERSION_RECORD_ID", + "type": "StorageVersionRecord", + "tags": {} + }, + "da65187b-f461-4f39-8597-b0d95531d40d": { + "value": { + "metadata": {}, + "id": "da65187b-f461-4f39-8597-b0d95531d40d", + "createdAt": "2024-02-05T06:44:47.600Z", + "credential": "eyJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtva3JzVm84RGJHRHNuTUFqbm9IaEpvdE1iRFppSGZ2eE00ajY1ZDhwclhVciIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImlzc3VlciI6eyJpZCI6ImRpZDprZXk6ejZNa29rcnNWbzhEYkdEc25NQWpub0hoSm90TWJEWmlIZnZ4TTRqNjVkOHByWFVyIn0sImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMTk6MjM6MjRaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEifX0sImp0aSI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8zNzMyIiwibmJmIjoxMjYyMzczODA0fQ.suzrfmzM07yiiibK0vOdP9Q0dARA7XVNRUa9DSbH519EWrUDgzsq6SiIG9yyBt39yaqsZc1-8byyuMrPziyWBg", + "updatedAt": "2024-02-05T06:44:47.600Z" + }, + "type": "W3cCredentialRecord", + "id": "da65187b-f461-4f39-8597-b0d95531d40d", + "tags": { + "algs": ["EdDSA"], + "contexts": ["https://www.w3.org/2018/credentials/v1"], + "givenId": "http://example.edu/credentials/3732", + "issuerId": "did:key:z6MkokrsVo8DbGDsnMAjnoHhJotMbDZiHfvxM4j65d8prXUr", + "subjectIds": ["did:example:ebfeb1f712ebc6f1c276e12ec21"], + "schemaIds": [] + } + }, + "0e1f070a-e31f-46cf-88db-25c1621b2f4e": { + "value": { + "metadata": {}, + "id": "0e1f070a-e31f-46cf-88db-25c1621b2f4e", + "createdAt": "2024-02-05T06:44:47.543Z", + "credential": { + "@context": ["https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"], + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "issuer": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "issuanceDate": "2017-10-22T12:23:48Z", + "credentialSubject": { + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts" + } + }, + "proof": { + "verificationMethod": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "type": "Ed25519Signature2018", + "created": "2022-04-18T23:13:10Z", + "proofPurpose": "assertionMethod", + "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..ECQsj_lABelr1jkehSkqaYpc5CBvbSjbi3ZvgiVVKxZFDYfj5xZmeXb_awa4aw_cGEVaoypeN2uCFmeG6WKkBw" + } + }, + "updatedAt": "2024-02-05T06:44:47.543Z" + }, + "type": "W3cCredentialRecord", + "id": "0e1f070a-e31f-46cf-88db-25c1621b2f4e", + "tags": { + "contexts": ["https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"], + "expandedTypes": ["https", "https"], + "issuerId": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "proofTypes": ["Ed25519Signature2018"], + "subjectIds": [], + "schemaIds": [] + } + } +} diff --git a/packages/core/src/storage/migration/__tests__/__fixtures__/alice-2-w3c-credential-records-0.4.json b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-2-w3c-credential-records-0.4.json new file mode 100644 index 0000000000..45e9d6f00b --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-2-w3c-credential-records-0.4.json @@ -0,0 +1,72 @@ +{ + "STORAGE_VERSION_RECORD_ID": { + "value": { + "metadata": {}, + "id": "STORAGE_VERSION_RECORD_ID", + "createdAt": "2023-03-18T18:35:02.888Z", + "storageVersion": "0.4", + "updatedAt": "2023-03-18T18:35:02.888Z" + }, + "id": "STORAGE_VERSION_RECORD_ID", + "type": "StorageVersionRecord", + "tags": {} + }, + "da65187b-f461-4f39-8597-b0d95531d40d": { + "value": { + "metadata": {}, + "id": "da65187b-f461-4f39-8597-b0d95531d40d", + "createdAt": "2024-02-05T06:44:47.600Z", + "credential": "eyJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtva3JzVm84RGJHRHNuTUFqbm9IaEpvdE1iRFppSGZ2eE00ajY1ZDhwclhVciIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImlzc3VlciI6eyJpZCI6ImRpZDprZXk6ejZNa29rcnNWbzhEYkdEc25NQWpub0hoSm90TWJEWmlIZnZ4TTRqNjVkOHByWFVyIn0sImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMTk6MjM6MjRaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEifX0sImp0aSI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8zNzMyIiwibmJmIjoxMjYyMzczODA0fQ.suzrfmzM07yiiibK0vOdP9Q0dARA7XVNRUa9DSbH519EWrUDgzsq6SiIG9yyBt39yaqsZc1-8byyuMrPziyWBg", + "updatedAt": "2024-02-05T06:44:47.600Z" + }, + "type": "W3cCredentialRecord", + "id": "da65187b-f461-4f39-8597-b0d95531d40d", + "tags": { + "algs": ["EdDSA"], + "claimFormat": "jwt_vc", + "contexts": ["https://www.w3.org/2018/credentials/v1"], + "givenId": "http://example.edu/credentials/3732", + "issuerId": "did:key:z6MkokrsVo8DbGDsnMAjnoHhJotMbDZiHfvxM4j65d8prXUr", + "subjectIds": ["did:example:ebfeb1f712ebc6f1c276e12ec21"], + "schemaIds": [] + } + }, + "0e1f070a-e31f-46cf-88db-25c1621b2f4e": { + "value": { + "metadata": {}, + "id": "0e1f070a-e31f-46cf-88db-25c1621b2f4e", + "createdAt": "2024-02-05T06:44:47.543Z", + "credential": { + "@context": ["https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"], + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "issuer": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "issuanceDate": "2017-10-22T12:23:48Z", + "credentialSubject": { + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts" + } + }, + "proof": { + "verificationMethod": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "type": "Ed25519Signature2018", + "created": "2022-04-18T23:13:10Z", + "proofPurpose": "assertionMethod", + "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..ECQsj_lABelr1jkehSkqaYpc5CBvbSjbi3ZvgiVVKxZFDYfj5xZmeXb_awa4aw_cGEVaoypeN2uCFmeG6WKkBw" + } + }, + "updatedAt": "2024-02-05T06:44:47.543Z" + }, + "type": "W3cCredentialRecord", + "id": "0e1f070a-e31f-46cf-88db-25c1621b2f4e", + "tags": { + "claimFormat": "ldp_vc", + "contexts": ["https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"], + "expandedTypes": ["https", "https"], + "issuerId": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "proofTypes": ["Ed25519Signature2018"], + "subjectIds": [], + "schemaIds": [] + } + } +} diff --git a/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-credentials-0.1.json b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-credentials-0.1.json new file mode 100644 index 0000000000..aff0d48799 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-credentials-0.1.json @@ -0,0 +1,442 @@ +{ + "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a": { + "value": { + "metadata": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0" + }, + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "createdAt": "2022-03-21T22:50:20.522Z", + "state": "done", + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "offerMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ] + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9" + } + } + ] + }, + "requestMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==" + } + } + ], + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae" + } + }, + "credentialMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9" + } + } + ], + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae" + }, + "~please_ack": {} + }, + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ], + "autoAcceptCredential": "contentApproved" + }, + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "type": "CredentialRecord", + "tags": { + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "state": "done" + } + }, + "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7": { + "value": { + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0" + } + }, + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "createdAt": "2022-03-21T22:50:20.740Z", + "state": "done", + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "offerMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ] + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=" + } + } + ] + }, + "requestMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==" + } + } + ], + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb" + } + }, + "credentialMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=" + } + } + ], + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb" + }, + "~please_ack": {} + }, + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ], + "autoAcceptCredential": "contentApproved" + }, + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "type": "CredentialRecord", + "tags": { + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "state": "done" + } + }, + "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a": { + "value": { + "metadata": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + "requestMetadata": { + "master_secret_blinding_data": { + "v_prime": "36456944381549782028917743247126995038265466209293312755125557271456380841610111892515020379470931691048072348420844231863825225515560265358581756565441268878364665494094789024845049226122885121039335781567964878826549149370097276812152226343824116049855825405977949749345353074025294938300401262824951638782220004732873597724698990420932910079362747837952520524827009393981876443737452031919055976088763615615890946142630576421462920865811255312740184209214306243871230276622595183415487741608569800898909023830922654063814555128779494528740438076748829436757078504882332589744263200806138145494157659396691564807976032319024007464003538934", + "vr_prime": null + }, + "nonce": "373984270150786864433163", + "master_secret_name": "Wallet: PopulateWallet2" + } + }, + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "createdAt": "2022-03-21T22:50:20.535Z", + "state": "done", + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "offerMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ] + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9" + } + } + ] + }, + "requestMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==" + } + } + ], + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae" + } + }, + "credentialMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9" + } + } + ], + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae" + }, + "~please_ack": {} + }, + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ], + "autoAcceptCredential": "contentApproved" + }, + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "type": "CredentialRecord", + "tags": { + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "state": "done", + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278" + } + }, + "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c": { + "value": { + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0" + }, + "_internal/indyRequest": { + "master_secret_blinding_data": { + "v_prime": "24405223168730122709164916892481085040205443709643249329100687534344659826655374235392514476392517756663433844139774514430993889493707631169979521764390851593418941181409704266182779162417466204970949168472702858363964258641437554267668466400711344128132909691514606077477555576087059339291048485225394874964325220472232903203038212033940680060605090839733163438385288769519855418153181511119637865605476043416048121313638627002888436809192752657860306784733123742838413845299796745569824223645588826964796075250758249133953560017373025169692866449286962430731916293683231375510684692358406054381559324718715654332979447698704161714028193478", + "vr_prime": null + }, + "nonce": "698370616023883730498375", + "master_secret_name": "Wallet: PopulateWallet2" + } + }, + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "createdAt": "2022-03-21T22:50:20.746Z", + "state": "done", + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "offerMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ] + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=" + } + } + ] + }, + "requestMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==" + } + } + ], + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb" + } + }, + "credentialMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=" + } + } + ], + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb" + }, + "~please_ack": {} + }, + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ], + "autoAcceptCredential": "contentApproved" + }, + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "type": "CredentialRecord", + "tags": { + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "state": "done", + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9" + } + } +} diff --git a/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-mediators-0.1.json b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-mediators-0.1.json new file mode 100644 index 0000000000..edc6a8728a --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-mediators-0.1.json @@ -0,0 +1,92 @@ +{ + "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": { + "value": { + "metadata": {}, + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "createdAt": "2022-03-21T22:50:17.132Z", + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "recipientKeys": [], + "routingKeys": [], + "state": "granted", + "role": "MEDIATOR" + }, + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "type": "MediationRecord", + "tags": { + "state": "granted", + "role": "MEDIATOR", + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "recipientKeys": [] + } + }, + "802ef124-36b7-490f-b152-e9d090ddf073": { + "value": { + "metadata": {}, + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "createdAt": "2022-03-21T22:50:17.161Z", + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "recipientKeys": [], + "routingKeys": [], + "state": "granted", + "role": "MEDIATOR" + }, + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "type": "MediationRecord", + "tags": { + "state": "granted", + "role": "MEDIATOR", + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "recipientKeys": [] + } + }, + "7f14c1ec-514c-49b2-a00b-04af7e600060": { + "value": { + "metadata": {}, + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "createdAt": "2022-03-21T22:50:17.126Z", + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "recipientKeys": [], + "routingKeys": ["D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu"], + "state": "granted", + "role": "MEDIATOR", + "endpoint": "rxjs:alice" + }, + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "type": "MediationRecord", + "tags": { + "state": "granted", + "role": "MEDIATOR", + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "recipientKeys": [] + } + }, + "0b47db94-c0fa-4476-87cf-a5f664440412": { + "value": { + "metadata": {}, + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "createdAt": "2022-03-21T22:50:17.157Z", + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "recipientKeys": [], + "routingKeys": ["D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu"], + "state": "granted", + "role": "MEDIATOR", + "endpoint": "rxjs:alice" + }, + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "type": "MediationRecord", + "tags": { + "state": "granted", + "role": "MEDIATOR", + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "recipientKeys": [] + } + } +} diff --git a/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-proofs-0.2.json b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-proofs-0.2.json new file mode 100644 index 0000000000..c5f554ff3e --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-proofs-0.2.json @@ -0,0 +1,235 @@ +{ + "STORAGE_VERSION_RECORD_ID": { + "value": { + "metadata": {}, + "id": "STORAGE_VERSION_RECORD_ID", + "createdAt": "2022-09-08T19:35:53.872Z", + "storageVersion": "0.2" + }, + "id": "STORAGE_VERSION_RECORD_ID", + "type": "StorageVersionRecord", + "tags": {} + }, + "72c96cd1-1f26-4bf3-8a00-5c00926859a8": { + "value": { + "metadata": {}, + "id": "72c96cd1-1f26-4bf3-8a00-5c00926859a8", + "createdAt": "2022-09-08T19:36:06.208Z", + "proposalMessage": { + "@type": "https://didcomm.org/present-proof/1.0/propose-presentation", + "@id": "00cdd404-82fc-431d-9c18-fb643636d2a5", + "presentation_proposal": { + "@type": "https://didcomm.org/present-proof/1.0/presentation-preview", + "attributes": [ + { + "name": "name", + "cred_def_id": "7yW6SoTjHNhD3zYgm4PbK8:3:CL:472319:TAG", + "value": "Alice" + } + ], + "predicates": [] + } + }, + "isVerified": true, + "requestMessage": { + "@type": "https://didcomm.org/present-proof/1.0/request-presentation", + "@id": "2862b25d-396c-46d7-ad09-f48ff4ea6db6", + "request_presentations~attach": [ + { + "@id": "libindy-request-presentation-0", + "mime-type": "application/json", + "data": { + "base64": "eyJuYW1lIjoicHJvb2YtcmVxdWVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjQwMzU1MDc0MDYxMTU0MzEwMzA5NzMyMiIsInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjp7IjBmZWExODk3LTQzMTEtNDhmMi1hMmMyLTM4NzRlOTkzYWVmMCI6eyJuYW1lIjoibmFtZSIsInJlc3RyaWN0aW9ucyI6W3siY3JlZF9kZWZfaWQiOiI3eVc2U29UakhOaEQzellnbTRQYks4OjM6Q0w6NDcyMzE5OlRBRyJ9XX19LCJyZXF1ZXN0ZWRfcHJlZGljYXRlcyI6e319" + } + } + ], + "~thread": { + "thid": "00cdd404-82fc-431d-9c18-fb643636d2a5" + } + }, + "presentationMessage": { + "@type": "https://didcomm.org/present-proof/1.0/presentation", + "@id": "4185f336-f307-4022-a27d-78d1271586f6", + "presentations~attach": [ + { + "@id": "libindy-presentation-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFfcHJpbWUiOiI1MjE1MTkyMzYwMDA2NTg5OTUyMjM1MTYwNjkxMzU1OTc4MjA2NDUzODA3NDUzNTk5OTE5OTkzNzM1NTczNzA1OTA2OTY5MTAwMjIzOTM4MjMyMzE5NDE1Njk1NTMzNjQ4MDMzNzc3NzI4NTE3NzI4Mjk4NDU1OTI0NjQ1NTU1NjQyNjE1NTQ3NTI0MDc0MTAyOTQyNjg4NzQwNDcwMjg4MDAyNTgwMjI4OTg4MDk4MDk0NjQxODc5NjkxNzQwMTY2OTc0MzA2OTczNjQ2MDA3ODg3NTU1NDU2NjM3NTUyODk2NjkxNDc3NDg2MDM1MTIxMjU0NDY0NzA2MDY0Mzg2NzA1MzU0MDk3MDcxNDc5OTkwMzIwNzIxMzQ4MjM1NTQ2NjI5OTcxNzAyNzgwNjUwNTgyMDQ3MzMxOTIwNjE5OTIzODg4NjU1NjI0NjYzMDE4NTAzMDUzOTQ4OTEyNzk4MDE1ODA2Mzk5MTAxNTg3MzAzNTgwNzkyMzQ3OTU2NjE1MDgzODczNDQ5MTM0MjEyOTE2MDY3MTExMjM4MjQzMjM1MTk5OTk1MTUzNjIyMTM1NjA4Nzg5OTc2MTk2NTkzMTczNTU4NTA0NDYxODA0MTk5MzY3MTkwODY2MDY5MTk1MDE5MjA4NjQyMzAwNzE5NjAzMDk5MzI1MjA0Njg4MzkwMjc2NTM5NjQxMDk0Mjc4NTY2NTUzOTU1Mzg4NDAwMDUyNzEyNzk0MjUwODgxODg3NDIzMDM5NTgyMjM5MjQ4NTk0NjYyMDYzMDA0ODI2MzE5NDQxNTUwNzEyNDA3MTQ4NDc4Njc1MCIsImUiOiIxNTk2OTcwODg4Njk3NTIwMjIzMzA3ODgyOTM5MzI5OTY0NTM3NjA4Mzc3NjQ0MTc2MjIyODczMjc2MTc0Mjg5ODkzNjgyNDAzODg0NzkzOTk1NDIwMjQ2MTEzNDk5NjMxNDAyMTQxMzQ0OTYxOTg3NDAyNjAxNDc4NTQ3NjEwNjI5MTgzNzc2NDQiLCJ2IjoiOTE0NzEwNTI1NDAwMDA2MDgzNTMwMTMzOTIxMjQyNTQwMzI5ODAzNjI4MjgyMDA0MDAzMTY1ODIyMjMyOTM0NDYxODAyMjQ4MDk4OTU5MzY1MTY2ODA3MjEwODgzNzIyNjIyMzA1ODQyNjM4NzM4NjIzNzM5NzYwNjEzODc5NDcwOTU5NDkyNjg1ODY5MTY4NTQyMDU1MjAyNzcxMDYxODI4MDEwMTAzNDY3Nzk2MDA5NDIzMTc4NzU2NDQ5OTk3NDQzODk5NDkyMDEyMTE0OTYyMDk5MDgwMDg1MzAxMjUyMDU1NDk0MDUzMTM4MzU4NDE0MjM0MDg5MzQyNDY2Nzg0MTA4ODkwMjA2NjY0NDI1NDE1ODYzNDA5NTI1MjI3NDY0NTg1OTU0MzQ3OTk2OTIwNTU0NTg5Nzc3OTA1MzMzMDMxMzAzMzEzNTI1OTY3NDg3NzAyMDQxODEwNzA3ODQ1OTAyMTAyODAxMTM0NDk3ODAxNjQyNTgwNzQyNjg0Njk5OTk3ODM2NTY0NzM2NDE5NDc0NzM1Mzg2ODkzMTQwMDA4ODIzNjIzMjA1MDA5MzE3NjgzNTIyNjI5NzkwMDY1NDExNDE5MzY3MTc3MTgwMTQ3MDk3ODkwNTkxNDM4NTkyMjQyNzA1MTg5NDM4NDE2MjA4MTA0MjczNjI3MjUyNzc2NzY1NjI0NjExMjk3NjQzODA5NzIwMjcxMDUzOTY1MzM4MTA5Njk1MTM4NjA3NTA0NzExMDc0NDY0ODU5Mzc5MzUxNTI1ODY4NzgyMjk2MzIzNDIwMzIwMTAwMzQ4MDA5MDMyMzIxOTM2ODMzNDk0Nzc2NDUxMjQ0NzMzNTQxMzI3Mzc4MTc3OTYwMTMzOTg2NzE4NTA2NTk0MzE2MjMyMzEwMTk5NjE2NjQ4NzI3MjU0MTUwNjg2MjA4OTQ1MTM3Mzg5MTI5Mjg4NzUyNzE3MjA5NjI3ODcyNjE3NTM3MjEwMDk2NTU2MTkzMzgxMzUwNTgzMDM1NjI5OTg3MjE5MTk3OTQ4OTEyNDI3NzI0MTA1ODE2NzA0NDkyOTMzNzQyMjcxOTQ5NTk1ODAwOTUyMDA5NDczNzQ2OTM5ODU0Mzg0NDI4MzY0MjE0NjUyNjI4MDgyOTQwOTMzODcwNTE1MjE4NTc1MzY4NDgwMjM4OTM2MTc0NDM5NzExNTUzNDI3NDEwNzA2NTgxMTkzMzg2NDM2ODEyIiwibSI6eyJtYXN0ZXJfc2VjcmV0IjoiNDU1NjIzMDY3NjQ4NDQ0NTEzODAyNzM4ODkxODAyMTkxNDg1MzY0NzMzMDkxMTIwMjM5MDg1ODY0NTQxMzU5Njg0MDgyMDczNTgzNDMzMjM4MzU2Njg2MTgwNzI1MzIyMjU2MzgwMjYxNzYyMTc5MTg3MDU3Mjk4Nzg3NzEzNDcyODY5MjE5NDI1ODgyNTEyNzI0ODk4NjkzMDk3OTkwODI4MDIzMjUwNTkyMTAwNDI5OSJ9LCJtMiI6IjEyMjUwNjAyNjEwOTE0Njk5NDEzNDk3MjEyOTYwNTA3OTQ1MDkwOTk1OTMwNjYyMDMyOTA2NTIxNDUxNjI0MDUxMTk2MTg3MjIxNjgxNzEzMDc1OTg0MDY5MjY5MTY3MDQ5ODExMDc3NDk1NTM2MDg4OTE0NTc3MDk2NTgwNjMwODIzNDQyMTk4Nzk5Nzg4NzIyNDIzMzg4MDIyNjE5MzM4MzA2MjM3NTQ2MzI5OTI4MDEifSwiZ2VfcHJvb2ZzIjpbXX0sIm5vbl9yZXZvY19wcm9vZiI6bnVsbH1dLCJhZ2dyZWdhdGVkX3Byb29mIjp7ImNfaGFzaCI6IjU4Njg3MDc5MDA2NTc0OTY1OTA5ODk2MDc5OTU0NTA2NDM2ODg0NzMxOTIxMjk0NDA5NDYxNzI5NjkzMTI4MDM1OTcxMjAwMjY5NzgzIiwiY19saXN0IjpbWzEsMTU3LDMxLDExMiwxNjQsMjA1LDIyNCw3NiwzMiwyMTIsMTczLDc3LDIxNyw2MiwxNzMsMTc3LDQsMTU5LDE0OSwyMzMsMTE3LDEwNCw5MiwxMzgsMzUsMjYsNDgsODUsMjQwLDIzNCw0MywxOTYsNTksMTE0LDI0MiwxNTMsNDAsMTg3LDIzOSwxMDAsMTcwLDIsMzYsMTA3LDIyOSwzNywxMTEsNjAsOTIsOSwxMTcsMjM4LDE0MCwyNDMsMywyNDIsMjM0LDE1MiwxNjYsNiwxNjEsMTM0LDIyMiwxNjMsNDMsNjIsMTIsODEsMjUzLDE3NiwxMjUsMjQsOCw5MiwxMywyMjIsMTcxLDIzMywxODIsMjE1LDIyLDIzNCw5NSwxMTIsMTIyLDEwMiwxNTIsMTA2LDE4Miw0LDIxOSw1NiwxMDEsMTQ3LDIyNywyNDYsMTE1LDY0LDE4Myw0LDY3LDEyNCwxNDAsMTY2LDEzOCwxNCwyNCw1MSwxODUsMTk5LDEwOSwyLDUxLDE5MCwxODEsMjQ3LDYxLDE5MCw4NSwyNDcsMjE3LDIyLDE2NCwxOTIsMjM1LDE4NywxNDgsMjYsMTA5LDI1MywxMTYsMjQxLDY1LDEzNiw0MSwyMjUsMjI5LDE3OCwxMjUsNzksMjQ2LDUzLDQ0LDE5MiwxNDEsMjQzLDE4NCwxNTEsMTIsOSwxMDQsMjE5LDE2Miw3MSw3Miw3LDg5LDgzLDgsMjI1LDEzLDQ4LDEzMywyNDcsMTg3LDExNCw4NSw3Nyw1OSw5OSw3MCw2OSw3MSwxMzMsMTY5LDExMSwxMTAsNzksNDQsMTc0LDIzLDEwMywxNjYsNjksNSwxNDcsNjYsNzcsMTYzLDExMiw0MiwxOTksMTQyLDE0LDIyLDE1MSwyNDgsMzMsMzQsMTk4LDE1NiwyMCwyMzEsMjA0LDcwLDgyLDI1MywxMzgsMTMxLDQyLDIwNSwzMywxMDksMjMyLDE3LDEwNiw2OSw3Miw2MCwyMTAsMjMyLDExMywxMjQsNzYsMTgxLDIwOCwwLDE4NiwyNTEsMTQzLDExMyw0NSwxNzEsMTAsNDMsMTUsMzAsMjA4LDkzLDYsMTY4LDEwNiwyMDMsNDgsMjUwLDIxOSw5LDE3LDEwNCwzNSw1OCwxNTksMTgwLDExMyw2NiwxMzYsNjJdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsiMGZlYTE4OTctNDMxMS00OGYyLWEyYzItMzg3NGU5OTNhZWYwIjp7InN1Yl9wcm9vZl9pbmRleCI6MCwicmF3IjoiQWxpY2UiLCJlbmNvZGVkIjoiMjcwMzQ2NDAwMjQxMTczMzEwMzMwNjMxMjgwNDQwMDQzMTgyMTg0ODY4MTY5MzE1MjA4ODY0MDU1MzU2NTk5MzQ0MTc0Mzg3ODE1MDcifX0sInNlbGZfYXR0ZXN0ZWRfYXR0cnMiOnt9LCJ1bnJldmVhbGVkX2F0dHJzIjp7fSwicHJlZGljYXRlcyI6e319LCJpZGVudGlmaWVycyI6W3sic2NoZW1hX2lkIjoiN3lXNlNvVGpITmhEM3pZZ200UGJLODoyOnRlc3Qtc2NoZW1hLTRlOTRjMmU0LTRmNDctNGZmMy1hODg4LTZmNjRkYTZhOTJkYzoxLjAiLCJjcmVkX2RlZl9pZCI6Ijd5VzZTb1RqSE5oRDN6WWdtNFBiSzg6MzpDTDo0NzIzMTk6VEFHIiwicmV2X3JlZ19pZCI6bnVsbCwidGltZXN0YW1wIjpudWxsfV19" + } + } + ], + "~thread": { + "thid": "00cdd404-82fc-431d-9c18-fb643636d2a5" + } + }, + "state": "done", + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "threadId": "00cdd404-82fc-431d-9c18-fb643636d2a5" + }, + "id": "72c96cd1-1f26-4bf3-8a00-5c00926859a8", + "type": "ProofExchangeRecord", + "tags": { + "threadId": "00cdd404-82fc-431d-9c18-fb643636d2a5", + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "state": "done" + } + }, + "ea840186-3c77-45f4-a2e6-349811ad8994": { + "value": { + "metadata": {}, + "isVerified": true, + "id": "ea840186-3c77-45f4-a2e6-349811ad8994", + "createdAt": "2022-09-08T19:36:06.261Z", + "requestMessage": { + "@type": "https://didcomm.org/present-proof/1.0/request-presentation", + "@id": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + "request_presentations~attach": [ + { + "@id": "libindy-request-presentation-0", + "mime-type": "application/json", + "data": { + "base64": "eyJuYW1lIjoicHJvb2YtcmVxdWVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjUyODExNDc1NTIxNzg3NzExMjI1Mzc0NSIsInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjp7Im5hbWUiOnsibmFtZSI6Im5hbWUiLCJyZXN0cmljdGlvbnMiOlt7ImNyZWRfZGVmX2lkIjoiN3lXNlNvVGpITmhEM3pZZ200UGJLODozOkNMOjQ3MjMxOTpUQUcifV19fSwicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOnt9fQ==" + } + } + ] + }, + "presentationMessage": { + "@type": "https://didcomm.org/present-proof/1.0/presentation", + "@id": "2481ce81-560b-4ce6-a22b-ee4b6ed369e8", + "presentations~attach": [ + { + "@id": "libindy-presentation-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFfcHJpbWUiOiI5MDg5MzE2Mjc0ODc5NTI4MjMzNDA1MTY0NTgwOTM1OTIxMjIwMjMyMzg5ODE0NjE4OTc3ODA3MjA0MDg4OTQ0ODkzNDc1OTE5NTE4Mjc0NDEwNTU3OTU3NjEwNjYzOTAxMTcwODM1NDM2Nzk1NDU4NDU1Mjg3NzEwOTk3NTk3OTA1OTM3NTYyODIyNjg5NTE3MjAyNzQ4NTUxODgzODQ5NjY3MjYwNTA3NjU0NDM5OTk0MjczNDQ0MTU5NTQyMzg3MzI0OTM5OTAzMDcyMDc2MjQ4Njg1MTgyMjA4NTA0OTkyOTg5MTk0NzUwMDgyODU1MTc1NDE1OTIzMjU3MzA0MTQ0NjYxMDc5MDU2NzExMTg3NzMzMDE3NDQ1MTEyOTQyNDAyMTEzNDg0NjM5MTMxMDY2MDc3ODE2NzQzMzY3OTMzMDI3MjY3MTQ3MDIxMTkxODQ0NTQzMzI5NzUzMTA1NTA3MDk0Mzc5OTA5OTYzNjcxMTQ4NzM3Mjk3NDA2MzUxMzk0NTcwNTM3Nzk0NDg1Njc1ODc3MDU5OTI2NTc3MDU4MzY1NTA0MDQwNjAzNDIxMDI3NDYyOTY0OTExNTc5MDAyNjg2NDAzMjMyOTc3OTU0ODM1Nzc2NzQwMDI3NDIxNjI0MTUzNDQ4NzYyODAxODM3OTU3MTQ3NzM0MDkxNDk3NjMwMjA3MTY3MzUzMjAwMTM5ODE4MDg1NjgwMDIzMTc1MDEyNzM4Mjk1NzIwODU2OTMwNDYxMzIxMDQ4NTIxMjQ0ODQ5MjQ5Njc5MDMwMzI0NDcyNjYyOTQxNjc5NDU3OTk3NzQ4NiIsImUiOiI0NTE0MTczNzExODM2MzMzOTk0NjA3MTMwNjQ5MjA0NjEyNzU2Njk1MDI4ODA2NTY0NzI4MzE3OTExNzYxNDA0NTE5Nzk0NjA3NDk4Njg5NjgyOTYxODk3MDc4MDcwMzQ5Nzk0MzUzMDQ1MTY3MTUyMTk2OTU4NTU0NTI5MzgxNjY3MDE5MDA2OSIsInYiOiI1MzM4NDg2NDY2MjE4MTg2ODg3MTUwNzY4NTQ0OTQyMTEyMDcyOTQ1MzczMDQ1MDAzNTk0MTk0MzAxMDA5NDUzMzk5MjMxMDM5NjExMjU4NTE3MTgzODUyMDc4NjI0NjMyMDExNDE2MzI3Mjc1MTM3Nzc1MTAxNjgxODcwMzI4MjY3MTE4MjExNjEwNzAwNDc2MjA5NzMwMTIwODI2NzMyMTkwMDg0ODkyOTc2NTEwMTgxODE2MTkzMzM5MTk0MjE5MDIxOTQ1OTI1NTg4NjEzODEwMjE1Nzg1NDk1NDk0NjQ0NzIwMDM4MjMwMTg1MDUyMDAxMTMxNjE3MjQwMDIyNjQzOTYxNTkwOTU5ODE3ODMxMzg2Mzc5NDQ1MzI2Mzg4NzYzNjQ5MDYxODk4Nzk1ODcwMjE2NTkxMDI3NDkwMzAwMjA0OTc1NzM0NDgyNDM1ODE4MjgwMTQxNzA0MzA0MjMzNDE5NTMyNjc1Mzk3MDE3MTc1MTE3ODI5NDUzNjAxNDM2OTM2MDM3NDMyMzg4OTYyMjMwOTAyNTk1MjE3MTA3MzkxOTMwOTA3NDI4NDQyNDg4ODE2NjQ4NTI4OTkyNjUwMzY0NjIyNDA2MTA5MDUxOTczMjYyOTM3MzYyMTg5NDcwNTUyNDQ2MjAzNTMzNTQzNTY4NjY5MzAwODY0MzQyMzQwMDgwNjg5Mjg5MjI0OTg1MjU4MjU5MTk1Nzc5NTA3MzgwMDI1ODcwNDk0MDIwNDkyMTE2MDExOTA3NjI0NjI0ODg1ODk5NjMxOTk4ODMwNjg4NTY2OTQ5OTgyNjI5Mjg2Mzk2MjIzNDc2NDE1ODM2Nzg3MDA2Mzg5Nzc0MjYxMzk5NjUxMTY3NTYwNzcyMDc5NjkzMDA1NzMwOTQzNTIzNjAwNzM4Mjc4MzA4NDE2MjA5NzQzNzA1ODQ1MzUxNjQzMDUyMTY1MTcyNTg5NTMwMTU0MTU3NjE2OTYzMDg5NjM4ODg4NDc0NDg3MDA3NjY0NTA2ODk5NTE1OTE5MDAxNzIyMDEyNzczMzU3MDc4MjI4OTIzMDMzNTA1NDQ2MzAxOTQxNzA2OTc2NTY3Mzg5NDk3MzgxMDI2NjIyNDEzNTYyODc5MjM0MTM0NTI5Nzk4NzY2ODY0Nzk1OTQ3NzY1ODcwNDgwMTIyNDk0ODE0MzU0MDQ3MzE2ODY0ODczODMzNDgyNDU5NTc1NTQxNDI4NTE0MTciLCJtIjp7Im1hc3Rlcl9zZWNyZXQiOiIxNjE3NTE3NzgwNjcyMjkxNDYzNTc4ODc1NDk1NTkxODgyOTA3ODYzNTk0NzgyMzk4NjczMTIwMDg2OTEwMjA3NzczODk0ODYyNzQxOTIxMzk2OTE2MDUxNDk2NjYzMjIxNDA5MzA3NjA4NTczMDg1ODExMzAyNTYxNDcyMzgxMjY1NjE4MzQyNzc1NTY5MjQ4OTQ3NzY4Mjc3OTQzMzIxMjcyMTY1MjEyMDAxNDI0NDAwMyJ9LCJtMiI6IjE1MDQ5MTk3MTU3NDcyNjQ0MDMzMzE4OTAxNTc5MDYyNTk5NzA2NzU5MzcwMDk1MTk3NzI1NTE3MTM4OTAyMzcwNDUwMTQ5NDk2NjU0MTEzMzA5NTQ4MTc4MDM3NDU1NjY3Njk2NDA0MDY1ODI5MTUzNDYzNDczNzgzMTk5ODA3MjEzNjg5NDE3MTM2NDI4NDg5NzUwNjUzNTc5MjU0NDY0ODk0OTM4MDkyODY2NTUzNjU5In0sImdlX3Byb29mcyI6W119LCJub25fcmV2b2NfcHJvb2YiOm51bGx9XSwiYWdncmVnYXRlZF9wcm9vZiI6eyJjX2hhc2giOiIzNjk4Mjk3ODU5OTY5Nzg3MjI5MTA5NDY2OTIwMDA3ODEwNDA2ODQ3NTI2MDE2NjgxMTIwNDE4OTQ1NDk0NzcwODQyNjI3MjA2MjEyNCIsImNfbGlzdCI6W1syLDIwOCwzLDUzLDIyOSwxMzksMTAyLDUxLDI0MCwxOTUsMTM1LDExNSwxNzYsMTcyLDE4NCw5OSwxMDksMTU2LDgzLDUyLDIxNSwyMjMsODQsMjU1LDY2LDIyNiwyMjMsMTA1LDExMSwyMjEsMTgwLDk1LDEyMiwxMzMsMjIyLDI3LDM5LDk5LDcwLDEzLDM3LDI0LDI1NSwxMTQsMjM1LDEwOSwxODMsNTEsMjEzLDE5MCwyMjYsMTI2LDExOCwyLDIyMCw3OCw0OSw5LDI0MCw1NSwxNzksMTQ3LDUxLDIwMSwyMTMsMjEzLDEzMCw0LDE4MCwxMDMsMTk1LDgsMjYsMTE4LDE0LDEzMCwxOCwxMzMsMTg3LDYyLDMsOTcsMjEwLDEwMiwxMiwxNjIsNzksMTg0LDU1LDIzMiwyMTksMjIwLDE3NSwyNTUsMTY5LDE5NywxMjMsMTI3LDE2MCwyNSwxNTEsMTg3LDg3LDE5MSwxMDksMTk4LDQ0LDcxLDM4LDUwLDEwNCwyNiwyMTYsMTgwLDIxOCwxNDUsMTAsNzYsMTgwLDE1Nyw5OCwyMzQsNzcsMTY5LDE1MSw2OCwxNzAsMTg5LDE1LDIxNyw5OCwyMzUsMTI0LDE2NywyMzYsMjUzLDExMiwyNDQsMTg5LDk1LDE3OCw2MCw3MiwyMjgsMjIzLDcwLDI3LDYwLDIzNiwyMTIsOTcsMjA1LDIyLDI1MCwzNCwyNDYsMTIyLDM0LDgsMjU1LDIyLDEyNywxNTEsMjQyLDE4MCwxNzEsMTIxLDIyNywzMiwxMDMsNTEsMTcwLDIzNCwyMDYsMjAsMTAyLDIwNCwxMTYsMTk5LDAsMTE5LDExNSwxODAsMjA3LDE2LDQzLDU5LDI0MiwxNzksMTksMTk5LDQ4LDEyNyw5LDYzLDg4LDIxLDAsMjE1LDE3NCw0NywxNzcsMjMyLDE4MiwyNTMsMjQ5LDI0OCwxMTgsMTk2LDI1NCwxMzksMTIsMjksMSw0OCwxMDUsMzMsNCwyMDgsMTA2LDIzNSwyNDcsMjEwLDExMiwyMTAsMTA2LDE5OSwxOTgsNDcsOCwyMzYsNTIsOSw2NywxMjgsMjQwLDI1NCwyMzIsMjEwLDQsMjM5LDE4MywzOSwxOTMsMjQyLDMyLDEzMywxOTQsMTQ4LDk4LDExMSw3NywxNTUsMjA1LDE3OCwxOTcsMTRdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6eyJzdWJfcHJvb2ZfaW5kZXgiOjAsInJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In19LCJzZWxmX2F0dGVzdGVkX2F0dHJzIjp7fSwidW5yZXZlYWxlZF9hdHRycyI6e30sInByZWRpY2F0ZXMiOnt9fSwiaWRlbnRpZmllcnMiOlt7InNjaGVtYV9pZCI6Ijd5VzZTb1RqSE5oRDN6WWdtNFBiSzg6Mjp0ZXN0LXNjaGVtYS00ZTk0YzJlNC00ZjQ3LTRmZjMtYTg4OC02ZjY0ZGE2YTkyZGM6MS4wIiwiY3JlZF9kZWZfaWQiOiI3eVc2U29UakhOaEQzellnbTRQYks4OjM6Q0w6NDcyMzE5OlRBRyIsInJldl9yZWdfaWQiOm51bGwsInRpbWVzdGFtcCI6bnVsbH1dfQ==" + } + } + ], + "~thread": { + "thid": "7fcfc074-43ac-43cc-92b9-76afceeebe82" + } + }, + "state": "done", + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "threadId": "7fcfc074-43ac-43cc-92b9-76afceeebe82" + }, + "id": "ea840186-3c77-45f4-a2e6-349811ad8994", + "type": "ProofExchangeRecord", + "tags": { + "threadId": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "state": "done" + } + }, + "ec02ba64-63e3-46bc-b2a4-9d549d642d30": { + "value": { + "metadata": {}, + "id": "ec02ba64-63e3-46bc-b2a4-9d549d642d30", + "createdAt": "2022-09-08T19:36:06.208Z", + "proposalMessage": { + "@type": "https://didcomm.org/present-proof/1.0/propose-presentation", + "@id": "00cdd404-82fc-431d-9c18-fb643636d2a5", + "presentation_proposal": { + "@type": "https://didcomm.org/present-proof/1.0/presentation-preview", + "attributes": [ + { + "name": "name", + "cred_def_id": "7yW6SoTjHNhD3zYgm4PbK8:3:CL:472319:TAG", + "value": "Alice" + } + ], + "predicates": [] + } + }, + "requestMessage": { + "@type": "https://didcomm.org/present-proof/1.0/request-presentation", + "@id": "2862b25d-396c-46d7-ad09-f48ff4ea6db6", + "request_presentations~attach": [ + { + "@id": "libindy-request-presentation-0", + "mime-type": "application/json", + "data": { + "base64": "eyJuYW1lIjoicHJvb2YtcmVxdWVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjQwMzU1MDc0MDYxMTU0MzEwMzA5NzMyMiIsInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjp7IjBmZWExODk3LTQzMTEtNDhmMi1hMmMyLTM4NzRlOTkzYWVmMCI6eyJuYW1lIjoibmFtZSIsInJlc3RyaWN0aW9ucyI6W3siY3JlZF9kZWZfaWQiOiI3eVc2U29UakhOaEQzellnbTRQYks4OjM6Q0w6NDcyMzE5OlRBRyJ9XX19LCJyZXF1ZXN0ZWRfcHJlZGljYXRlcyI6e319" + } + } + ], + "~thread": { + "thid": "00cdd404-82fc-431d-9c18-fb643636d2a5" + } + }, + "presentationMessage": { + "@type": "https://didcomm.org/present-proof/1.0/presentation", + "@id": "4185f336-f307-4022-a27d-78d1271586f6", + "presentations~attach": [ + { + "@id": "libindy-presentation-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFfcHJpbWUiOiI1MjE1MTkyMzYwMDA2NTg5OTUyMjM1MTYwNjkxMzU1OTc4MjA2NDUzODA3NDUzNTk5OTE5OTkzNzM1NTczNzA1OTA2OTY5MTAwMjIzOTM4MjMyMzE5NDE1Njk1NTMzNjQ4MDMzNzc3NzI4NTE3NzI4Mjk4NDU1OTI0NjQ1NTU1NjQyNjE1NTQ3NTI0MDc0MTAyOTQyNjg4NzQwNDcwMjg4MDAyNTgwMjI4OTg4MDk4MDk0NjQxODc5NjkxNzQwMTY2OTc0MzA2OTczNjQ2MDA3ODg3NTU1NDU2NjM3NTUyODk2NjkxNDc3NDg2MDM1MTIxMjU0NDY0NzA2MDY0Mzg2NzA1MzU0MDk3MDcxNDc5OTkwMzIwNzIxMzQ4MjM1NTQ2NjI5OTcxNzAyNzgwNjUwNTgyMDQ3MzMxOTIwNjE5OTIzODg4NjU1NjI0NjYzMDE4NTAzMDUzOTQ4OTEyNzk4MDE1ODA2Mzk5MTAxNTg3MzAzNTgwNzkyMzQ3OTU2NjE1MDgzODczNDQ5MTM0MjEyOTE2MDY3MTExMjM4MjQzMjM1MTk5OTk1MTUzNjIyMTM1NjA4Nzg5OTc2MTk2NTkzMTczNTU4NTA0NDYxODA0MTk5MzY3MTkwODY2MDY5MTk1MDE5MjA4NjQyMzAwNzE5NjAzMDk5MzI1MjA0Njg4MzkwMjc2NTM5NjQxMDk0Mjc4NTY2NTUzOTU1Mzg4NDAwMDUyNzEyNzk0MjUwODgxODg3NDIzMDM5NTgyMjM5MjQ4NTk0NjYyMDYzMDA0ODI2MzE5NDQxNTUwNzEyNDA3MTQ4NDc4Njc1MCIsImUiOiIxNTk2OTcwODg4Njk3NTIwMjIzMzA3ODgyOTM5MzI5OTY0NTM3NjA4Mzc3NjQ0MTc2MjIyODczMjc2MTc0Mjg5ODkzNjgyNDAzODg0NzkzOTk1NDIwMjQ2MTEzNDk5NjMxNDAyMTQxMzQ0OTYxOTg3NDAyNjAxNDc4NTQ3NjEwNjI5MTgzNzc2NDQiLCJ2IjoiOTE0NzEwNTI1NDAwMDA2MDgzNTMwMTMzOTIxMjQyNTQwMzI5ODAzNjI4MjgyMDA0MDAzMTY1ODIyMjMyOTM0NDYxODAyMjQ4MDk4OTU5MzY1MTY2ODA3MjEwODgzNzIyNjIyMzA1ODQyNjM4NzM4NjIzNzM5NzYwNjEzODc5NDcwOTU5NDkyNjg1ODY5MTY4NTQyMDU1MjAyNzcxMDYxODI4MDEwMTAzNDY3Nzk2MDA5NDIzMTc4NzU2NDQ5OTk3NDQzODk5NDkyMDEyMTE0OTYyMDk5MDgwMDg1MzAxMjUyMDU1NDk0MDUzMTM4MzU4NDE0MjM0MDg5MzQyNDY2Nzg0MTA4ODkwMjA2NjY0NDI1NDE1ODYzNDA5NTI1MjI3NDY0NTg1OTU0MzQ3OTk2OTIwNTU0NTg5Nzc3OTA1MzMzMDMxMzAzMzEzNTI1OTY3NDg3NzAyMDQxODEwNzA3ODQ1OTAyMTAyODAxMTM0NDk3ODAxNjQyNTgwNzQyNjg0Njk5OTk3ODM2NTY0NzM2NDE5NDc0NzM1Mzg2ODkzMTQwMDA4ODIzNjIzMjA1MDA5MzE3NjgzNTIyNjI5NzkwMDY1NDExNDE5MzY3MTc3MTgwMTQ3MDk3ODkwNTkxNDM4NTkyMjQyNzA1MTg5NDM4NDE2MjA4MTA0MjczNjI3MjUyNzc2NzY1NjI0NjExMjk3NjQzODA5NzIwMjcxMDUzOTY1MzM4MTA5Njk1MTM4NjA3NTA0NzExMDc0NDY0ODU5Mzc5MzUxNTI1ODY4NzgyMjk2MzIzNDIwMzIwMTAwMzQ4MDA5MDMyMzIxOTM2ODMzNDk0Nzc2NDUxMjQ0NzMzNTQxMzI3Mzc4MTc3OTYwMTMzOTg2NzE4NTA2NTk0MzE2MjMyMzEwMTk5NjE2NjQ4NzI3MjU0MTUwNjg2MjA4OTQ1MTM3Mzg5MTI5Mjg4NzUyNzE3MjA5NjI3ODcyNjE3NTM3MjEwMDk2NTU2MTkzMzgxMzUwNTgzMDM1NjI5OTg3MjE5MTk3OTQ4OTEyNDI3NzI0MTA1ODE2NzA0NDkyOTMzNzQyMjcxOTQ5NTk1ODAwOTUyMDA5NDczNzQ2OTM5ODU0Mzg0NDI4MzY0MjE0NjUyNjI4MDgyOTQwOTMzODcwNTE1MjE4NTc1MzY4NDgwMjM4OTM2MTc0NDM5NzExNTUzNDI3NDEwNzA2NTgxMTkzMzg2NDM2ODEyIiwibSI6eyJtYXN0ZXJfc2VjcmV0IjoiNDU1NjIzMDY3NjQ4NDQ0NTEzODAyNzM4ODkxODAyMTkxNDg1MzY0NzMzMDkxMTIwMjM5MDg1ODY0NTQxMzU5Njg0MDgyMDczNTgzNDMzMjM4MzU2Njg2MTgwNzI1MzIyMjU2MzgwMjYxNzYyMTc5MTg3MDU3Mjk4Nzg3NzEzNDcyODY5MjE5NDI1ODgyNTEyNzI0ODk4NjkzMDk3OTkwODI4MDIzMjUwNTkyMTAwNDI5OSJ9LCJtMiI6IjEyMjUwNjAyNjEwOTE0Njk5NDEzNDk3MjEyOTYwNTA3OTQ1MDkwOTk1OTMwNjYyMDMyOTA2NTIxNDUxNjI0MDUxMTk2MTg3MjIxNjgxNzEzMDc1OTg0MDY5MjY5MTY3MDQ5ODExMDc3NDk1NTM2MDg4OTE0NTc3MDk2NTgwNjMwODIzNDQyMTk4Nzk5Nzg4NzIyNDIzMzg4MDIyNjE5MzM4MzA2MjM3NTQ2MzI5OTI4MDEifSwiZ2VfcHJvb2ZzIjpbXX0sIm5vbl9yZXZvY19wcm9vZiI6bnVsbH1dLCJhZ2dyZWdhdGVkX3Byb29mIjp7ImNfaGFzaCI6IjU4Njg3MDc5MDA2NTc0OTY1OTA5ODk2MDc5OTU0NTA2NDM2ODg0NzMxOTIxMjk0NDA5NDYxNzI5NjkzMTI4MDM1OTcxMjAwMjY5NzgzIiwiY19saXN0IjpbWzEsMTU3LDMxLDExMiwxNjQsMjA1LDIyNCw3NiwzMiwyMTIsMTczLDc3LDIxNyw2MiwxNzMsMTc3LDQsMTU5LDE0OSwyMzMsMTE3LDEwNCw5MiwxMzgsMzUsMjYsNDgsODUsMjQwLDIzNCw0MywxOTYsNTksMTE0LDI0MiwxNTMsNDAsMTg3LDIzOSwxMDAsMTcwLDIsMzYsMTA3LDIyOSwzNywxMTEsNjAsOTIsOSwxMTcsMjM4LDE0MCwyNDMsMywyNDIsMjM0LDE1MiwxNjYsNiwxNjEsMTM0LDIyMiwxNjMsNDMsNjIsMTIsODEsMjUzLDE3NiwxMjUsMjQsOCw5MiwxMywyMjIsMTcxLDIzMywxODIsMjE1LDIyLDIzNCw5NSwxMTIsMTIyLDEwMiwxNTIsMTA2LDE4Miw0LDIxOSw1NiwxMDEsMTQ3LDIyNywyNDYsMTE1LDY0LDE4Myw0LDY3LDEyNCwxNDAsMTY2LDEzOCwxNCwyNCw1MSwxODUsMTk5LDEwOSwyLDUxLDE5MCwxODEsMjQ3LDYxLDE5MCw4NSwyNDcsMjE3LDIyLDE2NCwxOTIsMjM1LDE4NywxNDgsMjYsMTA5LDI1MywxMTYsMjQxLDY1LDEzNiw0MSwyMjUsMjI5LDE3OCwxMjUsNzksMjQ2LDUzLDQ0LDE5MiwxNDEsMjQzLDE4NCwxNTEsMTIsOSwxMDQsMjE5LDE2Miw3MSw3Miw3LDg5LDgzLDgsMjI1LDEzLDQ4LDEzMywyNDcsMTg3LDExNCw4NSw3Nyw1OSw5OSw3MCw2OSw3MSwxMzMsMTY5LDExMSwxMTAsNzksNDQsMTc0LDIzLDEwMywxNjYsNjksNSwxNDcsNjYsNzcsMTYzLDExMiw0MiwxOTksMTQyLDE0LDIyLDE1MSwyNDgsMzMsMzQsMTk4LDE1NiwyMCwyMzEsMjA0LDcwLDgyLDI1MywxMzgsMTMxLDQyLDIwNSwzMywxMDksMjMyLDE3LDEwNiw2OSw3Miw2MCwyMTAsMjMyLDExMywxMjQsNzYsMTgxLDIwOCwwLDE4NiwyNTEsMTQzLDExMyw0NSwxNzEsMTAsNDMsMTUsMzAsMjA4LDkzLDYsMTY4LDEwNiwyMDMsNDgsMjUwLDIxOSw5LDE3LDEwNCwzNSw1OCwxNTksMTgwLDExMyw2NiwxMzYsNjJdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsiMGZlYTE4OTctNDMxMS00OGYyLWEyYzItMzg3NGU5OTNhZWYwIjp7InN1Yl9wcm9vZl9pbmRleCI6MCwicmF3IjoiQWxpY2UiLCJlbmNvZGVkIjoiMjcwMzQ2NDAwMjQxMTczMzEwMzMwNjMxMjgwNDQwMDQzMTgyMTg0ODY4MTY5MzE1MjA4ODY0MDU1MzU2NTk5MzQ0MTc0Mzg3ODE1MDcifX0sInNlbGZfYXR0ZXN0ZWRfYXR0cnMiOnt9LCJ1bnJldmVhbGVkX2F0dHJzIjp7fSwicHJlZGljYXRlcyI6e319LCJpZGVudGlmaWVycyI6W3sic2NoZW1hX2lkIjoiN3lXNlNvVGpITmhEM3pZZ200UGJLODoyOnRlc3Qtc2NoZW1hLTRlOTRjMmU0LTRmNDctNGZmMy1hODg4LTZmNjRkYTZhOTJkYzoxLjAiLCJjcmVkX2RlZl9pZCI6Ijd5VzZTb1RqSE5oRDN6WWdtNFBiSzg6MzpDTDo0NzIzMTk6VEFHIiwicmV2X3JlZ19pZCI6bnVsbCwidGltZXN0YW1wIjpudWxsfV19" + } + } + ], + "~thread": { + "thid": "00cdd404-82fc-431d-9c18-fb643636d2a5" + } + }, + "state": "done", + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "threadId": "00cdd404-82fc-431d-9c18-fb643636d2a5" + }, + "id": "ec02ba64-63e3-46bc-b2a4-9d549d642d30", + "type": "ProofExchangeRecord", + "tags": { + "threadId": "00cdd404-82fc-431d-9c18-fb643636d2a5", + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "state": "done" + } + }, + "3d5d7ad4-f0aa-4b1b-8c2c-780ee383564e": { + "value": { + "metadata": {}, + "id": "3d5d7ad4-f0aa-4b1b-8c2c-780ee383564e", + "createdAt": "2022-09-08T19:36:06.261Z", + "requestMessage": { + "@type": "https://didcomm.org/present-proof/1.0/request-presentation", + "@id": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + "request_presentations~attach": [ + { + "@id": "libindy-request-presentation-0", + "mime-type": "application/json", + "data": { + "base64": "eyJuYW1lIjoicHJvb2YtcmVxdWVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjUyODExNDc1NTIxNzg3NzExMjI1Mzc0NSIsInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjp7Im5hbWUiOnsibmFtZSI6Im5hbWUiLCJyZXN0cmljdGlvbnMiOlt7ImNyZWRfZGVmX2lkIjoiN3lXNlNvVGpITmhEM3pZZ200UGJLODozOkNMOjQ3MjMxOTpUQUcifV19fSwicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOnt9fQ==" + } + } + ] + }, + "presentationMessage": { + "@type": "https://didcomm.org/present-proof/1.0/presentation", + "@id": "2481ce81-560b-4ce6-a22b-ee4b6ed369e8", + "presentations~attach": [ + { + "@id": "libindy-presentation-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFfcHJpbWUiOiI5MDg5MzE2Mjc0ODc5NTI4MjMzNDA1MTY0NTgwOTM1OTIxMjIwMjMyMzg5ODE0NjE4OTc3ODA3MjA0MDg4OTQ0ODkzNDc1OTE5NTE4Mjc0NDEwNTU3OTU3NjEwNjYzOTAxMTcwODM1NDM2Nzk1NDU4NDU1Mjg3NzEwOTk3NTk3OTA1OTM3NTYyODIyNjg5NTE3MjAyNzQ4NTUxODgzODQ5NjY3MjYwNTA3NjU0NDM5OTk0MjczNDQ0MTU5NTQyMzg3MzI0OTM5OTAzMDcyMDc2MjQ4Njg1MTgyMjA4NTA0OTkyOTg5MTk0NzUwMDgyODU1MTc1NDE1OTIzMjU3MzA0MTQ0NjYxMDc5MDU2NzExMTg3NzMzMDE3NDQ1MTEyOTQyNDAyMTEzNDg0NjM5MTMxMDY2MDc3ODE2NzQzMzY3OTMzMDI3MjY3MTQ3MDIxMTkxODQ0NTQzMzI5NzUzMTA1NTA3MDk0Mzc5OTA5OTYzNjcxMTQ4NzM3Mjk3NDA2MzUxMzk0NTcwNTM3Nzk0NDg1Njc1ODc3MDU5OTI2NTc3MDU4MzY1NTA0MDQwNjAzNDIxMDI3NDYyOTY0OTExNTc5MDAyNjg2NDAzMjMyOTc3OTU0ODM1Nzc2NzQwMDI3NDIxNjI0MTUzNDQ4NzYyODAxODM3OTU3MTQ3NzM0MDkxNDk3NjMwMjA3MTY3MzUzMjAwMTM5ODE4MDg1NjgwMDIzMTc1MDEyNzM4Mjk1NzIwODU2OTMwNDYxMzIxMDQ4NTIxMjQ0ODQ5MjQ5Njc5MDMwMzI0NDcyNjYyOTQxNjc5NDU3OTk3NzQ4NiIsImUiOiI0NTE0MTczNzExODM2MzMzOTk0NjA3MTMwNjQ5MjA0NjEyNzU2Njk1MDI4ODA2NTY0NzI4MzE3OTExNzYxNDA0NTE5Nzk0NjA3NDk4Njg5NjgyOTYxODk3MDc4MDcwMzQ5Nzk0MzUzMDQ1MTY3MTUyMTk2OTU4NTU0NTI5MzgxNjY3MDE5MDA2OSIsInYiOiI1MzM4NDg2NDY2MjE4MTg2ODg3MTUwNzY4NTQ0OTQyMTEyMDcyOTQ1MzczMDQ1MDAzNTk0MTk0MzAxMDA5NDUzMzk5MjMxMDM5NjExMjU4NTE3MTgzODUyMDc4NjI0NjMyMDExNDE2MzI3Mjc1MTM3Nzc1MTAxNjgxODcwMzI4MjY3MTE4MjExNjEwNzAwNDc2MjA5NzMwMTIwODI2NzMyMTkwMDg0ODkyOTc2NTEwMTgxODE2MTkzMzM5MTk0MjE5MDIxOTQ1OTI1NTg4NjEzODEwMjE1Nzg1NDk1NDk0NjQ0NzIwMDM4MjMwMTg1MDUyMDAxMTMxNjE3MjQwMDIyNjQzOTYxNTkwOTU5ODE3ODMxMzg2Mzc5NDQ1MzI2Mzg4NzYzNjQ5MDYxODk4Nzk1ODcwMjE2NTkxMDI3NDkwMzAwMjA0OTc1NzM0NDgyNDM1ODE4MjgwMTQxNzA0MzA0MjMzNDE5NTMyNjc1Mzk3MDE3MTc1MTE3ODI5NDUzNjAxNDM2OTM2MDM3NDMyMzg4OTYyMjMwOTAyNTk1MjE3MTA3MzkxOTMwOTA3NDI4NDQyNDg4ODE2NjQ4NTI4OTkyNjUwMzY0NjIyNDA2MTA5MDUxOTczMjYyOTM3MzYyMTg5NDcwNTUyNDQ2MjAzNTMzNTQzNTY4NjY5MzAwODY0MzQyMzQwMDgwNjg5Mjg5MjI0OTg1MjU4MjU5MTk1Nzc5NTA3MzgwMDI1ODcwNDk0MDIwNDkyMTE2MDExOTA3NjI0NjI0ODg1ODk5NjMxOTk4ODMwNjg4NTY2OTQ5OTgyNjI5Mjg2Mzk2MjIzNDc2NDE1ODM2Nzg3MDA2Mzg5Nzc0MjYxMzk5NjUxMTY3NTYwNzcyMDc5NjkzMDA1NzMwOTQzNTIzNjAwNzM4Mjc4MzA4NDE2MjA5NzQzNzA1ODQ1MzUxNjQzMDUyMTY1MTcyNTg5NTMwMTU0MTU3NjE2OTYzMDg5NjM4ODg4NDc0NDg3MDA3NjY0NTA2ODk5NTE1OTE5MDAxNzIyMDEyNzczMzU3MDc4MjI4OTIzMDMzNTA1NDQ2MzAxOTQxNzA2OTc2NTY3Mzg5NDk3MzgxMDI2NjIyNDEzNTYyODc5MjM0MTM0NTI5Nzk4NzY2ODY0Nzk1OTQ3NzY1ODcwNDgwMTIyNDk0ODE0MzU0MDQ3MzE2ODY0ODczODMzNDgyNDU5NTc1NTQxNDI4NTE0MTciLCJtIjp7Im1hc3Rlcl9zZWNyZXQiOiIxNjE3NTE3NzgwNjcyMjkxNDYzNTc4ODc1NDk1NTkxODgyOTA3ODYzNTk0NzgyMzk4NjczMTIwMDg2OTEwMjA3NzczODk0ODYyNzQxOTIxMzk2OTE2MDUxNDk2NjYzMjIxNDA5MzA3NjA4NTczMDg1ODExMzAyNTYxNDcyMzgxMjY1NjE4MzQyNzc1NTY5MjQ4OTQ3NzY4Mjc3OTQzMzIxMjcyMTY1MjEyMDAxNDI0NDAwMyJ9LCJtMiI6IjE1MDQ5MTk3MTU3NDcyNjQ0MDMzMzE4OTAxNTc5MDYyNTk5NzA2NzU5MzcwMDk1MTk3NzI1NTE3MTM4OTAyMzcwNDUwMTQ5NDk2NjU0MTEzMzA5NTQ4MTc4MDM3NDU1NjY3Njk2NDA0MDY1ODI5MTUzNDYzNDczNzgzMTk5ODA3MjEzNjg5NDE3MTM2NDI4NDg5NzUwNjUzNTc5MjU0NDY0ODk0OTM4MDkyODY2NTUzNjU5In0sImdlX3Byb29mcyI6W119LCJub25fcmV2b2NfcHJvb2YiOm51bGx9XSwiYWdncmVnYXRlZF9wcm9vZiI6eyJjX2hhc2giOiIzNjk4Mjk3ODU5OTY5Nzg3MjI5MTA5NDY2OTIwMDA3ODEwNDA2ODQ3NTI2MDE2NjgxMTIwNDE4OTQ1NDk0NzcwODQyNjI3MjA2MjEyNCIsImNfbGlzdCI6W1syLDIwOCwzLDUzLDIyOSwxMzksMTAyLDUxLDI0MCwxOTUsMTM1LDExNSwxNzYsMTcyLDE4NCw5OSwxMDksMTU2LDgzLDUyLDIxNSwyMjMsODQsMjU1LDY2LDIyNiwyMjMsMTA1LDExMSwyMjEsMTgwLDk1LDEyMiwxMzMsMjIyLDI3LDM5LDk5LDcwLDEzLDM3LDI0LDI1NSwxMTQsMjM1LDEwOSwxODMsNTEsMjEzLDE5MCwyMjYsMTI2LDExOCwyLDIyMCw3OCw0OSw5LDI0MCw1NSwxNzksMTQ3LDUxLDIwMSwyMTMsMjEzLDEzMCw0LDE4MCwxMDMsMTk1LDgsMjYsMTE4LDE0LDEzMCwxOCwxMzMsMTg3LDYyLDMsOTcsMjEwLDEwMiwxMiwxNjIsNzksMTg0LDU1LDIzMiwyMTksMjIwLDE3NSwyNTUsMTY5LDE5NywxMjMsMTI3LDE2MCwyNSwxNTEsMTg3LDg3LDE5MSwxMDksMTk4LDQ0LDcxLDM4LDUwLDEwNCwyNiwyMTYsMTgwLDIxOCwxNDUsMTAsNzYsMTgwLDE1Nyw5OCwyMzQsNzcsMTY5LDE1MSw2OCwxNzAsMTg5LDE1LDIxNyw5OCwyMzUsMTI0LDE2NywyMzYsMjUzLDExMiwyNDQsMTg5LDk1LDE3OCw2MCw3MiwyMjgsMjIzLDcwLDI3LDYwLDIzNiwyMTIsOTcsMjA1LDIyLDI1MCwzNCwyNDYsMTIyLDM0LDgsMjU1LDIyLDEyNywxNTEsMjQyLDE4MCwxNzEsMTIxLDIyNywzMiwxMDMsNTEsMTcwLDIzNCwyMDYsMjAsMTAyLDIwNCwxMTYsMTk5LDAsMTE5LDExNSwxODAsMjA3LDE2LDQzLDU5LDI0MiwxNzksMTksMTk5LDQ4LDEyNyw5LDYzLDg4LDIxLDAsMjE1LDE3NCw0NywxNzcsMjMyLDE4MiwyNTMsMjQ5LDI0OCwxMTgsMTk2LDI1NCwxMzksMTIsMjksMSw0OCwxMDUsMzMsNCwyMDgsMTA2LDIzNSwyNDcsMjEwLDExMiwyMTAsMTA2LDE5OSwxOTgsNDcsOCwyMzYsNTIsOSw2NywxMjgsMjQwLDI1NCwyMzIsMjEwLDQsMjM5LDE4MywzOSwxOTMsMjQyLDMyLDEzMywxOTQsMTQ4LDk4LDExMSw3NywxNTUsMjA1LDE3OCwxOTcsMTRdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6eyJzdWJfcHJvb2ZfaW5kZXgiOjAsInJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In19LCJzZWxmX2F0dGVzdGVkX2F0dHJzIjp7fSwidW5yZXZlYWxlZF9hdHRycyI6e30sInByZWRpY2F0ZXMiOnt9fSwiaWRlbnRpZmllcnMiOlt7InNjaGVtYV9pZCI6Ijd5VzZTb1RqSE5oRDN6WWdtNFBiSzg6Mjp0ZXN0LXNjaGVtYS00ZTk0YzJlNC00ZjQ3LTRmZjMtYTg4OC02ZjY0ZGE2YTkyZGM6MS4wIiwiY3JlZF9kZWZfaWQiOiI3eVc2U29UakhOaEQzellnbTRQYks4OjM6Q0w6NDcyMzE5OlRBRyIsInJldl9yZWdfaWQiOm51bGwsInRpbWVzdGFtcCI6bnVsbH1dfQ==" + } + } + ], + "~thread": { + "thid": "7fcfc074-43ac-43cc-92b9-76afceeebe82" + } + }, + "state": "done", + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "threadId": "7fcfc074-43ac-43cc-92b9-76afceeebe82" + }, + "id": "3d5d7ad4-f0aa-4b1b-8c2c-780ee383564e", + "type": "ProofExchangeRecord", + "tags": { + "threadId": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "state": "done" + } + } +} diff --git a/packages/core/src/storage/migration/__tests__/__fixtures__/alice-8-connections-0.1.json b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-8-connections-0.1.json new file mode 100644 index 0000000000..ece0f42270 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-8-connections-0.1.json @@ -0,0 +1,635 @@ +{ + "8f4908ee-15ad-4058-9106-eda26eae735c": { + "value": { + "metadata": {}, + "id": "8f4908ee-15ad-4058-9106-eda26eae735c", + "createdAt": "2022-04-30T13:02:21.577Z", + "did": "XajWZZmHGAWUvYCi7CApaG", + "didDoc": { + "@context": "https://w3id.org/did/v1", + "publicKey": [ + { + "id": "XajWZZmHGAWUvYCi7CApaG#1", + "controller": "XajWZZmHGAWUvYCi7CApaG", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "HfkCHGAHTz3j33TRDkKMabYLdnr2FKuWcaXTLzZkZcCp" + } + ], + "service": [ + { + "id": "XajWZZmHGAWUvYCi7CApaG#IndyAgentService", + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + "priority": 0, + "recipientKeys": ["HfkCHGAHTz3j33TRDkKMabYLdnr2FKuWcaXTLzZkZcCp"], + "routingKeys": [] + } + ], + "authentication": [ + { + "publicKey": "XajWZZmHGAWUvYCi7CApaG#1", + "type": "Ed25519SignatureAuthentication2018" + } + ], + "id": "XajWZZmHGAWUvYCi7CApaG" + }, + "verkey": "HfkCHGAHTz3j33TRDkKMabYLdnr2FKuWcaXTLzZkZcCp", + "theirDid": "3KAjJWF5NjiDTUm6JpPBQD", + "theirDidDoc": { + "@context": "https://w3id.org/did/v1", + "publicKey": [ + { + "id": "3KAjJWF5NjiDTUm6JpPBQD#1", + "controller": "3KAjJWF5NjiDTUm6JpPBQD", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy" + } + ], + "service": [ + { + "id": "3KAjJWF5NjiDTUm6JpPBQD#IndyAgentService", + "serviceEndpoint": "rxjs:faber", + "type": "IndyAgent", + "priority": 0, + "recipientKeys": ["2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy"], + "routingKeys": [] + } + ], + "authentication": [ + { + "publicKey": "3KAjJWF5NjiDTUm6JpPBQD#1", + "type": "Ed25519SignatureAuthentication2018" + } + ], + "id": "3KAjJWF5NjiDTUm6JpPBQD" + }, + "theirLabel": "Agent: PopulateWallet2", + "state": "complete", + "role": "invitee", + "alias": "connection alias", + "invitation": { + "@type": "https://didcomm.org/connections/1.0/invitation", + "@id": "d56fd7af-852e-458e-b750-7a4f4e53d6e6", + "label": "Agent: PopulateWallet2", + "recipientKeys": ["2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy"], + "serviceEndpoint": "rxjs:faber", + "routingKeys": [] + }, + "threadId": "fe287ec6-711b-4582-bb2b-d155aee86e61", + "multiUseInvitation": false + }, + "id": "8f4908ee-15ad-4058-9106-eda26eae735c", + "type": "ConnectionRecord", + "tags": { + "invitationKey": "2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy", + "state": "complete", + "role": "invitee", + "threadId": "fe287ec6-711b-4582-bb2b-d155aee86e61", + "verkey": "HfkCHGAHTz3j33TRDkKMabYLdnr2FKuWcaXTLzZkZcCp", + "theirKey": "2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy", + "did": "XajWZZmHGAWUvYCi7CApaG", + "theirDid": "3KAjJWF5NjiDTUm6JpPBQD" + } + }, + "9383d8e5-c002-4aae-8300-4a21384c919e": { + "value": { + "metadata": {}, + "id": "9383d8e5-c002-4aae-8300-4a21384c919e", + "createdAt": "2022-04-30T13:02:21.608Z", + "did": "SDqTzbVuCowusqGBNbNDjH", + "didDoc": { + "@context": "https://w3id.org/did/v1", + "publicKey": [ + { + "id": "SDqTzbVuCowusqGBNbNDjH#1", + "controller": "SDqTzbVuCowusqGBNbNDjH", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq" + } + ], + "service": [ + { + "id": "SDqTzbVuCowusqGBNbNDjH#IndyAgentService", + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + "priority": 0, + "recipientKeys": ["EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq"], + "routingKeys": [] + } + ], + "authentication": [ + { + "publicKey": "SDqTzbVuCowusqGBNbNDjH#1", + "type": "Ed25519SignatureAuthentication2018" + } + ], + "id": "SDqTzbVuCowusqGBNbNDjH" + }, + "verkey": "EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq", + "theirDid": "YUH4t3KMkEJiXgmqsncrY9", + "theirDidDoc": { + "@context": "https://w3id.org/did/v1", + "publicKey": [ + { + "id": "YUH4t3KMkEJiXgmqsncrY9#1", + "controller": "YUH4t3KMkEJiXgmqsncrY9", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "J9qc5VredX3YUBbFNoJz5oJpWPcUWURKVDiUv1DvYukX" + } + ], + "service": [ + { + "id": "YUH4t3KMkEJiXgmqsncrY9#IndyAgentService", + "serviceEndpoint": "rxjs:faber", + "type": "IndyAgent", + "priority": 0, + "recipientKeys": ["J9qc5VredX3YUBbFNoJz5oJpWPcUWURKVDiUv1DvYukX"], + "routingKeys": [] + } + ], + "authentication": [ + { + "publicKey": "YUH4t3KMkEJiXgmqsncrY9#1", + "type": "Ed25519SignatureAuthentication2018" + } + ], + "id": "YUH4t3KMkEJiXgmqsncrY9" + }, + "theirLabel": "Agent: PopulateWallet2", + "state": "complete", + "role": "inviter", + "invitation": { + "@type": "https://didcomm.org/connections/1.0/invitation", + "@id": "d939d371-3155-4d9c-87d1-46447f624f44", + "label": "Agent: PopulateWallet", + "recipientKeys": ["EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq"], + "serviceEndpoint": "rxjs:alice", + "routingKeys": [] + }, + "threadId": "0b2f1133-ced9-49f1-83a1-eb6ba1c24cdf", + "multiUseInvitation": false + }, + "id": "9383d8e5-c002-4aae-8300-4a21384c919e", + "type": "ConnectionRecord", + "tags": { + "state": "complete", + "role": "inviter", + "invitationKey": "EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq", + "threadId": "0b2f1133-ced9-49f1-83a1-eb6ba1c24cdf", + "verkey": "EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq", + "theirKey": "J9qc5VredX3YUBbFNoJz5oJpWPcUWURKVDiUv1DvYukX", + "did": "SDqTzbVuCowusqGBNbNDjH", + "theirDid": "YUH4t3KMkEJiXgmqsncrY9" + } + }, + "7781341d-be29-441b-9b79-4a957d8c6d37": { + "value": { + "metadata": {}, + "id": "7781341d-be29-441b-9b79-4a957d8c6d37", + "createdAt": "2022-04-30T13:02:21.628Z", + "did": "RtH4qxVPL1Dpmdv7GytjBv", + "didDoc": { + "@context": "https://w3id.org/did/v1", + "publicKey": [ + { + "id": "RtH4qxVPL1Dpmdv7GytjBv#1", + "controller": "RtH4qxVPL1Dpmdv7GytjBv", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF" + } + ], + "service": [ + { + "id": "RtH4qxVPL1Dpmdv7GytjBv#IndyAgentService", + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + "priority": 0, + "recipientKeys": ["EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF"], + "routingKeys": [] + } + ], + "authentication": [ + { + "publicKey": "RtH4qxVPL1Dpmdv7GytjBv#1", + "type": "Ed25519SignatureAuthentication2018" + } + ], + "id": "RtH4qxVPL1Dpmdv7GytjBv" + }, + "verkey": "EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF", + "theirDid": "Ak15GBhMYpdS8XX3QDMv31", + "theirDidDoc": { + "@context": "https://w3id.org/did/v1", + "publicKey": [ + { + "id": "Ak15GBhMYpdS8XX3QDMv31#1", + "controller": "Ak15GBhMYpdS8XX3QDMv31", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "6JwodG44KanZhhSvDS3dNmWjmWyeVFYRPxVaBntqmSCi" + } + ], + "service": [ + { + "id": "Ak15GBhMYpdS8XX3QDMv31#IndyAgentService", + "serviceEndpoint": "rxjs:faber", + "type": "IndyAgent", + "priority": 0, + "recipientKeys": ["6JwodG44KanZhhSvDS3dNmWjmWyeVFYRPxVaBntqmSCi"], + "routingKeys": [] + } + ], + "authentication": [ + { + "publicKey": "Ak15GBhMYpdS8XX3QDMv31#1", + "type": "Ed25519SignatureAuthentication2018" + } + ], + "id": "Ak15GBhMYpdS8XX3QDMv31" + }, + "theirLabel": "Agent: PopulateWallet2", + "state": "requested", + "role": "inviter", + "autoAcceptConnection": false, + "invitation": { + "@type": "https://didcomm.org/connections/1.0/invitation", + "@id": "21ef606f-b25b-48c6-bafa-e79193732413", + "label": "Agent: PopulateWallet", + "recipientKeys": ["EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF"], + "serviceEndpoint": "rxjs:alice", + "routingKeys": [] + }, + "threadId": "a0c0e4d2-1501-42a2-a09b-7d5adc90b353", + "multiUseInvitation": false + }, + "id": "7781341d-be29-441b-9b79-4a957d8c6d37", + "type": "ConnectionRecord", + "tags": { + "state": "requested", + "role": "inviter", + "invitationKey": "EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF", + "threadId": "a0c0e4d2-1501-42a2-a09b-7d5adc90b353", + "verkey": "EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF", + "theirKey": "6JwodG44KanZhhSvDS3dNmWjmWyeVFYRPxVaBntqmSCi", + "did": "RtH4qxVPL1Dpmdv7GytjBv", + "theirDid": "Ak15GBhMYpdS8XX3QDMv31" + } + }, + "ee88e2e1-e27e-46a6-a910-f87690109e32": { + "value": { + "metadata": {}, + "id": "ee88e2e1-e27e-46a6-a910-f87690109e32", + "createdAt": "2022-04-30T13:02:21.635Z", + "did": "WewvCdyBi4HL8ogyGviYVS", + "didDoc": { + "@context": "https://w3id.org/did/v1", + "publicKey": [ + { + "id": "WewvCdyBi4HL8ogyGviYVS#1", + "controller": "WewvCdyBi4HL8ogyGviYVS", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "HARupCd5jxffp7M74mbDFuEnsquRgh4oaXsswxWeZZd7" + } + ], + "service": [ + { + "id": "WewvCdyBi4HL8ogyGviYVS#IndyAgentService", + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + "priority": 0, + "recipientKeys": ["HARupCd5jxffp7M74mbDFuEnsquRgh4oaXsswxWeZZd7"], + "routingKeys": [] + } + ], + "authentication": [ + { + "publicKey": "WewvCdyBi4HL8ogyGviYVS#1", + "type": "Ed25519SignatureAuthentication2018" + } + ], + "id": "WewvCdyBi4HL8ogyGviYVS" + }, + "verkey": "HARupCd5jxffp7M74mbDFuEnsquRgh4oaXsswxWeZZd7", + "theirLabel": "Agent: PopulateWallet2", + "state": "requested", + "role": "invitee", + "invitation": { + "@type": "https://didcomm.org/connections/1.0/invitation", + "@id": "08eb8d8b-67cf-4ce2-9aca-c7d260a5c143", + "label": "Agent: PopulateWallet2", + "recipientKeys": ["8MN6LZnM8t1HmzMNw5Sp8kUVfQkFK1nCUMRSfQBoSNAC"], + "serviceEndpoint": "rxjs:faber", + "routingKeys": [] + }, + "multiUseInvitation": false + }, + "id": "ee88e2e1-e27e-46a6-a910-f87690109e32", + "type": "ConnectionRecord", + "tags": { + "invitationKey": "8MN6LZnM8t1HmzMNw5Sp8kUVfQkFK1nCUMRSfQBoSNAC", + "state": "requested", + "role": "invitee", + "verkey": "HARupCd5jxffp7M74mbDFuEnsquRgh4oaXsswxWeZZd7", + "did": "WewvCdyBi4HL8ogyGviYVS" + } + }, + "e3f9bc2b-f0a1-4a2c-ab81-2f0a3488c199": { + "value": { + "metadata": {}, + "id": "e3f9bc2b-f0a1-4a2c-ab81-2f0a3488c199", + "createdAt": "2022-04-30T13:02:21.641Z", + "did": "TMnQftvJJJwoYogYkQgVjg", + "didDoc": { + "@context": "https://w3id.org/did/v1", + "publicKey": [ + { + "id": "TMnQftvJJJwoYogYkQgVjg#1", + "controller": "TMnQftvJJJwoYogYkQgVjg", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "FNEqnwqHoU6WVmYkQFeosoaESjx8wCAzFpFdMdEg3iH7" + } + ], + "service": [ + { + "id": "TMnQftvJJJwoYogYkQgVjg#IndyAgentService", + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + "priority": 0, + "recipientKeys": ["FNEqnwqHoU6WVmYkQFeosoaESjx8wCAzFpFdMdEg3iH7"], + "routingKeys": [] + } + ], + "authentication": [ + { + "publicKey": "TMnQftvJJJwoYogYkQgVjg#1", + "type": "Ed25519SignatureAuthentication2018" + } + ], + "id": "TMnQftvJJJwoYogYkQgVjg" + }, + "verkey": "FNEqnwqHoU6WVmYkQFeosoaESjx8wCAzFpFdMdEg3iH7", + "theirDid": "9jTqUnV4k5ucxbyxumAaV7", + "theirDidDoc": { + "@context": "https://w3id.org/did/v1", + "publicKey": [ + { + "id": "9jTqUnV4k5ucxbyxumAaV7#1", + "controller": "9jTqUnV4k5ucxbyxumAaV7", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU" + } + ], + "service": [ + { + "id": "9jTqUnV4k5ucxbyxumAaV7#IndyAgentService", + "serviceEndpoint": "rxjs:faber", + "type": "IndyAgent", + "priority": 0, + "recipientKeys": ["5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU"], + "routingKeys": [] + } + ], + "authentication": [ + { + "publicKey": "9jTqUnV4k5ucxbyxumAaV7#1", + "type": "Ed25519SignatureAuthentication2018" + } + ], + "id": "9jTqUnV4k5ucxbyxumAaV7" + }, + "theirLabel": "Agent: PopulateWallet2", + "state": "responded", + "role": "invitee", + "autoAcceptConnection": false, + "invitation": { + "@type": "https://didcomm.org/connections/1.0/invitation", + "@id": "cc67fb5e-1414-4ba6-9030-7456ccd2aaea", + "label": "Agent: PopulateWallet2", + "recipientKeys": ["5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU"], + "serviceEndpoint": "rxjs:faber", + "routingKeys": [] + }, + "threadId": "daf3372c-1ee2-4246-a1f4-f62f54f7d68b", + "multiUseInvitation": false + }, + "id": "e3f9bc2b-f0a1-4a2c-ab81-2f0a3488c199", + "type": "ConnectionRecord", + "tags": { + "invitationKey": "5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU", + "state": "responded", + "role": "invitee", + "threadId": "daf3372c-1ee2-4246-a1f4-f62f54f7d68b", + "verkey": "FNEqnwqHoU6WVmYkQFeosoaESjx8wCAzFpFdMdEg3iH7", + "theirKey": "5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU", + "did": "TMnQftvJJJwoYogYkQgVjg", + "theirDid": "9jTqUnV4k5ucxbyxumAaV7" + } + }, + "da518433-0e55-4b74-a05b-aa75c1095a99": { + "value": { + "metadata": {}, + "id": "da518433-0e55-4b74-a05b-aa75c1095a99", + "createdAt": "2022-04-30T13:02:21.646Z", + "did": "GkEeb96MGT94K1HyQQzpj1", + "didDoc": { + "@context": "https://w3id.org/did/v1", + "publicKey": [ + { + "id": "GkEeb96MGT94K1HyQQzpj1#1", + "controller": "GkEeb96MGT94K1HyQQzpj1", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS" + } + ], + "service": [ + { + "id": "GkEeb96MGT94K1HyQQzpj1#IndyAgentService", + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + "priority": 0, + "recipientKeys": ["9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS"], + "routingKeys": [] + } + ], + "authentication": [ + { + "publicKey": "GkEeb96MGT94K1HyQQzpj1#1", + "type": "Ed25519SignatureAuthentication2018" + } + ], + "id": "GkEeb96MGT94K1HyQQzpj1" + }, + "verkey": "9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS", + "theirDid": "YKc7qhYN1TckZAMUf7jgwc", + "theirDidDoc": { + "@context": "https://w3id.org/did/v1", + "publicKey": [ + { + "id": "YKc7qhYN1TckZAMUf7jgwc#1", + "controller": "YKc7qhYN1TckZAMUf7jgwc", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "J57UsQT3wa4FcivfKpvjgUtaPDScZhFJ8kd5Q2iR5sBT" + } + ], + "service": [ + { + "id": "YKc7qhYN1TckZAMUf7jgwc#IndyAgentService", + "serviceEndpoint": "rxjs:faber", + "type": "IndyAgent", + "priority": 0, + "recipientKeys": ["J57UsQT3wa4FcivfKpvjgUtaPDScZhFJ8kd5Q2iR5sBT"], + "routingKeys": [] + } + ], + "authentication": [ + { + "publicKey": "YKc7qhYN1TckZAMUf7jgwc#1", + "type": "Ed25519SignatureAuthentication2018" + } + ], + "id": "YKc7qhYN1TckZAMUf7jgwc" + }, + "theirLabel": "Agent: PopulateWallet2", + "state": "responded", + "role": "inviter", + "autoAcceptConnection": true, + "invitation": { + "@type": "https://didcomm.org/connections/1.0/invitation", + "@id": "f0ca03d8-2e11-4ff2-a5fc-e0137a434b7e", + "label": "Agent: PopulateWallet", + "recipientKeys": ["9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS"], + "serviceEndpoint": "rxjs:alice", + "routingKeys": [] + }, + "threadId": "6eeb6a80-cd75-491d-b2e0-7bae65ced1c3", + "multiUseInvitation": false + }, + "id": "da518433-0e55-4b74-a05b-aa75c1095a99", + "type": "ConnectionRecord", + "tags": { + "state": "responded", + "role": "inviter", + "invitationKey": "9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS", + "threadId": "6eeb6a80-cd75-491d-b2e0-7bae65ced1c3", + "verkey": "9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS", + "theirKey": "J57UsQT3wa4FcivfKpvjgUtaPDScZhFJ8kd5Q2iR5sBT", + "did": "GkEeb96MGT94K1HyQQzpj1", + "theirDid": "YKc7qhYN1TckZAMUf7jgwc" + } + }, + "98260a2d-33df-450d-bc7c-c4305677978e": { + "value": { + "metadata": {}, + "id": "98260a2d-33df-450d-bc7c-c4305677978e", + "createdAt": "2022-04-20T13:02:21.646Z", + "did": "WSwJQMBHGZbQsq9LDBTWjX", + "didDoc": { + "@context": "https://w3id.org/did/v1", + "publicKey": [ + { + "id": "WSwJQMBHGZbQsq9LDBTWjX#1", + "controller": "WSwJQMBHGZbQsq9LDBTWjX", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "H3tENVV3HnfNi5tL9qYFh69CuzGG9skW4r8grYLZWXB3" + } + ], + "service": [ + { + "id": "WSwJQMBHGZbQsq9LDBTWjX#IndyAgentService", + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + "priority": 0, + "recipientKeys": ["H3tENVV3HnfNi5tL9qYFh69CuzGG9skW4r8grYLZWXB3"], + "routingKeys": [] + } + ], + "authentication": [ + { + "publicKey": "WSwJQMBHGZbQsq9LDBTWjX#1", + "type": "Ed25519SignatureAuthentication2018" + } + ], + "id": "WSwJQMBHGZbQsq9LDBTWjX" + }, + "verkey": "H3tENVV3HnfNi5tL9qYFh69CuzGG9skW4r8grYLZWXB3", + "state": "invited", + "role": "inviter", + "autoAcceptConnection": true, + "invitation": { + "@type": "https://didcomm.org/connections/1.0/invitation", + "@id": "f0ca03d8-2e11-4ff2-a5fc-e0137a434b7e", + "label": "Agent: PopulateWallet", + "recipientKeys": ["9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS"], + "serviceEndpoint": "rxjs:alice", + "routingKeys": [] + }, + "multiUseInvitation": true + }, + "id": "98260a2d-33df-450d-bc7c-c4305677978e", + "type": "ConnectionRecord", + "tags": { + "state": "invited", + "role": "inviter", + "invitationKey": "9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS", + "verkey": "H3tENVV3HnfNi5tL9qYFh69CuzGG9skW4r8grYLZWXB3", + "did": "WSwJQMBHGZbQsq9LDBTWjX" + } + }, + "b65c2ccd-277c-4140-9d87-c8dd30e7a98c": { + "value": { + "metadata": {}, + "id": "b65c2ccd-277c-4140-9d87-c8dd30e7a98c", + "createdAt": "2022-04-30T13:02:21.653Z", + "did": "Ud6AWCk6WrwfYKZUw5tJmt", + "didDoc": { + "@context": "https://w3id.org/did/v1", + "publicKey": [ + { + "id": "Ud6AWCk6WrwfYKZUw5tJmt#1", + "controller": "Ud6AWCk6WrwfYKZUw5tJmt", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe" + } + ], + "service": [ + { + "id": "Ud6AWCk6WrwfYKZUw5tJmt#IndyAgentService", + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + "priority": 0, + "recipientKeys": ["G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe"], + "routingKeys": [] + } + ], + "authentication": [ + { + "publicKey": "Ud6AWCk6WrwfYKZUw5tJmt#1", + "type": "Ed25519SignatureAuthentication2018" + } + ], + "id": "Ud6AWCk6WrwfYKZUw5tJmt" + }, + "verkey": "G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe", + "state": "invited", + "role": "inviter", + "autoAcceptConnection": true, + "invitation": { + "@type": "https://didcomm.org/connections/1.0/invitation", + "@id": "1f516e35-08d3-43d8-900c-99d5239f54da", + "label": "Agent: PopulateWallet", + "recipientKeys": ["G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe"], + "serviceEndpoint": "rxjs:alice", + "routingKeys": [] + }, + "multiUseInvitation": false + }, + "id": "b65c2ccd-277c-4140-9d87-c8dd30e7a98c", + "type": "ConnectionRecord", + "tags": { + "state": "invited", + "role": "inviter", + "invitationKey": "G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe", + "verkey": "G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe", + "did": "Ud6AWCk6WrwfYKZUw5tJmt" + } + } +} diff --git a/packages/core/src/storage/migration/__tests__/__fixtures__/alice-8-dids-0.2.json b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-8-dids-0.2.json new file mode 100644 index 0000000000..e6ee2718a1 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-8-dids-0.2.json @@ -0,0 +1,417 @@ +{ + "STORAGE_VERSION_RECORD_ID": { + "value": { + "metadata": {}, + "id": "STORAGE_VERSION_RECORD_ID", + "createdAt": "2022-09-08T19:35:53.872Z", + "storageVersion": "0.2" + }, + "id": "STORAGE_VERSION_RECORD_ID", + "type": "StorageVersionRecord", + "tags": {} + }, + "did:peer:1zQmWxKCTkKYQvzdUshMWy7b8vy3Y7uLtB9hp6BbQhKEtQCd": { + "value": { + "_tags": { + "role": "created", + "method": "peer", + "recipientKeyFingerprints": ["z6Mkpg6nDPuYdLMuzNEjaa2Xprh3J2MC1WtNakWUEFqC4wvU"] + }, + "metadata": {}, + "id": "did:peer:1zQmWxKCTkKYQvzdUshMWy7b8vy3Y7uLtB9hp6BbQhKEtQCd", + "role": "created", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmWxKCTkKYQvzdUshMWy7b8vy3Y7uLtB9hp6BbQhKEtQCd", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "ws://ssi.mediator.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#57a05508-1d1c-474c-8c68-1afcf3188720"], + "routingKeys": [ + "did:key:z6MkqhwtVT5V6kwsNkErPNgSWuRGd4QLFQ324FMfw5oWGHop#z6MkqhwtVT5V6kwsNkErPNgSWuRGd4QLFQ324FMfw5oWGHop" + ] + } + ], + "authentication": [ + { + "id": "#57a05508-1d1c-474c-8c68-1afcf3188720", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "BDqjd9f7HnsSssQ2u14gym93UT5Lbde1tjbYPysB9j96" + } + ], + "keyAgreement": [ + { + "id": "#59d2ce6f-c9fc-49c6-a8f6-eab14820b028", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "avivQP6GvWj6cBbxbXSSZnZTA4tGsvQ5DB9FXm45tZt" + } + ] + }, + "createdAt": "2022-12-27T13:51:21.344Z" + }, + "id": "did:peer:1zQmWxKCTkKYQvzdUshMWy7b8vy3Y7uLtB9hp6BbQhKEtQCd", + "type": "DidRecord", + "tags": { + "role": "created", + "method": "peer", + "recipientKeyFingerprints": ["z6Mkpg6nDPuYdLMuzNEjaa2Xprh3J2MC1WtNakWUEFqC4wvU"] + } + }, + "did:peer:1zQmavZzrLPRYRGd5CgyKV4GBZG43eGzjic9FHXc7jLHY14W": { + "value": { + "_tags": { + "recipientKeyFingerprints": ["z6MkuLL6bQykGpposTrQhfYceQRR4JDzvdm133xpwb7Dv1oz"], + "role": "received", + "method": "peer" + }, + "metadata": {}, + "id": "did:peer:1zQmavZzrLPRYRGd5CgyKV4GBZG43eGzjic9FHXc7jLHY14W", + "role": "received", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmavZzrLPRYRGd5CgyKV4GBZG43eGzjic9FHXc7jLHY14W", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "http://ssi.verifier.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#6173438e-09a5-4b1e-895a-0563f5a169b7"], + "routingKeys": [] + } + ], + "authentication": [ + { + "id": "#6173438e-09a5-4b1e-895a-0563f5a169b7", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "Ft541AjJwHLLky1i26amoJsREix9WkWeM33u7K9Czo2c" + } + ], + "keyAgreement": [ + { + "id": "#a0448ee3-093a-4b16-bc59-8bf8559d60a5", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "JCfZJ72mtgGE9xuekJKV6yoAGzLgQCNkdHKkct8uaNKb" + } + ] + }, + "createdAt": "2022-12-27T13:51:51.414Z" + }, + "id": "did:peer:1zQmavZzrLPRYRGd5CgyKV4GBZG43eGzjic9FHXc7jLHY14W", + "type": "DidRecord", + "tags": { + "role": "received", + "method": "peer", + "recipientKeyFingerprints": ["z6MkuLL6bQykGpposTrQhfYceQRR4JDzvdm133xpwb7Dv1oz"] + } + }, + "did:peer:1zQmZqQYzwqsYjj7z8kixxDXf9nux8TAsqj2izSpX1oCrd7q": { + "value": { + "_tags": { + "recipientKeyFingerprints": ["z6Mkv6Kd744JcBL5P9gi5Ddtehxn1gSMTgM9kbTdfQ9soB8G"], + "role": "created", + "method": "peer" + }, + "metadata": {}, + "id": "did:peer:1zQmZqQYzwqsYjj7z8kixxDXf9nux8TAsqj2izSpX1oCrd7q", + "role": "created", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmZqQYzwqsYjj7z8kixxDXf9nux8TAsqj2izSpX1oCrd7q", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "didcomm:transport/queue", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#d9d97b3e-5615-4e60-9366-a8ab1ec1a84d"], + "routingKeys": [] + } + ], + "authentication": [ + { + "id": "#d9d97b3e-5615-4e60-9366-a8ab1ec1a84d", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "Ge4aWoosGdqcGer1Peg3ocQnC7AW3o6o4aYhq8BrsxLt" + } + ], + "keyAgreement": [ + { + "id": "#c30ea91b-5f49-461f-b0bd-046f947ef668", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "HKBdBGRK8uxgCwge2QHPBzVuuayEQFhC2LM3g1fzMFGE" + } + ] + }, + "createdAt": "2022-12-27T13:50:32.815Z" + }, + "id": "did:peer:1zQmZqQYzwqsYjj7z8kixxDXf9nux8TAsqj2izSpX1oCrd7q", + "type": "DidRecord", + "tags": { + "role": "created", + "method": "peer", + "recipientKeyFingerprints": ["z6Mkv6Kd744JcBL5P9gi5Ddtehxn1gSMTgM9kbTdfQ9soB8G"] + } + }, + "did:peer:1zQmRnGYmYBdSinH2953ZgfHWEpqp5W6kJmmYBgRaCs4Yudx": { + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": ["z6MkvcuHjVcNRBM78E3xWy7MnVqQV9hAvTawBsPmg3bR1DaE"], + "role": "created" + }, + "metadata": {}, + "id": "did:peer:1zQmRnGYmYBdSinH2953ZgfHWEpqp5W6kJmmYBgRaCs4Yudx", + "role": "created", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmRnGYmYBdSinH2953ZgfHWEpqp5W6kJmmYBgRaCs4Yudx", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "ws://ssi.mediator.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#22219a28-b52a-4024-bc0f-62d3969131fd"], + "routingKeys": [ + "did:key:z6MkqhwtVT5V6kwsNkErPNgSWuRGd4QLFQ324FMfw5oWGHop#z6MkqhwtVT5V6kwsNkErPNgSWuRGd4QLFQ324FMfw5oWGHop" + ] + } + ], + "authentication": [ + { + "id": "#22219a28-b52a-4024-bc0f-62d3969131fd", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "HAeF9FMw5dre1jDFqQ9WwQHQfaRKWaLaVrUqqmdQ5znr" + } + ], + "keyAgreement": [ + { + "id": "#0f9535d1-9c9c-4491-be1e-1628f365b513", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "FztF8HCahTeL9gYHoHnDFo6HruwnKB19ZtbHFbLndAmE" + } + ] + }, + "createdAt": "2022-12-27T13:51:50.193Z" + }, + "id": "did:peer:1zQmRnGYmYBdSinH2953ZgfHWEpqp5W6kJmmYBgRaCs4Yudx", + "type": "DidRecord", + "tags": { + "role": "created", + "method": "peer", + "recipientKeyFingerprints": ["z6MkvcuHjVcNRBM78E3xWy7MnVqQV9hAvTawBsPmg3bR1DaE"] + } + }, + "did:peer:1zQmb9oo8fn8AZ7rWh1DAfTbM3mnPp9Hq2bJFdHiqjnZX2Ce": { + "value": { + "_tags": { + "method": "peer", + "role": "received", + "recipientKeyFingerprints": ["z6Mkp1wyxzqoUrZ3TvnerfZBq48o1XLDSPH8ibFALkNABSgh"] + }, + "metadata": {}, + "id": "did:peer:1zQmb9oo8fn8AZ7rWh1DAfTbM3mnPp9Hq2bJFdHiqjnZX2Ce", + "role": "received", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmb9oo8fn8AZ7rWh1DAfTbM3mnPp9Hq2bJFdHiqjnZX2Ce", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "ws://ssi.mediator.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#075b7e8b-a7bd-41e1-9b01-044f1ccab1a6"], + "routingKeys": [] + } + ], + "authentication": [ + { + "id": "#075b7e8b-a7bd-41e1-9b01-044f1ccab1a6", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "AZgwNkbN9K4aMRwxB6bLyxaoBx4N2W2n2aLEWUQ9GDuK" + } + ], + "keyAgreement": [ + { + "id": "#bf888dc5-0f54-4e71-9058-c43bebfc7d01", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "3Q8wpgxdCVdJfznYERZR1r9eVnCy7oxpjzGVucDLF2tG" + } + ] + }, + "createdAt": "2022-12-27T13:50:44.957Z" + }, + "id": "did:peer:1zQmb9oo8fn8AZ7rWh1DAfTbM3mnPp9Hq2bJFdHiqjnZX2Ce", + "type": "DidRecord", + "tags": { + "role": "received", + "method": "peer", + "recipientKeyFingerprints": ["z6Mkp1wyxzqoUrZ3TvnerfZBq48o1XLDSPH8ibFALkNABSgh"] + } + }, + "did:peer:1zQmSzC6uhWYcxtkr2fBepr66kjvSHuRsSfbmei7nrGj7HfN": { + "value": { + "_tags": { + "role": "received", + "recipientKeyFingerprints": ["z6MkrQzQqPtNjJHGLGjWC7H3mjMSNA36bCBTYJeoR44JoFvH"], + "method": "peer" + }, + "metadata": {}, + "id": "did:peer:1zQmSzC6uhWYcxtkr2fBepr66kjvSHuRsSfbmei7nrGj7HfN", + "role": "received", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmSzC6uhWYcxtkr2fBepr66kjvSHuRsSfbmei7nrGj7HfN", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "ws://ssi.issuer.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#b6d349fb-93fb-4298-b57a-3f2fecffccd8"], + "routingKeys": [] + } + ], + "authentication": [ + { + "id": "#b6d349fb-93fb-4298-b57a-3f2fecffccd8", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "CxjNF9dwPknoDmtoWYKCvdoSYamFBJw6rHjsan6Ht38u" + } + ], + "keyAgreement": [ + { + "id": "#c0917f9f-1102-4f13-800c-ff7b522999eb", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "Eso3A6AmL5qiWr9syqHx8NgdQ8EMkYcitmrW5G7bX9uU" + } + ] + }, + "createdAt": "2022-12-27T13:50:34.057Z" + }, + "id": "did:peer:1zQmSzC6uhWYcxtkr2fBepr66kjvSHuRsSfbmei7nrGj7HfN", + "type": "DidRecord", + "tags": { + "role": "received", + "method": "peer", + "recipientKeyFingerprints": ["z6MkrQzQqPtNjJHGLGjWC7H3mjMSNA36bCBTYJeoR44JoFvH"] + } + }, + "did:peer:1zQmPaCELen1JWMWmhVZS16nDmrAC9yGKQcJCcBs5spXakA3": { + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": ["z6Mkt4ohp4uT74HdENeGVGAyVFTerCzzuDXP8kpU3yo6SqqM"], + "role": "received" + }, + "metadata": {}, + "id": "did:peer:1zQmPaCELen1JWMWmhVZS16nDmrAC9yGKQcJCcBs5spXakA3", + "role": "received", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmPaCELen1JWMWmhVZS16nDmrAC9yGKQcJCcBs5spXakA3", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "http://ssi.verifier.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#5ea98568-dfcd-4614-9495-ba95ec2665d3"], + "routingKeys": [] + } + ], + "authentication": [ + { + "id": "#5ea98568-dfcd-4614-9495-ba95ec2665d3", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "EcYfDpf1mWoA7soZohD8e9uf2dj9VLH2SjuYDhq5Xd3y" + } + ], + "keyAgreement": [ + { + "id": "#506e5ead-ddbc-44ef-848b-6593a692a916", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "EeQHhb6CqWGrQR9PfWpS1L8CedsbK2mZfPdaxaHN4s8b" + } + ] + }, + "createdAt": "2022-12-27T13:51:22.817Z" + }, + "id": "did:peer:1zQmPaCELen1JWMWmhVZS16nDmrAC9yGKQcJCcBs5spXakA3", + "type": "DidRecord", + "tags": { + "role": "received", + "method": "peer", + "recipientKeyFingerprints": ["z6Mkt4ohp4uT74HdENeGVGAyVFTerCzzuDXP8kpU3yo6SqqM"] + } + }, + "did:peer:1zQmUo1HoiciJS628w4SHweg4Pzs4bZLM4KLZgroSmUwBw7S": { + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": ["z6MktdgEKUiH5kt5t2dKAH14WzFYhzj3JFobUf2PFFQAg2ma"], + "role": "created" + }, + "metadata": {}, + "id": "did:peer:1zQmUo1HoiciJS628w4SHweg4Pzs4bZLM4KLZgroSmUwBw7S", + "role": "created", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmUo1HoiciJS628w4SHweg4Pzs4bZLM4KLZgroSmUwBw7S", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "didcomm:transport/queue", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#12b8b7d4-87b9-4638-a929-f98df2f1f566"], + "routingKeys": [] + } + ], + "authentication": [ + { + "id": "#12b8b7d4-87b9-4638-a929-f98df2f1f566", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "FBRBjETqkDPcmXncUi3DfthYtRTBtNZEne7TQyS9kozC" + } + ], + "keyAgreement": [ + { + "id": "#77b9cf84-2441-419f-b295-945d06e29edc", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "EgsQArrmCUru9MxR1RNNiomnMFz6E3ia2GfjVvoCjAWY" + } + ] + }, + "createdAt": "2022-12-27T13:50:43.937Z" + }, + "id": "did:peer:1zQmUo1HoiciJS628w4SHweg4Pzs4bZLM4KLZgroSmUwBw7S", + "type": "DidRecord", + "tags": { + "role": "created", + "method": "peer", + "recipientKeyFingerprints": ["z6MktdgEKUiH5kt5t2dKAH14WzFYhzj3JFobUf2PFFQAg2ma"] + } + } +} diff --git a/packages/core/src/storage/migration/__tests__/__fixtures__/alice-8-dids-0.3.json b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-8-dids-0.3.json new file mode 100644 index 0000000000..c0a6597833 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-8-dids-0.3.json @@ -0,0 +1,417 @@ +{ + "STORAGE_VERSION_RECORD_ID": { + "value": { + "metadata": {}, + "id": "STORAGE_VERSION_RECORD_ID", + "createdAt": "2022-09-08T19:35:53.872Z", + "storageVersion": "0.3" + }, + "id": "STORAGE_VERSION_RECORD_ID", + "type": "StorageVersionRecord", + "tags": {} + }, + "did:peer:1zQmWxKCTkKYQvzdUshMWy7b8vy3Y7uLtB9hp6BbQhKEtQCd": { + "value": { + "_tags": { + "role": "created", + "method": "peer", + "recipientKeyFingerprints": ["z6Mkpg6nDPuYdLMuzNEjaa2Xprh3J2MC1WtNakWUEFqC4wvU"] + }, + "metadata": {}, + "id": "did:peer:1zQmWxKCTkKYQvzdUshMWy7b8vy3Y7uLtB9hp6BbQhKEtQCd", + "role": "created", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmWxKCTkKYQvzdUshMWy7b8vy3Y7uLtB9hp6BbQhKEtQCd", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "ws://ssi.mediator.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#57a05508-1d1c-474c-8c68-1afcf3188720"], + "routingKeys": [ + "did:key:z6MkqhwtVT5V6kwsNkErPNgSWuRGd4QLFQ324FMfw5oWGHop#z6MkqhwtVT5V6kwsNkErPNgSWuRGd4QLFQ324FMfw5oWGHop" + ] + } + ], + "authentication": [ + { + "id": "#57a05508-1d1c-474c-8c68-1afcf3188720", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "BDqjd9f7HnsSssQ2u14gym93UT5Lbde1tjbYPysB9j96" + } + ], + "keyAgreement": [ + { + "id": "#59d2ce6f-c9fc-49c6-a8f6-eab14820b028", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "avivQP6GvWj6cBbxbXSSZnZTA4tGsvQ5DB9FXm45tZt" + } + ] + }, + "createdAt": "2022-12-27T13:51:21.344Z" + }, + "id": "did:peer:1zQmWxKCTkKYQvzdUshMWy7b8vy3Y7uLtB9hp6BbQhKEtQCd", + "type": "DidRecord", + "tags": { + "role": "created", + "method": "peer", + "recipientKeyFingerprints": ["z6Mkpg6nDPuYdLMuzNEjaa2Xprh3J2MC1WtNakWUEFqC4wvU"] + } + }, + "did:peer:1zQmavZzrLPRYRGd5CgyKV4GBZG43eGzjic9FHXc7jLHY14W": { + "value": { + "_tags": { + "recipientKeyFingerprints": ["z6MkuLL6bQykGpposTrQhfYceQRR4JDzvdm133xpwb7Dv1oz"], + "role": "received", + "method": "peer" + }, + "metadata": {}, + "id": "did:peer:1zQmavZzrLPRYRGd5CgyKV4GBZG43eGzjic9FHXc7jLHY14W", + "role": "received", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmavZzrLPRYRGd5CgyKV4GBZG43eGzjic9FHXc7jLHY14W", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "http://ssi.verifier.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#6173438e-09a5-4b1e-895a-0563f5a169b7"], + "routingKeys": [] + } + ], + "authentication": [ + { + "id": "#6173438e-09a5-4b1e-895a-0563f5a169b7", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "Ft541AjJwHLLky1i26amoJsREix9WkWeM33u7K9Czo2c" + } + ], + "keyAgreement": [ + { + "id": "#a0448ee3-093a-4b16-bc59-8bf8559d60a5", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "JCfZJ72mtgGE9xuekJKV6yoAGzLgQCNkdHKkct8uaNKb" + } + ] + }, + "createdAt": "2022-12-27T13:51:51.414Z" + }, + "id": "did:peer:1zQmavZzrLPRYRGd5CgyKV4GBZG43eGzjic9FHXc7jLHY14W", + "type": "DidRecord", + "tags": { + "role": "received", + "method": "peer", + "recipientKeyFingerprints": ["z6MkuLL6bQykGpposTrQhfYceQRR4JDzvdm133xpwb7Dv1oz"] + } + }, + "did:peer:1zQmZqQYzwqsYjj7z8kixxDXf9nux8TAsqj2izSpX1oCrd7q": { + "value": { + "_tags": { + "recipientKeyFingerprints": ["z6Mkv6Kd744JcBL5P9gi5Ddtehxn1gSMTgM9kbTdfQ9soB8G"], + "role": "created", + "method": "peer" + }, + "metadata": {}, + "id": "did:peer:1zQmZqQYzwqsYjj7z8kixxDXf9nux8TAsqj2izSpX1oCrd7q", + "role": "created", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmZqQYzwqsYjj7z8kixxDXf9nux8TAsqj2izSpX1oCrd7q", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "didcomm:transport/queue", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#d9d97b3e-5615-4e60-9366-a8ab1ec1a84d"], + "routingKeys": [] + } + ], + "authentication": [ + { + "id": "#d9d97b3e-5615-4e60-9366-a8ab1ec1a84d", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "Ge4aWoosGdqcGer1Peg3ocQnC7AW3o6o4aYhq8BrsxLt" + } + ], + "keyAgreement": [ + { + "id": "#c30ea91b-5f49-461f-b0bd-046f947ef668", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "HKBdBGRK8uxgCwge2QHPBzVuuayEQFhC2LM3g1fzMFGE" + } + ] + }, + "createdAt": "2022-12-27T13:50:32.815Z" + }, + "id": "did:peer:1zQmZqQYzwqsYjj7z8kixxDXf9nux8TAsqj2izSpX1oCrd7q", + "type": "DidRecord", + "tags": { + "role": "created", + "method": "peer", + "recipientKeyFingerprints": ["z6Mkv6Kd744JcBL5P9gi5Ddtehxn1gSMTgM9kbTdfQ9soB8G"] + } + }, + "did:peer:1zQmRnGYmYBdSinH2953ZgfHWEpqp5W6kJmmYBgRaCs4Yudx": { + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": ["z6MkvcuHjVcNRBM78E3xWy7MnVqQV9hAvTawBsPmg3bR1DaE"], + "role": "created" + }, + "metadata": {}, + "id": "did:peer:1zQmRnGYmYBdSinH2953ZgfHWEpqp5W6kJmmYBgRaCs4Yudx", + "role": "created", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmRnGYmYBdSinH2953ZgfHWEpqp5W6kJmmYBgRaCs4Yudx", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "ws://ssi.mediator.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#22219a28-b52a-4024-bc0f-62d3969131fd"], + "routingKeys": [ + "did:key:z6MkqhwtVT5V6kwsNkErPNgSWuRGd4QLFQ324FMfw5oWGHop#z6MkqhwtVT5V6kwsNkErPNgSWuRGd4QLFQ324FMfw5oWGHop" + ] + } + ], + "authentication": [ + { + "id": "#22219a28-b52a-4024-bc0f-62d3969131fd", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "HAeF9FMw5dre1jDFqQ9WwQHQfaRKWaLaVrUqqmdQ5znr" + } + ], + "keyAgreement": [ + { + "id": "#0f9535d1-9c9c-4491-be1e-1628f365b513", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "FztF8HCahTeL9gYHoHnDFo6HruwnKB19ZtbHFbLndAmE" + } + ] + }, + "createdAt": "2022-12-27T13:51:50.193Z" + }, + "id": "did:peer:1zQmRnGYmYBdSinH2953ZgfHWEpqp5W6kJmmYBgRaCs4Yudx", + "type": "DidRecord", + "tags": { + "role": "created", + "method": "peer", + "recipientKeyFingerprints": ["z6MkvcuHjVcNRBM78E3xWy7MnVqQV9hAvTawBsPmg3bR1DaE"] + } + }, + "did:peer:1zQmb9oo8fn8AZ7rWh1DAfTbM3mnPp9Hq2bJFdHiqjnZX2Ce": { + "value": { + "_tags": { + "method": "peer", + "role": "received", + "recipientKeyFingerprints": ["z6Mkp1wyxzqoUrZ3TvnerfZBq48o1XLDSPH8ibFALkNABSgh"] + }, + "metadata": {}, + "id": "did:peer:1zQmb9oo8fn8AZ7rWh1DAfTbM3mnPp9Hq2bJFdHiqjnZX2Ce", + "role": "received", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmb9oo8fn8AZ7rWh1DAfTbM3mnPp9Hq2bJFdHiqjnZX2Ce", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "ws://ssi.mediator.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#075b7e8b-a7bd-41e1-9b01-044f1ccab1a6"], + "routingKeys": [] + } + ], + "authentication": [ + { + "id": "#075b7e8b-a7bd-41e1-9b01-044f1ccab1a6", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "AZgwNkbN9K4aMRwxB6bLyxaoBx4N2W2n2aLEWUQ9GDuK" + } + ], + "keyAgreement": [ + { + "id": "#bf888dc5-0f54-4e71-9058-c43bebfc7d01", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "3Q8wpgxdCVdJfznYERZR1r9eVnCy7oxpjzGVucDLF2tG" + } + ] + }, + "createdAt": "2022-12-27T13:50:44.957Z" + }, + "id": "did:peer:1zQmb9oo8fn8AZ7rWh1DAfTbM3mnPp9Hq2bJFdHiqjnZX2Ce", + "type": "DidRecord", + "tags": { + "role": "received", + "method": "peer", + "recipientKeyFingerprints": ["z6Mkp1wyxzqoUrZ3TvnerfZBq48o1XLDSPH8ibFALkNABSgh"] + } + }, + "did:peer:1zQmSzC6uhWYcxtkr2fBepr66kjvSHuRsSfbmei7nrGj7HfN": { + "value": { + "_tags": { + "role": "received", + "recipientKeyFingerprints": ["z6MkrQzQqPtNjJHGLGjWC7H3mjMSNA36bCBTYJeoR44JoFvH"], + "method": "peer" + }, + "metadata": {}, + "id": "did:peer:1zQmSzC6uhWYcxtkr2fBepr66kjvSHuRsSfbmei7nrGj7HfN", + "role": "received", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmSzC6uhWYcxtkr2fBepr66kjvSHuRsSfbmei7nrGj7HfN", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "ws://ssi.issuer.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#b6d349fb-93fb-4298-b57a-3f2fecffccd8"], + "routingKeys": [] + } + ], + "authentication": [ + { + "id": "#b6d349fb-93fb-4298-b57a-3f2fecffccd8", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "CxjNF9dwPknoDmtoWYKCvdoSYamFBJw6rHjsan6Ht38u" + } + ], + "keyAgreement": [ + { + "id": "#c0917f9f-1102-4f13-800c-ff7b522999eb", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "Eso3A6AmL5qiWr9syqHx8NgdQ8EMkYcitmrW5G7bX9uU" + } + ] + }, + "createdAt": "2022-12-27T13:50:34.057Z" + }, + "id": "did:peer:1zQmSzC6uhWYcxtkr2fBepr66kjvSHuRsSfbmei7nrGj7HfN", + "type": "DidRecord", + "tags": { + "role": "received", + "method": "peer", + "recipientKeyFingerprints": ["z6MkrQzQqPtNjJHGLGjWC7H3mjMSNA36bCBTYJeoR44JoFvH"] + } + }, + "did:peer:1zQmPaCELen1JWMWmhVZS16nDmrAC9yGKQcJCcBs5spXakA3": { + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": ["z6Mkt4ohp4uT74HdENeGVGAyVFTerCzzuDXP8kpU3yo6SqqM"], + "role": "received" + }, + "metadata": {}, + "id": "did:peer:1zQmPaCELen1JWMWmhVZS16nDmrAC9yGKQcJCcBs5spXakA3", + "role": "received", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmPaCELen1JWMWmhVZS16nDmrAC9yGKQcJCcBs5spXakA3", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "http://ssi.verifier.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#5ea98568-dfcd-4614-9495-ba95ec2665d3"], + "routingKeys": [] + } + ], + "authentication": [ + { + "id": "#5ea98568-dfcd-4614-9495-ba95ec2665d3", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "EcYfDpf1mWoA7soZohD8e9uf2dj9VLH2SjuYDhq5Xd3y" + } + ], + "keyAgreement": [ + { + "id": "#506e5ead-ddbc-44ef-848b-6593a692a916", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "EeQHhb6CqWGrQR9PfWpS1L8CedsbK2mZfPdaxaHN4s8b" + } + ] + }, + "createdAt": "2022-12-27T13:51:22.817Z" + }, + "id": "did:peer:1zQmPaCELen1JWMWmhVZS16nDmrAC9yGKQcJCcBs5spXakA3", + "type": "DidRecord", + "tags": { + "role": "received", + "method": "peer", + "recipientKeyFingerprints": ["z6Mkt4ohp4uT74HdENeGVGAyVFTerCzzuDXP8kpU3yo6SqqM"] + } + }, + "did:peer:1zQmUo1HoiciJS628w4SHweg4Pzs4bZLM4KLZgroSmUwBw7S": { + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": ["z6MktdgEKUiH5kt5t2dKAH14WzFYhzj3JFobUf2PFFQAg2ma"], + "role": "created" + }, + "metadata": {}, + "id": "did:peer:1zQmUo1HoiciJS628w4SHweg4Pzs4bZLM4KLZgroSmUwBw7S", + "role": "created", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmUo1HoiciJS628w4SHweg4Pzs4bZLM4KLZgroSmUwBw7S", + "service": [ + { + "id": "#inline-0", + "serviceEndpoint": "didcomm:transport/queue", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#12b8b7d4-87b9-4638-a929-f98df2f1f566"], + "routingKeys": [] + } + ], + "authentication": [ + { + "id": "#12b8b7d4-87b9-4638-a929-f98df2f1f566", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "FBRBjETqkDPcmXncUi3DfthYtRTBtNZEne7TQyS9kozC" + } + ], + "keyAgreement": [ + { + "id": "#77b9cf84-2441-419f-b295-945d06e29edc", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "EgsQArrmCUru9MxR1RNNiomnMFz6E3ia2GfjVvoCjAWY" + } + ] + }, + "createdAt": "2022-12-27T13:50:43.937Z" + }, + "id": "did:peer:1zQmUo1HoiciJS628w4SHweg4Pzs4bZLM4KLZgroSmUwBw7S", + "type": "DidRecord", + "tags": { + "role": "created", + "method": "peer", + "recipientKeyFingerprints": ["z6MktdgEKUiH5kt5t2dKAH14WzFYhzj3JFobUf2PFFQAg2ma"] + } + } +} diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap new file mode 100644 index 0000000000..d39faf48e5 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap @@ -0,0 +1,3679 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UpdateAssistant | v0.1 - v0.2 should correctly update credential records and create didcomm records 1`] = ` +{ + "1-4e4f-41d9-94c4-f49351b811f1": { + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "messageId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/offer-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9", + }, + "mime-type": "application/json", + }, + ], + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "10-4e4f-41d9-94c4-f49351b811f1": { + "id": "10-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "messageId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/offer-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "10-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=", + }, + "mime-type": "application/json", + }, + ], + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "11-4e4f-41d9-94c4-f49351b811f1": { + "id": "11-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "messageId": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/request-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "11-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "12-4e4f-41d9-94c4-f49351b811f1": { + "id": "12-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "messageId": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/issue-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "12-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": {}, + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "2-4e4f-41d9-94c4-f49351b811f1": { + "id": "2-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "messageId": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/request-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "2-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "3-4e4f-41d9-94c4-f49351b811f1": { + "id": "3-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "messageId": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/issue-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "3-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": {}, + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "4-4e4f-41d9-94c4-f49351b811f1": { + "id": "4-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "messageId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/offer-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "4-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=", + }, + "mime-type": "application/json", + }, + ], + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "5-4e4f-41d9-94c4-f49351b811f1": { + "id": "5-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "messageId": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/request-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "5-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a": { + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "tags": { + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "credentialIds": [], + "parentThreadId": undefined, + "role": undefined, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "CredentialRecord", + "value": { + "autoAcceptCredential": "contentApproved", + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "createdAt": "2022-03-21T22:50:20.522Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [], + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "protocolVersion": "v1", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a": { + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "tags": { + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "credentialIds": [ + "a77114e1-c812-4bff-a53c-3d5003fcc278", + ], + "parentThreadId": undefined, + "role": undefined, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "CredentialRecord", + "value": { + "autoAcceptCredential": "contentApproved", + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "createdAt": "2022-03-21T22:50:20.535Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [ + { + "credentialRecordId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "credentialRecordType": "indy", + }, + ], + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": { + "master_secret_blinding_data": { + "v_prime": "36456944381549782028917743247126995038265466209293312755125557271456380841610111892515020379470931691048072348420844231863825225515560265358581756565441268878364665494094789024845049226122885121039335781567964878826549149370097276812152226343824116049855825405977949749345353074025294938300401262824951638782220004732873597724698990420932910079362747837952520524827009393981876443737452031919055976088763615615890946142630576421462920865811255312740184209214306243871230276622595183415487741608569800898909023830922654063814555128779494528740438076748829436757078504882332589744263200806138145494157659396691564807976032319024007464003538934", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "373984270150786864433163", + }, + }, + "protocolVersion": "v1", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "6-4e4f-41d9-94c4-f49351b811f1": { + "id": "6-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "messageId": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/issue-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "6-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": {}, + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "7-4e4f-41d9-94c4-f49351b811f1": { + "id": "7-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "messageId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/offer-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "7-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9", + }, + "mime-type": "application/json", + }, + ], + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "8-4e4f-41d9-94c4-f49351b811f1": { + "id": "8-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "messageId": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/request-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "8-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "9-4e4f-41d9-94c4-f49351b811f1": { + "id": "9-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "messageId": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/issue-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "9-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": {}, + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7": { + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "tags": { + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "credentialIds": [], + "parentThreadId": undefined, + "role": undefined, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "CredentialRecord", + "value": { + "autoAcceptCredential": "contentApproved", + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "createdAt": "2022-03-21T22:50:20.740Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [], + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "protocolVersion": "v1", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c": { + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "tags": { + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "credentialIds": [ + "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + ], + "parentThreadId": undefined, + "role": undefined, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "CredentialRecord", + "value": { + "autoAcceptCredential": "contentApproved", + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "createdAt": "2022-03-21T22:50:20.746Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [ + { + "credentialRecordId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "credentialRecordType": "indy", + }, + ], + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": { + "master_secret_blinding_data": { + "v_prime": "24405223168730122709164916892481085040205443709643249329100687534344659826655374235392514476392517756663433844139774514430993889493707631169979521764390851593418941181409704266182779162417466204970949168472702858363964258641437554267668466400711344128132909691514606077477555576087059339291048485225394874964325220472232903203038212033940680060605090839733163438385288769519855418153181511119637865605476043416048121313638627002888436809192752657860306784733123742838413845299796745569824223645588826964796075250758249133953560017373025169692866449286962430731916293683231375510684692358406054381559324718715654332979447698704161714028193478", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "698370616023883730498375", + }, + }, + "protocolVersion": "v1", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection record and create the did and oob records 1`] = ` +{ + "1-4e4f-41d9-94c4-f49351b811f1": { + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "invitationId": "d56fd7af-852e-458e-b750-7a4f4e53d6e6", + "invitationRequestsThreadIds": undefined, + "recipientKeyFingerprints": [ + "z6MkfiPMPxCQeSDZGMkCvm1Y2rBoPsmw4ZHMv71jXtcWRRiM", + ], + "role": "receiver", + "state": "done", + "threadId": "d56fd7af-852e-458e-b750-7a4f4e53d6e6", + }, + "type": "OutOfBandRecord", + "value": { + "_tags": { + "recipientKeyFingerprints": [ + "z6MkfiPMPxCQeSDZGMkCvm1Y2rBoPsmw4ZHMv71jXtcWRRiM", + ], + }, + "alias": "connection alias", + "autoAcceptConnection": undefined, + "createdAt": "2022-04-30T13:02:21.577Z", + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "mediatorId": undefined, + "metadata": {}, + "outOfBandInvitation": { + "@id": "d56fd7af-852e-458e-b750-7a4f4e53d6e6", + "@type": "https://didcomm.org/out-of-band/1.1/invitation", + "accept": [ + "didcomm/aip1", + "didcomm/aip2;env=rfc19", + ], + "goal": undefined, + "goal_code": undefined, + "handshake_protocols": [ + "https://didcomm.org/connections/1.0", + ], + "imageUrl": undefined, + "label": "Agent: PopulateWallet2", + "requests~attach": undefined, + "services": [ + { + "accept": undefined, + "id": "#inline", + "recipientKeys": [ + "did:key:z6MkfiPMPxCQeSDZGMkCvm1Y2rBoPsmw4ZHMv71jXtcWRRiM", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:faber", + "type": "did-communication", + }, + ], + "~attach": undefined, + "~l10n": undefined, + "~please_ack": undefined, + "~service": undefined, + "~thread": undefined, + "~timing": undefined, + "~transport": undefined, + }, + "reusable": false, + "reuseConnectionId": undefined, + "role": "receiver", + "state": "done", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "2-4e4f-41d9-94c4-f49351b811f1": { + "id": "2-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "invitationId": "d939d371-3155-4d9c-87d1-46447f624f44", + "invitationRequestsThreadIds": undefined, + "recipientKeyFingerprints": [ + "z6MktCZAQNGvWb4WHAjwBqPtXhZdDYorbSJkGW9vj1uhw1HD", + ], + "role": "sender", + "state": "done", + "threadId": "d939d371-3155-4d9c-87d1-46447f624f44", + }, + "type": "OutOfBandRecord", + "value": { + "_tags": { + "recipientKeyFingerprints": [ + "z6MktCZAQNGvWb4WHAjwBqPtXhZdDYorbSJkGW9vj1uhw1HD", + ], + }, + "alias": undefined, + "autoAcceptConnection": undefined, + "createdAt": "2022-04-30T13:02:21.608Z", + "id": "2-4e4f-41d9-94c4-f49351b811f1", + "mediatorId": undefined, + "metadata": {}, + "outOfBandInvitation": { + "@id": "d939d371-3155-4d9c-87d1-46447f624f44", + "@type": "https://didcomm.org/out-of-band/1.1/invitation", + "accept": [ + "didcomm/aip1", + "didcomm/aip2;env=rfc19", + ], + "goal": undefined, + "goal_code": undefined, + "handshake_protocols": [ + "https://didcomm.org/connections/1.0", + ], + "imageUrl": undefined, + "label": "Agent: PopulateWallet", + "requests~attach": undefined, + "services": [ + { + "accept": undefined, + "id": "#inline", + "recipientKeys": [ + "did:key:z6MktCZAQNGvWb4WHAjwBqPtXhZdDYorbSJkGW9vj1uhw1HD", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:alice", + "type": "did-communication", + }, + ], + "~attach": undefined, + "~l10n": undefined, + "~please_ack": undefined, + "~service": undefined, + "~thread": undefined, + "~timing": undefined, + "~transport": undefined, + }, + "reusable": false, + "reuseConnectionId": undefined, + "role": "sender", + "state": "done", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "3-4e4f-41d9-94c4-f49351b811f1": { + "id": "3-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "invitationId": "21ef606f-b25b-48c6-bafa-e79193732413", + "invitationRequestsThreadIds": undefined, + "recipientKeyFingerprints": [ + "z6Mkt1tsp15cnDD7wBCFgehiR2SxHX1aPxt4sueE24twH9Bd", + ], + "role": "sender", + "state": "done", + "threadId": "21ef606f-b25b-48c6-bafa-e79193732413", + }, + "type": "OutOfBandRecord", + "value": { + "_tags": { + "recipientKeyFingerprints": [ + "z6Mkt1tsp15cnDD7wBCFgehiR2SxHX1aPxt4sueE24twH9Bd", + ], + }, + "alias": undefined, + "autoAcceptConnection": false, + "createdAt": "2022-04-30T13:02:21.628Z", + "id": "3-4e4f-41d9-94c4-f49351b811f1", + "mediatorId": undefined, + "metadata": {}, + "outOfBandInvitation": { + "@id": "21ef606f-b25b-48c6-bafa-e79193732413", + "@type": "https://didcomm.org/out-of-band/1.1/invitation", + "accept": [ + "didcomm/aip1", + "didcomm/aip2;env=rfc19", + ], + "goal": undefined, + "goal_code": undefined, + "handshake_protocols": [ + "https://didcomm.org/connections/1.0", + ], + "imageUrl": undefined, + "label": "Agent: PopulateWallet", + "requests~attach": undefined, + "services": [ + { + "accept": undefined, + "id": "#inline", + "recipientKeys": [ + "did:key:z6Mkt1tsp15cnDD7wBCFgehiR2SxHX1aPxt4sueE24twH9Bd", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:alice", + "type": "did-communication", + }, + ], + "~attach": undefined, + "~l10n": undefined, + "~please_ack": undefined, + "~service": undefined, + "~thread": undefined, + "~timing": undefined, + "~transport": undefined, + }, + "reusable": false, + "reuseConnectionId": undefined, + "role": "sender", + "state": "done", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "4-4e4f-41d9-94c4-f49351b811f1": { + "id": "4-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "invitationId": "08eb8d8b-67cf-4ce2-9aca-c7d260a5c143", + "invitationRequestsThreadIds": undefined, + "recipientKeyFingerprints": [ + "z6Mkmod8vp2nURVktVC5ceQeyr2VUz26iu2ZANLNVg9pMawa", + ], + "role": "receiver", + "state": "done", + "threadId": "08eb8d8b-67cf-4ce2-9aca-c7d260a5c143", + }, + "type": "OutOfBandRecord", + "value": { + "_tags": { + "recipientKeyFingerprints": [ + "z6Mkmod8vp2nURVktVC5ceQeyr2VUz26iu2ZANLNVg9pMawa", + ], + }, + "alias": undefined, + "autoAcceptConnection": undefined, + "createdAt": "2022-04-30T13:02:21.635Z", + "id": "4-4e4f-41d9-94c4-f49351b811f1", + "mediatorId": undefined, + "metadata": {}, + "outOfBandInvitation": { + "@id": "08eb8d8b-67cf-4ce2-9aca-c7d260a5c143", + "@type": "https://didcomm.org/out-of-band/1.1/invitation", + "accept": [ + "didcomm/aip1", + "didcomm/aip2;env=rfc19", + ], + "goal": undefined, + "goal_code": undefined, + "handshake_protocols": [ + "https://didcomm.org/connections/1.0", + ], + "imageUrl": undefined, + "label": "Agent: PopulateWallet2", + "requests~attach": undefined, + "services": [ + { + "accept": undefined, + "id": "#inline", + "recipientKeys": [ + "did:key:z6Mkmod8vp2nURVktVC5ceQeyr2VUz26iu2ZANLNVg9pMawa", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:faber", + "type": "did-communication", + }, + ], + "~attach": undefined, + "~l10n": undefined, + "~please_ack": undefined, + "~service": undefined, + "~thread": undefined, + "~timing": undefined, + "~transport": undefined, + }, + "reusable": false, + "reuseConnectionId": undefined, + "role": "receiver", + "state": "done", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "5-4e4f-41d9-94c4-f49351b811f1": { + "id": "5-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "invitationId": "cc67fb5e-1414-4ba6-9030-7456ccd2aaea", + "invitationRequestsThreadIds": undefined, + "recipientKeyFingerprints": [ + "z6MkjDJL4X7YGoH6gjamhZR2NzowPZqtJfX5kPuNuWiVdjMr", + ], + "role": "receiver", + "state": "done", + "threadId": "cc67fb5e-1414-4ba6-9030-7456ccd2aaea", + }, + "type": "OutOfBandRecord", + "value": { + "_tags": { + "recipientKeyFingerprints": [ + "z6MkjDJL4X7YGoH6gjamhZR2NzowPZqtJfX5kPuNuWiVdjMr", + ], + }, + "alias": undefined, + "autoAcceptConnection": false, + "createdAt": "2022-04-30T13:02:21.641Z", + "id": "5-4e4f-41d9-94c4-f49351b811f1", + "mediatorId": undefined, + "metadata": {}, + "outOfBandInvitation": { + "@id": "cc67fb5e-1414-4ba6-9030-7456ccd2aaea", + "@type": "https://didcomm.org/out-of-band/1.1/invitation", + "accept": [ + "didcomm/aip1", + "didcomm/aip2;env=rfc19", + ], + "goal": undefined, + "goal_code": undefined, + "handshake_protocols": [ + "https://didcomm.org/connections/1.0", + ], + "imageUrl": undefined, + "label": "Agent: PopulateWallet2", + "requests~attach": undefined, + "services": [ + { + "accept": undefined, + "id": "#inline", + "recipientKeys": [ + "did:key:z6MkjDJL4X7YGoH6gjamhZR2NzowPZqtJfX5kPuNuWiVdjMr", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:faber", + "type": "did-communication", + }, + ], + "~attach": undefined, + "~l10n": undefined, + "~please_ack": undefined, + "~service": undefined, + "~thread": undefined, + "~timing": undefined, + "~transport": undefined, + }, + "reusable": false, + "reuseConnectionId": undefined, + "role": "receiver", + "state": "done", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "6-4e4f-41d9-94c4-f49351b811f1": { + "id": "6-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "invitationId": "f0ca03d8-2e11-4ff2-a5fc-e0137a434b7e", + "invitationRequestsThreadIds": undefined, + "recipientKeyFingerprints": [ + "z6Mko31DNE3gqMRZj1JNhv2BHb1caQshcd9njgKkEQXsgFRp", + ], + "role": "sender", + "state": "await-response", + "threadId": "f0ca03d8-2e11-4ff2-a5fc-e0137a434b7e", + }, + "type": "OutOfBandRecord", + "value": { + "alias": undefined, + "autoAcceptConnection": true, + "createdAt": "2022-04-30T13:02:21.646Z", + "id": "6-4e4f-41d9-94c4-f49351b811f1", + "mediatorId": undefined, + "metadata": {}, + "outOfBandInvitation": { + "@id": "f0ca03d8-2e11-4ff2-a5fc-e0137a434b7e", + "@type": "https://didcomm.org/out-of-band/1.1/invitation", + "accept": [ + "didcomm/aip1", + "didcomm/aip2;env=rfc19", + ], + "goal": undefined, + "goal_code": undefined, + "handshake_protocols": [ + "https://didcomm.org/connections/1.0", + ], + "imageUrl": undefined, + "label": "Agent: PopulateWallet", + "requests~attach": undefined, + "services": [ + { + "accept": undefined, + "id": "#inline", + "recipientKeys": [ + "did:key:z6Mko31DNE3gqMRZj1JNhv2BHb1caQshcd9njgKkEQXsgFRp", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:alice", + "type": "did-communication", + }, + ], + "~attach": undefined, + "~l10n": undefined, + "~please_ack": undefined, + "~service": undefined, + "~thread": undefined, + "~timing": undefined, + "~transport": undefined, + }, + "reusable": true, + "reuseConnectionId": undefined, + "role": "sender", + "state": "await-response", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "7-4e4f-41d9-94c4-f49351b811f1": { + "id": "7-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "invitationId": "1f516e35-08d3-43d8-900c-99d5239f54da", + "invitationRequestsThreadIds": undefined, + "recipientKeyFingerprints": [ + "z6MkuWTEmH1mUo6W96zSWyH612hFHowRzNEscPYBL2CCMyC2", + ], + "role": "sender", + "state": "await-response", + "threadId": "1f516e35-08d3-43d8-900c-99d5239f54da", + }, + "type": "OutOfBandRecord", + "value": { + "_tags": { + "recipientKeyFingerprints": [ + "z6MkuWTEmH1mUo6W96zSWyH612hFHowRzNEscPYBL2CCMyC2", + ], + }, + "alias": undefined, + "autoAcceptConnection": true, + "createdAt": "2022-04-30T13:02:21.653Z", + "id": "7-4e4f-41d9-94c4-f49351b811f1", + "mediatorId": undefined, + "metadata": {}, + "outOfBandInvitation": { + "@id": "1f516e35-08d3-43d8-900c-99d5239f54da", + "@type": "https://didcomm.org/out-of-band/1.1/invitation", + "accept": [ + "didcomm/aip1", + "didcomm/aip2;env=rfc19", + ], + "goal": undefined, + "goal_code": undefined, + "handshake_protocols": [ + "https://didcomm.org/connections/1.0", + ], + "imageUrl": undefined, + "label": "Agent: PopulateWallet", + "requests~attach": undefined, + "services": [ + { + "accept": undefined, + "id": "#inline", + "recipientKeys": [ + "did:key:z6MkuWTEmH1mUo6W96zSWyH612hFHowRzNEscPYBL2CCMyC2", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:alice", + "type": "did-communication", + }, + ], + "~attach": undefined, + "~l10n": undefined, + "~please_ack": undefined, + "~service": undefined, + "~thread": undefined, + "~timing": undefined, + "~transport": undefined, + }, + "reusable": false, + "reuseConnectionId": undefined, + "role": "sender", + "state": "await-response", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "7781341d-be29-441b-9b79-4a957d8c6d37": { + "id": "7781341d-be29-441b-9b79-4a957d8c6d37", + "tags": { + "connectionTypes": [], + "did": "did:peer:1zQmSMBVNMDrh7fyE8bkAmk1ZatshjinpsEqPA3nx8JYjuKb", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3QxdHNwMTVjbkREN3dCQ0ZnZWhpUjJTeEhYMWFQeHQ0c3VlRTI0dHdIOUJkI3o2TWt0MXRzcDE1Y25ERDd3QkNGZ2VoaVIyU3hIWDFhUHh0NHN1ZUUyNHR3SDlCZCJdLCJyIjpbXX0", + "invitationKey": "EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF", + "mediatorId": undefined, + "outOfBandId": "3-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "responder", + "state": "request-received", + "theirDid": "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", + "theirKey": "6JwodG44KanZhhSvDS3dNmWjmWyeVFYRPxVaBntqmSCi", + "threadId": "a0c0e4d2-1501-42a2-a09b-7d5adc90b353", + "verkey": "EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF", + }, + "type": "ConnectionRecord", + "value": { + "autoAcceptConnection": false, + "connectionTypes": [], + "createdAt": "2022-04-30T13:02:21.628Z", + "did": "did:peer:1zQmSMBVNMDrh7fyE8bkAmk1ZatshjinpsEqPA3nx8JYjuKb", + "id": "7781341d-be29-441b-9b79-4a957d8c6d37", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3QxdHNwMTVjbkREN3dCQ0ZnZWhpUjJTeEhYMWFQeHQ0c3VlRTI0dHdIOUJkI3o2TWt0MXRzcDE1Y25ERDd3QkNGZ2VoaVIyU3hIWDFhUHh0NHN1ZUUyNHR3SDlCZCJdLCJyIjpbXX0", + "metadata": {}, + "outOfBandId": "3-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "responder", + "state": "request-received", + "theirDid": "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", + "theirLabel": "Agent: PopulateWallet2", + "threadId": "a0c0e4d2-1501-42a2-a09b-7d5adc90b353", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "8f4908ee-15ad-4058-9106-eda26eae735c": { + "id": "8f4908ee-15ad-4058-9106-eda26eae735c", + "tags": { + "connectionTypes": [], + "did": "did:peer:1zQmRAfQ6J5qk4qcbHyoStFVkhusazLT9xQcFhdC9dhhQ1cJ", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczpmYWJlciIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa2ZpUE1QeENRZVNEWkdNa0N2bTFZMnJCb1BzbXc0WkhNdjcxalh0Y1dSUmlNI3o2TWtmaVBNUHhDUWVTRFpHTWtDdm0xWTJyQm9Qc213NFpITXY3MWpYdGNXUlJpTSJdLCJyIjpbXX0", + "invitationKey": "2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy", + "mediatorId": undefined, + "outOfBandId": "1-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "requester", + "state": "completed", + "theirDid": "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", + "theirKey": "2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy", + "threadId": "fe287ec6-711b-4582-bb2b-d155aee86e61", + "verkey": "HfkCHGAHTz3j33TRDkKMabYLdnr2FKuWcaXTLzZkZcCp", + }, + "type": "ConnectionRecord", + "value": { + "alias": "connection alias", + "connectionTypes": [], + "createdAt": "2022-04-30T13:02:21.577Z", + "did": "did:peer:1zQmRAfQ6J5qk4qcbHyoStFVkhusazLT9xQcFhdC9dhhQ1cJ", + "id": "8f4908ee-15ad-4058-9106-eda26eae735c", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczpmYWJlciIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa2ZpUE1QeENRZVNEWkdNa0N2bTFZMnJCb1BzbXc0WkhNdjcxalh0Y1dSUmlNI3o2TWtmaVBNUHhDUWVTRFpHTWtDdm0xWTJyQm9Qc213NFpITXY3MWpYdGNXUlJpTSJdLCJyIjpbXX0", + "metadata": {}, + "outOfBandId": "1-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "requester", + "state": "completed", + "theirDid": "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", + "theirLabel": "Agent: PopulateWallet2", + "threadId": "fe287ec6-711b-4582-bb2b-d155aee86e61", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "9383d8e5-c002-4aae-8300-4a21384c919e": { + "id": "9383d8e5-c002-4aae-8300-4a21384c919e", + "tags": { + "connectionTypes": [], + "did": "did:peer:1zQmP96nW6vbNjzwPt19z1NYqhnAfgnAFqfLHcktkmdUFzhT", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3RDWkFRTkd2V2I0V0hBandCcVB0WGhaZERZb3JiU0prR1c5dmoxdWh3MUhEI3o2TWt0Q1pBUU5HdldiNFdIQWp3QnFQdFhoWmREWW9yYlNKa0dXOXZqMXVodzFIRCJdLCJyIjpbXX0", + "invitationKey": "EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq", + "mediatorId": undefined, + "outOfBandId": "2-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "responder", + "state": "completed", + "theirDid": "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", + "theirKey": "J9qc5VredX3YUBbFNoJz5oJpWPcUWURKVDiUv1DvYukX", + "threadId": "0b2f1133-ced9-49f1-83a1-eb6ba1c24cdf", + "verkey": "EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq", + }, + "type": "ConnectionRecord", + "value": { + "connectionTypes": [], + "createdAt": "2022-04-30T13:02:21.608Z", + "did": "did:peer:1zQmP96nW6vbNjzwPt19z1NYqhnAfgnAFqfLHcktkmdUFzhT", + "id": "9383d8e5-c002-4aae-8300-4a21384c919e", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3RDWkFRTkd2V2I0V0hBandCcVB0WGhaZERZb3JiU0prR1c5dmoxdWh3MUhEI3o2TWt0Q1pBUU5HdldiNFdIQWp3QnFQdFhoWmREWW9yYlNKa0dXOXZqMXVodzFIRCJdLCJyIjpbXX0", + "metadata": {}, + "outOfBandId": "2-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "responder", + "state": "completed", + "theirDid": "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", + "theirLabel": "Agent: PopulateWallet2", + "threadId": "0b2f1133-ced9-49f1-83a1-eb6ba1c24cdf", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "b65c2ccd-277c-4140-9d87-c8dd30e7a98c": { + "id": "b65c2ccd-277c-4140-9d87-c8dd30e7a98c", + "tags": { + "connectionTypes": [], + "did": "did:peer:1zQmfDAtfDZcK4trJBsvVTXrBx9uaLCHSUZH9X2LFaAd3JKv", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3VXVEVtSDFtVW82Vzk2elNXeUg2MTJoRkhvd1J6TkVzY1BZQkwyQ0NNeUMyI3o2TWt1V1RFbUgxbVVvNlc5NnpTV3lINjEyaEZIb3dSek5Fc2NQWUJMMkNDTXlDMiJdLCJyIjpbXX0", + "invitationKey": "G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe", + "mediatorId": undefined, + "outOfBandId": "7-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "responder", + "state": "invitation-sent", + "theirDid": undefined, + "threadId": undefined, + "verkey": "G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe", + }, + "type": "ConnectionRecord", + "value": { + "autoAcceptConnection": true, + "connectionTypes": [], + "createdAt": "2022-04-30T13:02:21.653Z", + "did": "did:peer:1zQmfDAtfDZcK4trJBsvVTXrBx9uaLCHSUZH9X2LFaAd3JKv", + "id": "b65c2ccd-277c-4140-9d87-c8dd30e7a98c", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3VXVEVtSDFtVW82Vzk2elNXeUg2MTJoRkhvd1J6TkVzY1BZQkwyQ0NNeUMyI3o2TWt1V1RFbUgxbVVvNlc5NnpTV3lINjEyaEZIb3dSek5Fc2NQWUJMMkNDTXlDMiJdLCJyIjpbXX0", + "metadata": {}, + "outOfBandId": "7-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "responder", + "state": "invitation-sent", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "da518433-0e55-4b74-a05b-aa75c1095a99": { + "id": "da518433-0e55-4b74-a05b-aa75c1095a99", + "tags": { + "connectionTypes": [], + "did": "did:peer:1zQmPbGa8KDwyjcw9UgwCCgJMV7jU5kKCyvBuwFVc88WxA56", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa28zMURORTNncU1SWmoxSk5odjJCSGIxY2FRc2hjZDluamdLa0VRWHNnRlJwI3o2TWtvMzFETkUzZ3FNUlpqMUpOaHYyQkhiMWNhUXNoY2Q5bmpnS2tFUVhzZ0ZScCJdLCJyIjpbXX0", + "invitationKey": "9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS", + "mediatorId": undefined, + "outOfBandId": "6-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "responder", + "state": "response-sent", + "theirDid": "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", + "theirKey": "J57UsQT3wa4FcivfKpvjgUtaPDScZhFJ8kd5Q2iR5sBT", + "threadId": "6eeb6a80-cd75-491d-b2e0-7bae65ced1c3", + "verkey": "9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS", + }, + "type": "ConnectionRecord", + "value": { + "autoAcceptConnection": true, + "connectionTypes": [], + "createdAt": "2022-04-30T13:02:21.646Z", + "did": "did:peer:1zQmPbGa8KDwyjcw9UgwCCgJMV7jU5kKCyvBuwFVc88WxA56", + "id": "da518433-0e55-4b74-a05b-aa75c1095a99", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa28zMURORTNncU1SWmoxSk5odjJCSGIxY2FRc2hjZDluamdLa0VRWHNnRlJwI3o2TWtvMzFETkUzZ3FNUlpqMUpOaHYyQkhiMWNhUXNoY2Q5bmpnS2tFUVhzZ0ZScCJdLCJyIjpbXX0", + "metadata": {}, + "outOfBandId": "6-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "responder", + "state": "response-sent", + "theirDid": "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", + "theirLabel": "Agent: PopulateWallet2", + "threadId": "6eeb6a80-cd75-491d-b2e0-7bae65ced1c3", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "did:peer:1zQmP96nW6vbNjzwPt19z1NYqhnAfgnAFqfLHcktkmdUFzhT": { + "id": "did:peer:1zQmP96nW6vbNjzwPt19z1NYqhnAfgnAFqfLHcktkmdUFzhT", + "tags": { + "did": "did:peer:1zQmP96nW6vbNjzwPt19z1NYqhnAfgnAFqfLHcktkmdUFzhT", + "legacyUnqualifiedDid": "SDqTzbVuCowusqGBNbNDjH", + "method": "peer", + "methodSpecificIdentifier": "1zQmP96nW6vbNjzwPt19z1NYqhnAfgnAFqfLHcktkmdUFzhT", + "recipientKeyFingerprints": [ + "z6MktCZAQNGvWb4WHAjwBqPtXhZdDYorbSJkGW9vj1uhw1HD", + ], + "role": "created", + }, + "type": "DidRecord", + "value": { + "_tags": {}, + "createdAt": "2022-04-30T13:02:21.608Z", + "did": "did:peer:1zQmP96nW6vbNjzwPt19z1NYqhnAfgnAFqfLHcktkmdUFzhT", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "alsoKnownAs": undefined, + "assertionMethod": undefined, + "authentication": [ + "#EkJ7p82V", + ], + "capabilityDelegation": undefined, + "capabilityInvocation": undefined, + "controller": undefined, + "id": "did:peer:1zQmP96nW6vbNjzwPt19z1NYqhnAfgnAFqfLHcktkmdUFzhT", + "keyAgreement": undefined, + "service": [ + { + "id": "#IndyAgentService", + "priority": 0, + "recipientKeys": [ + "EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + }, + ], + "verificationMethod": [ + { + "blockchainAccountId": undefined, + "controller": "#id", + "ethereumAddress": undefined, + "id": "#EkJ7p82V", + "publicKeyBase58": "EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq", + "publicKeyBase64": undefined, + "publicKeyHex": undefined, + "publicKeyJwk": undefined, + "publicKeyMultibase": undefined, + "publicKeyPem": undefined, + "type": "Ed25519VerificationKey2018", + }, + ], + }, + "id": "did:peer:1zQmP96nW6vbNjzwPt19z1NYqhnAfgnAFqfLHcktkmdUFzhT", + "metadata": { + "_internal/legacyDid": { + "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"SDqTzbVuCowusqGBNbNDjH#1","controller":"SDqTzbVuCowusqGBNbNDjH","type":"Ed25519VerificationKey2018","publicKeyBase58":"EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq"}],"service":[{"id":"SDqTzbVuCowusqGBNbNDjH#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq"],"routingKeys":[]}],"authentication":[{"publicKey":"SDqTzbVuCowusqGBNbNDjH#1","type":"Ed25519SignatureAuthentication2018"}],"id":"SDqTzbVuCowusqGBNbNDjH"}", + "unqualifiedDid": "SDqTzbVuCowusqGBNbNDjH", + }, + }, + "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "did:peer:1zQmPbGa8KDwyjcw9UgwCCgJMV7jU5kKCyvBuwFVc88WxA56": { + "id": "did:peer:1zQmPbGa8KDwyjcw9UgwCCgJMV7jU5kKCyvBuwFVc88WxA56", + "tags": { + "did": "did:peer:1zQmPbGa8KDwyjcw9UgwCCgJMV7jU5kKCyvBuwFVc88WxA56", + "legacyUnqualifiedDid": "GkEeb96MGT94K1HyQQzpj1", + "method": "peer", + "methodSpecificIdentifier": "1zQmPbGa8KDwyjcw9UgwCCgJMV7jU5kKCyvBuwFVc88WxA56", + "recipientKeyFingerprints": [ + "z6Mko31DNE3gqMRZj1JNhv2BHb1caQshcd9njgKkEQXsgFRp", + ], + "role": "created", + }, + "type": "DidRecord", + "value": { + "_tags": {}, + "createdAt": "2022-04-30T13:02:21.646Z", + "did": "did:peer:1zQmPbGa8KDwyjcw9UgwCCgJMV7jU5kKCyvBuwFVc88WxA56", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "alsoKnownAs": undefined, + "assertionMethod": undefined, + "authentication": [ + "#9akAmyoF", + ], + "capabilityDelegation": undefined, + "capabilityInvocation": undefined, + "controller": undefined, + "id": "did:peer:1zQmPbGa8KDwyjcw9UgwCCgJMV7jU5kKCyvBuwFVc88WxA56", + "keyAgreement": undefined, + "service": [ + { + "id": "#IndyAgentService", + "priority": 0, + "recipientKeys": [ + "9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + }, + ], + "verificationMethod": [ + { + "blockchainAccountId": undefined, + "controller": "#id", + "ethereumAddress": undefined, + "id": "#9akAmyoF", + "publicKeyBase58": "9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS", + "publicKeyBase64": undefined, + "publicKeyHex": undefined, + "publicKeyJwk": undefined, + "publicKeyMultibase": undefined, + "publicKeyPem": undefined, + "type": "Ed25519VerificationKey2018", + }, + ], + }, + "id": "did:peer:1zQmPbGa8KDwyjcw9UgwCCgJMV7jU5kKCyvBuwFVc88WxA56", + "metadata": { + "_internal/legacyDid": { + "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"GkEeb96MGT94K1HyQQzpj1#1","controller":"GkEeb96MGT94K1HyQQzpj1","type":"Ed25519VerificationKey2018","publicKeyBase58":"9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS"}],"service":[{"id":"GkEeb96MGT94K1HyQQzpj1#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS"],"routingKeys":[]}],"authentication":[{"publicKey":"GkEeb96MGT94K1HyQQzpj1#1","type":"Ed25519SignatureAuthentication2018"}],"id":"GkEeb96MGT94K1HyQQzpj1"}", + "unqualifiedDid": "GkEeb96MGT94K1HyQQzpj1", + }, + }, + "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "did:peer:1zQmRAfQ6J5qk4qcbHyoStFVkhusazLT9xQcFhdC9dhhQ1cJ": { + "id": "did:peer:1zQmRAfQ6J5qk4qcbHyoStFVkhusazLT9xQcFhdC9dhhQ1cJ", + "tags": { + "did": "did:peer:1zQmRAfQ6J5qk4qcbHyoStFVkhusazLT9xQcFhdC9dhhQ1cJ", + "legacyUnqualifiedDid": "XajWZZmHGAWUvYCi7CApaG", + "method": "peer", + "methodSpecificIdentifier": "1zQmRAfQ6J5qk4qcbHyoStFVkhusazLT9xQcFhdC9dhhQ1cJ", + "recipientKeyFingerprints": [ + "z6Mkw81EsWQioXYC9YJ7uKHCRh6LTN7sfD9sJbSPBGXmUpzC", + ], + "role": "created", + }, + "type": "DidRecord", + "value": { + "_tags": {}, + "createdAt": "2022-04-30T13:02:21.577Z", + "did": "did:peer:1zQmRAfQ6J5qk4qcbHyoStFVkhusazLT9xQcFhdC9dhhQ1cJ", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "alsoKnownAs": undefined, + "assertionMethod": undefined, + "authentication": [ + "#HfkCHGAH", + ], + "capabilityDelegation": undefined, + "capabilityInvocation": undefined, + "controller": undefined, + "id": "did:peer:1zQmRAfQ6J5qk4qcbHyoStFVkhusazLT9xQcFhdC9dhhQ1cJ", + "keyAgreement": undefined, + "service": [ + { + "id": "#IndyAgentService", + "priority": 0, + "recipientKeys": [ + "HfkCHGAHTz3j33TRDkKMabYLdnr2FKuWcaXTLzZkZcCp", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + }, + ], + "verificationMethod": [ + { + "blockchainAccountId": undefined, + "controller": "#id", + "ethereumAddress": undefined, + "id": "#HfkCHGAH", + "publicKeyBase58": "HfkCHGAHTz3j33TRDkKMabYLdnr2FKuWcaXTLzZkZcCp", + "publicKeyBase64": undefined, + "publicKeyHex": undefined, + "publicKeyJwk": undefined, + "publicKeyMultibase": undefined, + "publicKeyPem": undefined, + "type": "Ed25519VerificationKey2018", + }, + ], + }, + "id": "did:peer:1zQmRAfQ6J5qk4qcbHyoStFVkhusazLT9xQcFhdC9dhhQ1cJ", + "metadata": { + "_internal/legacyDid": { + "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"XajWZZmHGAWUvYCi7CApaG#1","controller":"XajWZZmHGAWUvYCi7CApaG","type":"Ed25519VerificationKey2018","publicKeyBase58":"HfkCHGAHTz3j33TRDkKMabYLdnr2FKuWcaXTLzZkZcCp"}],"service":[{"id":"XajWZZmHGAWUvYCi7CApaG#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["HfkCHGAHTz3j33TRDkKMabYLdnr2FKuWcaXTLzZkZcCp"],"routingKeys":[]}],"authentication":[{"publicKey":"XajWZZmHGAWUvYCi7CApaG#1","type":"Ed25519SignatureAuthentication2018"}],"id":"XajWZZmHGAWUvYCi7CApaG"}", + "unqualifiedDid": "XajWZZmHGAWUvYCi7CApaG", + }, + }, + "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "did:peer:1zQmSMBVNMDrh7fyE8bkAmk1ZatshjinpsEqPA3nx8JYjuKb": { + "id": "did:peer:1zQmSMBVNMDrh7fyE8bkAmk1ZatshjinpsEqPA3nx8JYjuKb", + "tags": { + "did": "did:peer:1zQmSMBVNMDrh7fyE8bkAmk1ZatshjinpsEqPA3nx8JYjuKb", + "legacyUnqualifiedDid": "RtH4qxVPL1Dpmdv7GytjBv", + "method": "peer", + "methodSpecificIdentifier": "1zQmSMBVNMDrh7fyE8bkAmk1ZatshjinpsEqPA3nx8JYjuKb", + "recipientKeyFingerprints": [ + "z6Mkt1tsp15cnDD7wBCFgehiR2SxHX1aPxt4sueE24twH9Bd", + ], + "role": "created", + }, + "type": "DidRecord", + "value": { + "_tags": {}, + "createdAt": "2022-04-30T13:02:21.628Z", + "did": "did:peer:1zQmSMBVNMDrh7fyE8bkAmk1ZatshjinpsEqPA3nx8JYjuKb", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "alsoKnownAs": undefined, + "assertionMethod": undefined, + "authentication": [ + "#EZdqDkqB", + ], + "capabilityDelegation": undefined, + "capabilityInvocation": undefined, + "controller": undefined, + "id": "did:peer:1zQmSMBVNMDrh7fyE8bkAmk1ZatshjinpsEqPA3nx8JYjuKb", + "keyAgreement": undefined, + "service": [ + { + "id": "#IndyAgentService", + "priority": 0, + "recipientKeys": [ + "EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + }, + ], + "verificationMethod": [ + { + "blockchainAccountId": undefined, + "controller": "#id", + "ethereumAddress": undefined, + "id": "#EZdqDkqB", + "publicKeyBase58": "EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF", + "publicKeyBase64": undefined, + "publicKeyHex": undefined, + "publicKeyJwk": undefined, + "publicKeyMultibase": undefined, + "publicKeyPem": undefined, + "type": "Ed25519VerificationKey2018", + }, + ], + }, + "id": "did:peer:1zQmSMBVNMDrh7fyE8bkAmk1ZatshjinpsEqPA3nx8JYjuKb", + "metadata": { + "_internal/legacyDid": { + "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"RtH4qxVPL1Dpmdv7GytjBv#1","controller":"RtH4qxVPL1Dpmdv7GytjBv","type":"Ed25519VerificationKey2018","publicKeyBase58":"EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF"}],"service":[{"id":"RtH4qxVPL1Dpmdv7GytjBv#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF"],"routingKeys":[]}],"authentication":[{"publicKey":"RtH4qxVPL1Dpmdv7GytjBv#1","type":"Ed25519SignatureAuthentication2018"}],"id":"RtH4qxVPL1Dpmdv7GytjBv"}", + "unqualifiedDid": "RtH4qxVPL1Dpmdv7GytjBv", + }, + }, + "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU": { + "id": "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", + "tags": { + "did": "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", + "legacyUnqualifiedDid": "YUH4t3KMkEJiXgmqsncrY9", + "method": "peer", + "methodSpecificIdentifier": "1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", + "recipientKeyFingerprints": [ + "z6Mkwc6efk75y4Y1agRx4NGpvtrpKxtKvMfgBEdQkHBwU8Xu", + ], + "role": "received", + }, + "type": "DidRecord", + "value": { + "_tags": {}, + "createdAt": "2022-04-30T13:02:21.608Z", + "did": "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "alsoKnownAs": undefined, + "assertionMethod": undefined, + "authentication": [ + "#J9qc5Vre", + ], + "capabilityDelegation": undefined, + "capabilityInvocation": undefined, + "controller": undefined, + "id": "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", + "keyAgreement": undefined, + "service": [ + { + "id": "#IndyAgentService", + "priority": 0, + "recipientKeys": [ + "J9qc5VredX3YUBbFNoJz5oJpWPcUWURKVDiUv1DvYukX", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:faber", + "type": "IndyAgent", + }, + ], + "verificationMethod": [ + { + "blockchainAccountId": undefined, + "controller": "#id", + "ethereumAddress": undefined, + "id": "#J9qc5Vre", + "publicKeyBase58": "J9qc5VredX3YUBbFNoJz5oJpWPcUWURKVDiUv1DvYukX", + "publicKeyBase64": undefined, + "publicKeyHex": undefined, + "publicKeyJwk": undefined, + "publicKeyMultibase": undefined, + "publicKeyPem": undefined, + "type": "Ed25519VerificationKey2018", + }, + ], + }, + "id": "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", + "metadata": { + "_internal/legacyDid": { + "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"YUH4t3KMkEJiXgmqsncrY9#1","controller":"YUH4t3KMkEJiXgmqsncrY9","type":"Ed25519VerificationKey2018","publicKeyBase58":"J9qc5VredX3YUBbFNoJz5oJpWPcUWURKVDiUv1DvYukX"}],"service":[{"id":"YUH4t3KMkEJiXgmqsncrY9#IndyAgentService","serviceEndpoint":"rxjs:faber","type":"IndyAgent","priority":0,"recipientKeys":["J9qc5VredX3YUBbFNoJz5oJpWPcUWURKVDiUv1DvYukX"],"routingKeys":[]}],"authentication":[{"publicKey":"YUH4t3KMkEJiXgmqsncrY9#1","type":"Ed25519SignatureAuthentication2018"}],"id":"YUH4t3KMkEJiXgmqsncrY9"}", + "unqualifiedDid": "YUH4t3KMkEJiXgmqsncrY9", + }, + }, + "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "did:peer:1zQmZ2tdw35SaLncSHhf9zBv3e9QmJmLErZRSLsDdYowPHXy": { + "id": "did:peer:1zQmZ2tdw35SaLncSHhf9zBv3e9QmJmLErZRSLsDdYowPHXy", + "tags": { + "did": "did:peer:1zQmZ2tdw35SaLncSHhf9zBv3e9QmJmLErZRSLsDdYowPHXy", + "legacyUnqualifiedDid": "WSwJQMBHGZbQsq9LDBTWjX", + "method": "peer", + "methodSpecificIdentifier": "1zQmZ2tdw35SaLncSHhf9zBv3e9QmJmLErZRSLsDdYowPHXy", + "recipientKeyFingerprints": [ + "z6MkvW9GxjjUdL9qpaj2qQW6YBhCjZY7Zkzrks3cgpJaRjxR", + ], + "role": "created", + }, + "type": "DidRecord", + "value": { + "_tags": {}, + "createdAt": "2022-04-20T13:02:21.646Z", + "did": "did:peer:1zQmZ2tdw35SaLncSHhf9zBv3e9QmJmLErZRSLsDdYowPHXy", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "alsoKnownAs": undefined, + "assertionMethod": undefined, + "authentication": [ + "#H3tENVV3", + ], + "capabilityDelegation": undefined, + "capabilityInvocation": undefined, + "controller": undefined, + "id": "did:peer:1zQmZ2tdw35SaLncSHhf9zBv3e9QmJmLErZRSLsDdYowPHXy", + "keyAgreement": undefined, + "service": [ + { + "id": "#IndyAgentService", + "priority": 0, + "recipientKeys": [ + "H3tENVV3HnfNi5tL9qYFh69CuzGG9skW4r8grYLZWXB3", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + }, + ], + "verificationMethod": [ + { + "blockchainAccountId": undefined, + "controller": "#id", + "ethereumAddress": undefined, + "id": "#H3tENVV3", + "publicKeyBase58": "H3tENVV3HnfNi5tL9qYFh69CuzGG9skW4r8grYLZWXB3", + "publicKeyBase64": undefined, + "publicKeyHex": undefined, + "publicKeyJwk": undefined, + "publicKeyMultibase": undefined, + "publicKeyPem": undefined, + "type": "Ed25519VerificationKey2018", + }, + ], + }, + "id": "did:peer:1zQmZ2tdw35SaLncSHhf9zBv3e9QmJmLErZRSLsDdYowPHXy", + "metadata": { + "_internal/legacyDid": { + "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"WSwJQMBHGZbQsq9LDBTWjX#1","controller":"WSwJQMBHGZbQsq9LDBTWjX","type":"Ed25519VerificationKey2018","publicKeyBase58":"H3tENVV3HnfNi5tL9qYFh69CuzGG9skW4r8grYLZWXB3"}],"service":[{"id":"WSwJQMBHGZbQsq9LDBTWjX#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["H3tENVV3HnfNi5tL9qYFh69CuzGG9skW4r8grYLZWXB3"],"routingKeys":[]}],"authentication":[{"publicKey":"WSwJQMBHGZbQsq9LDBTWjX#1","type":"Ed25519SignatureAuthentication2018"}],"id":"WSwJQMBHGZbQsq9LDBTWjX"}", + "unqualifiedDid": "WSwJQMBHGZbQsq9LDBTWjX", + }, + }, + "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "did:peer:1zQma8LpnJ22GxQdyASV5jP6psacAGtJ6ytk4pVayYp4erRf": { + "id": "did:peer:1zQma8LpnJ22GxQdyASV5jP6psacAGtJ6ytk4pVayYp4erRf", + "tags": { + "did": "did:peer:1zQma8LpnJ22GxQdyASV5jP6psacAGtJ6ytk4pVayYp4erRf", + "legacyUnqualifiedDid": "TMnQftvJJJwoYogYkQgVjg", + "method": "peer", + "methodSpecificIdentifier": "1zQma8LpnJ22GxQdyASV5jP6psacAGtJ6ytk4pVayYp4erRf", + "recipientKeyFingerprints": [ + "z6MktpVtPC5j91aycGPT5pceiu8EGKDzM5RLwqAZBuCgxw4V", + ], + "role": "created", + }, + "type": "DidRecord", + "value": { + "_tags": {}, + "createdAt": "2022-04-30T13:02:21.641Z", + "did": "did:peer:1zQma8LpnJ22GxQdyASV5jP6psacAGtJ6ytk4pVayYp4erRf", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "alsoKnownAs": undefined, + "assertionMethod": undefined, + "authentication": [ + "#FNEqnwqH", + ], + "capabilityDelegation": undefined, + "capabilityInvocation": undefined, + "controller": undefined, + "id": "did:peer:1zQma8LpnJ22GxQdyASV5jP6psacAGtJ6ytk4pVayYp4erRf", + "keyAgreement": undefined, + "service": [ + { + "id": "#IndyAgentService", + "priority": 0, + "recipientKeys": [ + "FNEqnwqHoU6WVmYkQFeosoaESjx8wCAzFpFdMdEg3iH7", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + }, + ], + "verificationMethod": [ + { + "blockchainAccountId": undefined, + "controller": "#id", + "ethereumAddress": undefined, + "id": "#FNEqnwqH", + "publicKeyBase58": "FNEqnwqHoU6WVmYkQFeosoaESjx8wCAzFpFdMdEg3iH7", + "publicKeyBase64": undefined, + "publicKeyHex": undefined, + "publicKeyJwk": undefined, + "publicKeyMultibase": undefined, + "publicKeyPem": undefined, + "type": "Ed25519VerificationKey2018", + }, + ], + }, + "id": "did:peer:1zQma8LpnJ22GxQdyASV5jP6psacAGtJ6ytk4pVayYp4erRf", + "metadata": { + "_internal/legacyDid": { + "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"TMnQftvJJJwoYogYkQgVjg#1","controller":"TMnQftvJJJwoYogYkQgVjg","type":"Ed25519VerificationKey2018","publicKeyBase58":"FNEqnwqHoU6WVmYkQFeosoaESjx8wCAzFpFdMdEg3iH7"}],"service":[{"id":"TMnQftvJJJwoYogYkQgVjg#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["FNEqnwqHoU6WVmYkQFeosoaESjx8wCAzFpFdMdEg3iH7"],"routingKeys":[]}],"authentication":[{"publicKey":"TMnQftvJJJwoYogYkQgVjg#1","type":"Ed25519SignatureAuthentication2018"}],"id":"TMnQftvJJJwoYogYkQgVjg"}", + "unqualifiedDid": "TMnQftvJJJwoYogYkQgVjg", + }, + }, + "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga": { + "id": "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", + "tags": { + "did": "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", + "legacyUnqualifiedDid": "YKc7qhYN1TckZAMUf7jgwc", + "method": "peer", + "methodSpecificIdentifier": "1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", + "recipientKeyFingerprints": [ + "z6MkwXNXTehVH7YijDmN1PtaXaSaCniTyaVepmY1EJgS15xq", + ], + "role": "received", + }, + "type": "DidRecord", + "value": { + "_tags": {}, + "createdAt": "2022-04-30T13:02:21.646Z", + "did": "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "alsoKnownAs": undefined, + "assertionMethod": undefined, + "authentication": [ + "#J57UsQT3", + ], + "capabilityDelegation": undefined, + "capabilityInvocation": undefined, + "controller": undefined, + "id": "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", + "keyAgreement": undefined, + "service": [ + { + "id": "#IndyAgentService", + "priority": 0, + "recipientKeys": [ + "J57UsQT3wa4FcivfKpvjgUtaPDScZhFJ8kd5Q2iR5sBT", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:faber", + "type": "IndyAgent", + }, + ], + "verificationMethod": [ + { + "blockchainAccountId": undefined, + "controller": "#id", + "ethereumAddress": undefined, + "id": "#J57UsQT3", + "publicKeyBase58": "J57UsQT3wa4FcivfKpvjgUtaPDScZhFJ8kd5Q2iR5sBT", + "publicKeyBase64": undefined, + "publicKeyHex": undefined, + "publicKeyJwk": undefined, + "publicKeyMultibase": undefined, + "publicKeyPem": undefined, + "type": "Ed25519VerificationKey2018", + }, + ], + }, + "id": "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", + "metadata": { + "_internal/legacyDid": { + "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"YKc7qhYN1TckZAMUf7jgwc#1","controller":"YKc7qhYN1TckZAMUf7jgwc","type":"Ed25519VerificationKey2018","publicKeyBase58":"J57UsQT3wa4FcivfKpvjgUtaPDScZhFJ8kd5Q2iR5sBT"}],"service":[{"id":"YKc7qhYN1TckZAMUf7jgwc#IndyAgentService","serviceEndpoint":"rxjs:faber","type":"IndyAgent","priority":0,"recipientKeys":["J57UsQT3wa4FcivfKpvjgUtaPDScZhFJ8kd5Q2iR5sBT"],"routingKeys":[]}],"authentication":[{"publicKey":"YKc7qhYN1TckZAMUf7jgwc#1","type":"Ed25519SignatureAuthentication2018"}],"id":"YKc7qhYN1TckZAMUf7jgwc"}", + "unqualifiedDid": "YKc7qhYN1TckZAMUf7jgwc", + }, + }, + "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ": { + "id": "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", + "tags": { + "did": "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", + "legacyUnqualifiedDid": "Ak15GBhMYpdS8XX3QDMv31", + "method": "peer", + "methodSpecificIdentifier": "1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", + "recipientKeyFingerprints": [ + "z6MkjmCrDWJVf8H2pCHcu11UDs4jb6FVu8nn5yQW24rrgez6", + ], + "role": "received", + }, + "type": "DidRecord", + "value": { + "_tags": {}, + "createdAt": "2022-04-30T13:02:21.628Z", + "did": "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "alsoKnownAs": undefined, + "assertionMethod": undefined, + "authentication": [ + "#6JwodG44", + ], + "capabilityDelegation": undefined, + "capabilityInvocation": undefined, + "controller": undefined, + "id": "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", + "keyAgreement": undefined, + "service": [ + { + "id": "#IndyAgentService", + "priority": 0, + "recipientKeys": [ + "6JwodG44KanZhhSvDS3dNmWjmWyeVFYRPxVaBntqmSCi", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:faber", + "type": "IndyAgent", + }, + ], + "verificationMethod": [ + { + "blockchainAccountId": undefined, + "controller": "#id", + "ethereumAddress": undefined, + "id": "#6JwodG44", + "publicKeyBase58": "6JwodG44KanZhhSvDS3dNmWjmWyeVFYRPxVaBntqmSCi", + "publicKeyBase64": undefined, + "publicKeyHex": undefined, + "publicKeyJwk": undefined, + "publicKeyMultibase": undefined, + "publicKeyPem": undefined, + "type": "Ed25519VerificationKey2018", + }, + ], + }, + "id": "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", + "metadata": { + "_internal/legacyDid": { + "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"Ak15GBhMYpdS8XX3QDMv31#1","controller":"Ak15GBhMYpdS8XX3QDMv31","type":"Ed25519VerificationKey2018","publicKeyBase58":"6JwodG44KanZhhSvDS3dNmWjmWyeVFYRPxVaBntqmSCi"}],"service":[{"id":"Ak15GBhMYpdS8XX3QDMv31#IndyAgentService","serviceEndpoint":"rxjs:faber","type":"IndyAgent","priority":0,"recipientKeys":["6JwodG44KanZhhSvDS3dNmWjmWyeVFYRPxVaBntqmSCi"],"routingKeys":[]}],"authentication":[{"publicKey":"Ak15GBhMYpdS8XX3QDMv31#1","type":"Ed25519SignatureAuthentication2018"}],"id":"Ak15GBhMYpdS8XX3QDMv31"}", + "unqualifiedDid": "Ak15GBhMYpdS8XX3QDMv31", + }, + }, + "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui": { + "id": "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", + "tags": { + "did": "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", + "legacyUnqualifiedDid": "9jTqUnV4k5ucxbyxumAaV7", + "method": "peer", + "methodSpecificIdentifier": "1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", + "recipientKeyFingerprints": [ + "z6MkjDJL4X7YGoH6gjamhZR2NzowPZqtJfX5kPuNuWiVdjMr", + ], + "role": "received", + }, + "type": "DidRecord", + "value": { + "_tags": {}, + "createdAt": "2022-04-30T13:02:21.641Z", + "did": "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "alsoKnownAs": undefined, + "assertionMethod": undefined, + "authentication": [ + "#5m3HUGs6", + ], + "capabilityDelegation": undefined, + "capabilityInvocation": undefined, + "controller": undefined, + "id": "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", + "keyAgreement": undefined, + "service": [ + { + "id": "#IndyAgentService", + "priority": 0, + "recipientKeys": [ + "5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:faber", + "type": "IndyAgent", + }, + ], + "verificationMethod": [ + { + "blockchainAccountId": undefined, + "controller": "#id", + "ethereumAddress": undefined, + "id": "#5m3HUGs6", + "publicKeyBase58": "5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU", + "publicKeyBase64": undefined, + "publicKeyHex": undefined, + "publicKeyJwk": undefined, + "publicKeyMultibase": undefined, + "publicKeyPem": undefined, + "type": "Ed25519VerificationKey2018", + }, + ], + }, + "id": "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", + "metadata": { + "_internal/legacyDid": { + "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"9jTqUnV4k5ucxbyxumAaV7#1","controller":"9jTqUnV4k5ucxbyxumAaV7","type":"Ed25519VerificationKey2018","publicKeyBase58":"5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU"}],"service":[{"id":"9jTqUnV4k5ucxbyxumAaV7#IndyAgentService","serviceEndpoint":"rxjs:faber","type":"IndyAgent","priority":0,"recipientKeys":["5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU"],"routingKeys":[]}],"authentication":[{"publicKey":"9jTqUnV4k5ucxbyxumAaV7#1","type":"Ed25519SignatureAuthentication2018"}],"id":"9jTqUnV4k5ucxbyxumAaV7"}", + "unqualifiedDid": "9jTqUnV4k5ucxbyxumAaV7", + }, + }, + "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "did:peer:1zQmduuYkxRKJuVyvDqttdd9eDfBwDnF1DAU5FFQo4whx7Uw": { + "id": "did:peer:1zQmduuYkxRKJuVyvDqttdd9eDfBwDnF1DAU5FFQo4whx7Uw", + "tags": { + "did": "did:peer:1zQmduuYkxRKJuVyvDqttdd9eDfBwDnF1DAU5FFQo4whx7Uw", + "legacyUnqualifiedDid": "WewvCdyBi4HL8ogyGviYVS", + "method": "peer", + "methodSpecificIdentifier": "1zQmduuYkxRKJuVyvDqttdd9eDfBwDnF1DAU5FFQo4whx7Uw", + "recipientKeyFingerprints": [ + "z6MkvcgxQSsX5WA8vcBokLZ46znnhRBH6aKAGYnonEUfUnQV", + ], + "role": "created", + }, + "type": "DidRecord", + "value": { + "_tags": {}, + "createdAt": "2022-04-30T13:02:21.635Z", + "did": "did:peer:1zQmduuYkxRKJuVyvDqttdd9eDfBwDnF1DAU5FFQo4whx7Uw", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "alsoKnownAs": undefined, + "assertionMethod": undefined, + "authentication": [ + "#HARupCd5", + ], + "capabilityDelegation": undefined, + "capabilityInvocation": undefined, + "controller": undefined, + "id": "did:peer:1zQmduuYkxRKJuVyvDqttdd9eDfBwDnF1DAU5FFQo4whx7Uw", + "keyAgreement": undefined, + "service": [ + { + "id": "#IndyAgentService", + "priority": 0, + "recipientKeys": [ + "HARupCd5jxffp7M74mbDFuEnsquRgh4oaXsswxWeZZd7", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + }, + ], + "verificationMethod": [ + { + "blockchainAccountId": undefined, + "controller": "#id", + "ethereumAddress": undefined, + "id": "#HARupCd5", + "publicKeyBase58": "HARupCd5jxffp7M74mbDFuEnsquRgh4oaXsswxWeZZd7", + "publicKeyBase64": undefined, + "publicKeyHex": undefined, + "publicKeyJwk": undefined, + "publicKeyMultibase": undefined, + "publicKeyPem": undefined, + "type": "Ed25519VerificationKey2018", + }, + ], + }, + "id": "did:peer:1zQmduuYkxRKJuVyvDqttdd9eDfBwDnF1DAU5FFQo4whx7Uw", + "metadata": { + "_internal/legacyDid": { + "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"WewvCdyBi4HL8ogyGviYVS#1","controller":"WewvCdyBi4HL8ogyGviYVS","type":"Ed25519VerificationKey2018","publicKeyBase58":"HARupCd5jxffp7M74mbDFuEnsquRgh4oaXsswxWeZZd7"}],"service":[{"id":"WewvCdyBi4HL8ogyGviYVS#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["HARupCd5jxffp7M74mbDFuEnsquRgh4oaXsswxWeZZd7"],"routingKeys":[]}],"authentication":[{"publicKey":"WewvCdyBi4HL8ogyGviYVS#1","type":"Ed25519SignatureAuthentication2018"}],"id":"WewvCdyBi4HL8ogyGviYVS"}", + "unqualifiedDid": "WewvCdyBi4HL8ogyGviYVS", + }, + }, + "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX": { + "id": "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", + "tags": { + "did": "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", + "legacyUnqualifiedDid": "3KAjJWF5NjiDTUm6JpPBQD", + "method": "peer", + "methodSpecificIdentifier": "1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", + "recipientKeyFingerprints": [ + "z6MkfiPMPxCQeSDZGMkCvm1Y2rBoPsmw4ZHMv71jXtcWRRiM", + ], + "role": "received", + }, + "type": "DidRecord", + "value": { + "_tags": {}, + "createdAt": "2022-04-30T13:02:21.577Z", + "did": "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "alsoKnownAs": undefined, + "assertionMethod": undefined, + "authentication": [ + "#2G8Johwy", + ], + "capabilityDelegation": undefined, + "capabilityInvocation": undefined, + "controller": undefined, + "id": "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", + "keyAgreement": undefined, + "service": [ + { + "id": "#IndyAgentService", + "priority": 0, + "recipientKeys": [ + "2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:faber", + "type": "IndyAgent", + }, + ], + "verificationMethod": [ + { + "blockchainAccountId": undefined, + "controller": "#id", + "ethereumAddress": undefined, + "id": "#2G8Johwy", + "publicKeyBase58": "2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy", + "publicKeyBase64": undefined, + "publicKeyHex": undefined, + "publicKeyJwk": undefined, + "publicKeyMultibase": undefined, + "publicKeyPem": undefined, + "type": "Ed25519VerificationKey2018", + }, + ], + }, + "id": "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", + "metadata": { + "_internal/legacyDid": { + "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"3KAjJWF5NjiDTUm6JpPBQD#1","controller":"3KAjJWF5NjiDTUm6JpPBQD","type":"Ed25519VerificationKey2018","publicKeyBase58":"2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy"}],"service":[{"id":"3KAjJWF5NjiDTUm6JpPBQD#IndyAgentService","serviceEndpoint":"rxjs:faber","type":"IndyAgent","priority":0,"recipientKeys":["2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy"],"routingKeys":[]}],"authentication":[{"publicKey":"3KAjJWF5NjiDTUm6JpPBQD#1","type":"Ed25519SignatureAuthentication2018"}],"id":"3KAjJWF5NjiDTUm6JpPBQD"}", + "unqualifiedDid": "3KAjJWF5NjiDTUm6JpPBQD", + }, + }, + "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "did:peer:1zQmfDAtfDZcK4trJBsvVTXrBx9uaLCHSUZH9X2LFaAd3JKv": { + "id": "did:peer:1zQmfDAtfDZcK4trJBsvVTXrBx9uaLCHSUZH9X2LFaAd3JKv", + "tags": { + "did": "did:peer:1zQmfDAtfDZcK4trJBsvVTXrBx9uaLCHSUZH9X2LFaAd3JKv", + "legacyUnqualifiedDid": "Ud6AWCk6WrwfYKZUw5tJmt", + "method": "peer", + "methodSpecificIdentifier": "1zQmfDAtfDZcK4trJBsvVTXrBx9uaLCHSUZH9X2LFaAd3JKv", + "recipientKeyFingerprints": [ + "z6MkuWTEmH1mUo6W96zSWyH612hFHowRzNEscPYBL2CCMyC2", + ], + "role": "created", + }, + "type": "DidRecord", + "value": { + "_tags": {}, + "createdAt": "2022-04-30T13:02:21.653Z", + "did": "did:peer:1zQmfDAtfDZcK4trJBsvVTXrBx9uaLCHSUZH9X2LFaAd3JKv", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "alsoKnownAs": undefined, + "assertionMethod": undefined, + "authentication": [ + "#G4CCB2mL", + ], + "capabilityDelegation": undefined, + "capabilityInvocation": undefined, + "controller": undefined, + "id": "did:peer:1zQmfDAtfDZcK4trJBsvVTXrBx9uaLCHSUZH9X2LFaAd3JKv", + "keyAgreement": undefined, + "service": [ + { + "id": "#IndyAgentService", + "priority": 0, + "recipientKeys": [ + "G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe", + ], + "routingKeys": [], + "serviceEndpoint": "rxjs:alice", + "type": "IndyAgent", + }, + ], + "verificationMethod": [ + { + "blockchainAccountId": undefined, + "controller": "#id", + "ethereumAddress": undefined, + "id": "#G4CCB2mL", + "publicKeyBase58": "G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe", + "publicKeyBase64": undefined, + "publicKeyHex": undefined, + "publicKeyJwk": undefined, + "publicKeyMultibase": undefined, + "publicKeyPem": undefined, + "type": "Ed25519VerificationKey2018", + }, + ], + }, + "id": "did:peer:1zQmfDAtfDZcK4trJBsvVTXrBx9uaLCHSUZH9X2LFaAd3JKv", + "metadata": { + "_internal/legacyDid": { + "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"Ud6AWCk6WrwfYKZUw5tJmt#1","controller":"Ud6AWCk6WrwfYKZUw5tJmt","type":"Ed25519VerificationKey2018","publicKeyBase58":"G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe"}],"service":[{"id":"Ud6AWCk6WrwfYKZUw5tJmt#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe"],"routingKeys":[]}],"authentication":[{"publicKey":"Ud6AWCk6WrwfYKZUw5tJmt#1","type":"Ed25519SignatureAuthentication2018"}],"id":"Ud6AWCk6WrwfYKZUw5tJmt"}", + "unqualifiedDid": "Ud6AWCk6WrwfYKZUw5tJmt", + }, + }, + "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "e3f9bc2b-f0a1-4a2c-ab81-2f0a3488c199": { + "id": "e3f9bc2b-f0a1-4a2c-ab81-2f0a3488c199", + "tags": { + "connectionTypes": [], + "did": "did:peer:1zQma8LpnJ22GxQdyASV5jP6psacAGtJ6ytk4pVayYp4erRf", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczpmYWJlciIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa2pESkw0WDdZR29INmdqYW1oWlIyTnpvd1BacXRKZlg1a1B1TnVXaVZkak1yI3o2TWtqREpMNFg3WUdvSDZnamFtaFpSMk56b3dQWnF0SmZYNWtQdU51V2lWZGpNciJdLCJyIjpbXX0", + "invitationKey": "5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU", + "mediatorId": undefined, + "outOfBandId": "5-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "requester", + "state": "response-received", + "theirDid": "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", + "theirKey": "5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU", + "threadId": "daf3372c-1ee2-4246-a1f4-f62f54f7d68b", + "verkey": "FNEqnwqHoU6WVmYkQFeosoaESjx8wCAzFpFdMdEg3iH7", + }, + "type": "ConnectionRecord", + "value": { + "autoAcceptConnection": false, + "connectionTypes": [], + "createdAt": "2022-04-30T13:02:21.641Z", + "did": "did:peer:1zQma8LpnJ22GxQdyASV5jP6psacAGtJ6ytk4pVayYp4erRf", + "id": "e3f9bc2b-f0a1-4a2c-ab81-2f0a3488c199", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczpmYWJlciIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa2pESkw0WDdZR29INmdqYW1oWlIyTnpvd1BacXRKZlg1a1B1TnVXaVZkak1yI3o2TWtqREpMNFg3WUdvSDZnamFtaFpSMk56b3dQWnF0SmZYNWtQdU51V2lWZGpNciJdLCJyIjpbXX0", + "metadata": {}, + "outOfBandId": "5-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "requester", + "state": "response-received", + "theirDid": "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", + "theirLabel": "Agent: PopulateWallet2", + "threadId": "daf3372c-1ee2-4246-a1f4-f62f54f7d68b", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "ee88e2e1-e27e-46a6-a910-f87690109e32": { + "id": "ee88e2e1-e27e-46a6-a910-f87690109e32", + "tags": { + "connectionTypes": [], + "did": "did:peer:1zQmduuYkxRKJuVyvDqttdd9eDfBwDnF1DAU5FFQo4whx7Uw", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczpmYWJlciIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa21vZDh2cDJuVVJWa3RWQzVjZVFleXIyVlV6MjZpdTJaQU5MTlZnOXBNYXdhI3o2TWttb2Q4dnAyblVSVmt0VkM1Y2VRZXlyMlZVejI2aXUyWkFOTE5WZzlwTWF3YSJdLCJyIjpbXX0", + "invitationKey": "8MN6LZnM8t1HmzMNw5Sp8kUVfQkFK1nCUMRSfQBoSNAC", + "mediatorId": undefined, + "outOfBandId": "4-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "requester", + "state": "request-sent", + "theirDid": undefined, + "threadId": undefined, + "verkey": "HARupCd5jxffp7M74mbDFuEnsquRgh4oaXsswxWeZZd7", + }, + "type": "ConnectionRecord", + "value": { + "connectionTypes": [], + "createdAt": "2022-04-30T13:02:21.635Z", + "did": "did:peer:1zQmduuYkxRKJuVyvDqttdd9eDfBwDnF1DAU5FFQo4whx7Uw", + "id": "ee88e2e1-e27e-46a6-a910-f87690109e32", + "invitationDid": "did:peer:2.SeyJzIjoicnhqczpmYWJlciIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa21vZDh2cDJuVVJWa3RWQzVjZVFleXIyVlV6MjZpdTJaQU5MTlZnOXBNYXdhI3o2TWttb2Q4dnAyblVSVmt0VkM1Y2VRZXlyMlZVejI2aXUyWkFOTE5WZzlwTWF3YSJdLCJyIjpbXX0", + "metadata": {}, + "outOfBandId": "4-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], + "role": "requester", + "state": "request-sent", + "theirLabel": "Agent: PopulateWallet2", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the credential records and create didcomm records with auto update 1`] = ` +{ + "1-4e4f-41d9-94c4-f49351b811f1": { + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "messageId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/offer-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9", + }, + "mime-type": "application/json", + }, + ], + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "10-4e4f-41d9-94c4-f49351b811f1": { + "id": "10-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "messageId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/offer-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "10-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=", + }, + "mime-type": "application/json", + }, + ], + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "11-4e4f-41d9-94c4-f49351b811f1": { + "id": "11-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "messageId": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/request-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "11-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "12-4e4f-41d9-94c4-f49351b811f1": { + "id": "12-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "messageId": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/issue-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "12-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": {}, + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "2-4e4f-41d9-94c4-f49351b811f1": { + "id": "2-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "messageId": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/request-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "2-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "3-4e4f-41d9-94c4-f49351b811f1": { + "id": "3-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "messageId": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/issue-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "3-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": {}, + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "4-4e4f-41d9-94c4-f49351b811f1": { + "id": "4-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "messageId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/offer-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "4-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=", + }, + "mime-type": "application/json", + }, + ], + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "5-4e4f-41d9-94c4-f49351b811f1": { + "id": "5-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "messageId": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/request-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "5-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a": { + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "tags": { + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "credentialIds": [], + "parentThreadId": undefined, + "role": undefined, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "CredentialRecord", + "value": { + "autoAcceptCredential": "contentApproved", + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "createdAt": "2022-03-21T22:50:20.522Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [], + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "protocolVersion": "v1", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a": { + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "tags": { + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "credentialIds": [ + "a77114e1-c812-4bff-a53c-3d5003fcc278", + ], + "parentThreadId": undefined, + "role": undefined, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "CredentialRecord", + "value": { + "autoAcceptCredential": "contentApproved", + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "createdAt": "2022-03-21T22:50:20.535Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [ + { + "credentialRecordId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "credentialRecordType": "indy", + }, + ], + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": { + "master_secret_blinding_data": { + "v_prime": "36456944381549782028917743247126995038265466209293312755125557271456380841610111892515020379470931691048072348420844231863825225515560265358581756565441268878364665494094789024845049226122885121039335781567964878826549149370097276812152226343824116049855825405977949749345353074025294938300401262824951638782220004732873597724698990420932910079362747837952520524827009393981876443737452031919055976088763615615890946142630576421462920865811255312740184209214306243871230276622595183415487741608569800898909023830922654063814555128779494528740438076748829436757078504882332589744263200806138145494157659396691564807976032319024007464003538934", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "373984270150786864433163", + }, + }, + "protocolVersion": "v1", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "6-4e4f-41d9-94c4-f49351b811f1": { + "id": "6-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "messageId": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/issue-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "6-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": {}, + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "7-4e4f-41d9-94c4-f49351b811f1": { + "id": "7-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "messageId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/offer-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "7-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9", + }, + "mime-type": "application/json", + }, + ], + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "8-4e4f-41d9-94c4-f49351b811f1": { + "id": "8-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "messageId": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/request-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "8-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "9-4e4f-41d9-94c4-f49351b811f1": { + "id": "9-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "associatedRecordId": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "messageId": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/1.0/issue-credential", + "protocolMajorVersion": "1", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "DidCommMessageRecord", + "value": { + "_tags": {}, + "associatedRecordId": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "9-4e4f-41d9-94c4-f49351b811f1", + "message": { + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": {}, + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7": { + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "tags": { + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "credentialIds": [], + "parentThreadId": undefined, + "role": undefined, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "CredentialRecord", + "value": { + "autoAcceptCredential": "contentApproved", + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "createdAt": "2022-03-21T22:50:20.740Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [], + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "protocolVersion": "v1", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c": { + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "tags": { + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "credentialIds": [ + "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + ], + "parentThreadId": undefined, + "role": undefined, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "CredentialRecord", + "value": { + "autoAcceptCredential": "contentApproved", + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "createdAt": "2022-03-21T22:50:20.746Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [ + { + "credentialRecordId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "credentialRecordType": "indy", + }, + ], + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": { + "master_secret_blinding_data": { + "v_prime": "24405223168730122709164916892481085040205443709643249329100687534344659826655374235392514476392517756663433844139774514430993889493707631169979521764390851593418941181409704266182779162417466204970949168472702858363964258641437554267668466400711344128132909691514606077477555576087059339291048485225394874964325220472232903203038212033940680060605090839733163438385288769519855418153181511119637865605476043416048121313638627002888436809192752657860306784733123742838413845299796745569824223645588826964796075250758249133953560017373025169692866449286962430731916293683231375510684692358406054381559324718715654332979447698704161714028193478", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "698370616023883730498375", + }, + }, + "protocolVersion": "v1", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the role in the mediation record: allMediator 1`] = ` +{ + "0b47db94-c0fa-4476-87cf-a5f664440412": { + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "tags": { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "recipientKeys": [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "createdAt": "2022-03-21T22:50:17.157Z", + "endpoint": "rxjs:alice", + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "metadata": {}, + "recipientKeys": [], + "role": "MEDIATOR", + "routingKeys": [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "7f14c1ec-514c-49b2-a00b-04af7e600060": { + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "tags": { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "recipientKeys": [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "createdAt": "2022-03-21T22:50:17.126Z", + "endpoint": "rxjs:alice", + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "metadata": {}, + "recipientKeys": [], + "role": "MEDIATOR", + "routingKeys": [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "802ef124-36b7-490f-b152-e9d090ddf073": { + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "tags": { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "recipientKeys": [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "createdAt": "2022-03-21T22:50:17.161Z", + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "metadata": {}, + "recipientKeys": [], + "role": "MEDIATOR", + "routingKeys": [], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": { + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "tags": { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "recipientKeys": [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "createdAt": "2022-03-21T22:50:17.132Z", + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "metadata": {}, + "recipientKeys": [], + "role": "MEDIATOR", + "routingKeys": [], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the role in the mediation record: allRecipient 1`] = ` +{ + "0b47db94-c0fa-4476-87cf-a5f664440412": { + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "tags": { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "recipientKeys": [], + "role": "RECIPIENT", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "createdAt": "2022-03-21T22:50:17.157Z", + "endpoint": "rxjs:alice", + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "metadata": {}, + "recipientKeys": [], + "role": "RECIPIENT", + "routingKeys": [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "7f14c1ec-514c-49b2-a00b-04af7e600060": { + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "tags": { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "recipientKeys": [], + "role": "RECIPIENT", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "createdAt": "2022-03-21T22:50:17.126Z", + "endpoint": "rxjs:alice", + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "metadata": {}, + "recipientKeys": [], + "role": "RECIPIENT", + "routingKeys": [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "802ef124-36b7-490f-b152-e9d090ddf073": { + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "tags": { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "recipientKeys": [], + "role": "RECIPIENT", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "createdAt": "2022-03-21T22:50:17.161Z", + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "metadata": {}, + "recipientKeys": [], + "role": "RECIPIENT", + "routingKeys": [], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": { + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "tags": { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "recipientKeys": [], + "role": "RECIPIENT", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "createdAt": "2022-03-21T22:50:17.132Z", + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "metadata": {}, + "recipientKeys": [], + "role": "RECIPIENT", + "routingKeys": [], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the role in the mediation record: doNotChange 1`] = ` +{ + "0b47db94-c0fa-4476-87cf-a5f664440412": { + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "tags": { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "recipientKeys": [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "createdAt": "2022-03-21T22:50:17.157Z", + "endpoint": "rxjs:alice", + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "metadata": {}, + "recipientKeys": [], + "role": "MEDIATOR", + "routingKeys": [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "7f14c1ec-514c-49b2-a00b-04af7e600060": { + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "tags": { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "recipientKeys": [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "createdAt": "2022-03-21T22:50:17.126Z", + "endpoint": "rxjs:alice", + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "metadata": {}, + "recipientKeys": [], + "role": "MEDIATOR", + "routingKeys": [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "802ef124-36b7-490f-b152-e9d090ddf073": { + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "tags": { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "recipientKeys": [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "createdAt": "2022-03-21T22:50:17.161Z", + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "metadata": {}, + "recipientKeys": [], + "role": "MEDIATOR", + "routingKeys": [], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": { + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "tags": { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "recipientKeys": [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "createdAt": "2022-03-21T22:50:17.132Z", + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "metadata": {}, + "recipientKeys": [], + "role": "MEDIATOR", + "routingKeys": [], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the role in the mediation record: recipientIfEndpoint 1`] = ` +{ + "0b47db94-c0fa-4476-87cf-a5f664440412": { + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "tags": { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "recipientKeys": [], + "role": "RECIPIENT", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "createdAt": "2022-03-21T22:50:17.157Z", + "endpoint": "rxjs:alice", + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "metadata": {}, + "recipientKeys": [], + "role": "RECIPIENT", + "routingKeys": [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "7f14c1ec-514c-49b2-a00b-04af7e600060": { + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "tags": { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "recipientKeys": [], + "role": "RECIPIENT", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "createdAt": "2022-03-21T22:50:17.126Z", + "endpoint": "rxjs:alice", + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "metadata": {}, + "recipientKeys": [], + "role": "RECIPIENT", + "routingKeys": [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "802ef124-36b7-490f-b152-e9d090ddf073": { + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "tags": { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "recipientKeys": [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "createdAt": "2022-03-21T22:50:17.161Z", + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "metadata": {}, + "recipientKeys": [], + "role": "MEDIATOR", + "routingKeys": [], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, + "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": { + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "tags": { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "recipientKeys": [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "createdAt": "2022-03-21T22:50:17.132Z", + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "metadata": {}, + "recipientKeys": [], + "role": "MEDIATOR", + "routingKeys": [], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", + }, + }, +} +`; diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.2.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.2.test.ts.snap new file mode 100644 index 0000000000..16848424be --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.2.test.ts.snap @@ -0,0 +1,1004 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update proof records and create didcomm records 1`] = ` +{ + "3d5d7ad4-f0aa-4b1b-8c2c-780ee383564e": { + "id": "3d5d7ad4-f0aa-4b1b-8c2c-780ee383564e", + "tags": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "state": "done", + "threadId": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + }, + "type": "ProofExchangeRecord", + "value": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "createdAt": "2022-09-08T19:36:06.261Z", + "id": "3d5d7ad4-f0aa-4b1b-8c2c-780ee383564e", + "metadata": {}, + "presentationMessage": { + "@id": "2481ce81-560b-4ce6-a22b-ee4b6ed369e8", + "@type": "https://didcomm.org/present-proof/1.0/presentation", + "presentations~attach": [ + { + "@id": "libindy-presentation-0", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFfcHJpbWUiOiI5MDg5MzE2Mjc0ODc5NTI4MjMzNDA1MTY0NTgwOTM1OTIxMjIwMjMyMzg5ODE0NjE4OTc3ODA3MjA0MDg4OTQ0ODkzNDc1OTE5NTE4Mjc0NDEwNTU3OTU3NjEwNjYzOTAxMTcwODM1NDM2Nzk1NDU4NDU1Mjg3NzEwOTk3NTk3OTA1OTM3NTYyODIyNjg5NTE3MjAyNzQ4NTUxODgzODQ5NjY3MjYwNTA3NjU0NDM5OTk0MjczNDQ0MTU5NTQyMzg3MzI0OTM5OTAzMDcyMDc2MjQ4Njg1MTgyMjA4NTA0OTkyOTg5MTk0NzUwMDgyODU1MTc1NDE1OTIzMjU3MzA0MTQ0NjYxMDc5MDU2NzExMTg3NzMzMDE3NDQ1MTEyOTQyNDAyMTEzNDg0NjM5MTMxMDY2MDc3ODE2NzQzMzY3OTMzMDI3MjY3MTQ3MDIxMTkxODQ0NTQzMzI5NzUzMTA1NTA3MDk0Mzc5OTA5OTYzNjcxMTQ4NzM3Mjk3NDA2MzUxMzk0NTcwNTM3Nzk0NDg1Njc1ODc3MDU5OTI2NTc3MDU4MzY1NTA0MDQwNjAzNDIxMDI3NDYyOTY0OTExNTc5MDAyNjg2NDAzMjMyOTc3OTU0ODM1Nzc2NzQwMDI3NDIxNjI0MTUzNDQ4NzYyODAxODM3OTU3MTQ3NzM0MDkxNDk3NjMwMjA3MTY3MzUzMjAwMTM5ODE4MDg1NjgwMDIzMTc1MDEyNzM4Mjk1NzIwODU2OTMwNDYxMzIxMDQ4NTIxMjQ0ODQ5MjQ5Njc5MDMwMzI0NDcyNjYyOTQxNjc5NDU3OTk3NzQ4NiIsImUiOiI0NTE0MTczNzExODM2MzMzOTk0NjA3MTMwNjQ5MjA0NjEyNzU2Njk1MDI4ODA2NTY0NzI4MzE3OTExNzYxNDA0NTE5Nzk0NjA3NDk4Njg5NjgyOTYxODk3MDc4MDcwMzQ5Nzk0MzUzMDQ1MTY3MTUyMTk2OTU4NTU0NTI5MzgxNjY3MDE5MDA2OSIsInYiOiI1MzM4NDg2NDY2MjE4MTg2ODg3MTUwNzY4NTQ0OTQyMTEyMDcyOTQ1MzczMDQ1MDAzNTk0MTk0MzAxMDA5NDUzMzk5MjMxMDM5NjExMjU4NTE3MTgzODUyMDc4NjI0NjMyMDExNDE2MzI3Mjc1MTM3Nzc1MTAxNjgxODcwMzI4MjY3MTE4MjExNjEwNzAwNDc2MjA5NzMwMTIwODI2NzMyMTkwMDg0ODkyOTc2NTEwMTgxODE2MTkzMzM5MTk0MjE5MDIxOTQ1OTI1NTg4NjEzODEwMjE1Nzg1NDk1NDk0NjQ0NzIwMDM4MjMwMTg1MDUyMDAxMTMxNjE3MjQwMDIyNjQzOTYxNTkwOTU5ODE3ODMxMzg2Mzc5NDQ1MzI2Mzg4NzYzNjQ5MDYxODk4Nzk1ODcwMjE2NTkxMDI3NDkwMzAwMjA0OTc1NzM0NDgyNDM1ODE4MjgwMTQxNzA0MzA0MjMzNDE5NTMyNjc1Mzk3MDE3MTc1MTE3ODI5NDUzNjAxNDM2OTM2MDM3NDMyMzg4OTYyMjMwOTAyNTk1MjE3MTA3MzkxOTMwOTA3NDI4NDQyNDg4ODE2NjQ4NTI4OTkyNjUwMzY0NjIyNDA2MTA5MDUxOTczMjYyOTM3MzYyMTg5NDcwNTUyNDQ2MjAzNTMzNTQzNTY4NjY5MzAwODY0MzQyMzQwMDgwNjg5Mjg5MjI0OTg1MjU4MjU5MTk1Nzc5NTA3MzgwMDI1ODcwNDk0MDIwNDkyMTE2MDExOTA3NjI0NjI0ODg1ODk5NjMxOTk4ODMwNjg4NTY2OTQ5OTgyNjI5Mjg2Mzk2MjIzNDc2NDE1ODM2Nzg3MDA2Mzg5Nzc0MjYxMzk5NjUxMTY3NTYwNzcyMDc5NjkzMDA1NzMwOTQzNTIzNjAwNzM4Mjc4MzA4NDE2MjA5NzQzNzA1ODQ1MzUxNjQzMDUyMTY1MTcyNTg5NTMwMTU0MTU3NjE2OTYzMDg5NjM4ODg4NDc0NDg3MDA3NjY0NTA2ODk5NTE1OTE5MDAxNzIyMDEyNzczMzU3MDc4MjI4OTIzMDMzNTA1NDQ2MzAxOTQxNzA2OTc2NTY3Mzg5NDk3MzgxMDI2NjIyNDEzNTYyODc5MjM0MTM0NTI5Nzk4NzY2ODY0Nzk1OTQ3NzY1ODcwNDgwMTIyNDk0ODE0MzU0MDQ3MzE2ODY0ODczODMzNDgyNDU5NTc1NTQxNDI4NTE0MTciLCJtIjp7Im1hc3Rlcl9zZWNyZXQiOiIxNjE3NTE3NzgwNjcyMjkxNDYzNTc4ODc1NDk1NTkxODgyOTA3ODYzNTk0NzgyMzk4NjczMTIwMDg2OTEwMjA3NzczODk0ODYyNzQxOTIxMzk2OTE2MDUxNDk2NjYzMjIxNDA5MzA3NjA4NTczMDg1ODExMzAyNTYxNDcyMzgxMjY1NjE4MzQyNzc1NTY5MjQ4OTQ3NzY4Mjc3OTQzMzIxMjcyMTY1MjEyMDAxNDI0NDAwMyJ9LCJtMiI6IjE1MDQ5MTk3MTU3NDcyNjQ0MDMzMzE4OTAxNTc5MDYyNTk5NzA2NzU5MzcwMDk1MTk3NzI1NTE3MTM4OTAyMzcwNDUwMTQ5NDk2NjU0MTEzMzA5NTQ4MTc4MDM3NDU1NjY3Njk2NDA0MDY1ODI5MTUzNDYzNDczNzgzMTk5ODA3MjEzNjg5NDE3MTM2NDI4NDg5NzUwNjUzNTc5MjU0NDY0ODk0OTM4MDkyODY2NTUzNjU5In0sImdlX3Byb29mcyI6W119LCJub25fcmV2b2NfcHJvb2YiOm51bGx9XSwiYWdncmVnYXRlZF9wcm9vZiI6eyJjX2hhc2giOiIzNjk4Mjk3ODU5OTY5Nzg3MjI5MTA5NDY2OTIwMDA3ODEwNDA2ODQ3NTI2MDE2NjgxMTIwNDE4OTQ1NDk0NzcwODQyNjI3MjA2MjEyNCIsImNfbGlzdCI6W1syLDIwOCwzLDUzLDIyOSwxMzksMTAyLDUxLDI0MCwxOTUsMTM1LDExNSwxNzYsMTcyLDE4NCw5OSwxMDksMTU2LDgzLDUyLDIxNSwyMjMsODQsMjU1LDY2LDIyNiwyMjMsMTA1LDExMSwyMjEsMTgwLDk1LDEyMiwxMzMsMjIyLDI3LDM5LDk5LDcwLDEzLDM3LDI0LDI1NSwxMTQsMjM1LDEwOSwxODMsNTEsMjEzLDE5MCwyMjYsMTI2LDExOCwyLDIyMCw3OCw0OSw5LDI0MCw1NSwxNzksMTQ3LDUxLDIwMSwyMTMsMjEzLDEzMCw0LDE4MCwxMDMsMTk1LDgsMjYsMTE4LDE0LDEzMCwxOCwxMzMsMTg3LDYyLDMsOTcsMjEwLDEwMiwxMiwxNjIsNzksMTg0LDU1LDIzMiwyMTksMjIwLDE3NSwyNTUsMTY5LDE5NywxMjMsMTI3LDE2MCwyNSwxNTEsMTg3LDg3LDE5MSwxMDksMTk4LDQ0LDcxLDM4LDUwLDEwNCwyNiwyMTYsMTgwLDIxOCwxNDUsMTAsNzYsMTgwLDE1Nyw5OCwyMzQsNzcsMTY5LDE1MSw2OCwxNzAsMTg5LDE1LDIxNyw5OCwyMzUsMTI0LDE2NywyMzYsMjUzLDExMiwyNDQsMTg5LDk1LDE3OCw2MCw3MiwyMjgsMjIzLDcwLDI3LDYwLDIzNiwyMTIsOTcsMjA1LDIyLDI1MCwzNCwyNDYsMTIyLDM0LDgsMjU1LDIyLDEyNywxNTEsMjQyLDE4MCwxNzEsMTIxLDIyNywzMiwxMDMsNTEsMTcwLDIzNCwyMDYsMjAsMTAyLDIwNCwxMTYsMTk5LDAsMTE5LDExNSwxODAsMjA3LDE2LDQzLDU5LDI0MiwxNzksMTksMTk5LDQ4LDEyNyw5LDYzLDg4LDIxLDAsMjE1LDE3NCw0NywxNzcsMjMyLDE4MiwyNTMsMjQ5LDI0OCwxMTgsMTk2LDI1NCwxMzksMTIsMjksMSw0OCwxMDUsMzMsNCwyMDgsMTA2LDIzNSwyNDcsMjEwLDExMiwyMTAsMTA2LDE5OSwxOTgsNDcsOCwyMzYsNTIsOSw2NywxMjgsMjQwLDI1NCwyMzIsMjEwLDQsMjM5LDE4MywzOSwxOTMsMjQyLDMyLDEzMywxOTQsMTQ4LDk4LDExMSw3NywxNTUsMjA1LDE3OCwxOTcsMTRdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6eyJzdWJfcHJvb2ZfaW5kZXgiOjAsInJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In19LCJzZWxmX2F0dGVzdGVkX2F0dHJzIjp7fSwidW5yZXZlYWxlZF9hdHRycyI6e30sInByZWRpY2F0ZXMiOnt9fSwiaWRlbnRpZmllcnMiOlt7InNjaGVtYV9pZCI6Ijd5VzZTb1RqSE5oRDN6WWdtNFBiSzg6Mjp0ZXN0LXNjaGVtYS00ZTk0YzJlNC00ZjQ3LTRmZjMtYTg4OC02ZjY0ZGE2YTkyZGM6MS4wIiwiY3JlZF9kZWZfaWQiOiI3eVc2U29UakhOaEQzellnbTRQYks4OjM6Q0w6NDcyMzE5OlRBRyIsInJldl9yZWdfaWQiOm51bGwsInRpbWVzdGFtcCI6bnVsbH1dfQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + }, + }, + "requestMessage": { + "@id": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + "@type": "https://didcomm.org/present-proof/1.0/request-presentation", + "request_presentations~attach": [ + { + "@id": "libindy-request-presentation-0", + "data": { + "base64": "eyJuYW1lIjoicHJvb2YtcmVxdWVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjUyODExNDc1NTIxNzg3NzExMjI1Mzc0NSIsInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjp7Im5hbWUiOnsibmFtZSI6Im5hbWUiLCJyZXN0cmljdGlvbnMiOlt7ImNyZWRfZGVmX2lkIjoiN3lXNlNvVGpITmhEM3pZZ200UGJLODozOkNMOjQ3MjMxOTpUQUcifV19fSwicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOnt9fQ==", + }, + "mime-type": "application/json", + }, + ], + }, + "state": "done", + "threadId": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + }, + }, + "72c96cd1-1f26-4bf3-8a00-5c00926859a8": { + "id": "72c96cd1-1f26-4bf3-8a00-5c00926859a8", + "tags": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "state": "done", + "threadId": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + "type": "ProofExchangeRecord", + "value": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "createdAt": "2022-09-08T19:36:06.208Z", + "id": "72c96cd1-1f26-4bf3-8a00-5c00926859a8", + "isVerified": true, + "metadata": {}, + "presentationMessage": { + "@id": "4185f336-f307-4022-a27d-78d1271586f6", + "@type": "https://didcomm.org/present-proof/1.0/presentation", + "presentations~attach": [ + { + "@id": "libindy-presentation-0", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFfcHJpbWUiOiI1MjE1MTkyMzYwMDA2NTg5OTUyMjM1MTYwNjkxMzU1OTc4MjA2NDUzODA3NDUzNTk5OTE5OTkzNzM1NTczNzA1OTA2OTY5MTAwMjIzOTM4MjMyMzE5NDE1Njk1NTMzNjQ4MDMzNzc3NzI4NTE3NzI4Mjk4NDU1OTI0NjQ1NTU1NjQyNjE1NTQ3NTI0MDc0MTAyOTQyNjg4NzQwNDcwMjg4MDAyNTgwMjI4OTg4MDk4MDk0NjQxODc5NjkxNzQwMTY2OTc0MzA2OTczNjQ2MDA3ODg3NTU1NDU2NjM3NTUyODk2NjkxNDc3NDg2MDM1MTIxMjU0NDY0NzA2MDY0Mzg2NzA1MzU0MDk3MDcxNDc5OTkwMzIwNzIxMzQ4MjM1NTQ2NjI5OTcxNzAyNzgwNjUwNTgyMDQ3MzMxOTIwNjE5OTIzODg4NjU1NjI0NjYzMDE4NTAzMDUzOTQ4OTEyNzk4MDE1ODA2Mzk5MTAxNTg3MzAzNTgwNzkyMzQ3OTU2NjE1MDgzODczNDQ5MTM0MjEyOTE2MDY3MTExMjM4MjQzMjM1MTk5OTk1MTUzNjIyMTM1NjA4Nzg5OTc2MTk2NTkzMTczNTU4NTA0NDYxODA0MTk5MzY3MTkwODY2MDY5MTk1MDE5MjA4NjQyMzAwNzE5NjAzMDk5MzI1MjA0Njg4MzkwMjc2NTM5NjQxMDk0Mjc4NTY2NTUzOTU1Mzg4NDAwMDUyNzEyNzk0MjUwODgxODg3NDIzMDM5NTgyMjM5MjQ4NTk0NjYyMDYzMDA0ODI2MzE5NDQxNTUwNzEyNDA3MTQ4NDc4Njc1MCIsImUiOiIxNTk2OTcwODg4Njk3NTIwMjIzMzA3ODgyOTM5MzI5OTY0NTM3NjA4Mzc3NjQ0MTc2MjIyODczMjc2MTc0Mjg5ODkzNjgyNDAzODg0NzkzOTk1NDIwMjQ2MTEzNDk5NjMxNDAyMTQxMzQ0OTYxOTg3NDAyNjAxNDc4NTQ3NjEwNjI5MTgzNzc2NDQiLCJ2IjoiOTE0NzEwNTI1NDAwMDA2MDgzNTMwMTMzOTIxMjQyNTQwMzI5ODAzNjI4MjgyMDA0MDAzMTY1ODIyMjMyOTM0NDYxODAyMjQ4MDk4OTU5MzY1MTY2ODA3MjEwODgzNzIyNjIyMzA1ODQyNjM4NzM4NjIzNzM5NzYwNjEzODc5NDcwOTU5NDkyNjg1ODY5MTY4NTQyMDU1MjAyNzcxMDYxODI4MDEwMTAzNDY3Nzk2MDA5NDIzMTc4NzU2NDQ5OTk3NDQzODk5NDkyMDEyMTE0OTYyMDk5MDgwMDg1MzAxMjUyMDU1NDk0MDUzMTM4MzU4NDE0MjM0MDg5MzQyNDY2Nzg0MTA4ODkwMjA2NjY0NDI1NDE1ODYzNDA5NTI1MjI3NDY0NTg1OTU0MzQ3OTk2OTIwNTU0NTg5Nzc3OTA1MzMzMDMxMzAzMzEzNTI1OTY3NDg3NzAyMDQxODEwNzA3ODQ1OTAyMTAyODAxMTM0NDk3ODAxNjQyNTgwNzQyNjg0Njk5OTk3ODM2NTY0NzM2NDE5NDc0NzM1Mzg2ODkzMTQwMDA4ODIzNjIzMjA1MDA5MzE3NjgzNTIyNjI5NzkwMDY1NDExNDE5MzY3MTc3MTgwMTQ3MDk3ODkwNTkxNDM4NTkyMjQyNzA1MTg5NDM4NDE2MjA4MTA0MjczNjI3MjUyNzc2NzY1NjI0NjExMjk3NjQzODA5NzIwMjcxMDUzOTY1MzM4MTA5Njk1MTM4NjA3NTA0NzExMDc0NDY0ODU5Mzc5MzUxNTI1ODY4NzgyMjk2MzIzNDIwMzIwMTAwMzQ4MDA5MDMyMzIxOTM2ODMzNDk0Nzc2NDUxMjQ0NzMzNTQxMzI3Mzc4MTc3OTYwMTMzOTg2NzE4NTA2NTk0MzE2MjMyMzEwMTk5NjE2NjQ4NzI3MjU0MTUwNjg2MjA4OTQ1MTM3Mzg5MTI5Mjg4NzUyNzE3MjA5NjI3ODcyNjE3NTM3MjEwMDk2NTU2MTkzMzgxMzUwNTgzMDM1NjI5OTg3MjE5MTk3OTQ4OTEyNDI3NzI0MTA1ODE2NzA0NDkyOTMzNzQyMjcxOTQ5NTk1ODAwOTUyMDA5NDczNzQ2OTM5ODU0Mzg0NDI4MzY0MjE0NjUyNjI4MDgyOTQwOTMzODcwNTE1MjE4NTc1MzY4NDgwMjM4OTM2MTc0NDM5NzExNTUzNDI3NDEwNzA2NTgxMTkzMzg2NDM2ODEyIiwibSI6eyJtYXN0ZXJfc2VjcmV0IjoiNDU1NjIzMDY3NjQ4NDQ0NTEzODAyNzM4ODkxODAyMTkxNDg1MzY0NzMzMDkxMTIwMjM5MDg1ODY0NTQxMzU5Njg0MDgyMDczNTgzNDMzMjM4MzU2Njg2MTgwNzI1MzIyMjU2MzgwMjYxNzYyMTc5MTg3MDU3Mjk4Nzg3NzEzNDcyODY5MjE5NDI1ODgyNTEyNzI0ODk4NjkzMDk3OTkwODI4MDIzMjUwNTkyMTAwNDI5OSJ9LCJtMiI6IjEyMjUwNjAyNjEwOTE0Njk5NDEzNDk3MjEyOTYwNTA3OTQ1MDkwOTk1OTMwNjYyMDMyOTA2NTIxNDUxNjI0MDUxMTk2MTg3MjIxNjgxNzEzMDc1OTg0MDY5MjY5MTY3MDQ5ODExMDc3NDk1NTM2MDg4OTE0NTc3MDk2NTgwNjMwODIzNDQyMTk4Nzk5Nzg4NzIyNDIzMzg4MDIyNjE5MzM4MzA2MjM3NTQ2MzI5OTI4MDEifSwiZ2VfcHJvb2ZzIjpbXX0sIm5vbl9yZXZvY19wcm9vZiI6bnVsbH1dLCJhZ2dyZWdhdGVkX3Byb29mIjp7ImNfaGFzaCI6IjU4Njg3MDc5MDA2NTc0OTY1OTA5ODk2MDc5OTU0NTA2NDM2ODg0NzMxOTIxMjk0NDA5NDYxNzI5NjkzMTI4MDM1OTcxMjAwMjY5NzgzIiwiY19saXN0IjpbWzEsMTU3LDMxLDExMiwxNjQsMjA1LDIyNCw3NiwzMiwyMTIsMTczLDc3LDIxNyw2MiwxNzMsMTc3LDQsMTU5LDE0OSwyMzMsMTE3LDEwNCw5MiwxMzgsMzUsMjYsNDgsODUsMjQwLDIzNCw0MywxOTYsNTksMTE0LDI0MiwxNTMsNDAsMTg3LDIzOSwxMDAsMTcwLDIsMzYsMTA3LDIyOSwzNywxMTEsNjAsOTIsOSwxMTcsMjM4LDE0MCwyNDMsMywyNDIsMjM0LDE1MiwxNjYsNiwxNjEsMTM0LDIyMiwxNjMsNDMsNjIsMTIsODEsMjUzLDE3NiwxMjUsMjQsOCw5MiwxMywyMjIsMTcxLDIzMywxODIsMjE1LDIyLDIzNCw5NSwxMTIsMTIyLDEwMiwxNTIsMTA2LDE4Miw0LDIxOSw1NiwxMDEsMTQ3LDIyNywyNDYsMTE1LDY0LDE4Myw0LDY3LDEyNCwxNDAsMTY2LDEzOCwxNCwyNCw1MSwxODUsMTk5LDEwOSwyLDUxLDE5MCwxODEsMjQ3LDYxLDE5MCw4NSwyNDcsMjE3LDIyLDE2NCwxOTIsMjM1LDE4NywxNDgsMjYsMTA5LDI1MywxMTYsMjQxLDY1LDEzNiw0MSwyMjUsMjI5LDE3OCwxMjUsNzksMjQ2LDUzLDQ0LDE5MiwxNDEsMjQzLDE4NCwxNTEsMTIsOSwxMDQsMjE5LDE2Miw3MSw3Miw3LDg5LDgzLDgsMjI1LDEzLDQ4LDEzMywyNDcsMTg3LDExNCw4NSw3Nyw1OSw5OSw3MCw2OSw3MSwxMzMsMTY5LDExMSwxMTAsNzksNDQsMTc0LDIzLDEwMywxNjYsNjksNSwxNDcsNjYsNzcsMTYzLDExMiw0MiwxOTksMTQyLDE0LDIyLDE1MSwyNDgsMzMsMzQsMTk4LDE1NiwyMCwyMzEsMjA0LDcwLDgyLDI1MywxMzgsMTMxLDQyLDIwNSwzMywxMDksMjMyLDE3LDEwNiw2OSw3Miw2MCwyMTAsMjMyLDExMywxMjQsNzYsMTgxLDIwOCwwLDE4NiwyNTEsMTQzLDExMyw0NSwxNzEsMTAsNDMsMTUsMzAsMjA4LDkzLDYsMTY4LDEwNiwyMDMsNDgsMjUwLDIxOSw5LDE3LDEwNCwzNSw1OCwxNTksMTgwLDExMyw2NiwxMzYsNjJdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsiMGZlYTE4OTctNDMxMS00OGYyLWEyYzItMzg3NGU5OTNhZWYwIjp7InN1Yl9wcm9vZl9pbmRleCI6MCwicmF3IjoiQWxpY2UiLCJlbmNvZGVkIjoiMjcwMzQ2NDAwMjQxMTczMzEwMzMwNjMxMjgwNDQwMDQzMTgyMTg0ODY4MTY5MzE1MjA4ODY0MDU1MzU2NTk5MzQ0MTc0Mzg3ODE1MDcifX0sInNlbGZfYXR0ZXN0ZWRfYXR0cnMiOnt9LCJ1bnJldmVhbGVkX2F0dHJzIjp7fSwicHJlZGljYXRlcyI6e319LCJpZGVudGlmaWVycyI6W3sic2NoZW1hX2lkIjoiN3lXNlNvVGpITmhEM3pZZ200UGJLODoyOnRlc3Qtc2NoZW1hLTRlOTRjMmU0LTRmNDctNGZmMy1hODg4LTZmNjRkYTZhOTJkYzoxLjAiLCJjcmVkX2RlZl9pZCI6Ijd5VzZTb1RqSE5oRDN6WWdtNFBiSzg6MzpDTDo0NzIzMTk6VEFHIiwicmV2X3JlZ19pZCI6bnVsbCwidGltZXN0YW1wIjpudWxsfV19", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + }, + "proposalMessage": { + "@id": "00cdd404-82fc-431d-9c18-fb643636d2a5", + "@type": "https://didcomm.org/present-proof/1.0/propose-presentation", + "presentation_proposal": { + "@type": "https://didcomm.org/present-proof/1.0/presentation-preview", + "attributes": [ + { + "cred_def_id": "7yW6SoTjHNhD3zYgm4PbK8:3:CL:472319:TAG", + "name": "name", + "value": "Alice", + }, + ], + "predicates": [], + }, + }, + "requestMessage": { + "@id": "2862b25d-396c-46d7-ad09-f48ff4ea6db6", + "@type": "https://didcomm.org/present-proof/1.0/request-presentation", + "request_presentations~attach": [ + { + "@id": "libindy-request-presentation-0", + "data": { + "base64": "eyJuYW1lIjoicHJvb2YtcmVxdWVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjQwMzU1MDc0MDYxMTU0MzEwMzA5NzMyMiIsInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjp7IjBmZWExODk3LTQzMTEtNDhmMi1hMmMyLTM4NzRlOTkzYWVmMCI6eyJuYW1lIjoibmFtZSIsInJlc3RyaWN0aW9ucyI6W3siY3JlZF9kZWZfaWQiOiI3eVc2U29UakhOaEQzellnbTRQYks4OjM6Q0w6NDcyMzE5OlRBRyJ9XX19LCJyZXF1ZXN0ZWRfcHJlZGljYXRlcyI6e319", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + }, + "state": "done", + "threadId": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2022-09-08T19:35:53.872Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.3.1", + "updatedAt": "2023-01-21T22:50:20.522Z", + }, + }, + "ea840186-3c77-45f4-a2e6-349811ad8994": { + "id": "ea840186-3c77-45f4-a2e6-349811ad8994", + "tags": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "state": "done", + "threadId": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + }, + "type": "ProofExchangeRecord", + "value": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "createdAt": "2022-09-08T19:36:06.261Z", + "id": "ea840186-3c77-45f4-a2e6-349811ad8994", + "isVerified": true, + "metadata": {}, + "presentationMessage": { + "@id": "2481ce81-560b-4ce6-a22b-ee4b6ed369e8", + "@type": "https://didcomm.org/present-proof/1.0/presentation", + "presentations~attach": [ + { + "@id": "libindy-presentation-0", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFfcHJpbWUiOiI5MDg5MzE2Mjc0ODc5NTI4MjMzNDA1MTY0NTgwOTM1OTIxMjIwMjMyMzg5ODE0NjE4OTc3ODA3MjA0MDg4OTQ0ODkzNDc1OTE5NTE4Mjc0NDEwNTU3OTU3NjEwNjYzOTAxMTcwODM1NDM2Nzk1NDU4NDU1Mjg3NzEwOTk3NTk3OTA1OTM3NTYyODIyNjg5NTE3MjAyNzQ4NTUxODgzODQ5NjY3MjYwNTA3NjU0NDM5OTk0MjczNDQ0MTU5NTQyMzg3MzI0OTM5OTAzMDcyMDc2MjQ4Njg1MTgyMjA4NTA0OTkyOTg5MTk0NzUwMDgyODU1MTc1NDE1OTIzMjU3MzA0MTQ0NjYxMDc5MDU2NzExMTg3NzMzMDE3NDQ1MTEyOTQyNDAyMTEzNDg0NjM5MTMxMDY2MDc3ODE2NzQzMzY3OTMzMDI3MjY3MTQ3MDIxMTkxODQ0NTQzMzI5NzUzMTA1NTA3MDk0Mzc5OTA5OTYzNjcxMTQ4NzM3Mjk3NDA2MzUxMzk0NTcwNTM3Nzk0NDg1Njc1ODc3MDU5OTI2NTc3MDU4MzY1NTA0MDQwNjAzNDIxMDI3NDYyOTY0OTExNTc5MDAyNjg2NDAzMjMyOTc3OTU0ODM1Nzc2NzQwMDI3NDIxNjI0MTUzNDQ4NzYyODAxODM3OTU3MTQ3NzM0MDkxNDk3NjMwMjA3MTY3MzUzMjAwMTM5ODE4MDg1NjgwMDIzMTc1MDEyNzM4Mjk1NzIwODU2OTMwNDYxMzIxMDQ4NTIxMjQ0ODQ5MjQ5Njc5MDMwMzI0NDcyNjYyOTQxNjc5NDU3OTk3NzQ4NiIsImUiOiI0NTE0MTczNzExODM2MzMzOTk0NjA3MTMwNjQ5MjA0NjEyNzU2Njk1MDI4ODA2NTY0NzI4MzE3OTExNzYxNDA0NTE5Nzk0NjA3NDk4Njg5NjgyOTYxODk3MDc4MDcwMzQ5Nzk0MzUzMDQ1MTY3MTUyMTk2OTU4NTU0NTI5MzgxNjY3MDE5MDA2OSIsInYiOiI1MzM4NDg2NDY2MjE4MTg2ODg3MTUwNzY4NTQ0OTQyMTEyMDcyOTQ1MzczMDQ1MDAzNTk0MTk0MzAxMDA5NDUzMzk5MjMxMDM5NjExMjU4NTE3MTgzODUyMDc4NjI0NjMyMDExNDE2MzI3Mjc1MTM3Nzc1MTAxNjgxODcwMzI4MjY3MTE4MjExNjEwNzAwNDc2MjA5NzMwMTIwODI2NzMyMTkwMDg0ODkyOTc2NTEwMTgxODE2MTkzMzM5MTk0MjE5MDIxOTQ1OTI1NTg4NjEzODEwMjE1Nzg1NDk1NDk0NjQ0NzIwMDM4MjMwMTg1MDUyMDAxMTMxNjE3MjQwMDIyNjQzOTYxNTkwOTU5ODE3ODMxMzg2Mzc5NDQ1MzI2Mzg4NzYzNjQ5MDYxODk4Nzk1ODcwMjE2NTkxMDI3NDkwMzAwMjA0OTc1NzM0NDgyNDM1ODE4MjgwMTQxNzA0MzA0MjMzNDE5NTMyNjc1Mzk3MDE3MTc1MTE3ODI5NDUzNjAxNDM2OTM2MDM3NDMyMzg4OTYyMjMwOTAyNTk1MjE3MTA3MzkxOTMwOTA3NDI4NDQyNDg4ODE2NjQ4NTI4OTkyNjUwMzY0NjIyNDA2MTA5MDUxOTczMjYyOTM3MzYyMTg5NDcwNTUyNDQ2MjAzNTMzNTQzNTY4NjY5MzAwODY0MzQyMzQwMDgwNjg5Mjg5MjI0OTg1MjU4MjU5MTk1Nzc5NTA3MzgwMDI1ODcwNDk0MDIwNDkyMTE2MDExOTA3NjI0NjI0ODg1ODk5NjMxOTk4ODMwNjg4NTY2OTQ5OTgyNjI5Mjg2Mzk2MjIzNDc2NDE1ODM2Nzg3MDA2Mzg5Nzc0MjYxMzk5NjUxMTY3NTYwNzcyMDc5NjkzMDA1NzMwOTQzNTIzNjAwNzM4Mjc4MzA4NDE2MjA5NzQzNzA1ODQ1MzUxNjQzMDUyMTY1MTcyNTg5NTMwMTU0MTU3NjE2OTYzMDg5NjM4ODg4NDc0NDg3MDA3NjY0NTA2ODk5NTE1OTE5MDAxNzIyMDEyNzczMzU3MDc4MjI4OTIzMDMzNTA1NDQ2MzAxOTQxNzA2OTc2NTY3Mzg5NDk3MzgxMDI2NjIyNDEzNTYyODc5MjM0MTM0NTI5Nzk4NzY2ODY0Nzk1OTQ3NzY1ODcwNDgwMTIyNDk0ODE0MzU0MDQ3MzE2ODY0ODczODMzNDgyNDU5NTc1NTQxNDI4NTE0MTciLCJtIjp7Im1hc3Rlcl9zZWNyZXQiOiIxNjE3NTE3NzgwNjcyMjkxNDYzNTc4ODc1NDk1NTkxODgyOTA3ODYzNTk0NzgyMzk4NjczMTIwMDg2OTEwMjA3NzczODk0ODYyNzQxOTIxMzk2OTE2MDUxNDk2NjYzMjIxNDA5MzA3NjA4NTczMDg1ODExMzAyNTYxNDcyMzgxMjY1NjE4MzQyNzc1NTY5MjQ4OTQ3NzY4Mjc3OTQzMzIxMjcyMTY1MjEyMDAxNDI0NDAwMyJ9LCJtMiI6IjE1MDQ5MTk3MTU3NDcyNjQ0MDMzMzE4OTAxNTc5MDYyNTk5NzA2NzU5MzcwMDk1MTk3NzI1NTE3MTM4OTAyMzcwNDUwMTQ5NDk2NjU0MTEzMzA5NTQ4MTc4MDM3NDU1NjY3Njk2NDA0MDY1ODI5MTUzNDYzNDczNzgzMTk5ODA3MjEzNjg5NDE3MTM2NDI4NDg5NzUwNjUzNTc5MjU0NDY0ODk0OTM4MDkyODY2NTUzNjU5In0sImdlX3Byb29mcyI6W119LCJub25fcmV2b2NfcHJvb2YiOm51bGx9XSwiYWdncmVnYXRlZF9wcm9vZiI6eyJjX2hhc2giOiIzNjk4Mjk3ODU5OTY5Nzg3MjI5MTA5NDY2OTIwMDA3ODEwNDA2ODQ3NTI2MDE2NjgxMTIwNDE4OTQ1NDk0NzcwODQyNjI3MjA2MjEyNCIsImNfbGlzdCI6W1syLDIwOCwzLDUzLDIyOSwxMzksMTAyLDUxLDI0MCwxOTUsMTM1LDExNSwxNzYsMTcyLDE4NCw5OSwxMDksMTU2LDgzLDUyLDIxNSwyMjMsODQsMjU1LDY2LDIyNiwyMjMsMTA1LDExMSwyMjEsMTgwLDk1LDEyMiwxMzMsMjIyLDI3LDM5LDk5LDcwLDEzLDM3LDI0LDI1NSwxMTQsMjM1LDEwOSwxODMsNTEsMjEzLDE5MCwyMjYsMTI2LDExOCwyLDIyMCw3OCw0OSw5LDI0MCw1NSwxNzksMTQ3LDUxLDIwMSwyMTMsMjEzLDEzMCw0LDE4MCwxMDMsMTk1LDgsMjYsMTE4LDE0LDEzMCwxOCwxMzMsMTg3LDYyLDMsOTcsMjEwLDEwMiwxMiwxNjIsNzksMTg0LDU1LDIzMiwyMTksMjIwLDE3NSwyNTUsMTY5LDE5NywxMjMsMTI3LDE2MCwyNSwxNTEsMTg3LDg3LDE5MSwxMDksMTk4LDQ0LDcxLDM4LDUwLDEwNCwyNiwyMTYsMTgwLDIxOCwxNDUsMTAsNzYsMTgwLDE1Nyw5OCwyMzQsNzcsMTY5LDE1MSw2OCwxNzAsMTg5LDE1LDIxNyw5OCwyMzUsMTI0LDE2NywyMzYsMjUzLDExMiwyNDQsMTg5LDk1LDE3OCw2MCw3MiwyMjgsMjIzLDcwLDI3LDYwLDIzNiwyMTIsOTcsMjA1LDIyLDI1MCwzNCwyNDYsMTIyLDM0LDgsMjU1LDIyLDEyNywxNTEsMjQyLDE4MCwxNzEsMTIxLDIyNywzMiwxMDMsNTEsMTcwLDIzNCwyMDYsMjAsMTAyLDIwNCwxMTYsMTk5LDAsMTE5LDExNSwxODAsMjA3LDE2LDQzLDU5LDI0MiwxNzksMTksMTk5LDQ4LDEyNyw5LDYzLDg4LDIxLDAsMjE1LDE3NCw0NywxNzcsMjMyLDE4MiwyNTMsMjQ5LDI0OCwxMTgsMTk2LDI1NCwxMzksMTIsMjksMSw0OCwxMDUsMzMsNCwyMDgsMTA2LDIzNSwyNDcsMjEwLDExMiwyMTAsMTA2LDE5OSwxOTgsNDcsOCwyMzYsNTIsOSw2NywxMjgsMjQwLDI1NCwyMzIsMjEwLDQsMjM5LDE4MywzOSwxOTMsMjQyLDMyLDEzMywxOTQsMTQ4LDk4LDExMSw3NywxNTUsMjA1LDE3OCwxOTcsMTRdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6eyJzdWJfcHJvb2ZfaW5kZXgiOjAsInJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In19LCJzZWxmX2F0dGVzdGVkX2F0dHJzIjp7fSwidW5yZXZlYWxlZF9hdHRycyI6e30sInByZWRpY2F0ZXMiOnt9fSwiaWRlbnRpZmllcnMiOlt7InNjaGVtYV9pZCI6Ijd5VzZTb1RqSE5oRDN6WWdtNFBiSzg6Mjp0ZXN0LXNjaGVtYS00ZTk0YzJlNC00ZjQ3LTRmZjMtYTg4OC02ZjY0ZGE2YTkyZGM6MS4wIiwiY3JlZF9kZWZfaWQiOiI3eVc2U29UakhOaEQzellnbTRQYks4OjM6Q0w6NDcyMzE5OlRBRyIsInJldl9yZWdfaWQiOm51bGwsInRpbWVzdGFtcCI6bnVsbH1dfQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + }, + }, + "requestMessage": { + "@id": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + "@type": "https://didcomm.org/present-proof/1.0/request-presentation", + "request_presentations~attach": [ + { + "@id": "libindy-request-presentation-0", + "data": { + "base64": "eyJuYW1lIjoicHJvb2YtcmVxdWVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjUyODExNDc1NTIxNzg3NzExMjI1Mzc0NSIsInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjp7Im5hbWUiOnsibmFtZSI6Im5hbWUiLCJyZXN0cmljdGlvbnMiOlt7ImNyZWRfZGVmX2lkIjoiN3lXNlNvVGpITmhEM3pZZ200UGJLODozOkNMOjQ3MjMxOTpUQUcifV19fSwicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOnt9fQ==", + }, + "mime-type": "application/json", + }, + ], + }, + "state": "done", + "threadId": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + }, + }, + "ec02ba64-63e3-46bc-b2a4-9d549d642d30": { + "id": "ec02ba64-63e3-46bc-b2a4-9d549d642d30", + "tags": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "state": "done", + "threadId": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + "type": "ProofExchangeRecord", + "value": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "createdAt": "2022-09-08T19:36:06.208Z", + "id": "ec02ba64-63e3-46bc-b2a4-9d549d642d30", + "metadata": {}, + "presentationMessage": { + "@id": "4185f336-f307-4022-a27d-78d1271586f6", + "@type": "https://didcomm.org/present-proof/1.0/presentation", + "presentations~attach": [ + { + "@id": "libindy-presentation-0", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFfcHJpbWUiOiI1MjE1MTkyMzYwMDA2NTg5OTUyMjM1MTYwNjkxMzU1OTc4MjA2NDUzODA3NDUzNTk5OTE5OTkzNzM1NTczNzA1OTA2OTY5MTAwMjIzOTM4MjMyMzE5NDE1Njk1NTMzNjQ4MDMzNzc3NzI4NTE3NzI4Mjk4NDU1OTI0NjQ1NTU1NjQyNjE1NTQ3NTI0MDc0MTAyOTQyNjg4NzQwNDcwMjg4MDAyNTgwMjI4OTg4MDk4MDk0NjQxODc5NjkxNzQwMTY2OTc0MzA2OTczNjQ2MDA3ODg3NTU1NDU2NjM3NTUyODk2NjkxNDc3NDg2MDM1MTIxMjU0NDY0NzA2MDY0Mzg2NzA1MzU0MDk3MDcxNDc5OTkwMzIwNzIxMzQ4MjM1NTQ2NjI5OTcxNzAyNzgwNjUwNTgyMDQ3MzMxOTIwNjE5OTIzODg4NjU1NjI0NjYzMDE4NTAzMDUzOTQ4OTEyNzk4MDE1ODA2Mzk5MTAxNTg3MzAzNTgwNzkyMzQ3OTU2NjE1MDgzODczNDQ5MTM0MjEyOTE2MDY3MTExMjM4MjQzMjM1MTk5OTk1MTUzNjIyMTM1NjA4Nzg5OTc2MTk2NTkzMTczNTU4NTA0NDYxODA0MTk5MzY3MTkwODY2MDY5MTk1MDE5MjA4NjQyMzAwNzE5NjAzMDk5MzI1MjA0Njg4MzkwMjc2NTM5NjQxMDk0Mjc4NTY2NTUzOTU1Mzg4NDAwMDUyNzEyNzk0MjUwODgxODg3NDIzMDM5NTgyMjM5MjQ4NTk0NjYyMDYzMDA0ODI2MzE5NDQxNTUwNzEyNDA3MTQ4NDc4Njc1MCIsImUiOiIxNTk2OTcwODg4Njk3NTIwMjIzMzA3ODgyOTM5MzI5OTY0NTM3NjA4Mzc3NjQ0MTc2MjIyODczMjc2MTc0Mjg5ODkzNjgyNDAzODg0NzkzOTk1NDIwMjQ2MTEzNDk5NjMxNDAyMTQxMzQ0OTYxOTg3NDAyNjAxNDc4NTQ3NjEwNjI5MTgzNzc2NDQiLCJ2IjoiOTE0NzEwNTI1NDAwMDA2MDgzNTMwMTMzOTIxMjQyNTQwMzI5ODAzNjI4MjgyMDA0MDAzMTY1ODIyMjMyOTM0NDYxODAyMjQ4MDk4OTU5MzY1MTY2ODA3MjEwODgzNzIyNjIyMzA1ODQyNjM4NzM4NjIzNzM5NzYwNjEzODc5NDcwOTU5NDkyNjg1ODY5MTY4NTQyMDU1MjAyNzcxMDYxODI4MDEwMTAzNDY3Nzk2MDA5NDIzMTc4NzU2NDQ5OTk3NDQzODk5NDkyMDEyMTE0OTYyMDk5MDgwMDg1MzAxMjUyMDU1NDk0MDUzMTM4MzU4NDE0MjM0MDg5MzQyNDY2Nzg0MTA4ODkwMjA2NjY0NDI1NDE1ODYzNDA5NTI1MjI3NDY0NTg1OTU0MzQ3OTk2OTIwNTU0NTg5Nzc3OTA1MzMzMDMxMzAzMzEzNTI1OTY3NDg3NzAyMDQxODEwNzA3ODQ1OTAyMTAyODAxMTM0NDk3ODAxNjQyNTgwNzQyNjg0Njk5OTk3ODM2NTY0NzM2NDE5NDc0NzM1Mzg2ODkzMTQwMDA4ODIzNjIzMjA1MDA5MzE3NjgzNTIyNjI5NzkwMDY1NDExNDE5MzY3MTc3MTgwMTQ3MDk3ODkwNTkxNDM4NTkyMjQyNzA1MTg5NDM4NDE2MjA4MTA0MjczNjI3MjUyNzc2NzY1NjI0NjExMjk3NjQzODA5NzIwMjcxMDUzOTY1MzM4MTA5Njk1MTM4NjA3NTA0NzExMDc0NDY0ODU5Mzc5MzUxNTI1ODY4NzgyMjk2MzIzNDIwMzIwMTAwMzQ4MDA5MDMyMzIxOTM2ODMzNDk0Nzc2NDUxMjQ0NzMzNTQxMzI3Mzc4MTc3OTYwMTMzOTg2NzE4NTA2NTk0MzE2MjMyMzEwMTk5NjE2NjQ4NzI3MjU0MTUwNjg2MjA4OTQ1MTM3Mzg5MTI5Mjg4NzUyNzE3MjA5NjI3ODcyNjE3NTM3MjEwMDk2NTU2MTkzMzgxMzUwNTgzMDM1NjI5OTg3MjE5MTk3OTQ4OTEyNDI3NzI0MTA1ODE2NzA0NDkyOTMzNzQyMjcxOTQ5NTk1ODAwOTUyMDA5NDczNzQ2OTM5ODU0Mzg0NDI4MzY0MjE0NjUyNjI4MDgyOTQwOTMzODcwNTE1MjE4NTc1MzY4NDgwMjM4OTM2MTc0NDM5NzExNTUzNDI3NDEwNzA2NTgxMTkzMzg2NDM2ODEyIiwibSI6eyJtYXN0ZXJfc2VjcmV0IjoiNDU1NjIzMDY3NjQ4NDQ0NTEzODAyNzM4ODkxODAyMTkxNDg1MzY0NzMzMDkxMTIwMjM5MDg1ODY0NTQxMzU5Njg0MDgyMDczNTgzNDMzMjM4MzU2Njg2MTgwNzI1MzIyMjU2MzgwMjYxNzYyMTc5MTg3MDU3Mjk4Nzg3NzEzNDcyODY5MjE5NDI1ODgyNTEyNzI0ODk4NjkzMDk3OTkwODI4MDIzMjUwNTkyMTAwNDI5OSJ9LCJtMiI6IjEyMjUwNjAyNjEwOTE0Njk5NDEzNDk3MjEyOTYwNTA3OTQ1MDkwOTk1OTMwNjYyMDMyOTA2NTIxNDUxNjI0MDUxMTk2MTg3MjIxNjgxNzEzMDc1OTg0MDY5MjY5MTY3MDQ5ODExMDc3NDk1NTM2MDg4OTE0NTc3MDk2NTgwNjMwODIzNDQyMTk4Nzk5Nzg4NzIyNDIzMzg4MDIyNjE5MzM4MzA2MjM3NTQ2MzI5OTI4MDEifSwiZ2VfcHJvb2ZzIjpbXX0sIm5vbl9yZXZvY19wcm9vZiI6bnVsbH1dLCJhZ2dyZWdhdGVkX3Byb29mIjp7ImNfaGFzaCI6IjU4Njg3MDc5MDA2NTc0OTY1OTA5ODk2MDc5OTU0NTA2NDM2ODg0NzMxOTIxMjk0NDA5NDYxNzI5NjkzMTI4MDM1OTcxMjAwMjY5NzgzIiwiY19saXN0IjpbWzEsMTU3LDMxLDExMiwxNjQsMjA1LDIyNCw3NiwzMiwyMTIsMTczLDc3LDIxNyw2MiwxNzMsMTc3LDQsMTU5LDE0OSwyMzMsMTE3LDEwNCw5MiwxMzgsMzUsMjYsNDgsODUsMjQwLDIzNCw0MywxOTYsNTksMTE0LDI0MiwxNTMsNDAsMTg3LDIzOSwxMDAsMTcwLDIsMzYsMTA3LDIyOSwzNywxMTEsNjAsOTIsOSwxMTcsMjM4LDE0MCwyNDMsMywyNDIsMjM0LDE1MiwxNjYsNiwxNjEsMTM0LDIyMiwxNjMsNDMsNjIsMTIsODEsMjUzLDE3NiwxMjUsMjQsOCw5MiwxMywyMjIsMTcxLDIzMywxODIsMjE1LDIyLDIzNCw5NSwxMTIsMTIyLDEwMiwxNTIsMTA2LDE4Miw0LDIxOSw1NiwxMDEsMTQ3LDIyNywyNDYsMTE1LDY0LDE4Myw0LDY3LDEyNCwxNDAsMTY2LDEzOCwxNCwyNCw1MSwxODUsMTk5LDEwOSwyLDUxLDE5MCwxODEsMjQ3LDYxLDE5MCw4NSwyNDcsMjE3LDIyLDE2NCwxOTIsMjM1LDE4NywxNDgsMjYsMTA5LDI1MywxMTYsMjQxLDY1LDEzNiw0MSwyMjUsMjI5LDE3OCwxMjUsNzksMjQ2LDUzLDQ0LDE5MiwxNDEsMjQzLDE4NCwxNTEsMTIsOSwxMDQsMjE5LDE2Miw3MSw3Miw3LDg5LDgzLDgsMjI1LDEzLDQ4LDEzMywyNDcsMTg3LDExNCw4NSw3Nyw1OSw5OSw3MCw2OSw3MSwxMzMsMTY5LDExMSwxMTAsNzksNDQsMTc0LDIzLDEwMywxNjYsNjksNSwxNDcsNjYsNzcsMTYzLDExMiw0MiwxOTksMTQyLDE0LDIyLDE1MSwyNDgsMzMsMzQsMTk4LDE1NiwyMCwyMzEsMjA0LDcwLDgyLDI1MywxMzgsMTMxLDQyLDIwNSwzMywxMDksMjMyLDE3LDEwNiw2OSw3Miw2MCwyMTAsMjMyLDExMywxMjQsNzYsMTgxLDIwOCwwLDE4NiwyNTEsMTQzLDExMyw0NSwxNzEsMTAsNDMsMTUsMzAsMjA4LDkzLDYsMTY4LDEwNiwyMDMsNDgsMjUwLDIxOSw5LDE3LDEwNCwzNSw1OCwxNTksMTgwLDExMyw2NiwxMzYsNjJdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsiMGZlYTE4OTctNDMxMS00OGYyLWEyYzItMzg3NGU5OTNhZWYwIjp7InN1Yl9wcm9vZl9pbmRleCI6MCwicmF3IjoiQWxpY2UiLCJlbmNvZGVkIjoiMjcwMzQ2NDAwMjQxMTczMzEwMzMwNjMxMjgwNDQwMDQzMTgyMTg0ODY4MTY5MzE1MjA4ODY0MDU1MzU2NTk5MzQ0MTc0Mzg3ODE1MDcifX0sInNlbGZfYXR0ZXN0ZWRfYXR0cnMiOnt9LCJ1bnJldmVhbGVkX2F0dHJzIjp7fSwicHJlZGljYXRlcyI6e319LCJpZGVudGlmaWVycyI6W3sic2NoZW1hX2lkIjoiN3lXNlNvVGpITmhEM3pZZ200UGJLODoyOnRlc3Qtc2NoZW1hLTRlOTRjMmU0LTRmNDctNGZmMy1hODg4LTZmNjRkYTZhOTJkYzoxLjAiLCJjcmVkX2RlZl9pZCI6Ijd5VzZTb1RqSE5oRDN6WWdtNFBiSzg6MzpDTDo0NzIzMTk6VEFHIiwicmV2X3JlZ19pZCI6bnVsbCwidGltZXN0YW1wIjpudWxsfV19", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + }, + "proposalMessage": { + "@id": "00cdd404-82fc-431d-9c18-fb643636d2a5", + "@type": "https://didcomm.org/present-proof/1.0/propose-presentation", + "presentation_proposal": { + "@type": "https://didcomm.org/present-proof/1.0/presentation-preview", + "attributes": [ + { + "cred_def_id": "7yW6SoTjHNhD3zYgm4PbK8:3:CL:472319:TAG", + "name": "name", + "value": "Alice", + }, + ], + "predicates": [], + }, + }, + "requestMessage": { + "@id": "2862b25d-396c-46d7-ad09-f48ff4ea6db6", + "@type": "https://didcomm.org/present-proof/1.0/request-presentation", + "request_presentations~attach": [ + { + "@id": "libindy-request-presentation-0", + "data": { + "base64": "eyJuYW1lIjoicHJvb2YtcmVxdWVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjQwMzU1MDc0MDYxMTU0MzEwMzA5NzMyMiIsInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjp7IjBmZWExODk3LTQzMTEtNDhmMi1hMmMyLTM4NzRlOTkzYWVmMCI6eyJuYW1lIjoibmFtZSIsInJlc3RyaWN0aW9ucyI6W3siY3JlZF9kZWZfaWQiOiI3eVc2U29UakhOaEQzellnbTRQYks4OjM6Q0w6NDcyMzE5OlRBRyJ9XX19LCJyZXF1ZXN0ZWRfcHJlZGljYXRlcyI6e319", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + }, + "state": "done", + "threadId": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update the did records 1`] = ` +{ + "1-4e4f-41d9-94c4-f49351b811f1": { + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "did": "did:peer:1zQmWxKCTkKYQvzdUshMWy7b8vy3Y7uLtB9hp6BbQhKEtQCd", + "legacyUnqualifiedDid": undefined, + "method": "peer", + "methodSpecificIdentifier": "1zQmWxKCTkKYQvzdUshMWy7b8vy3Y7uLtB9hp6BbQhKEtQCd", + "recipientKeyFingerprints": [ + "z6Mkpg6nDPuYdLMuzNEjaa2Xprh3J2MC1WtNakWUEFqC4wvU", + ], + "role": "created", + }, + "type": "DidRecord", + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": [ + "z6Mkpg6nDPuYdLMuzNEjaa2Xprh3J2MC1WtNakWUEFqC4wvU", + ], + "role": "created", + }, + "createdAt": "2022-12-27T13:51:21.344Z", + "did": "did:peer:1zQmWxKCTkKYQvzdUshMWy7b8vy3Y7uLtB9hp6BbQhKEtQCd", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "authentication": [ + { + "controller": "#id", + "id": "#57a05508-1d1c-474c-8c68-1afcf3188720", + "publicKeyBase58": "BDqjd9f7HnsSssQ2u14gym93UT5Lbde1tjbYPysB9j96", + "type": "Ed25519VerificationKey2018", + }, + ], + "id": "did:peer:1zQmWxKCTkKYQvzdUshMWy7b8vy3Y7uLtB9hp6BbQhKEtQCd", + "keyAgreement": [ + { + "controller": "#id", + "id": "#59d2ce6f-c9fc-49c6-a8f6-eab14820b028", + "publicKeyBase58": "avivQP6GvWj6cBbxbXSSZnZTA4tGsvQ5DB9FXm45tZt", + "type": "X25519KeyAgreementKey2019", + }, + ], + "service": [ + { + "id": "#inline-0", + "priority": 0, + "recipientKeys": [ + "#57a05508-1d1c-474c-8c68-1afcf3188720", + ], + "routingKeys": [ + "did:key:z6MkqhwtVT5V6kwsNkErPNgSWuRGd4QLFQ324FMfw5oWGHop#z6MkqhwtVT5V6kwsNkErPNgSWuRGd4QLFQ324FMfw5oWGHop", + ], + "serviceEndpoint": "ws://ssi.mediator.com", + "type": "did-communication", + }, + ], + }, + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "metadata": {}, + "role": "created", + "updatedAt": "2023-01-21T22:50:20.522Z", + }, + }, + "2-4e4f-41d9-94c4-f49351b811f1": { + "id": "2-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "did": "did:peer:1zQmavZzrLPRYRGd5CgyKV4GBZG43eGzjic9FHXc7jLHY14W", + "legacyUnqualifiedDid": undefined, + "method": "peer", + "methodSpecificIdentifier": "1zQmavZzrLPRYRGd5CgyKV4GBZG43eGzjic9FHXc7jLHY14W", + "recipientKeyFingerprints": [ + "z6MkuLL6bQykGpposTrQhfYceQRR4JDzvdm133xpwb7Dv1oz", + ], + "role": "received", + }, + "type": "DidRecord", + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": [ + "z6MkuLL6bQykGpposTrQhfYceQRR4JDzvdm133xpwb7Dv1oz", + ], + "role": "received", + }, + "createdAt": "2022-12-27T13:51:51.414Z", + "did": "did:peer:1zQmavZzrLPRYRGd5CgyKV4GBZG43eGzjic9FHXc7jLHY14W", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "authentication": [ + { + "controller": "#id", + "id": "#6173438e-09a5-4b1e-895a-0563f5a169b7", + "publicKeyBase58": "Ft541AjJwHLLky1i26amoJsREix9WkWeM33u7K9Czo2c", + "type": "Ed25519VerificationKey2018", + }, + ], + "id": "did:peer:1zQmavZzrLPRYRGd5CgyKV4GBZG43eGzjic9FHXc7jLHY14W", + "keyAgreement": [ + { + "controller": "#id", + "id": "#a0448ee3-093a-4b16-bc59-8bf8559d60a5", + "publicKeyBase58": "JCfZJ72mtgGE9xuekJKV6yoAGzLgQCNkdHKkct8uaNKb", + "type": "X25519KeyAgreementKey2019", + }, + ], + "service": [ + { + "id": "#inline-0", + "priority": 0, + "recipientKeys": [ + "#6173438e-09a5-4b1e-895a-0563f5a169b7", + ], + "routingKeys": [], + "serviceEndpoint": "http://ssi.verifier.com", + "type": "did-communication", + }, + ], + }, + "id": "2-4e4f-41d9-94c4-f49351b811f1", + "metadata": {}, + "role": "received", + "updatedAt": "2023-01-21T22:50:20.522Z", + }, + }, + "3-4e4f-41d9-94c4-f49351b811f1": { + "id": "3-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "did": "did:peer:1zQmZqQYzwqsYjj7z8kixxDXf9nux8TAsqj2izSpX1oCrd7q", + "legacyUnqualifiedDid": undefined, + "method": "peer", + "methodSpecificIdentifier": "1zQmZqQYzwqsYjj7z8kixxDXf9nux8TAsqj2izSpX1oCrd7q", + "recipientKeyFingerprints": [ + "z6Mkv6Kd744JcBL5P9gi5Ddtehxn1gSMTgM9kbTdfQ9soB8G", + ], + "role": "created", + }, + "type": "DidRecord", + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": [ + "z6Mkv6Kd744JcBL5P9gi5Ddtehxn1gSMTgM9kbTdfQ9soB8G", + ], + "role": "created", + }, + "createdAt": "2022-12-27T13:50:32.815Z", + "did": "did:peer:1zQmZqQYzwqsYjj7z8kixxDXf9nux8TAsqj2izSpX1oCrd7q", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "authentication": [ + { + "controller": "#id", + "id": "#d9d97b3e-5615-4e60-9366-a8ab1ec1a84d", + "publicKeyBase58": "Ge4aWoosGdqcGer1Peg3ocQnC7AW3o6o4aYhq8BrsxLt", + "type": "Ed25519VerificationKey2018", + }, + ], + "id": "did:peer:1zQmZqQYzwqsYjj7z8kixxDXf9nux8TAsqj2izSpX1oCrd7q", + "keyAgreement": [ + { + "controller": "#id", + "id": "#c30ea91b-5f49-461f-b0bd-046f947ef668", + "publicKeyBase58": "HKBdBGRK8uxgCwge2QHPBzVuuayEQFhC2LM3g1fzMFGE", + "type": "X25519KeyAgreementKey2019", + }, + ], + "service": [ + { + "id": "#inline-0", + "priority": 0, + "recipientKeys": [ + "#d9d97b3e-5615-4e60-9366-a8ab1ec1a84d", + ], + "routingKeys": [], + "serviceEndpoint": "didcomm:transport/queue", + "type": "did-communication", + }, + ], + }, + "id": "3-4e4f-41d9-94c4-f49351b811f1", + "metadata": {}, + "role": "created", + "updatedAt": "2023-01-21T22:50:20.522Z", + }, + }, + "4-4e4f-41d9-94c4-f49351b811f1": { + "id": "4-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "did": "did:peer:1zQmRnGYmYBdSinH2953ZgfHWEpqp5W6kJmmYBgRaCs4Yudx", + "legacyUnqualifiedDid": undefined, + "method": "peer", + "methodSpecificIdentifier": "1zQmRnGYmYBdSinH2953ZgfHWEpqp5W6kJmmYBgRaCs4Yudx", + "recipientKeyFingerprints": [ + "z6MkvcuHjVcNRBM78E3xWy7MnVqQV9hAvTawBsPmg3bR1DaE", + ], + "role": "created", + }, + "type": "DidRecord", + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": [ + "z6MkvcuHjVcNRBM78E3xWy7MnVqQV9hAvTawBsPmg3bR1DaE", + ], + "role": "created", + }, + "createdAt": "2022-12-27T13:51:50.193Z", + "did": "did:peer:1zQmRnGYmYBdSinH2953ZgfHWEpqp5W6kJmmYBgRaCs4Yudx", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "authentication": [ + { + "controller": "#id", + "id": "#22219a28-b52a-4024-bc0f-62d3969131fd", + "publicKeyBase58": "HAeF9FMw5dre1jDFqQ9WwQHQfaRKWaLaVrUqqmdQ5znr", + "type": "Ed25519VerificationKey2018", + }, + ], + "id": "did:peer:1zQmRnGYmYBdSinH2953ZgfHWEpqp5W6kJmmYBgRaCs4Yudx", + "keyAgreement": [ + { + "controller": "#id", + "id": "#0f9535d1-9c9c-4491-be1e-1628f365b513", + "publicKeyBase58": "FztF8HCahTeL9gYHoHnDFo6HruwnKB19ZtbHFbLndAmE", + "type": "X25519KeyAgreementKey2019", + }, + ], + "service": [ + { + "id": "#inline-0", + "priority": 0, + "recipientKeys": [ + "#22219a28-b52a-4024-bc0f-62d3969131fd", + ], + "routingKeys": [ + "did:key:z6MkqhwtVT5V6kwsNkErPNgSWuRGd4QLFQ324FMfw5oWGHop#z6MkqhwtVT5V6kwsNkErPNgSWuRGd4QLFQ324FMfw5oWGHop", + ], + "serviceEndpoint": "ws://ssi.mediator.com", + "type": "did-communication", + }, + ], + }, + "id": "4-4e4f-41d9-94c4-f49351b811f1", + "metadata": {}, + "role": "created", + "updatedAt": "2023-01-21T22:50:20.522Z", + }, + }, + "5-4e4f-41d9-94c4-f49351b811f1": { + "id": "5-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "did": "did:peer:1zQmb9oo8fn8AZ7rWh1DAfTbM3mnPp9Hq2bJFdHiqjnZX2Ce", + "legacyUnqualifiedDid": undefined, + "method": "peer", + "methodSpecificIdentifier": "1zQmb9oo8fn8AZ7rWh1DAfTbM3mnPp9Hq2bJFdHiqjnZX2Ce", + "recipientKeyFingerprints": [ + "z6Mkp1wyxzqoUrZ3TvnerfZBq48o1XLDSPH8ibFALkNABSgh", + ], + "role": "received", + }, + "type": "DidRecord", + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": [ + "z6Mkp1wyxzqoUrZ3TvnerfZBq48o1XLDSPH8ibFALkNABSgh", + ], + "role": "received", + }, + "createdAt": "2022-12-27T13:50:44.957Z", + "did": "did:peer:1zQmb9oo8fn8AZ7rWh1DAfTbM3mnPp9Hq2bJFdHiqjnZX2Ce", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "authentication": [ + { + "controller": "#id", + "id": "#075b7e8b-a7bd-41e1-9b01-044f1ccab1a6", + "publicKeyBase58": "AZgwNkbN9K4aMRwxB6bLyxaoBx4N2W2n2aLEWUQ9GDuK", + "type": "Ed25519VerificationKey2018", + }, + ], + "id": "did:peer:1zQmb9oo8fn8AZ7rWh1DAfTbM3mnPp9Hq2bJFdHiqjnZX2Ce", + "keyAgreement": [ + { + "controller": "#id", + "id": "#bf888dc5-0f54-4e71-9058-c43bebfc7d01", + "publicKeyBase58": "3Q8wpgxdCVdJfznYERZR1r9eVnCy7oxpjzGVucDLF2tG", + "type": "X25519KeyAgreementKey2019", + }, + ], + "service": [ + { + "id": "#inline-0", + "priority": 0, + "recipientKeys": [ + "#075b7e8b-a7bd-41e1-9b01-044f1ccab1a6", + ], + "routingKeys": [], + "serviceEndpoint": "ws://ssi.mediator.com", + "type": "did-communication", + }, + ], + }, + "id": "5-4e4f-41d9-94c4-f49351b811f1", + "metadata": {}, + "role": "received", + "updatedAt": "2023-01-21T22:50:20.522Z", + }, + }, + "6-4e4f-41d9-94c4-f49351b811f1": { + "id": "6-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "did": "did:peer:1zQmSzC6uhWYcxtkr2fBepr66kjvSHuRsSfbmei7nrGj7HfN", + "legacyUnqualifiedDid": undefined, + "method": "peer", + "methodSpecificIdentifier": "1zQmSzC6uhWYcxtkr2fBepr66kjvSHuRsSfbmei7nrGj7HfN", + "recipientKeyFingerprints": [ + "z6MkrQzQqPtNjJHGLGjWC7H3mjMSNA36bCBTYJeoR44JoFvH", + ], + "role": "received", + }, + "type": "DidRecord", + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": [ + "z6MkrQzQqPtNjJHGLGjWC7H3mjMSNA36bCBTYJeoR44JoFvH", + ], + "role": "received", + }, + "createdAt": "2022-12-27T13:50:34.057Z", + "did": "did:peer:1zQmSzC6uhWYcxtkr2fBepr66kjvSHuRsSfbmei7nrGj7HfN", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "authentication": [ + { + "controller": "#id", + "id": "#b6d349fb-93fb-4298-b57a-3f2fecffccd8", + "publicKeyBase58": "CxjNF9dwPknoDmtoWYKCvdoSYamFBJw6rHjsan6Ht38u", + "type": "Ed25519VerificationKey2018", + }, + ], + "id": "did:peer:1zQmSzC6uhWYcxtkr2fBepr66kjvSHuRsSfbmei7nrGj7HfN", + "keyAgreement": [ + { + "controller": "#id", + "id": "#c0917f9f-1102-4f13-800c-ff7b522999eb", + "publicKeyBase58": "Eso3A6AmL5qiWr9syqHx8NgdQ8EMkYcitmrW5G7bX9uU", + "type": "X25519KeyAgreementKey2019", + }, + ], + "service": [ + { + "id": "#inline-0", + "priority": 0, + "recipientKeys": [ + "#b6d349fb-93fb-4298-b57a-3f2fecffccd8", + ], + "routingKeys": [], + "serviceEndpoint": "ws://ssi.issuer.com", + "type": "did-communication", + }, + ], + }, + "id": "6-4e4f-41d9-94c4-f49351b811f1", + "metadata": {}, + "role": "received", + "updatedAt": "2023-01-21T22:50:20.522Z", + }, + }, + "7-4e4f-41d9-94c4-f49351b811f1": { + "id": "7-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "did": "did:peer:1zQmPaCELen1JWMWmhVZS16nDmrAC9yGKQcJCcBs5spXakA3", + "legacyUnqualifiedDid": undefined, + "method": "peer", + "methodSpecificIdentifier": "1zQmPaCELen1JWMWmhVZS16nDmrAC9yGKQcJCcBs5spXakA3", + "recipientKeyFingerprints": [ + "z6Mkt4ohp4uT74HdENeGVGAyVFTerCzzuDXP8kpU3yo6SqqM", + ], + "role": "received", + }, + "type": "DidRecord", + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": [ + "z6Mkt4ohp4uT74HdENeGVGAyVFTerCzzuDXP8kpU3yo6SqqM", + ], + "role": "received", + }, + "createdAt": "2022-12-27T13:51:22.817Z", + "did": "did:peer:1zQmPaCELen1JWMWmhVZS16nDmrAC9yGKQcJCcBs5spXakA3", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "authentication": [ + { + "controller": "#id", + "id": "#5ea98568-dfcd-4614-9495-ba95ec2665d3", + "publicKeyBase58": "EcYfDpf1mWoA7soZohD8e9uf2dj9VLH2SjuYDhq5Xd3y", + "type": "Ed25519VerificationKey2018", + }, + ], + "id": "did:peer:1zQmPaCELen1JWMWmhVZS16nDmrAC9yGKQcJCcBs5spXakA3", + "keyAgreement": [ + { + "controller": "#id", + "id": "#506e5ead-ddbc-44ef-848b-6593a692a916", + "publicKeyBase58": "EeQHhb6CqWGrQR9PfWpS1L8CedsbK2mZfPdaxaHN4s8b", + "type": "X25519KeyAgreementKey2019", + }, + ], + "service": [ + { + "id": "#inline-0", + "priority": 0, + "recipientKeys": [ + "#5ea98568-dfcd-4614-9495-ba95ec2665d3", + ], + "routingKeys": [], + "serviceEndpoint": "http://ssi.verifier.com", + "type": "did-communication", + }, + ], + }, + "id": "7-4e4f-41d9-94c4-f49351b811f1", + "metadata": {}, + "role": "received", + "updatedAt": "2023-01-21T22:50:20.522Z", + }, + }, + "8-4e4f-41d9-94c4-f49351b811f1": { + "id": "8-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "did": "did:peer:1zQmUo1HoiciJS628w4SHweg4Pzs4bZLM4KLZgroSmUwBw7S", + "legacyUnqualifiedDid": undefined, + "method": "peer", + "methodSpecificIdentifier": "1zQmUo1HoiciJS628w4SHweg4Pzs4bZLM4KLZgroSmUwBw7S", + "recipientKeyFingerprints": [ + "z6MktdgEKUiH5kt5t2dKAH14WzFYhzj3JFobUf2PFFQAg2ma", + ], + "role": "created", + }, + "type": "DidRecord", + "value": { + "_tags": { + "method": "peer", + "recipientKeyFingerprints": [ + "z6MktdgEKUiH5kt5t2dKAH14WzFYhzj3JFobUf2PFFQAg2ma", + ], + "role": "created", + }, + "createdAt": "2022-12-27T13:50:43.937Z", + "did": "did:peer:1zQmUo1HoiciJS628w4SHweg4Pzs4bZLM4KLZgroSmUwBw7S", + "didDocument": { + "@context": [ + "https://w3id.org/did/v1", + ], + "authentication": [ + { + "controller": "#id", + "id": "#12b8b7d4-87b9-4638-a929-f98df2f1f566", + "publicKeyBase58": "FBRBjETqkDPcmXncUi3DfthYtRTBtNZEne7TQyS9kozC", + "type": "Ed25519VerificationKey2018", + }, + ], + "id": "did:peer:1zQmUo1HoiciJS628w4SHweg4Pzs4bZLM4KLZgroSmUwBw7S", + "keyAgreement": [ + { + "controller": "#id", + "id": "#77b9cf84-2441-419f-b295-945d06e29edc", + "publicKeyBase58": "EgsQArrmCUru9MxR1RNNiomnMFz6E3ia2GfjVvoCjAWY", + "type": "X25519KeyAgreementKey2019", + }, + ], + "service": [ + { + "id": "#inline-0", + "priority": 0, + "recipientKeys": [ + "#12b8b7d4-87b9-4638-a929-f98df2f1f566", + ], + "routingKeys": [], + "serviceEndpoint": "didcomm:transport/queue", + "type": "did-communication", + }, + ], + }, + "id": "8-4e4f-41d9-94c4-f49351b811f1", + "metadata": {}, + "role": "created", + "updatedAt": "2023-01-21T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2022-09-08T19:35:53.872Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.5", + "updatedAt": "2023-01-21T22:50:20.522Z", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update the proofs records and create didcomm records with auto update 1`] = ` +{ + "3d5d7ad4-f0aa-4b1b-8c2c-780ee383564e": { + "id": "3d5d7ad4-f0aa-4b1b-8c2c-780ee383564e", + "tags": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "state": "done", + "threadId": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + }, + "type": "ProofExchangeRecord", + "value": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "createdAt": "2022-09-08T19:36:06.261Z", + "id": "3d5d7ad4-f0aa-4b1b-8c2c-780ee383564e", + "metadata": {}, + "presentationMessage": { + "@id": "2481ce81-560b-4ce6-a22b-ee4b6ed369e8", + "@type": "https://didcomm.org/present-proof/1.0/presentation", + "presentations~attach": [ + { + "@id": "libindy-presentation-0", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFfcHJpbWUiOiI5MDg5MzE2Mjc0ODc5NTI4MjMzNDA1MTY0NTgwOTM1OTIxMjIwMjMyMzg5ODE0NjE4OTc3ODA3MjA0MDg4OTQ0ODkzNDc1OTE5NTE4Mjc0NDEwNTU3OTU3NjEwNjYzOTAxMTcwODM1NDM2Nzk1NDU4NDU1Mjg3NzEwOTk3NTk3OTA1OTM3NTYyODIyNjg5NTE3MjAyNzQ4NTUxODgzODQ5NjY3MjYwNTA3NjU0NDM5OTk0MjczNDQ0MTU5NTQyMzg3MzI0OTM5OTAzMDcyMDc2MjQ4Njg1MTgyMjA4NTA0OTkyOTg5MTk0NzUwMDgyODU1MTc1NDE1OTIzMjU3MzA0MTQ0NjYxMDc5MDU2NzExMTg3NzMzMDE3NDQ1MTEyOTQyNDAyMTEzNDg0NjM5MTMxMDY2MDc3ODE2NzQzMzY3OTMzMDI3MjY3MTQ3MDIxMTkxODQ0NTQzMzI5NzUzMTA1NTA3MDk0Mzc5OTA5OTYzNjcxMTQ4NzM3Mjk3NDA2MzUxMzk0NTcwNTM3Nzk0NDg1Njc1ODc3MDU5OTI2NTc3MDU4MzY1NTA0MDQwNjAzNDIxMDI3NDYyOTY0OTExNTc5MDAyNjg2NDAzMjMyOTc3OTU0ODM1Nzc2NzQwMDI3NDIxNjI0MTUzNDQ4NzYyODAxODM3OTU3MTQ3NzM0MDkxNDk3NjMwMjA3MTY3MzUzMjAwMTM5ODE4MDg1NjgwMDIzMTc1MDEyNzM4Mjk1NzIwODU2OTMwNDYxMzIxMDQ4NTIxMjQ0ODQ5MjQ5Njc5MDMwMzI0NDcyNjYyOTQxNjc5NDU3OTk3NzQ4NiIsImUiOiI0NTE0MTczNzExODM2MzMzOTk0NjA3MTMwNjQ5MjA0NjEyNzU2Njk1MDI4ODA2NTY0NzI4MzE3OTExNzYxNDA0NTE5Nzk0NjA3NDk4Njg5NjgyOTYxODk3MDc4MDcwMzQ5Nzk0MzUzMDQ1MTY3MTUyMTk2OTU4NTU0NTI5MzgxNjY3MDE5MDA2OSIsInYiOiI1MzM4NDg2NDY2MjE4MTg2ODg3MTUwNzY4NTQ0OTQyMTEyMDcyOTQ1MzczMDQ1MDAzNTk0MTk0MzAxMDA5NDUzMzk5MjMxMDM5NjExMjU4NTE3MTgzODUyMDc4NjI0NjMyMDExNDE2MzI3Mjc1MTM3Nzc1MTAxNjgxODcwMzI4MjY3MTE4MjExNjEwNzAwNDc2MjA5NzMwMTIwODI2NzMyMTkwMDg0ODkyOTc2NTEwMTgxODE2MTkzMzM5MTk0MjE5MDIxOTQ1OTI1NTg4NjEzODEwMjE1Nzg1NDk1NDk0NjQ0NzIwMDM4MjMwMTg1MDUyMDAxMTMxNjE3MjQwMDIyNjQzOTYxNTkwOTU5ODE3ODMxMzg2Mzc5NDQ1MzI2Mzg4NzYzNjQ5MDYxODk4Nzk1ODcwMjE2NTkxMDI3NDkwMzAwMjA0OTc1NzM0NDgyNDM1ODE4MjgwMTQxNzA0MzA0MjMzNDE5NTMyNjc1Mzk3MDE3MTc1MTE3ODI5NDUzNjAxNDM2OTM2MDM3NDMyMzg4OTYyMjMwOTAyNTk1MjE3MTA3MzkxOTMwOTA3NDI4NDQyNDg4ODE2NjQ4NTI4OTkyNjUwMzY0NjIyNDA2MTA5MDUxOTczMjYyOTM3MzYyMTg5NDcwNTUyNDQ2MjAzNTMzNTQzNTY4NjY5MzAwODY0MzQyMzQwMDgwNjg5Mjg5MjI0OTg1MjU4MjU5MTk1Nzc5NTA3MzgwMDI1ODcwNDk0MDIwNDkyMTE2MDExOTA3NjI0NjI0ODg1ODk5NjMxOTk4ODMwNjg4NTY2OTQ5OTgyNjI5Mjg2Mzk2MjIzNDc2NDE1ODM2Nzg3MDA2Mzg5Nzc0MjYxMzk5NjUxMTY3NTYwNzcyMDc5NjkzMDA1NzMwOTQzNTIzNjAwNzM4Mjc4MzA4NDE2MjA5NzQzNzA1ODQ1MzUxNjQzMDUyMTY1MTcyNTg5NTMwMTU0MTU3NjE2OTYzMDg5NjM4ODg4NDc0NDg3MDA3NjY0NTA2ODk5NTE1OTE5MDAxNzIyMDEyNzczMzU3MDc4MjI4OTIzMDMzNTA1NDQ2MzAxOTQxNzA2OTc2NTY3Mzg5NDk3MzgxMDI2NjIyNDEzNTYyODc5MjM0MTM0NTI5Nzk4NzY2ODY0Nzk1OTQ3NzY1ODcwNDgwMTIyNDk0ODE0MzU0MDQ3MzE2ODY0ODczODMzNDgyNDU5NTc1NTQxNDI4NTE0MTciLCJtIjp7Im1hc3Rlcl9zZWNyZXQiOiIxNjE3NTE3NzgwNjcyMjkxNDYzNTc4ODc1NDk1NTkxODgyOTA3ODYzNTk0NzgyMzk4NjczMTIwMDg2OTEwMjA3NzczODk0ODYyNzQxOTIxMzk2OTE2MDUxNDk2NjYzMjIxNDA5MzA3NjA4NTczMDg1ODExMzAyNTYxNDcyMzgxMjY1NjE4MzQyNzc1NTY5MjQ4OTQ3NzY4Mjc3OTQzMzIxMjcyMTY1MjEyMDAxNDI0NDAwMyJ9LCJtMiI6IjE1MDQ5MTk3MTU3NDcyNjQ0MDMzMzE4OTAxNTc5MDYyNTk5NzA2NzU5MzcwMDk1MTk3NzI1NTE3MTM4OTAyMzcwNDUwMTQ5NDk2NjU0MTEzMzA5NTQ4MTc4MDM3NDU1NjY3Njk2NDA0MDY1ODI5MTUzNDYzNDczNzgzMTk5ODA3MjEzNjg5NDE3MTM2NDI4NDg5NzUwNjUzNTc5MjU0NDY0ODk0OTM4MDkyODY2NTUzNjU5In0sImdlX3Byb29mcyI6W119LCJub25fcmV2b2NfcHJvb2YiOm51bGx9XSwiYWdncmVnYXRlZF9wcm9vZiI6eyJjX2hhc2giOiIzNjk4Mjk3ODU5OTY5Nzg3MjI5MTA5NDY2OTIwMDA3ODEwNDA2ODQ3NTI2MDE2NjgxMTIwNDE4OTQ1NDk0NzcwODQyNjI3MjA2MjEyNCIsImNfbGlzdCI6W1syLDIwOCwzLDUzLDIyOSwxMzksMTAyLDUxLDI0MCwxOTUsMTM1LDExNSwxNzYsMTcyLDE4NCw5OSwxMDksMTU2LDgzLDUyLDIxNSwyMjMsODQsMjU1LDY2LDIyNiwyMjMsMTA1LDExMSwyMjEsMTgwLDk1LDEyMiwxMzMsMjIyLDI3LDM5LDk5LDcwLDEzLDM3LDI0LDI1NSwxMTQsMjM1LDEwOSwxODMsNTEsMjEzLDE5MCwyMjYsMTI2LDExOCwyLDIyMCw3OCw0OSw5LDI0MCw1NSwxNzksMTQ3LDUxLDIwMSwyMTMsMjEzLDEzMCw0LDE4MCwxMDMsMTk1LDgsMjYsMTE4LDE0LDEzMCwxOCwxMzMsMTg3LDYyLDMsOTcsMjEwLDEwMiwxMiwxNjIsNzksMTg0LDU1LDIzMiwyMTksMjIwLDE3NSwyNTUsMTY5LDE5NywxMjMsMTI3LDE2MCwyNSwxNTEsMTg3LDg3LDE5MSwxMDksMTk4LDQ0LDcxLDM4LDUwLDEwNCwyNiwyMTYsMTgwLDIxOCwxNDUsMTAsNzYsMTgwLDE1Nyw5OCwyMzQsNzcsMTY5LDE1MSw2OCwxNzAsMTg5LDE1LDIxNyw5OCwyMzUsMTI0LDE2NywyMzYsMjUzLDExMiwyNDQsMTg5LDk1LDE3OCw2MCw3MiwyMjgsMjIzLDcwLDI3LDYwLDIzNiwyMTIsOTcsMjA1LDIyLDI1MCwzNCwyNDYsMTIyLDM0LDgsMjU1LDIyLDEyNywxNTEsMjQyLDE4MCwxNzEsMTIxLDIyNywzMiwxMDMsNTEsMTcwLDIzNCwyMDYsMjAsMTAyLDIwNCwxMTYsMTk5LDAsMTE5LDExNSwxODAsMjA3LDE2LDQzLDU5LDI0MiwxNzksMTksMTk5LDQ4LDEyNyw5LDYzLDg4LDIxLDAsMjE1LDE3NCw0NywxNzcsMjMyLDE4MiwyNTMsMjQ5LDI0OCwxMTgsMTk2LDI1NCwxMzksMTIsMjksMSw0OCwxMDUsMzMsNCwyMDgsMTA2LDIzNSwyNDcsMjEwLDExMiwyMTAsMTA2LDE5OSwxOTgsNDcsOCwyMzYsNTIsOSw2NywxMjgsMjQwLDI1NCwyMzIsMjEwLDQsMjM5LDE4MywzOSwxOTMsMjQyLDMyLDEzMywxOTQsMTQ4LDk4LDExMSw3NywxNTUsMjA1LDE3OCwxOTcsMTRdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6eyJzdWJfcHJvb2ZfaW5kZXgiOjAsInJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In19LCJzZWxmX2F0dGVzdGVkX2F0dHJzIjp7fSwidW5yZXZlYWxlZF9hdHRycyI6e30sInByZWRpY2F0ZXMiOnt9fSwiaWRlbnRpZmllcnMiOlt7InNjaGVtYV9pZCI6Ijd5VzZTb1RqSE5oRDN6WWdtNFBiSzg6Mjp0ZXN0LXNjaGVtYS00ZTk0YzJlNC00ZjQ3LTRmZjMtYTg4OC02ZjY0ZGE2YTkyZGM6MS4wIiwiY3JlZF9kZWZfaWQiOiI3eVc2U29UakhOaEQzellnbTRQYks4OjM6Q0w6NDcyMzE5OlRBRyIsInJldl9yZWdfaWQiOm51bGwsInRpbWVzdGFtcCI6bnVsbH1dfQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + }, + }, + "requestMessage": { + "@id": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + "@type": "https://didcomm.org/present-proof/1.0/request-presentation", + "request_presentations~attach": [ + { + "@id": "libindy-request-presentation-0", + "data": { + "base64": "eyJuYW1lIjoicHJvb2YtcmVxdWVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjUyODExNDc1NTIxNzg3NzExMjI1Mzc0NSIsInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjp7Im5hbWUiOnsibmFtZSI6Im5hbWUiLCJyZXN0cmljdGlvbnMiOlt7ImNyZWRfZGVmX2lkIjoiN3lXNlNvVGpITmhEM3pZZ200UGJLODozOkNMOjQ3MjMxOTpUQUcifV19fSwicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOnt9fQ==", + }, + "mime-type": "application/json", + }, + ], + }, + "state": "done", + "threadId": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + }, + }, + "72c96cd1-1f26-4bf3-8a00-5c00926859a8": { + "id": "72c96cd1-1f26-4bf3-8a00-5c00926859a8", + "tags": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "state": "done", + "threadId": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + "type": "ProofExchangeRecord", + "value": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "createdAt": "2022-09-08T19:36:06.208Z", + "id": "72c96cd1-1f26-4bf3-8a00-5c00926859a8", + "isVerified": true, + "metadata": {}, + "presentationMessage": { + "@id": "4185f336-f307-4022-a27d-78d1271586f6", + "@type": "https://didcomm.org/present-proof/1.0/presentation", + "presentations~attach": [ + { + "@id": "libindy-presentation-0", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFfcHJpbWUiOiI1MjE1MTkyMzYwMDA2NTg5OTUyMjM1MTYwNjkxMzU1OTc4MjA2NDUzODA3NDUzNTk5OTE5OTkzNzM1NTczNzA1OTA2OTY5MTAwMjIzOTM4MjMyMzE5NDE1Njk1NTMzNjQ4MDMzNzc3NzI4NTE3NzI4Mjk4NDU1OTI0NjQ1NTU1NjQyNjE1NTQ3NTI0MDc0MTAyOTQyNjg4NzQwNDcwMjg4MDAyNTgwMjI4OTg4MDk4MDk0NjQxODc5NjkxNzQwMTY2OTc0MzA2OTczNjQ2MDA3ODg3NTU1NDU2NjM3NTUyODk2NjkxNDc3NDg2MDM1MTIxMjU0NDY0NzA2MDY0Mzg2NzA1MzU0MDk3MDcxNDc5OTkwMzIwNzIxMzQ4MjM1NTQ2NjI5OTcxNzAyNzgwNjUwNTgyMDQ3MzMxOTIwNjE5OTIzODg4NjU1NjI0NjYzMDE4NTAzMDUzOTQ4OTEyNzk4MDE1ODA2Mzk5MTAxNTg3MzAzNTgwNzkyMzQ3OTU2NjE1MDgzODczNDQ5MTM0MjEyOTE2MDY3MTExMjM4MjQzMjM1MTk5OTk1MTUzNjIyMTM1NjA4Nzg5OTc2MTk2NTkzMTczNTU4NTA0NDYxODA0MTk5MzY3MTkwODY2MDY5MTk1MDE5MjA4NjQyMzAwNzE5NjAzMDk5MzI1MjA0Njg4MzkwMjc2NTM5NjQxMDk0Mjc4NTY2NTUzOTU1Mzg4NDAwMDUyNzEyNzk0MjUwODgxODg3NDIzMDM5NTgyMjM5MjQ4NTk0NjYyMDYzMDA0ODI2MzE5NDQxNTUwNzEyNDA3MTQ4NDc4Njc1MCIsImUiOiIxNTk2OTcwODg4Njk3NTIwMjIzMzA3ODgyOTM5MzI5OTY0NTM3NjA4Mzc3NjQ0MTc2MjIyODczMjc2MTc0Mjg5ODkzNjgyNDAzODg0NzkzOTk1NDIwMjQ2MTEzNDk5NjMxNDAyMTQxMzQ0OTYxOTg3NDAyNjAxNDc4NTQ3NjEwNjI5MTgzNzc2NDQiLCJ2IjoiOTE0NzEwNTI1NDAwMDA2MDgzNTMwMTMzOTIxMjQyNTQwMzI5ODAzNjI4MjgyMDA0MDAzMTY1ODIyMjMyOTM0NDYxODAyMjQ4MDk4OTU5MzY1MTY2ODA3MjEwODgzNzIyNjIyMzA1ODQyNjM4NzM4NjIzNzM5NzYwNjEzODc5NDcwOTU5NDkyNjg1ODY5MTY4NTQyMDU1MjAyNzcxMDYxODI4MDEwMTAzNDY3Nzk2MDA5NDIzMTc4NzU2NDQ5OTk3NDQzODk5NDkyMDEyMTE0OTYyMDk5MDgwMDg1MzAxMjUyMDU1NDk0MDUzMTM4MzU4NDE0MjM0MDg5MzQyNDY2Nzg0MTA4ODkwMjA2NjY0NDI1NDE1ODYzNDA5NTI1MjI3NDY0NTg1OTU0MzQ3OTk2OTIwNTU0NTg5Nzc3OTA1MzMzMDMxMzAzMzEzNTI1OTY3NDg3NzAyMDQxODEwNzA3ODQ1OTAyMTAyODAxMTM0NDk3ODAxNjQyNTgwNzQyNjg0Njk5OTk3ODM2NTY0NzM2NDE5NDc0NzM1Mzg2ODkzMTQwMDA4ODIzNjIzMjA1MDA5MzE3NjgzNTIyNjI5NzkwMDY1NDExNDE5MzY3MTc3MTgwMTQ3MDk3ODkwNTkxNDM4NTkyMjQyNzA1MTg5NDM4NDE2MjA4MTA0MjczNjI3MjUyNzc2NzY1NjI0NjExMjk3NjQzODA5NzIwMjcxMDUzOTY1MzM4MTA5Njk1MTM4NjA3NTA0NzExMDc0NDY0ODU5Mzc5MzUxNTI1ODY4NzgyMjk2MzIzNDIwMzIwMTAwMzQ4MDA5MDMyMzIxOTM2ODMzNDk0Nzc2NDUxMjQ0NzMzNTQxMzI3Mzc4MTc3OTYwMTMzOTg2NzE4NTA2NTk0MzE2MjMyMzEwMTk5NjE2NjQ4NzI3MjU0MTUwNjg2MjA4OTQ1MTM3Mzg5MTI5Mjg4NzUyNzE3MjA5NjI3ODcyNjE3NTM3MjEwMDk2NTU2MTkzMzgxMzUwNTgzMDM1NjI5OTg3MjE5MTk3OTQ4OTEyNDI3NzI0MTA1ODE2NzA0NDkyOTMzNzQyMjcxOTQ5NTk1ODAwOTUyMDA5NDczNzQ2OTM5ODU0Mzg0NDI4MzY0MjE0NjUyNjI4MDgyOTQwOTMzODcwNTE1MjE4NTc1MzY4NDgwMjM4OTM2MTc0NDM5NzExNTUzNDI3NDEwNzA2NTgxMTkzMzg2NDM2ODEyIiwibSI6eyJtYXN0ZXJfc2VjcmV0IjoiNDU1NjIzMDY3NjQ4NDQ0NTEzODAyNzM4ODkxODAyMTkxNDg1MzY0NzMzMDkxMTIwMjM5MDg1ODY0NTQxMzU5Njg0MDgyMDczNTgzNDMzMjM4MzU2Njg2MTgwNzI1MzIyMjU2MzgwMjYxNzYyMTc5MTg3MDU3Mjk4Nzg3NzEzNDcyODY5MjE5NDI1ODgyNTEyNzI0ODk4NjkzMDk3OTkwODI4MDIzMjUwNTkyMTAwNDI5OSJ9LCJtMiI6IjEyMjUwNjAyNjEwOTE0Njk5NDEzNDk3MjEyOTYwNTA3OTQ1MDkwOTk1OTMwNjYyMDMyOTA2NTIxNDUxNjI0MDUxMTk2MTg3MjIxNjgxNzEzMDc1OTg0MDY5MjY5MTY3MDQ5ODExMDc3NDk1NTM2MDg4OTE0NTc3MDk2NTgwNjMwODIzNDQyMTk4Nzk5Nzg4NzIyNDIzMzg4MDIyNjE5MzM4MzA2MjM3NTQ2MzI5OTI4MDEifSwiZ2VfcHJvb2ZzIjpbXX0sIm5vbl9yZXZvY19wcm9vZiI6bnVsbH1dLCJhZ2dyZWdhdGVkX3Byb29mIjp7ImNfaGFzaCI6IjU4Njg3MDc5MDA2NTc0OTY1OTA5ODk2MDc5OTU0NTA2NDM2ODg0NzMxOTIxMjk0NDA5NDYxNzI5NjkzMTI4MDM1OTcxMjAwMjY5NzgzIiwiY19saXN0IjpbWzEsMTU3LDMxLDExMiwxNjQsMjA1LDIyNCw3NiwzMiwyMTIsMTczLDc3LDIxNyw2MiwxNzMsMTc3LDQsMTU5LDE0OSwyMzMsMTE3LDEwNCw5MiwxMzgsMzUsMjYsNDgsODUsMjQwLDIzNCw0MywxOTYsNTksMTE0LDI0MiwxNTMsNDAsMTg3LDIzOSwxMDAsMTcwLDIsMzYsMTA3LDIyOSwzNywxMTEsNjAsOTIsOSwxMTcsMjM4LDE0MCwyNDMsMywyNDIsMjM0LDE1MiwxNjYsNiwxNjEsMTM0LDIyMiwxNjMsNDMsNjIsMTIsODEsMjUzLDE3NiwxMjUsMjQsOCw5MiwxMywyMjIsMTcxLDIzMywxODIsMjE1LDIyLDIzNCw5NSwxMTIsMTIyLDEwMiwxNTIsMTA2LDE4Miw0LDIxOSw1NiwxMDEsMTQ3LDIyNywyNDYsMTE1LDY0LDE4Myw0LDY3LDEyNCwxNDAsMTY2LDEzOCwxNCwyNCw1MSwxODUsMTk5LDEwOSwyLDUxLDE5MCwxODEsMjQ3LDYxLDE5MCw4NSwyNDcsMjE3LDIyLDE2NCwxOTIsMjM1LDE4NywxNDgsMjYsMTA5LDI1MywxMTYsMjQxLDY1LDEzNiw0MSwyMjUsMjI5LDE3OCwxMjUsNzksMjQ2LDUzLDQ0LDE5MiwxNDEsMjQzLDE4NCwxNTEsMTIsOSwxMDQsMjE5LDE2Miw3MSw3Miw3LDg5LDgzLDgsMjI1LDEzLDQ4LDEzMywyNDcsMTg3LDExNCw4NSw3Nyw1OSw5OSw3MCw2OSw3MSwxMzMsMTY5LDExMSwxMTAsNzksNDQsMTc0LDIzLDEwMywxNjYsNjksNSwxNDcsNjYsNzcsMTYzLDExMiw0MiwxOTksMTQyLDE0LDIyLDE1MSwyNDgsMzMsMzQsMTk4LDE1NiwyMCwyMzEsMjA0LDcwLDgyLDI1MywxMzgsMTMxLDQyLDIwNSwzMywxMDksMjMyLDE3LDEwNiw2OSw3Miw2MCwyMTAsMjMyLDExMywxMjQsNzYsMTgxLDIwOCwwLDE4NiwyNTEsMTQzLDExMyw0NSwxNzEsMTAsNDMsMTUsMzAsMjA4LDkzLDYsMTY4LDEwNiwyMDMsNDgsMjUwLDIxOSw5LDE3LDEwNCwzNSw1OCwxNTksMTgwLDExMyw2NiwxMzYsNjJdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsiMGZlYTE4OTctNDMxMS00OGYyLWEyYzItMzg3NGU5OTNhZWYwIjp7InN1Yl9wcm9vZl9pbmRleCI6MCwicmF3IjoiQWxpY2UiLCJlbmNvZGVkIjoiMjcwMzQ2NDAwMjQxMTczMzEwMzMwNjMxMjgwNDQwMDQzMTgyMTg0ODY4MTY5MzE1MjA4ODY0MDU1MzU2NTk5MzQ0MTc0Mzg3ODE1MDcifX0sInNlbGZfYXR0ZXN0ZWRfYXR0cnMiOnt9LCJ1bnJldmVhbGVkX2F0dHJzIjp7fSwicHJlZGljYXRlcyI6e319LCJpZGVudGlmaWVycyI6W3sic2NoZW1hX2lkIjoiN3lXNlNvVGpITmhEM3pZZ200UGJLODoyOnRlc3Qtc2NoZW1hLTRlOTRjMmU0LTRmNDctNGZmMy1hODg4LTZmNjRkYTZhOTJkYzoxLjAiLCJjcmVkX2RlZl9pZCI6Ijd5VzZTb1RqSE5oRDN6WWdtNFBiSzg6MzpDTDo0NzIzMTk6VEFHIiwicmV2X3JlZ19pZCI6bnVsbCwidGltZXN0YW1wIjpudWxsfV19", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + }, + "proposalMessage": { + "@id": "00cdd404-82fc-431d-9c18-fb643636d2a5", + "@type": "https://didcomm.org/present-proof/1.0/propose-presentation", + "presentation_proposal": { + "@type": "https://didcomm.org/present-proof/1.0/presentation-preview", + "attributes": [ + { + "cred_def_id": "7yW6SoTjHNhD3zYgm4PbK8:3:CL:472319:TAG", + "name": "name", + "value": "Alice", + }, + ], + "predicates": [], + }, + }, + "requestMessage": { + "@id": "2862b25d-396c-46d7-ad09-f48ff4ea6db6", + "@type": "https://didcomm.org/present-proof/1.0/request-presentation", + "request_presentations~attach": [ + { + "@id": "libindy-request-presentation-0", + "data": { + "base64": "eyJuYW1lIjoicHJvb2YtcmVxdWVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjQwMzU1MDc0MDYxMTU0MzEwMzA5NzMyMiIsInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjp7IjBmZWExODk3LTQzMTEtNDhmMi1hMmMyLTM4NzRlOTkzYWVmMCI6eyJuYW1lIjoibmFtZSIsInJlc3RyaWN0aW9ucyI6W3siY3JlZF9kZWZfaWQiOiI3eVc2U29UakhOaEQzellnbTRQYks4OjM6Q0w6NDcyMzE5OlRBRyJ9XX19LCJyZXF1ZXN0ZWRfcHJlZGljYXRlcyI6e319", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + }, + "state": "done", + "threadId": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2022-09-08T19:35:53.872Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.5", + "updatedAt": "2023-01-21T22:50:20.522Z", + }, + }, + "ea840186-3c77-45f4-a2e6-349811ad8994": { + "id": "ea840186-3c77-45f4-a2e6-349811ad8994", + "tags": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "state": "done", + "threadId": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + }, + "type": "ProofExchangeRecord", + "value": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "createdAt": "2022-09-08T19:36:06.261Z", + "id": "ea840186-3c77-45f4-a2e6-349811ad8994", + "isVerified": true, + "metadata": {}, + "presentationMessage": { + "@id": "2481ce81-560b-4ce6-a22b-ee4b6ed369e8", + "@type": "https://didcomm.org/present-proof/1.0/presentation", + "presentations~attach": [ + { + "@id": "libindy-presentation-0", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFfcHJpbWUiOiI5MDg5MzE2Mjc0ODc5NTI4MjMzNDA1MTY0NTgwOTM1OTIxMjIwMjMyMzg5ODE0NjE4OTc3ODA3MjA0MDg4OTQ0ODkzNDc1OTE5NTE4Mjc0NDEwNTU3OTU3NjEwNjYzOTAxMTcwODM1NDM2Nzk1NDU4NDU1Mjg3NzEwOTk3NTk3OTA1OTM3NTYyODIyNjg5NTE3MjAyNzQ4NTUxODgzODQ5NjY3MjYwNTA3NjU0NDM5OTk0MjczNDQ0MTU5NTQyMzg3MzI0OTM5OTAzMDcyMDc2MjQ4Njg1MTgyMjA4NTA0OTkyOTg5MTk0NzUwMDgyODU1MTc1NDE1OTIzMjU3MzA0MTQ0NjYxMDc5MDU2NzExMTg3NzMzMDE3NDQ1MTEyOTQyNDAyMTEzNDg0NjM5MTMxMDY2MDc3ODE2NzQzMzY3OTMzMDI3MjY3MTQ3MDIxMTkxODQ0NTQzMzI5NzUzMTA1NTA3MDk0Mzc5OTA5OTYzNjcxMTQ4NzM3Mjk3NDA2MzUxMzk0NTcwNTM3Nzk0NDg1Njc1ODc3MDU5OTI2NTc3MDU4MzY1NTA0MDQwNjAzNDIxMDI3NDYyOTY0OTExNTc5MDAyNjg2NDAzMjMyOTc3OTU0ODM1Nzc2NzQwMDI3NDIxNjI0MTUzNDQ4NzYyODAxODM3OTU3MTQ3NzM0MDkxNDk3NjMwMjA3MTY3MzUzMjAwMTM5ODE4MDg1NjgwMDIzMTc1MDEyNzM4Mjk1NzIwODU2OTMwNDYxMzIxMDQ4NTIxMjQ0ODQ5MjQ5Njc5MDMwMzI0NDcyNjYyOTQxNjc5NDU3OTk3NzQ4NiIsImUiOiI0NTE0MTczNzExODM2MzMzOTk0NjA3MTMwNjQ5MjA0NjEyNzU2Njk1MDI4ODA2NTY0NzI4MzE3OTExNzYxNDA0NTE5Nzk0NjA3NDk4Njg5NjgyOTYxODk3MDc4MDcwMzQ5Nzk0MzUzMDQ1MTY3MTUyMTk2OTU4NTU0NTI5MzgxNjY3MDE5MDA2OSIsInYiOiI1MzM4NDg2NDY2MjE4MTg2ODg3MTUwNzY4NTQ0OTQyMTEyMDcyOTQ1MzczMDQ1MDAzNTk0MTk0MzAxMDA5NDUzMzk5MjMxMDM5NjExMjU4NTE3MTgzODUyMDc4NjI0NjMyMDExNDE2MzI3Mjc1MTM3Nzc1MTAxNjgxODcwMzI4MjY3MTE4MjExNjEwNzAwNDc2MjA5NzMwMTIwODI2NzMyMTkwMDg0ODkyOTc2NTEwMTgxODE2MTkzMzM5MTk0MjE5MDIxOTQ1OTI1NTg4NjEzODEwMjE1Nzg1NDk1NDk0NjQ0NzIwMDM4MjMwMTg1MDUyMDAxMTMxNjE3MjQwMDIyNjQzOTYxNTkwOTU5ODE3ODMxMzg2Mzc5NDQ1MzI2Mzg4NzYzNjQ5MDYxODk4Nzk1ODcwMjE2NTkxMDI3NDkwMzAwMjA0OTc1NzM0NDgyNDM1ODE4MjgwMTQxNzA0MzA0MjMzNDE5NTMyNjc1Mzk3MDE3MTc1MTE3ODI5NDUzNjAxNDM2OTM2MDM3NDMyMzg4OTYyMjMwOTAyNTk1MjE3MTA3MzkxOTMwOTA3NDI4NDQyNDg4ODE2NjQ4NTI4OTkyNjUwMzY0NjIyNDA2MTA5MDUxOTczMjYyOTM3MzYyMTg5NDcwNTUyNDQ2MjAzNTMzNTQzNTY4NjY5MzAwODY0MzQyMzQwMDgwNjg5Mjg5MjI0OTg1MjU4MjU5MTk1Nzc5NTA3MzgwMDI1ODcwNDk0MDIwNDkyMTE2MDExOTA3NjI0NjI0ODg1ODk5NjMxOTk4ODMwNjg4NTY2OTQ5OTgyNjI5Mjg2Mzk2MjIzNDc2NDE1ODM2Nzg3MDA2Mzg5Nzc0MjYxMzk5NjUxMTY3NTYwNzcyMDc5NjkzMDA1NzMwOTQzNTIzNjAwNzM4Mjc4MzA4NDE2MjA5NzQzNzA1ODQ1MzUxNjQzMDUyMTY1MTcyNTg5NTMwMTU0MTU3NjE2OTYzMDg5NjM4ODg4NDc0NDg3MDA3NjY0NTA2ODk5NTE1OTE5MDAxNzIyMDEyNzczMzU3MDc4MjI4OTIzMDMzNTA1NDQ2MzAxOTQxNzA2OTc2NTY3Mzg5NDk3MzgxMDI2NjIyNDEzNTYyODc5MjM0MTM0NTI5Nzk4NzY2ODY0Nzk1OTQ3NzY1ODcwNDgwMTIyNDk0ODE0MzU0MDQ3MzE2ODY0ODczODMzNDgyNDU5NTc1NTQxNDI4NTE0MTciLCJtIjp7Im1hc3Rlcl9zZWNyZXQiOiIxNjE3NTE3NzgwNjcyMjkxNDYzNTc4ODc1NDk1NTkxODgyOTA3ODYzNTk0NzgyMzk4NjczMTIwMDg2OTEwMjA3NzczODk0ODYyNzQxOTIxMzk2OTE2MDUxNDk2NjYzMjIxNDA5MzA3NjA4NTczMDg1ODExMzAyNTYxNDcyMzgxMjY1NjE4MzQyNzc1NTY5MjQ4OTQ3NzY4Mjc3OTQzMzIxMjcyMTY1MjEyMDAxNDI0NDAwMyJ9LCJtMiI6IjE1MDQ5MTk3MTU3NDcyNjQ0MDMzMzE4OTAxNTc5MDYyNTk5NzA2NzU5MzcwMDk1MTk3NzI1NTE3MTM4OTAyMzcwNDUwMTQ5NDk2NjU0MTEzMzA5NTQ4MTc4MDM3NDU1NjY3Njk2NDA0MDY1ODI5MTUzNDYzNDczNzgzMTk5ODA3MjEzNjg5NDE3MTM2NDI4NDg5NzUwNjUzNTc5MjU0NDY0ODk0OTM4MDkyODY2NTUzNjU5In0sImdlX3Byb29mcyI6W119LCJub25fcmV2b2NfcHJvb2YiOm51bGx9XSwiYWdncmVnYXRlZF9wcm9vZiI6eyJjX2hhc2giOiIzNjk4Mjk3ODU5OTY5Nzg3MjI5MTA5NDY2OTIwMDA3ODEwNDA2ODQ3NTI2MDE2NjgxMTIwNDE4OTQ1NDk0NzcwODQyNjI3MjA2MjEyNCIsImNfbGlzdCI6W1syLDIwOCwzLDUzLDIyOSwxMzksMTAyLDUxLDI0MCwxOTUsMTM1LDExNSwxNzYsMTcyLDE4NCw5OSwxMDksMTU2LDgzLDUyLDIxNSwyMjMsODQsMjU1LDY2LDIyNiwyMjMsMTA1LDExMSwyMjEsMTgwLDk1LDEyMiwxMzMsMjIyLDI3LDM5LDk5LDcwLDEzLDM3LDI0LDI1NSwxMTQsMjM1LDEwOSwxODMsNTEsMjEzLDE5MCwyMjYsMTI2LDExOCwyLDIyMCw3OCw0OSw5LDI0MCw1NSwxNzksMTQ3LDUxLDIwMSwyMTMsMjEzLDEzMCw0LDE4MCwxMDMsMTk1LDgsMjYsMTE4LDE0LDEzMCwxOCwxMzMsMTg3LDYyLDMsOTcsMjEwLDEwMiwxMiwxNjIsNzksMTg0LDU1LDIzMiwyMTksMjIwLDE3NSwyNTUsMTY5LDE5NywxMjMsMTI3LDE2MCwyNSwxNTEsMTg3LDg3LDE5MSwxMDksMTk4LDQ0LDcxLDM4LDUwLDEwNCwyNiwyMTYsMTgwLDIxOCwxNDUsMTAsNzYsMTgwLDE1Nyw5OCwyMzQsNzcsMTY5LDE1MSw2OCwxNzAsMTg5LDE1LDIxNyw5OCwyMzUsMTI0LDE2NywyMzYsMjUzLDExMiwyNDQsMTg5LDk1LDE3OCw2MCw3MiwyMjgsMjIzLDcwLDI3LDYwLDIzNiwyMTIsOTcsMjA1LDIyLDI1MCwzNCwyNDYsMTIyLDM0LDgsMjU1LDIyLDEyNywxNTEsMjQyLDE4MCwxNzEsMTIxLDIyNywzMiwxMDMsNTEsMTcwLDIzNCwyMDYsMjAsMTAyLDIwNCwxMTYsMTk5LDAsMTE5LDExNSwxODAsMjA3LDE2LDQzLDU5LDI0MiwxNzksMTksMTk5LDQ4LDEyNyw5LDYzLDg4LDIxLDAsMjE1LDE3NCw0NywxNzcsMjMyLDE4MiwyNTMsMjQ5LDI0OCwxMTgsMTk2LDI1NCwxMzksMTIsMjksMSw0OCwxMDUsMzMsNCwyMDgsMTA2LDIzNSwyNDcsMjEwLDExMiwyMTAsMTA2LDE5OSwxOTgsNDcsOCwyMzYsNTIsOSw2NywxMjgsMjQwLDI1NCwyMzIsMjEwLDQsMjM5LDE4MywzOSwxOTMsMjQyLDMyLDEzMywxOTQsMTQ4LDk4LDExMSw3NywxNTUsMjA1LDE3OCwxOTcsMTRdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6eyJzdWJfcHJvb2ZfaW5kZXgiOjAsInJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In19LCJzZWxmX2F0dGVzdGVkX2F0dHJzIjp7fSwidW5yZXZlYWxlZF9hdHRycyI6e30sInByZWRpY2F0ZXMiOnt9fSwiaWRlbnRpZmllcnMiOlt7InNjaGVtYV9pZCI6Ijd5VzZTb1RqSE5oRDN6WWdtNFBiSzg6Mjp0ZXN0LXNjaGVtYS00ZTk0YzJlNC00ZjQ3LTRmZjMtYTg4OC02ZjY0ZGE2YTkyZGM6MS4wIiwiY3JlZF9kZWZfaWQiOiI3eVc2U29UakhOaEQzellnbTRQYks4OjM6Q0w6NDcyMzE5OlRBRyIsInJldl9yZWdfaWQiOm51bGwsInRpbWVzdGFtcCI6bnVsbH1dfQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + }, + }, + "requestMessage": { + "@id": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + "@type": "https://didcomm.org/present-proof/1.0/request-presentation", + "request_presentations~attach": [ + { + "@id": "libindy-request-presentation-0", + "data": { + "base64": "eyJuYW1lIjoicHJvb2YtcmVxdWVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjUyODExNDc1NTIxNzg3NzExMjI1Mzc0NSIsInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjp7Im5hbWUiOnsibmFtZSI6Im5hbWUiLCJyZXN0cmljdGlvbnMiOlt7ImNyZWRfZGVmX2lkIjoiN3lXNlNvVGpITmhEM3pZZ200UGJLODozOkNMOjQ3MjMxOTpUQUcifV19fSwicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOnt9fQ==", + }, + "mime-type": "application/json", + }, + ], + }, + "state": "done", + "threadId": "7fcfc074-43ac-43cc-92b9-76afceeebe82", + }, + }, + "ec02ba64-63e3-46bc-b2a4-9d549d642d30": { + "id": "ec02ba64-63e3-46bc-b2a4-9d549d642d30", + "tags": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "state": "done", + "threadId": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + "type": "ProofExchangeRecord", + "value": { + "connectionId": "946f660f-bfa6-4a98-a801-ebde5da95e2c", + "createdAt": "2022-09-08T19:36:06.208Z", + "id": "ec02ba64-63e3-46bc-b2a4-9d549d642d30", + "metadata": {}, + "presentationMessage": { + "@id": "4185f336-f307-4022-a27d-78d1271586f6", + "@type": "https://didcomm.org/present-proof/1.0/presentation", + "presentations~attach": [ + { + "@id": "libindy-presentation-0", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsibmFtZSI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFfcHJpbWUiOiI1MjE1MTkyMzYwMDA2NTg5OTUyMjM1MTYwNjkxMzU1OTc4MjA2NDUzODA3NDUzNTk5OTE5OTkzNzM1NTczNzA1OTA2OTY5MTAwMjIzOTM4MjMyMzE5NDE1Njk1NTMzNjQ4MDMzNzc3NzI4NTE3NzI4Mjk4NDU1OTI0NjQ1NTU1NjQyNjE1NTQ3NTI0MDc0MTAyOTQyNjg4NzQwNDcwMjg4MDAyNTgwMjI4OTg4MDk4MDk0NjQxODc5NjkxNzQwMTY2OTc0MzA2OTczNjQ2MDA3ODg3NTU1NDU2NjM3NTUyODk2NjkxNDc3NDg2MDM1MTIxMjU0NDY0NzA2MDY0Mzg2NzA1MzU0MDk3MDcxNDc5OTkwMzIwNzIxMzQ4MjM1NTQ2NjI5OTcxNzAyNzgwNjUwNTgyMDQ3MzMxOTIwNjE5OTIzODg4NjU1NjI0NjYzMDE4NTAzMDUzOTQ4OTEyNzk4MDE1ODA2Mzk5MTAxNTg3MzAzNTgwNzkyMzQ3OTU2NjE1MDgzODczNDQ5MTM0MjEyOTE2MDY3MTExMjM4MjQzMjM1MTk5OTk1MTUzNjIyMTM1NjA4Nzg5OTc2MTk2NTkzMTczNTU4NTA0NDYxODA0MTk5MzY3MTkwODY2MDY5MTk1MDE5MjA4NjQyMzAwNzE5NjAzMDk5MzI1MjA0Njg4MzkwMjc2NTM5NjQxMDk0Mjc4NTY2NTUzOTU1Mzg4NDAwMDUyNzEyNzk0MjUwODgxODg3NDIzMDM5NTgyMjM5MjQ4NTk0NjYyMDYzMDA0ODI2MzE5NDQxNTUwNzEyNDA3MTQ4NDc4Njc1MCIsImUiOiIxNTk2OTcwODg4Njk3NTIwMjIzMzA3ODgyOTM5MzI5OTY0NTM3NjA4Mzc3NjQ0MTc2MjIyODczMjc2MTc0Mjg5ODkzNjgyNDAzODg0NzkzOTk1NDIwMjQ2MTEzNDk5NjMxNDAyMTQxMzQ0OTYxOTg3NDAyNjAxNDc4NTQ3NjEwNjI5MTgzNzc2NDQiLCJ2IjoiOTE0NzEwNTI1NDAwMDA2MDgzNTMwMTMzOTIxMjQyNTQwMzI5ODAzNjI4MjgyMDA0MDAzMTY1ODIyMjMyOTM0NDYxODAyMjQ4MDk4OTU5MzY1MTY2ODA3MjEwODgzNzIyNjIyMzA1ODQyNjM4NzM4NjIzNzM5NzYwNjEzODc5NDcwOTU5NDkyNjg1ODY5MTY4NTQyMDU1MjAyNzcxMDYxODI4MDEwMTAzNDY3Nzk2MDA5NDIzMTc4NzU2NDQ5OTk3NDQzODk5NDkyMDEyMTE0OTYyMDk5MDgwMDg1MzAxMjUyMDU1NDk0MDUzMTM4MzU4NDE0MjM0MDg5MzQyNDY2Nzg0MTA4ODkwMjA2NjY0NDI1NDE1ODYzNDA5NTI1MjI3NDY0NTg1OTU0MzQ3OTk2OTIwNTU0NTg5Nzc3OTA1MzMzMDMxMzAzMzEzNTI1OTY3NDg3NzAyMDQxODEwNzA3ODQ1OTAyMTAyODAxMTM0NDk3ODAxNjQyNTgwNzQyNjg0Njk5OTk3ODM2NTY0NzM2NDE5NDc0NzM1Mzg2ODkzMTQwMDA4ODIzNjIzMjA1MDA5MzE3NjgzNTIyNjI5NzkwMDY1NDExNDE5MzY3MTc3MTgwMTQ3MDk3ODkwNTkxNDM4NTkyMjQyNzA1MTg5NDM4NDE2MjA4MTA0MjczNjI3MjUyNzc2NzY1NjI0NjExMjk3NjQzODA5NzIwMjcxMDUzOTY1MzM4MTA5Njk1MTM4NjA3NTA0NzExMDc0NDY0ODU5Mzc5MzUxNTI1ODY4NzgyMjk2MzIzNDIwMzIwMTAwMzQ4MDA5MDMyMzIxOTM2ODMzNDk0Nzc2NDUxMjQ0NzMzNTQxMzI3Mzc4MTc3OTYwMTMzOTg2NzE4NTA2NTk0MzE2MjMyMzEwMTk5NjE2NjQ4NzI3MjU0MTUwNjg2MjA4OTQ1MTM3Mzg5MTI5Mjg4NzUyNzE3MjA5NjI3ODcyNjE3NTM3MjEwMDk2NTU2MTkzMzgxMzUwNTgzMDM1NjI5OTg3MjE5MTk3OTQ4OTEyNDI3NzI0MTA1ODE2NzA0NDkyOTMzNzQyMjcxOTQ5NTk1ODAwOTUyMDA5NDczNzQ2OTM5ODU0Mzg0NDI4MzY0MjE0NjUyNjI4MDgyOTQwOTMzODcwNTE1MjE4NTc1MzY4NDgwMjM4OTM2MTc0NDM5NzExNTUzNDI3NDEwNzA2NTgxMTkzMzg2NDM2ODEyIiwibSI6eyJtYXN0ZXJfc2VjcmV0IjoiNDU1NjIzMDY3NjQ4NDQ0NTEzODAyNzM4ODkxODAyMTkxNDg1MzY0NzMzMDkxMTIwMjM5MDg1ODY0NTQxMzU5Njg0MDgyMDczNTgzNDMzMjM4MzU2Njg2MTgwNzI1MzIyMjU2MzgwMjYxNzYyMTc5MTg3MDU3Mjk4Nzg3NzEzNDcyODY5MjE5NDI1ODgyNTEyNzI0ODk4NjkzMDk3OTkwODI4MDIzMjUwNTkyMTAwNDI5OSJ9LCJtMiI6IjEyMjUwNjAyNjEwOTE0Njk5NDEzNDk3MjEyOTYwNTA3OTQ1MDkwOTk1OTMwNjYyMDMyOTA2NTIxNDUxNjI0MDUxMTk2MTg3MjIxNjgxNzEzMDc1OTg0MDY5MjY5MTY3MDQ5ODExMDc3NDk1NTM2MDg4OTE0NTc3MDk2NTgwNjMwODIzNDQyMTk4Nzk5Nzg4NzIyNDIzMzg4MDIyNjE5MzM4MzA2MjM3NTQ2MzI5OTI4MDEifSwiZ2VfcHJvb2ZzIjpbXX0sIm5vbl9yZXZvY19wcm9vZiI6bnVsbH1dLCJhZ2dyZWdhdGVkX3Byb29mIjp7ImNfaGFzaCI6IjU4Njg3MDc5MDA2NTc0OTY1OTA5ODk2MDc5OTU0NTA2NDM2ODg0NzMxOTIxMjk0NDA5NDYxNzI5NjkzMTI4MDM1OTcxMjAwMjY5NzgzIiwiY19saXN0IjpbWzEsMTU3LDMxLDExMiwxNjQsMjA1LDIyNCw3NiwzMiwyMTIsMTczLDc3LDIxNyw2MiwxNzMsMTc3LDQsMTU5LDE0OSwyMzMsMTE3LDEwNCw5MiwxMzgsMzUsMjYsNDgsODUsMjQwLDIzNCw0MywxOTYsNTksMTE0LDI0MiwxNTMsNDAsMTg3LDIzOSwxMDAsMTcwLDIsMzYsMTA3LDIyOSwzNywxMTEsNjAsOTIsOSwxMTcsMjM4LDE0MCwyNDMsMywyNDIsMjM0LDE1MiwxNjYsNiwxNjEsMTM0LDIyMiwxNjMsNDMsNjIsMTIsODEsMjUzLDE3NiwxMjUsMjQsOCw5MiwxMywyMjIsMTcxLDIzMywxODIsMjE1LDIyLDIzNCw5NSwxMTIsMTIyLDEwMiwxNTIsMTA2LDE4Miw0LDIxOSw1NiwxMDEsMTQ3LDIyNywyNDYsMTE1LDY0LDE4Myw0LDY3LDEyNCwxNDAsMTY2LDEzOCwxNCwyNCw1MSwxODUsMTk5LDEwOSwyLDUxLDE5MCwxODEsMjQ3LDYxLDE5MCw4NSwyNDcsMjE3LDIyLDE2NCwxOTIsMjM1LDE4NywxNDgsMjYsMTA5LDI1MywxMTYsMjQxLDY1LDEzNiw0MSwyMjUsMjI5LDE3OCwxMjUsNzksMjQ2LDUzLDQ0LDE5MiwxNDEsMjQzLDE4NCwxNTEsMTIsOSwxMDQsMjE5LDE2Miw3MSw3Miw3LDg5LDgzLDgsMjI1LDEzLDQ4LDEzMywyNDcsMTg3LDExNCw4NSw3Nyw1OSw5OSw3MCw2OSw3MSwxMzMsMTY5LDExMSwxMTAsNzksNDQsMTc0LDIzLDEwMywxNjYsNjksNSwxNDcsNjYsNzcsMTYzLDExMiw0MiwxOTksMTQyLDE0LDIyLDE1MSwyNDgsMzMsMzQsMTk4LDE1NiwyMCwyMzEsMjA0LDcwLDgyLDI1MywxMzgsMTMxLDQyLDIwNSwzMywxMDksMjMyLDE3LDEwNiw2OSw3Miw2MCwyMTAsMjMyLDExMywxMjQsNzYsMTgxLDIwOCwwLDE4NiwyNTEsMTQzLDExMyw0NSwxNzEsMTAsNDMsMTUsMzAsMjA4LDkzLDYsMTY4LDEwNiwyMDMsNDgsMjUwLDIxOSw5LDE3LDEwNCwzNSw1OCwxNTksMTgwLDExMyw2NiwxMzYsNjJdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsiMGZlYTE4OTctNDMxMS00OGYyLWEyYzItMzg3NGU5OTNhZWYwIjp7InN1Yl9wcm9vZl9pbmRleCI6MCwicmF3IjoiQWxpY2UiLCJlbmNvZGVkIjoiMjcwMzQ2NDAwMjQxMTczMzEwMzMwNjMxMjgwNDQwMDQzMTgyMTg0ODY4MTY5MzE1MjA4ODY0MDU1MzU2NTk5MzQ0MTc0Mzg3ODE1MDcifX0sInNlbGZfYXR0ZXN0ZWRfYXR0cnMiOnt9LCJ1bnJldmVhbGVkX2F0dHJzIjp7fSwicHJlZGljYXRlcyI6e319LCJpZGVudGlmaWVycyI6W3sic2NoZW1hX2lkIjoiN3lXNlNvVGpITmhEM3pZZ200UGJLODoyOnRlc3Qtc2NoZW1hLTRlOTRjMmU0LTRmNDctNGZmMy1hODg4LTZmNjRkYTZhOTJkYzoxLjAiLCJjcmVkX2RlZl9pZCI6Ijd5VzZTb1RqSE5oRDN6WWdtNFBiSzg6MzpDTDo0NzIzMTk6VEFHIiwicmV2X3JlZ19pZCI6bnVsbCwidGltZXN0YW1wIjpudWxsfV19", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + }, + "proposalMessage": { + "@id": "00cdd404-82fc-431d-9c18-fb643636d2a5", + "@type": "https://didcomm.org/present-proof/1.0/propose-presentation", + "presentation_proposal": { + "@type": "https://didcomm.org/present-proof/1.0/presentation-preview", + "attributes": [ + { + "cred_def_id": "7yW6SoTjHNhD3zYgm4PbK8:3:CL:472319:TAG", + "name": "name", + "value": "Alice", + }, + ], + "predicates": [], + }, + }, + "requestMessage": { + "@id": "2862b25d-396c-46d7-ad09-f48ff4ea6db6", + "@type": "https://didcomm.org/present-proof/1.0/request-presentation", + "request_presentations~attach": [ + { + "@id": "libindy-request-presentation-0", + "data": { + "base64": "eyJuYW1lIjoicHJvb2YtcmVxdWVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjQwMzU1MDc0MDYxMTU0MzEwMzA5NzMyMiIsInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjp7IjBmZWExODk3LTQzMTEtNDhmMi1hMmMyLTM4NzRlOTkzYWVmMCI6eyJuYW1lIjoibmFtZSIsInJlc3RyaWN0aW9ucyI6W3siY3JlZF9kZWZfaWQiOiI3eVc2U29UakhOaEQzellnbTRQYks4OjM6Q0w6NDcyMzE5OlRBRyJ9XX19LCJyZXF1ZXN0ZWRfcHJlZGljYXRlcyI6e319", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + }, + "state": "done", + "threadId": "00cdd404-82fc-431d-9c18-fb643636d2a5", + }, + }, +} +`; diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.3.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.3.test.ts.snap new file mode 100644 index 0000000000..d1267f8270 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.3.test.ts.snap @@ -0,0 +1,164 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UpdateAssistant | v0.3.1 - v0.4 should correctly update 'claimFormat' tag to w3c records 1`] = ` +{ + "0e1f070a-e31f-46cf-88db-25c1621b2f4e": { + "id": "0e1f070a-e31f-46cf-88db-25c1621b2f4e", + "tags": { + "claimFormat": "ldp_vc", + "contexts": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + "cryptosuites": [], + "expandedTypes": [ + "https", + "https", + ], + "givenId": undefined, + "issuerId": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "proofTypes": [ + "Ed25519Signature2018", + ], + "schemaIds": [], + "subjectIds": [], + "types": [ + "VerifiableCredential", + "UniversityDegreeCredential", + ], + }, + "type": "W3cCredentialRecord", + "value": { + "createdAt": "2024-02-05T06:44:47.543Z", + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + "credentialSubject": { + "degree": { + "name": "Bachelor of Science and Arts", + "type": "BachelorDegree", + }, + }, + "issuanceDate": "2017-10-22T12:23:48Z", + "issuer": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "proof": { + "created": "2022-04-18T23:13:10Z", + "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..ECQsj_lABelr1jkehSkqaYpc5CBvbSjbi3ZvgiVVKxZFDYfj5xZmeXb_awa4aw_cGEVaoypeN2uCFmeG6WKkBw", + "proofPurpose": "assertionMethod", + "type": "Ed25519Signature2018", + "verificationMethod": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + }, + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential", + ], + }, + "id": "0e1f070a-e31f-46cf-88db-25c1621b2f4e", + "metadata": {}, + "updatedAt": "2023-03-18T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2023-03-18T18:35:02.888Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.4", + "updatedAt": "2023-03-18T22:50:20.522Z", + }, + }, + "da65187b-f461-4f39-8597-b0d95531d40d": { + "id": "da65187b-f461-4f39-8597-b0d95531d40d", + "tags": { + "algs": [ + "EdDSA", + ], + "claimFormat": "jwt_vc", + "contexts": [ + "https://www.w3.org/2018/credentials/v1", + ], + "givenId": "http://example.edu/credentials/3732", + "issuerId": "did:key:z6MkokrsVo8DbGDsnMAjnoHhJotMbDZiHfvxM4j65d8prXUr", + "schemaIds": [], + "subjectIds": [ + "did:example:ebfeb1f712ebc6f1c276e12ec21", + ], + "types": [ + "VerifiableCredential", + ], + }, + "type": "W3cCredentialRecord", + "value": { + "createdAt": "2024-02-05T06:44:47.600Z", + "credential": "eyJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtva3JzVm84RGJHRHNuTUFqbm9IaEpvdE1iRFppSGZ2eE00ajY1ZDhwclhVciIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImlzc3VlciI6eyJpZCI6ImRpZDprZXk6ejZNa29rcnNWbzhEYkdEc25NQWpub0hoSm90TWJEWmlIZnZ4TTRqNjVkOHByWFVyIn0sImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMTk6MjM6MjRaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEifX0sImp0aSI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8zNzMyIiwibmJmIjoxMjYyMzczODA0fQ.suzrfmzM07yiiibK0vOdP9Q0dARA7XVNRUa9DSbH519EWrUDgzsq6SiIG9yyBt39yaqsZc1-8byyuMrPziyWBg", + "id": "da65187b-f461-4f39-8597-b0d95531d40d", + "metadata": {}, + "updatedAt": "2023-03-18T22:50:20.522Z", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.3.1 - v0.4 should correctly update the did records and remove cache records 1`] = ` +{ + "4993c740-5cd9-4c79-a7d8-23d1266d31be": { + "id": "4993c740-5cd9-4c79-a7d8-23d1266d31be", + "tags": { + "did": "did:indy:bcovrin:test:Pow4pdnPgTS7JAXvWkoF2c", + "legacyUnqualifiedDid": undefined, + "method": "indy", + "methodSpecificIdentifier": "bcovrin:test:Pow4pdnPgTS7JAXvWkoF2c", + "qualifiedIndyDid": undefined, + "recipientKeyFingerprints": [], + "role": "created", + }, + "type": "DidRecord", + "value": { + "createdAt": "2023-03-18T18:35:07.208Z", + "did": "did:indy:bcovrin:test:Pow4pdnPgTS7JAXvWkoF2c", + "id": "4993c740-5cd9-4c79-a7d8-23d1266d31be", + "metadata": {}, + "role": "created", + "updatedAt": "2023-03-18T22:50:20.522Z", + }, + }, + "8168612b-73d1-4917-9a61-84e8102988f0": { + "id": "8168612b-73d1-4917-9a61-84e8102988f0", + "tags": { + "did": "did:indy:bcovrin:test:8DFqUo6UtQLLZETE7Gm29k", + "legacyUnqualifiedDid": undefined, + "method": "indy", + "methodSpecificIdentifier": "bcovrin:test:8DFqUo6UtQLLZETE7Gm29k", + "qualifiedIndyDid": undefined, + "recipientKeyFingerprints": [], + "role": "created", + }, + "type": "DidRecord", + "value": { + "createdAt": "2023-03-18T18:35:04.191Z", + "did": "did:indy:bcovrin:test:8DFqUo6UtQLLZETE7Gm29k", + "id": "8168612b-73d1-4917-9a61-84e8102988f0", + "metadata": {}, + "role": "created", + "updatedAt": "2023-03-18T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2023-03-18T18:35:02.888Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.4", + "updatedAt": "2023-03-18T22:50:20.522Z", + }, + }, +} +`; diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.4.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.4.test.ts.snap new file mode 100644 index 0000000000..6995df98d4 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.4.test.ts.snap @@ -0,0 +1,898 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UpdateAssistant | v0.4 - v0.5 should correctly add 'type' tag to w3c records 1`] = ` +{ + "0e1f070a-e31f-46cf-88db-25c1621b2f4e": { + "id": "0e1f070a-e31f-46cf-88db-25c1621b2f4e", + "tags": { + "claimFormat": "ldp_vc", + "contexts": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + "cryptosuites": [], + "expandedTypes": [ + "https://www.w3.org/2018/credentials#VerifiableCredential", + "https://example.org/examples#UniversityDegreeCredential", + ], + "givenId": undefined, + "issuerId": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "proofTypes": [ + "Ed25519Signature2018", + ], + "schemaIds": [], + "subjectIds": [], + "types": [ + "VerifiableCredential", + "UniversityDegreeCredential", + ], + }, + "type": "W3cCredentialRecord", + "value": { + "createdAt": "2024-02-05T06:44:47.543Z", + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + "credentialSubject": { + "degree": { + "name": "Bachelor of Science and Arts", + "type": "BachelorDegree", + }, + }, + "issuanceDate": "2017-10-22T12:23:48Z", + "issuer": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "proof": { + "created": "2022-04-18T23:13:10Z", + "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..ECQsj_lABelr1jkehSkqaYpc5CBvbSjbi3ZvgiVVKxZFDYfj5xZmeXb_awa4aw_cGEVaoypeN2uCFmeG6WKkBw", + "proofPurpose": "assertionMethod", + "type": "Ed25519Signature2018", + "verificationMethod": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + }, + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential", + ], + }, + "id": "0e1f070a-e31f-46cf-88db-25c1621b2f4e", + "metadata": {}, + "updatedAt": "2024-02-05T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2023-03-18T18:35:02.888Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.5", + "updatedAt": "2024-02-05T22:50:20.522Z", + }, + }, + "da65187b-f461-4f39-8597-b0d95531d40d": { + "id": "da65187b-f461-4f39-8597-b0d95531d40d", + "tags": { + "algs": [ + "EdDSA", + ], + "claimFormat": "jwt_vc", + "contexts": [ + "https://www.w3.org/2018/credentials/v1", + ], + "givenId": "http://example.edu/credentials/3732", + "issuerId": "did:key:z6MkokrsVo8DbGDsnMAjnoHhJotMbDZiHfvxM4j65d8prXUr", + "schemaIds": [], + "subjectIds": [ + "did:example:ebfeb1f712ebc6f1c276e12ec21", + ], + "types": [ + "VerifiableCredential", + ], + }, + "type": "W3cCredentialRecord", + "value": { + "createdAt": "2024-02-05T06:44:47.600Z", + "credential": "eyJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtva3JzVm84RGJHRHNuTUFqbm9IaEpvdE1iRFppSGZ2eE00ajY1ZDhwclhVciIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImlzc3VlciI6eyJpZCI6ImRpZDprZXk6ejZNa29rcnNWbzhEYkdEc25NQWpub0hoSm90TWJEWmlIZnZ4TTRqNjVkOHByWFVyIn0sImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMTk6MjM6MjRaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEifX0sImp0aSI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8zNzMyIiwibmJmIjoxMjYyMzczODA0fQ.suzrfmzM07yiiibK0vOdP9Q0dARA7XVNRUa9DSbH519EWrUDgzsq6SiIG9yyBt39yaqsZc1-8byyuMrPziyWBg", + "id": "da65187b-f461-4f39-8597-b0d95531d40d", + "metadata": {}, + "updatedAt": "2024-02-05T22:50:20.522Z", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.4 - v0.5 should correctly add role to credential exchange records 1`] = ` +{ + "6bab4ff7-45a1-4d66-98a4-ae81efd7f460": { + "id": "6bab4ff7-45a1-4d66-98a4-ae81efd7f460", + "tags": { + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "messageId": "6a49a044-b5cb-4d91-9a6f-18d808656cc8", + "messageName": "ack", + "messageType": "https://didcomm.org/issue-credential/2.0/ack", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "createdAt": "2024-02-28T09:36:46.755Z", + "id": "6bab4ff7-45a1-4d66-98a4-ae81efd7f460", + "message": { + "@id": "6a49a044-b5cb-4d91-9a6f-18d808656cc8", + "@type": "https://didcomm.org/issue-credential/2.0/ack", + "status": "OK", + "~service": { + "recipientKeys": [ + "Ab2MHChPWKG1f7xSMp8tKJFZj8qdTAnsQvN4nJ5vuZdA", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001", + }, + "~thread": { + "pthid": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349", + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2024-02-28T09:36:46.755Z", + }, + }, + "8df00782-5090-434e-8f34-96d5e484658a": { + "id": "8df00782-5090-434e-8f34-96d5e484658a", + "tags": { + "connectionId": undefined, + "credentialIds": [ + "c5775c27-93d1-46e0-bb00-65e408a58d97", + ], + "parentThreadId": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349", + "role": "holder", + "state": "done", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + "type": "CredentialRecord", + "value": { + "createdAt": "2024-02-28T09:36:11.555Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "test", + "value": "test", + }, + ], + "credentials": [ + { + "credentialRecordId": "c5775c27-93d1-46e0-bb00-65e408a58d97", + "credentialRecordType": "anoncreds", + }, + ], + "id": "8df00782-5090-434e-8f34-96d5e484658a", + "metadata": { + "_anoncreds/credential": { + "credentialDefinitionId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/CLAIM_DEF/400832/test", + "schemaId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/SCHEMA/test0.1599221872308001/1.0", + }, + "_anoncreds/credentialRequest": { + "link_secret_blinding_data": { + "v_prime": "13120265176299908185898873509373450263031037373213459182327318255420442817142729461299281465699119807362298565594454759288247538050340146656288128953997075333312454192263427271050503267075194365317557114968037548461894334655900837672256313231303463277753218922239512751838553222838565650935887880786124571846070357367162926133614147020173182949018785393466888036067039449038012778965503244623829426114742052867541393595950207886123033595140571273680755807414104278569809550597205459544801806826874048588059800699107031186607221972879349194293825608495858147307683980675532601970303518939133216185993672407675581367508904617978807526724974743", + "vr_prime": null, + }, + "link_secret_name": "ae343b1d-e2af-4706-9aae-2010a7f2c882", + "nonce": "533240365625577424098040", + }, + }, + "parentThreadId": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349", + "protocolVersion": "v2", + "role": "holder", + "state": "done", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + "updatedAt": "2024-02-05T22:50:20.522Z", + }, + }, + "90d4edf7-c408-48a0-a711-2502f3c649ac": { + "id": "90d4edf7-c408-48a0-a711-2502f3c649ac", + "tags": { + "associatedRecordId": "e04ca938-64e1-441e-b5d9-4525befe4686", + "messageId": "0f3c0bb1-737e-4e9c-9f6a-85337222941e", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/request-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "e04ca938-64e1-441e-b5d9-4525befe4686", + "createdAt": "2024-02-28T09:36:26.745Z", + "id": "90d4edf7-c408-48a0-a711-2502f3c649ac", + "message": { + "@id": "0f3c0bb1-737e-4e9c-9f6a-85337222941e", + "@type": "https://didcomm.org/issue-credential/2.0/request-credential", + "formats": [ + { + "attach_id": "a81bbf5e-e766-44b8-8ba4-4a92819473b9", + "format": "anoncreds/credential-request@v1.0", + }, + ], + "requests~attach": [ + { + "@id": "a81bbf5e-e766-44b8-8ba4-4a92819473b9", + "data": { + "base64": "eyJlbnRyb3B5IjoiNDYwNDIxODQ1MzE0ODU4NDI3ODE2OTAxIiwiY3JlZF9kZWZfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvQ0xBSU1fREVGLzQwMDgzMi90ZXN0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiODYwNjAyODA1NjM1NTgzNTUwNTc0MTAyOTkxNTExOTA3NTMxNjQ1NTE5MTEwMTI0MTUyMzA0MjMzNTMzMTA4NDcxMDc2NDc5MzA3NzMwMTI3NzQ1NDE1NTE1ODUwOTA1ODAxNjg1ODc0NDM2MDE5NjU2NTg0NjU0ODc0NTEyNjMwMzEwMjc2MzIzNzY2MzE2ODk2NTM3MDk4MzAxNTg1NzE5NTU1MjQ2NDc4NjY5OTI2NDcwMzc1MTgzMzUwOTMwNjU2ODgwMTY1MjcyMjU4NDY2MzcyOTAxMjA5MjkwMTMyNTEwNTgzNTA3NjAyNTc5MTg5NDc4OTU1OTMzNzA0OTc2NzA3OTI3NTg2MjU5NjQ4MjQ2NDQxMTEyOTYyOTczMDA3NDkyNzUzMTY1Nzc3MzQwNzQ0MTA2MzcxOTAyMzc0OTc4NzQ4MDI0NTgxODc2MjEwNTU0NDI5NDcyMTc0NzgzNTI5NTY2MjcxMTEyOTgyNjkxNTgwMTgxMDI4ODc5NjgxNjQxMDg4MDQwODY3OTAxODcxNTY4NjY3NzEzNzE1Njc2MzM4NTcwNDE1NTUwNTIyMzAzMjAyNDYxMTg4NDIwMjUyNDA2ODUyODUzOTczODUyOTY1ODI3NjUyMjEzMTA1NzM4OTE1NzIyNzQ4MTg4NTc0MDIxMzE3OTEwMDY5NDQyMDM3MDQyMzI5NzczNTQ4MzEzNTQyNDAxMzcyMzMzMTMyODQ1NzkzMzUwMDQ2MDk3Nzg4NjA4MTA2NTA5OTUxNDE1NDkzNjkxNjAxNjExMTIwNzE1MzM0NjExMTM3MjY0NDM0NjUiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI4MzE2NjQxOTY2MDcyMzc1MzAxNzYxOTMxMzY2OTMxMjEyNjA3NzUyMTk5MDI1Nzc5MDYyMDUwMjA5NDI4OTkzMzg2NTQ0Mzc3NjY5MCIsInZfZGFzaF9jYXAiOiIxMDkxMTY1NDc5NzEyMTM3ODgxNzIxMjQ3NjYzNjE2ODU5MDE5Njg1MTg2MDY5OTI4NjIyNjE1NDg2MzM0MTQzNzYzMzU0ODMxNTc2NDMyNDQxMjEyODQ4MTYyNjI5Mzg4ODc0NTQyMTYyNTMwNTU5MjMxNTU0NzIwNTMyNTc3ODY0MDQ1NjMwNzA4MjkyOTkzODQzMzU0ODE3NjEyMzQ3OTQ1MzU3NzM0NjMyNTc0NTA3ODM2MjAwODAxMjY2NDc3Mzc5MDUwMTMzNjQ1MzE3MTM2Nzg1MzM4OTU5NDg0MjUzNTEyNTEzOTcyNDM3MDM2MjcyNDU3Mzk4ODE4NjExODYyMjE1OTA3NDQzOTAwNDE2ODMyMDI3Nzk3NDE5MTQxNTk5MTY0MTY1MTA2NzA0MjgyMjg1NjcyOTEwMTMwMzc4NDA5NjExNzI3NjA3MjIxODA3ODg0MjI3OTgyNjM1MjY2NjI2NTEzOTg1OTIxNTgyMjM4NzU4NTM0MjkxMTU5MDgxMTk0NjUwNjg5Mjk0NzM5MTA1MTQwNTQzNjQ0NDUzMzg1NzU4ODY2NDg2MzgyNDY1MjA0MjQ0NTYyNTkxNzI5ODQzNDI1Mjc4MDAzMTk1MTk1MzU4MzgxODAxNzA5NTI5MzEwOTU4NDk5OTg1MDcwNjk0NzA2MjcwNjUyOTQ2MzU0MDg1MjYyOTMxNzI0NzI5MTk2MjY3MDczMDAyODk4NzQxMzc3ODEzMjgzNzAzNjU4MDkwNDE3ODcxMjE5NzQ1NzIwNTc1OTE4MTU5MzMzMDczMTA4OTE3Mjk5MzA2MzE0NTk4ODc5NDQ4NjQ1NjA4NDM2NTYzOTU5OTA2ODY2NzcwMjQ3MzIwMTkwMDQ0MTQxNjc2MDAyNTI5Njg2MTA1Nzc1NjI0MTU1NTkwMjMxMjMyNjQyNTY0MDE4NTM1ODQxNzU4MDYxMzk2IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxODk2Nzk5NDIxNjYxODE0MjQxMTcxMDU0NTcxOTYwMDc5MzM1MDUyOTU1NTM4MDM2OTExNzk1NjUxMzgyOTc5NTkxMTA0MTM3MDU3NTA1NTI1ODYyNjY5MTc1NjAxNDE3MDM3NzAyMTYyNzY4ODQ4MTc5MzA5MTU3MDAzMTI3NzI0NDc5NjQwNDQzNjcyOTQzNTg1MjkwMTk3Mjg3MTM3NjA3MDEzOTY1MzUzNjI0MzU4NSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI1MzMyNDAzNjU2MjU1Nzc0MjQwOTgwNDAifQ==", + }, + "mime-type": "application/json", + }, + ], + "~service": { + "recipientKeys": [ + "Ab2MHChPWKG1f7xSMp8tKJFZj8qdTAnsQvN4nJ5vuZdA", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001", + }, + "~thread": { + "pthid": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349", + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + "~transport": { + "return_route": "all", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2024-02-28T09:36:26.745Z", + }, + }, + "9a9b7488-2205-4e98-9aae-2b5e2b56a988": { + "id": "9a9b7488-2205-4e98-9aae-2b5e2b56a988", + "tags": { + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "messageId": "0f3c0bb1-737e-4e9c-9f6a-85337222941e", + "messageName": "request-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/request-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "createdAt": "2024-02-28T09:36:19.670Z", + "id": "9a9b7488-2205-4e98-9aae-2b5e2b56a988", + "message": { + "@id": "0f3c0bb1-737e-4e9c-9f6a-85337222941e", + "@type": "https://didcomm.org/issue-credential/2.0/request-credential", + "formats": [ + { + "attach_id": "a81bbf5e-e766-44b8-8ba4-4a92819473b9", + "format": "anoncreds/credential-request@v1.0", + }, + ], + "requests~attach": [ + { + "@id": "a81bbf5e-e766-44b8-8ba4-4a92819473b9", + "data": { + "base64": "eyJlbnRyb3B5IjoiNDYwNDIxODQ1MzE0ODU4NDI3ODE2OTAxIiwiY3JlZF9kZWZfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvQ0xBSU1fREVGLzQwMDgzMi90ZXN0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiODYwNjAyODA1NjM1NTgzNTUwNTc0MTAyOTkxNTExOTA3NTMxNjQ1NTE5MTEwMTI0MTUyMzA0MjMzNTMzMTA4NDcxMDc2NDc5MzA3NzMwMTI3NzQ1NDE1NTE1ODUwOTA1ODAxNjg1ODc0NDM2MDE5NjU2NTg0NjU0ODc0NTEyNjMwMzEwMjc2MzIzNzY2MzE2ODk2NTM3MDk4MzAxNTg1NzE5NTU1MjQ2NDc4NjY5OTI2NDcwMzc1MTgzMzUwOTMwNjU2ODgwMTY1MjcyMjU4NDY2MzcyOTAxMjA5MjkwMTMyNTEwNTgzNTA3NjAyNTc5MTg5NDc4OTU1OTMzNzA0OTc2NzA3OTI3NTg2MjU5NjQ4MjQ2NDQxMTEyOTYyOTczMDA3NDkyNzUzMTY1Nzc3MzQwNzQ0MTA2MzcxOTAyMzc0OTc4NzQ4MDI0NTgxODc2MjEwNTU0NDI5NDcyMTc0NzgzNTI5NTY2MjcxMTEyOTgyNjkxNTgwMTgxMDI4ODc5NjgxNjQxMDg4MDQwODY3OTAxODcxNTY4NjY3NzEzNzE1Njc2MzM4NTcwNDE1NTUwNTIyMzAzMjAyNDYxMTg4NDIwMjUyNDA2ODUyODUzOTczODUyOTY1ODI3NjUyMjEzMTA1NzM4OTE1NzIyNzQ4MTg4NTc0MDIxMzE3OTEwMDY5NDQyMDM3MDQyMzI5NzczNTQ4MzEzNTQyNDAxMzcyMzMzMTMyODQ1NzkzMzUwMDQ2MDk3Nzg4NjA4MTA2NTA5OTUxNDE1NDkzNjkxNjAxNjExMTIwNzE1MzM0NjExMTM3MjY0NDM0NjUiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI4MzE2NjQxOTY2MDcyMzc1MzAxNzYxOTMxMzY2OTMxMjEyNjA3NzUyMTk5MDI1Nzc5MDYyMDUwMjA5NDI4OTkzMzg2NTQ0Mzc3NjY5MCIsInZfZGFzaF9jYXAiOiIxMDkxMTY1NDc5NzEyMTM3ODgxNzIxMjQ3NjYzNjE2ODU5MDE5Njg1MTg2MDY5OTI4NjIyNjE1NDg2MzM0MTQzNzYzMzU0ODMxNTc2NDMyNDQxMjEyODQ4MTYyNjI5Mzg4ODc0NTQyMTYyNTMwNTU5MjMxNTU0NzIwNTMyNTc3ODY0MDQ1NjMwNzA4MjkyOTkzODQzMzU0ODE3NjEyMzQ3OTQ1MzU3NzM0NjMyNTc0NTA3ODM2MjAwODAxMjY2NDc3Mzc5MDUwMTMzNjQ1MzE3MTM2Nzg1MzM4OTU5NDg0MjUzNTEyNTEzOTcyNDM3MDM2MjcyNDU3Mzk4ODE4NjExODYyMjE1OTA3NDQzOTAwNDE2ODMyMDI3Nzk3NDE5MTQxNTk5MTY0MTY1MTA2NzA0MjgyMjg1NjcyOTEwMTMwMzc4NDA5NjExNzI3NjA3MjIxODA3ODg0MjI3OTgyNjM1MjY2NjI2NTEzOTg1OTIxNTgyMjM4NzU4NTM0MjkxMTU5MDgxMTk0NjUwNjg5Mjk0NzM5MTA1MTQwNTQzNjQ0NDUzMzg1NzU4ODY2NDg2MzgyNDY1MjA0MjQ0NTYyNTkxNzI5ODQzNDI1Mjc4MDAzMTk1MTk1MzU4MzgxODAxNzA5NTI5MzEwOTU4NDk5OTg1MDcwNjk0NzA2MjcwNjUyOTQ2MzU0MDg1MjYyOTMxNzI0NzI5MTk2MjY3MDczMDAyODk4NzQxMzc3ODEzMjgzNzAzNjU4MDkwNDE3ODcxMjE5NzQ1NzIwNTc1OTE4MTU5MzMzMDczMTA4OTE3Mjk5MzA2MzE0NTk4ODc5NDQ4NjQ1NjA4NDM2NTYzOTU5OTA2ODY2NzcwMjQ3MzIwMTkwMDQ0MTQxNjc2MDAyNTI5Njg2MTA1Nzc1NjI0MTU1NTkwMjMxMjMyNjQyNTY0MDE4NTM1ODQxNzU4MDYxMzk2IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxODk2Nzk5NDIxNjYxODE0MjQxMTcxMDU0NTcxOTYwMDc5MzM1MDUyOTU1NTM4MDM2OTExNzk1NjUxMzgyOTc5NTkxMTA0MTM3MDU3NTA1NTI1ODYyNjY5MTc1NjAxNDE3MDM3NzAyMTYyNzY4ODQ4MTc5MzA5MTU3MDAzMTI3NzI0NDc5NjQwNDQzNjcyOTQzNTg1MjkwMTk3Mjg3MTM3NjA3MDEzOTY1MzUzNjI0MzU4NSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI1MzMyNDAzNjU2MjU1Nzc0MjQwOTgwNDAifQ==", + }, + "mime-type": "application/json", + }, + ], + "~service": { + "recipientKeys": [ + "Ab2MHChPWKG1f7xSMp8tKJFZj8qdTAnsQvN4nJ5vuZdA", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001", + }, + "~thread": { + "pthid": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349", + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2024-02-28T09:36:21.638Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2023-03-18T18:35:02.888Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.5", + "updatedAt": "2024-02-05T22:50:20.522Z", + }, + }, + "b576366d-7fa2-4ede-b83c-3d29e6e31a78": { + "id": "b576366d-7fa2-4ede-b83c-3d29e6e31a78", + "tags": { + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "messageId": "44bead94-eb8a-46c8-abb7-f55e99fb8e28", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/issue-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "createdAt": "2024-02-28T09:36:43.506Z", + "id": "b576366d-7fa2-4ede-b83c-3d29e6e31a78", + "message": { + "@id": "44bead94-eb8a-46c8-abb7-f55e99fb8e28", + "@type": "https://didcomm.org/issue-credential/2.0/issue-credential", + "credentials~attach": [ + { + "@id": "e5c0d797-67f3-4d1d-a3e1-336786820213", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvU0NIRU1BL3Rlc3QwLjE1OTkyMjE4NzIzMDgwMDEvMS4wIiwiY3JlZF9kZWZfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvQ0xBSU1fREVGLzQwMDgzMi90ZXN0IiwicmV2X3JlZ19pZCI6bnVsbCwidmFsdWVzIjp7InRlc3QiOnsicmF3IjoidGVzdCIsImVuY29kZWQiOiI3MjE1NTkzOTQ4Njg0Njg0OTUwOTc1OTM2OTczMzI2NjQ4Njk4MjgyMTc5NTgxMDQ0ODI0NTQyMzE2ODk1NzM5MDYwNzY0NDM2MzI3MiJ9fSwic2lnbmF0dXJlIjp7InBfY3JlZGVudGlhbCI6eyJtXzIiOiI4NTY3NjYyMzQ4NDI3NzYyNDY4MjQ0NDgyMDQ0NDcwMjQ2NzQ5OTM1NzkxMDI3NDE2NTAxMzQxODM3OTEyNTEzNDA3NTMyNTU0MzcyOSIsImEiOiIyOTA3ODU4MzAyMzY0NDcyMzQxNzI1Njk2Mzk1MTgzNDc2OTQyOTU0NDg5MzU2MTMxMjYwODA0MTk5OTAzOTcyNTc3OTA0NTQ3ODk1NzY2NTYxMDUzOTM0NTY2OTI0OTAzMzY0Mzg0NTg3MTMxMDYzODIwNTM1OTgyNzkxMzM0NTUzOTg5MzYwNDc4NDE3MjIyMTE3Nzg3NDE1MTg2NzMyMzc4OTc4MzM1MjA2OTg1ODExNDY3NzA2MjM4MjMyOTIwMTk0MTMzNjExNDY0MDA5NDQ0ODk4MjQ5MjU0ODYyMTc0NTQxODE0MDI4NzUyMzM1ODQ1NTc5Njk4NTQ1MjI5MjYwMzgxNDA1MDYxMzAyMjQ5OTIwNjM1MDQ4MTk2NjQ1MDk0OTE0Nzc5ODYwNzI0ODM2NzM5MzAyMjQwMDg3MTM4OTQ0MTk4NTQ4MzI1MTk5NTY2NjExMDA2ODQzMTM1NjEwMjQyMDA2OTQ1MDIzMjcxMjM3MDQxMDA3MTAzODUyOTIzNzMzODU5MDAyMjI1NTY3NTQ1MTE0ODM2MjYwMDcyNzU4NDk5NDY2NTE3NzI4NTg1MzA4MTc5OTY4MzYyMzY3NDI5MTYwMzY0MDMxNDczMzY2NjQ2ODkxODY2NjE3NTEwMzYyMjgzMTYwNDQwMjIxNTMyMjI4OTUwOTU5NDE1Nzg3NTIxMjQwNjU4NDU1ODk3MzY4MjMyNzEyNzUzMjgwMzM3MzQyNDU3NTIzMDk0NjcyMTUyMTk2ODY4MDA3NTYwODE3ODU4NjE2NTU2NTE2MjkzMjY0MzM3ODIzNjQyODQwMzMzMSIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxMDM5MjcwODI5MzQzMDYyMDQxODk0NTM5MTQ4NTQ1Njg2MzMiLCJ2IjoiMTAxMTMyNDY1OTQ5MTk5Njg2OTQyNDM3NDkxNTA1MzM0MjEyNTE5MDMyMTI1MTYxNzYyODA0NTA5NDQzNDM1NjQ0MzExMDk1MDU5Mzg4NDE4OTE2MjUyNDc2NjczNjQzNzk0MTIxMjk0MDU0MDUzNDg0NzcwOTU2MTAwMDI0MzM1NjgzNDc4NTY1MDkxNTk2MTEzMzU3NTg4MTg3NTY5MjY4MjUwNjM1NjA3NzAzMTQ4NzMxNTE5MDg5NjQwMDAwNDQzNjA1MzM5OTQ4MzM1MTc4Nzg5ODcxNDEwMTYyOTA1NTA2OTM0MDc0OTQxMjE4NjY4NjA1ODgwMTc3MjEyOTQ4NjYxNzc5MTI0MzI4NDA1MjgzMzIwNjU2NDQzMDg2ODk1MDY0NDQ3NTMwMjI4NTgzODE1OTg3ODk0NzYxMzcwMjg0ODExMjIwNTY2NTgxNzM2NzQwNzY1NzUyNzIyMTE3MDEwODEzODkyMTE5MDE1NzAyNDI1Njk3OTg4NzQwNjIzMDA4NDQzOTY4MTQ5NDAwNjk3ODI2NzMzNjg5NzU0ODYwNjk4NzIwMjkzMjI3ODU3NjU3NzM0MTc5ODEyOTQ2MDkwNTU0ODE3MDcyNjQ4NDgwOTA4MTg4NTI4NDY4MjAzMzM2MDEyMzY2OTUyNjUyODgwNDY5NjUwMTEzODU1NjQ4OTc0Mzk0NzU4NjM5NjUwMjM0NzY0Mzk5OTQ5NjMyNzk3NTYwMzc4NDU2NDIzNjc4NDM1NDIzMDUwMzg4MDU0NDEwMzg3NjIyMDEzMTYxMDc2OTEwOTQ3MjI3Mzk3NDQxMTgzMDA0NDM3MTI0NDU1Nzc0NTI1NzIwMDcxMjg4MjA5NDY2OTcwNDQwNDk3MTY1MTE1MTQ1OTc3NDM5MjkxNDI3MjgyNzI2MTAxMzAwNTg0NTU3MjYzNzMzNDY0NzA3NzA0NTk0NTQxODgzNjE0MTA3MzIwNDIxNDM3MjMxNzY5MzM2NDcxNTE3NjgyNzg1NDk3OTA1MzAzODM4ODk0ODM2NjE3NjU0MTc3Mzk3MDEwOTQ1NDI5ODU0NjM1NzAyODgwNDA3NjkyOTAxNjQzNTEifSwicl9jcmVkZW50aWFsIjpudWxsfSwic2lnbmF0dXJlX2NvcnJlY3RuZXNzX3Byb29mIjp7InNlIjoiMjE0NzgwNDA1MTAyNzUxNzA2MTIzMzc5MTYwODIxMTUzMjkwMDU5MjQ3MjkxNjAxMzg3Mjg1MDA3MzE5NjY2ODk3Njg4NjQ4NzYzNTAzMTQxMjc1ODIwNjUyMjIwNzAzNTg0NTMwOTEzMjY1NTc0NzYxMzI2NDA1MTI5MTUxNjQzOTM0NjkxNjI0MDAyNDE1NTU0ODY0NjUwMzEzNTIxMjczMDk2MTc4NjMwOTY2NDI2ODU0NzE1Nzk3MzYyNzk3NTM0ODM3OTE4NDUzOTQxMjAwNTIwNTI4NTA1Nzk3NjEwOTcwNTk3Njc2Mzc2NDE2MjA2MzcyNDYyNzU5NjcyNTE2NTYyNDE5Mzk0NDk5OTk3Mjg5MzQzOTg0MDE3MjM5OTg1MjA3OTg4OTYxNzc5NDU2NTAzODg3Njk2MzA3MjE3NzczNDI2MjMxMDU0MTc1NzYzNzgzMDA4MDIxMzgyMDU5MDY1OTU3MjI2NDg3OTkzOTk3MjI5OTg3NTgzNDU1ODE5NTI4MTA4Nzk2MTIxNzA2MjY0MTc4ODI1MDM5NTA2MzkzODk3MTc5NDk5Mjc5OTUzODYwODY1OTUzNzA3NjQyNTkxMDY0ODIyMjg4ODg1NDE5MjMyMTc1NTYwMzIwMDczNDgzNTg0Mzc3NDUxMDMxNTA2NDQ0NTcwNzQ1MTEyNTYxNjIxNzMzNjY1NjE2MzU0NjUxMzE0MjI3OTgzNjEyODM2NjkwMzgwNDg4MDY4NDk2MTkzMzc4OTUxMjY2NDI0Nzg2MzIxODY1NjYyOTMyNTM1OTc4MjY4NDgxMzQ0MzQ4NDQ5NzkiLCJjIjoiNzE1NzQ0NjIzMTA1OTU5NjM4NDg4ODk3ODU3NTYwNzIzOTYxMDE5NjE5MjIwNTc2NTkyMTgyMjc4NDk3Mjk4NjE4MzQzNTU4MDAyMDEifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=", + }, + "mime-type": "application/json", + }, + ], + "formats": [ + { + "attach_id": "e5c0d797-67f3-4d1d-a3e1-336786820213", + "format": "anoncreds/credential@v1.0", + }, + ], + "~please_ack": { + "on": [ + "RECEIPT", + ], + }, + "~service": { + "recipientKeys": [ + "xaRkB1mi5rQxihB5T2pAyx3m54fuT65nq9C1mjVNdZy", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:2001", + }, + "~thread": { + "pthid": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349", + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2024-02-28T09:36:43.506Z", + }, + }, + "c30fcbff-c5c5-4676-89ee-9c68e2c39d1c": { + "id": "c30fcbff-c5c5-4676-89ee-9c68e2c39d1c", + "tags": { + "associatedRecordId": "e04ca938-64e1-441e-b5d9-4525befe4686", + "messageId": "a1ec787c-8df6-4efe-9280-6b4407db026e", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/offer-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "e04ca938-64e1-441e-b5d9-4525befe4686", + "createdAt": "2024-02-28T09:36:08.529Z", + "id": "c30fcbff-c5c5-4676-89ee-9c68e2c39d1c", + "message": { + "@id": "a1ec787c-8df6-4efe-9280-6b4407db026e", + "@type": "https://didcomm.org/issue-credential/2.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/2.0/credential-preview", + "attributes": [ + { + "name": "test", + "value": "test", + }, + ], + }, + "formats": [ + { + "attach_id": "d6883b94-7a6b-4fe7-9ddc-98b23c46c478", + "format": "anoncreds/credential@v1.0", + }, + ], + "offers~attach": [ + { + "@id": "d6883b94-7a6b-4fe7-9ddc-98b23c46c478", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvU0NIRU1BL3Rlc3QwLjE1OTkyMjE4NzIzMDgwMDEvMS4wIiwiY3JlZF9kZWZfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvQ0xBSU1fREVGLzQwMDgzMi90ZXN0Iiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiIxMDk0MzkxMjM2ODU1Nzg0MTgzNDIyMDY2NjI2MzE2OTQ4ODQ1ODMwNzA0MjY0MjUzNDUyMTkwNTAxMTM2ODIyNTcwNTY0MTIzMzY4NDAiLCJ4el9jYXAiOiIyMTk4OTc3Nzk0MzA1NjU5NzMwNDk2MDU0MjM4NDE4OTYxODUzMDEyMTgzODgxODQxNjk4NDUwMTY2ODU5MTA1MDg1MDY0MzIzNzcyMjE3MDUxMDQwODYwNDcwODY0NTM1MDI5Mzk2NTY4Njc0ODkyNzg4Nzg5MTIzNTU3MzE2MTkwNjAyNjczMTczODM0NDUwNjcxMjA2ODA2MjYxOTg2Mzc3NjE1NTc3MzU4MjI0MTM4NDc1OTExNjgyOTQ2MTAzOTkzODg5MTIxODMyNjExNTg4NDc0NzUwMjIxNTQ3MjcwNDAyMjUwMTIyMjc5ODcwNDQ4MDA3OTgwMjQ0NDA5OTc5NDgyNDg1MjU2MDk3OTY4ODQyNDg3ODM2MzMyNTA4MjA0OTIwNjM3ODE0NDMyNDczMjg0NDc0MzQzNzQ3Njg0MjI2MTMxMjAwNjQyMDI3NjQ2NjMwMzkzMDE4MTk1NTAzOTQ3MjkxNjA0Nzg5MjM5MTY3ODUwNjA5Mzc3MTE4NjA3NjUwMzE0NTYyOTk3MDc2NjQ3MDUzNzcxNzgxMjAwMTAxNjExOTc2MTI4ODY5OTE0NTM0NzQ5MDc0MDc3NzcyOTUzMjkzNjMwMzMyMDc5MTk4MzAxNjMwNTY3MjQ3Mjc0OTY5MTM5ODI2Nzc2NTM2NzEwMTgxMjQ3MDY2NDE4OTY1NTQyNDY5MjMyMDkxMDYwNjI4Njc0MTM4OTgwMDcwODE0Njg1OTMyMjg0MzIyMjMzMDQ3NjQ2NTkxODc3NjkyODgyMTM5ODQ4MzgxNjQxMTg1ODE1ODAxMDg0OTM5NTk2NzMwMTYxMjA1MDg2MzMzNzgwNjI5OTEyMTc1NDA0ODQ2MDk5MTI5MjY0NjM3ODA2MjQ3MzE2NzU2NTg3NDI5MjEwNjkzNDM5NTQyMjEzNzI2IiwieHJfY2FwIjpbWyJtYXN0ZXJfc2VjcmV0IiwiMTA4NjM5NjM3NDg4MzU1Nzc0MDI5ODE3OTk0NDkxOTE1MjIyMzc2ODk2NzQwMjM0MDk3MzI0NjcwNzU3NDQ1NjQ0OTAxNTk2NjUwMjk1MDc3NjM0Nzk0Mzc5MzMwODUyNzUxNjQwODk5NjYwMDUwOTY5MDUzNDYyMjcyMzQ5MTEzNjYxOTg3NzMxNDQ3NDMzNTIzNTc0OTc5NjYyMzE0MTAxMTI3Njc5MTAwNzcwMjY0NDMxMTIxNjYzMzMyMTQ2MTU4NzM3MzU0NTk0MDM1NTM1MjI5Mzc4MjU3NzUyOTAwMzA3Mjg3NjQ4NzcwOTU4NTg5ODg1NzA3NDEyOTgzNzYwNTE2ODk0NzkzMTE3NTQ5Nzc3Njg1NTg0MjQ3MzQ1ODMxNzk2MjUzMzQ1MDk1NzIyODU4NjM4MjAxMjgxMjIzNDYyOTg2NjE2MjYzNzIyMDk1MjMxMjg0MTgzMzM3ODYwODQ2Njc5Njg5ODM2MTM4NzAxNjE4MzI1MDAyNTM5NjczNzM4NjUwMTMxNzMzODIzNDk0Mjk5MzQzNDQxNjc5MzM1MTQ5NTIwODQ4Mzg4Njk5MTk5ODA1NTk4MTAzNTk0NTk4OTE0OTkyOTI2MDMzNTA0MzAxNTY5MjMwODYyMzc3NTg5ODg2OTYyMDIxOTIxODM3ODI3MDYyNTI0MzIzMTg3OTcwNjg0MzIxNzY0MzcwMzc4NDc0Mjc4MzA4NzMzOTY4ODgzNTMyNzM2MTE1NTQ3MzA0MzU4ODgyMzc1Njc0MTQwMjEzNzc1OTE1OTU3NzU5MTM3NjUwMjY0Njg3OTUzMTk4NTE5OTY0MDcyMzEwNDY3OTA2Njk0OTQxMzM4NDAzNDg4NTYyMjgxMDE5MTQzMDk5MTE0NjM3MTY1OTI2MzQxOTY4NTk3MjE4MjU3OTU5OCJdLFsidGVzdCIsIjcwMDE5MTUyMzc5MTExNTQzNzgwOTgxNTYyMDU5OTYzMDYzMzg4ODg5NDg1NjkxOTM5MzM3NzgxNjkwNTU3NjA5MDA4MTA2NjY5NzAwNjA3MzI1OTAyNjQyMzA4NTIzMDc5MjA3NjEwNTU1MjQ0NTY0MjkwNzc5ODA5Mzg5ODEzNTI0OTc5MzE5MTg4NDI0NzIwNzUxMjQwMzQ3NTY0NTQ3MDY2NDE1NTE3MzU5NjUxODU1NzU3MzY4NDcxOTM5OTk3NjY1MTk5NTE4OTQ2ODMzMDY1MTMyNjYxMDI4Nzc5ODg1NDQ5ODMwMzg1MTA3MzUxOTgxMDM1NTAzMzM1MDg0NjgxMDQ4MzE0NjQzMDQ4NzIwMzQxMzk0MjI5NTEyNDcyNDY0NjUwNDI3NDA4NTI5ODkwMzg1ODc1MzkzMjA0ODExMTUwODgxNDA4NzY3NTMzNjI1MjU4MDUwNDc2NzU2NDIyMzk5NjMxNjA5NTU2MjI0NjQ1Mjg5NDM0Mjk3NDkwMzg0MzYwNDM0Mzg4MDU1NDAxODgyNDIxNDU1OTI0NjQxMTUwNjQ1NTkzNzUwMjQ2MTI4NTYzNzMzMzgzMzQ2MTYyNjYzNjE5MTYyMzIxMDM3MDgzNTcxNzc0MzQ5MzYwMTcxNzkwNzUzNzYyMDI3NTczMDc0MDI1NTgyOTQyODk4MzMwMTY3NDY0MTExNjA1MTMxMjk5MDE2MjQ5MzU2MDY5MjM3OTAzNjAyNjgwMjMwNzgzMjg0MDIwOTMxMjY3MDkzNTE1MzU3MjQ1MDEwOTI0MTI2MTIyNjUwNTM1MDI5MjIxMzY2NzA5NTI2NjY1Mjc5NzIyMDg0NjI0MzQwOTkyODQ4NDU0OTgxNTExOTIwNDE4NzM2NTE1NjIxMTgxNjkxMzcyNDE5ODU2OTg1NDkzMSJdXX0sIm5vbmNlIjoiOTczNDk4MTg3NTMwNzE2MjQ3MDE5NzU0In0=", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2024-02-28T09:36:08.529Z", + }, + }, + "de893e30-a2f3-458e-b8ec-aaca324c0806": { + "id": "de893e30-a2f3-458e-b8ec-aaca324c0806", + "tags": { + "associatedRecordId": "e04ca938-64e1-441e-b5d9-4525befe4686", + "messageId": "44bead94-eb8a-46c8-abb7-f55e99fb8e28", + "messageName": "issue-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/issue-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "sender", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "e04ca938-64e1-441e-b5d9-4525befe4686", + "createdAt": "2024-02-28T09:36:31.458Z", + "id": "de893e30-a2f3-458e-b8ec-aaca324c0806", + "message": { + "@id": "44bead94-eb8a-46c8-abb7-f55e99fb8e28", + "@type": "https://didcomm.org/issue-credential/2.0/issue-credential", + "credentials~attach": [ + { + "@id": "e5c0d797-67f3-4d1d-a3e1-336786820213", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvU0NIRU1BL3Rlc3QwLjE1OTkyMjE4NzIzMDgwMDEvMS4wIiwiY3JlZF9kZWZfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvQ0xBSU1fREVGLzQwMDgzMi90ZXN0IiwicmV2X3JlZ19pZCI6bnVsbCwidmFsdWVzIjp7InRlc3QiOnsicmF3IjoidGVzdCIsImVuY29kZWQiOiI3MjE1NTkzOTQ4Njg0Njg0OTUwOTc1OTM2OTczMzI2NjQ4Njk4MjgyMTc5NTgxMDQ0ODI0NTQyMzE2ODk1NzM5MDYwNzY0NDM2MzI3MiJ9fSwic2lnbmF0dXJlIjp7InBfY3JlZGVudGlhbCI6eyJtXzIiOiI4NTY3NjYyMzQ4NDI3NzYyNDY4MjQ0NDgyMDQ0NDcwMjQ2NzQ5OTM1NzkxMDI3NDE2NTAxMzQxODM3OTEyNTEzNDA3NTMyNTU0MzcyOSIsImEiOiIyOTA3ODU4MzAyMzY0NDcyMzQxNzI1Njk2Mzk1MTgzNDc2OTQyOTU0NDg5MzU2MTMxMjYwODA0MTk5OTAzOTcyNTc3OTA0NTQ3ODk1NzY2NTYxMDUzOTM0NTY2OTI0OTAzMzY0Mzg0NTg3MTMxMDYzODIwNTM1OTgyNzkxMzM0NTUzOTg5MzYwNDc4NDE3MjIyMTE3Nzg3NDE1MTg2NzMyMzc4OTc4MzM1MjA2OTg1ODExNDY3NzA2MjM4MjMyOTIwMTk0MTMzNjExNDY0MDA5NDQ0ODk4MjQ5MjU0ODYyMTc0NTQxODE0MDI4NzUyMzM1ODQ1NTc5Njk4NTQ1MjI5MjYwMzgxNDA1MDYxMzAyMjQ5OTIwNjM1MDQ4MTk2NjQ1MDk0OTE0Nzc5ODYwNzI0ODM2NzM5MzAyMjQwMDg3MTM4OTQ0MTk4NTQ4MzI1MTk5NTY2NjExMDA2ODQzMTM1NjEwMjQyMDA2OTQ1MDIzMjcxMjM3MDQxMDA3MTAzODUyOTIzNzMzODU5MDAyMjI1NTY3NTQ1MTE0ODM2MjYwMDcyNzU4NDk5NDY2NTE3NzI4NTg1MzA4MTc5OTY4MzYyMzY3NDI5MTYwMzY0MDMxNDczMzY2NjQ2ODkxODY2NjE3NTEwMzYyMjgzMTYwNDQwMjIxNTMyMjI4OTUwOTU5NDE1Nzg3NTIxMjQwNjU4NDU1ODk3MzY4MjMyNzEyNzUzMjgwMzM3MzQyNDU3NTIzMDk0NjcyMTUyMTk2ODY4MDA3NTYwODE3ODU4NjE2NTU2NTE2MjkzMjY0MzM3ODIzNjQyODQwMzMzMSIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxMDM5MjcwODI5MzQzMDYyMDQxODk0NTM5MTQ4NTQ1Njg2MzMiLCJ2IjoiMTAxMTMyNDY1OTQ5MTk5Njg2OTQyNDM3NDkxNTA1MzM0MjEyNTE5MDMyMTI1MTYxNzYyODA0NTA5NDQzNDM1NjQ0MzExMDk1MDU5Mzg4NDE4OTE2MjUyNDc2NjczNjQzNzk0MTIxMjk0MDU0MDUzNDg0NzcwOTU2MTAwMDI0MzM1NjgzNDc4NTY1MDkxNTk2MTEzMzU3NTg4MTg3NTY5MjY4MjUwNjM1NjA3NzAzMTQ4NzMxNTE5MDg5NjQwMDAwNDQzNjA1MzM5OTQ4MzM1MTc4Nzg5ODcxNDEwMTYyOTA1NTA2OTM0MDc0OTQxMjE4NjY4NjA1ODgwMTc3MjEyOTQ4NjYxNzc5MTI0MzI4NDA1MjgzMzIwNjU2NDQzMDg2ODk1MDY0NDQ3NTMwMjI4NTgzODE1OTg3ODk0NzYxMzcwMjg0ODExMjIwNTY2NTgxNzM2NzQwNzY1NzUyNzIyMTE3MDEwODEzODkyMTE5MDE1NzAyNDI1Njk3OTg4NzQwNjIzMDA4NDQzOTY4MTQ5NDAwNjk3ODI2NzMzNjg5NzU0ODYwNjk4NzIwMjkzMjI3ODU3NjU3NzM0MTc5ODEyOTQ2MDkwNTU0ODE3MDcyNjQ4NDgwOTA4MTg4NTI4NDY4MjAzMzM2MDEyMzY2OTUyNjUyODgwNDY5NjUwMTEzODU1NjQ4OTc0Mzk0NzU4NjM5NjUwMjM0NzY0Mzk5OTQ5NjMyNzk3NTYwMzc4NDU2NDIzNjc4NDM1NDIzMDUwMzg4MDU0NDEwMzg3NjIyMDEzMTYxMDc2OTEwOTQ3MjI3Mzk3NDQxMTgzMDA0NDM3MTI0NDU1Nzc0NTI1NzIwMDcxMjg4MjA5NDY2OTcwNDQwNDk3MTY1MTE1MTQ1OTc3NDM5MjkxNDI3MjgyNzI2MTAxMzAwNTg0NTU3MjYzNzMzNDY0NzA3NzA0NTk0NTQxODgzNjE0MTA3MzIwNDIxNDM3MjMxNzY5MzM2NDcxNTE3NjgyNzg1NDk3OTA1MzAzODM4ODk0ODM2NjE3NjU0MTc3Mzk3MDEwOTQ1NDI5ODU0NjM1NzAyODgwNDA3NjkyOTAxNjQzNTEifSwicl9jcmVkZW50aWFsIjpudWxsfSwic2lnbmF0dXJlX2NvcnJlY3RuZXNzX3Byb29mIjp7InNlIjoiMjE0NzgwNDA1MTAyNzUxNzA2MTIzMzc5MTYwODIxMTUzMjkwMDU5MjQ3MjkxNjAxMzg3Mjg1MDA3MzE5NjY2ODk3Njg4NjQ4NzYzNTAzMTQxMjc1ODIwNjUyMjIwNzAzNTg0NTMwOTEzMjY1NTc0NzYxMzI2NDA1MTI5MTUxNjQzOTM0NjkxNjI0MDAyNDE1NTU0ODY0NjUwMzEzNTIxMjczMDk2MTc4NjMwOTY2NDI2ODU0NzE1Nzk3MzYyNzk3NTM0ODM3OTE4NDUzOTQxMjAwNTIwNTI4NTA1Nzk3NjEwOTcwNTk3Njc2Mzc2NDE2MjA2MzcyNDYyNzU5NjcyNTE2NTYyNDE5Mzk0NDk5OTk3Mjg5MzQzOTg0MDE3MjM5OTg1MjA3OTg4OTYxNzc5NDU2NTAzODg3Njk2MzA3MjE3NzczNDI2MjMxMDU0MTc1NzYzNzgzMDA4MDIxMzgyMDU5MDY1OTU3MjI2NDg3OTkzOTk3MjI5OTg3NTgzNDU1ODE5NTI4MTA4Nzk2MTIxNzA2MjY0MTc4ODI1MDM5NTA2MzkzODk3MTc5NDk5Mjc5OTUzODYwODY1OTUzNzA3NjQyNTkxMDY0ODIyMjg4ODg1NDE5MjMyMTc1NTYwMzIwMDczNDgzNTg0Mzc3NDUxMDMxNTA2NDQ0NTcwNzQ1MTEyNTYxNjIxNzMzNjY1NjE2MzU0NjUxMzE0MjI3OTgzNjEyODM2NjkwMzgwNDg4MDY4NDk2MTkzMzc4OTUxMjY2NDI0Nzg2MzIxODY1NjYyOTMyNTM1OTc4MjY4NDgxMzQ0MzQ4NDQ5NzkiLCJjIjoiNzE1NzQ0NjIzMTA1OTU5NjM4NDg4ODk3ODU3NTYwNzIzOTYxMDE5NjE5MjIwNTc2NTkyMTgyMjc4NDk3Mjk4NjE4MzQzNTU4MDAyMDEifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=", + }, + "mime-type": "application/json", + }, + ], + "formats": [ + { + "attach_id": "e5c0d797-67f3-4d1d-a3e1-336786820213", + "format": "anoncreds/credential@v1.0", + }, + ], + "~please_ack": { + "on": [ + "RECEIPT", + ], + }, + "~service": { + "recipientKeys": [ + "xaRkB1mi5rQxihB5T2pAyx3m54fuT65nq9C1mjVNdZy", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:2001", + }, + "~thread": { + "pthid": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349", + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2024-02-28T09:36:32.927Z", + }, + }, + "e04ca938-64e1-441e-b5d9-4525befe4686": { + "id": "e04ca938-64e1-441e-b5d9-4525befe4686", + "tags": { + "connectionId": undefined, + "credentialIds": [], + "parentThreadId": undefined, + "role": "issuer", + "state": "done", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + "type": "CredentialRecord", + "value": { + "createdAt": "2024-02-28T09:36:04.294Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "test", + "value": "test", + }, + ], + "credentials": [], + "id": "e04ca938-64e1-441e-b5d9-4525befe4686", + "metadata": { + "_anoncreds/credential": { + "credentialDefinitionId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/CLAIM_DEF/400832/test", + "schemaId": "did:indy:bcovrin:test:6LHqdUeWDWsL94zRc1ULEx/anoncreds/v0/SCHEMA/test0.1599221872308001/1.0", + }, + }, + "protocolVersion": "v2", + "role": "issuer", + "state": "done", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + "updatedAt": "2024-02-05T22:50:20.522Z", + }, + }, + "f189e88c-4743-460a-bccc-443f2a692b98": { + "id": "f189e88c-4743-460a-bccc-443f2a692b98", + "tags": { + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "messageId": "a1ec787c-8df6-4efe-9280-6b4407db026e", + "messageName": "offer-credential", + "messageType": "https://didcomm.org/issue-credential/2.0/offer-credential", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "issue-credential", + "role": "receiver", + "threadId": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "8df00782-5090-434e-8f34-96d5e484658a", + "createdAt": "2024-02-28T09:36:11.829Z", + "id": "f189e88c-4743-460a-bccc-443f2a692b98", + "message": { + "@id": "a1ec787c-8df6-4efe-9280-6b4407db026e", + "@type": "https://didcomm.org/issue-credential/2.0/offer-credential", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/2.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "test", + "value": "test", + }, + ], + }, + "formats": [ + { + "attach_id": "d6883b94-7a6b-4fe7-9ddc-98b23c46c478", + "format": "anoncreds/credential@v1.0", + }, + ], + "offers~attach": [ + { + "@id": "d6883b94-7a6b-4fe7-9ddc-98b23c46c478", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvU0NIRU1BL3Rlc3QwLjE1OTkyMjE4NzIzMDgwMDEvMS4wIiwiY3JlZF9kZWZfaWQiOiJkaWQ6aW5keTpiY292cmluOnRlc3Q6NkxIcWRVZVdEV3NMOTR6UmMxVUxFeC9hbm9uY3JlZHMvdjAvQ0xBSU1fREVGLzQwMDgzMi90ZXN0Iiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiIxMDk0MzkxMjM2ODU1Nzg0MTgzNDIyMDY2NjI2MzE2OTQ4ODQ1ODMwNzA0MjY0MjUzNDUyMTkwNTAxMTM2ODIyNTcwNTY0MTIzMzY4NDAiLCJ4el9jYXAiOiIyMTk4OTc3Nzk0MzA1NjU5NzMwNDk2MDU0MjM4NDE4OTYxODUzMDEyMTgzODgxODQxNjk4NDUwMTY2ODU5MTA1MDg1MDY0MzIzNzcyMjE3MDUxMDQwODYwNDcwODY0NTM1MDI5Mzk2NTY4Njc0ODkyNzg4Nzg5MTIzNTU3MzE2MTkwNjAyNjczMTczODM0NDUwNjcxMjA2ODA2MjYxOTg2Mzc3NjE1NTc3MzU4MjI0MTM4NDc1OTExNjgyOTQ2MTAzOTkzODg5MTIxODMyNjExNTg4NDc0NzUwMjIxNTQ3MjcwNDAyMjUwMTIyMjc5ODcwNDQ4MDA3OTgwMjQ0NDA5OTc5NDgyNDg1MjU2MDk3OTY4ODQyNDg3ODM2MzMyNTA4MjA0OTIwNjM3ODE0NDMyNDczMjg0NDc0MzQzNzQ3Njg0MjI2MTMxMjAwNjQyMDI3NjQ2NjMwMzkzMDE4MTk1NTAzOTQ3MjkxNjA0Nzg5MjM5MTY3ODUwNjA5Mzc3MTE4NjA3NjUwMzE0NTYyOTk3MDc2NjQ3MDUzNzcxNzgxMjAwMTAxNjExOTc2MTI4ODY5OTE0NTM0NzQ5MDc0MDc3NzcyOTUzMjkzNjMwMzMyMDc5MTk4MzAxNjMwNTY3MjQ3Mjc0OTY5MTM5ODI2Nzc2NTM2NzEwMTgxMjQ3MDY2NDE4OTY1NTQyNDY5MjMyMDkxMDYwNjI4Njc0MTM4OTgwMDcwODE0Njg1OTMyMjg0MzIyMjMzMDQ3NjQ2NTkxODc3NjkyODgyMTM5ODQ4MzgxNjQxMTg1ODE1ODAxMDg0OTM5NTk2NzMwMTYxMjA1MDg2MzMzNzgwNjI5OTEyMTc1NDA0ODQ2MDk5MTI5MjY0NjM3ODA2MjQ3MzE2NzU2NTg3NDI5MjEwNjkzNDM5NTQyMjEzNzI2IiwieHJfY2FwIjpbWyJtYXN0ZXJfc2VjcmV0IiwiMTA4NjM5NjM3NDg4MzU1Nzc0MDI5ODE3OTk0NDkxOTE1MjIyMzc2ODk2NzQwMjM0MDk3MzI0NjcwNzU3NDQ1NjQ0OTAxNTk2NjUwMjk1MDc3NjM0Nzk0Mzc5MzMwODUyNzUxNjQwODk5NjYwMDUwOTY5MDUzNDYyMjcyMzQ5MTEzNjYxOTg3NzMxNDQ3NDMzNTIzNTc0OTc5NjYyMzE0MTAxMTI3Njc5MTAwNzcwMjY0NDMxMTIxNjYzMzMyMTQ2MTU4NzM3MzU0NTk0MDM1NTM1MjI5Mzc4MjU3NzUyOTAwMzA3Mjg3NjQ4NzcwOTU4NTg5ODg1NzA3NDEyOTgzNzYwNTE2ODk0NzkzMTE3NTQ5Nzc3Njg1NTg0MjQ3MzQ1ODMxNzk2MjUzMzQ1MDk1NzIyODU4NjM4MjAxMjgxMjIzNDYyOTg2NjE2MjYzNzIyMDk1MjMxMjg0MTgzMzM3ODYwODQ2Njc5Njg5ODM2MTM4NzAxNjE4MzI1MDAyNTM5NjczNzM4NjUwMTMxNzMzODIzNDk0Mjk5MzQzNDQxNjc5MzM1MTQ5NTIwODQ4Mzg4Njk5MTk5ODA1NTk4MTAzNTk0NTk4OTE0OTkyOTI2MDMzNTA0MzAxNTY5MjMwODYyMzc3NTg5ODg2OTYyMDIxOTIxODM3ODI3MDYyNTI0MzIzMTg3OTcwNjg0MzIxNzY0MzcwMzc4NDc0Mjc4MzA4NzMzOTY4ODgzNTMyNzM2MTE1NTQ3MzA0MzU4ODgyMzc1Njc0MTQwMjEzNzc1OTE1OTU3NzU5MTM3NjUwMjY0Njg3OTUzMTk4NTE5OTY0MDcyMzEwNDY3OTA2Njk0OTQxMzM4NDAzNDg4NTYyMjgxMDE5MTQzMDk5MTE0NjM3MTY1OTI2MzQxOTY4NTk3MjE4MjU3OTU5OCJdLFsidGVzdCIsIjcwMDE5MTUyMzc5MTExNTQzNzgwOTgxNTYyMDU5OTYzMDYzMzg4ODg5NDg1NjkxOTM5MzM3NzgxNjkwNTU3NjA5MDA4MTA2NjY5NzAwNjA3MzI1OTAyNjQyMzA4NTIzMDc5MjA3NjEwNTU1MjQ0NTY0MjkwNzc5ODA5Mzg5ODEzNTI0OTc5MzE5MTg4NDI0NzIwNzUxMjQwMzQ3NTY0NTQ3MDY2NDE1NTE3MzU5NjUxODU1NzU3MzY4NDcxOTM5OTk3NjY1MTk5NTE4OTQ2ODMzMDY1MTMyNjYxMDI4Nzc5ODg1NDQ5ODMwMzg1MTA3MzUxOTgxMDM1NTAzMzM1MDg0NjgxMDQ4MzE0NjQzMDQ4NzIwMzQxMzk0MjI5NTEyNDcyNDY0NjUwNDI3NDA4NTI5ODkwMzg1ODc1MzkzMjA0ODExMTUwODgxNDA4NzY3NTMzNjI1MjU4MDUwNDc2NzU2NDIyMzk5NjMxNjA5NTU2MjI0NjQ1Mjg5NDM0Mjk3NDkwMzg0MzYwNDM0Mzg4MDU1NDAxODgyNDIxNDU1OTI0NjQxMTUwNjQ1NTkzNzUwMjQ2MTI4NTYzNzMzMzgzMzQ2MTYyNjYzNjE5MTYyMzIxMDM3MDgzNTcxNzc0MzQ5MzYwMTcxNzkwNzUzNzYyMDI3NTczMDc0MDI1NTgyOTQyODk4MzMwMTY3NDY0MTExNjA1MTMxMjk5MDE2MjQ5MzU2MDY5MjM3OTAzNjAyNjgwMjMwNzgzMjg0MDIwOTMxMjY3MDkzNTE1MzU3MjQ1MDEwOTI0MTI2MTIyNjUwNTM1MDI5MjIxMzY2NzA5NTI2NjY1Mjc5NzIyMDg0NjI0MzQwOTkyODQ4NDU0OTgxNTExOTIwNDE4NzM2NTE1NjIxMTgxNjkxMzcyNDE5ODU2OTg1NDkzMSJdXX0sIm5vbmNlIjoiOTczNDk4MTg3NTMwNzE2MjQ3MDE5NzU0In0=", + }, + "mime-type": "application/json", + }, + ], + "~thread": { + "pthid": "9cf43eb2-2690-4bbc-8e29-7ece0acf9349", + "thid": "ba17e7c5-74e0-46e5-8862-7f433e201286", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2024-02-28T09:36:11.829Z", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.4 - v0.5 should correctly add role to proof exchange records 1`] = ` +{ + "3f3351dd-7b56-4288-8ed7-b9c46f33718e": { + "id": "3f3351dd-7b56-4288-8ed7-b9c46f33718e", + "tags": { + "associatedRecordId": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "messageId": "9e1568e0-7c3e-4112-b76e-869ea9d12f06", + "messageName": "request-presentation", + "messageType": "https://didcomm.org/present-proof/2.0/request-presentation", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "present-proof", + "role": "sender", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "createdAt": "2024-02-28T09:36:44.806Z", + "id": "3f3351dd-7b56-4288-8ed7-b9c46f33718e", + "message": { + "@id": "9e1568e0-7c3e-4112-b76e-869ea9d12f06", + "@type": "https://didcomm.org/present-proof/2.0/request-presentation", + "formats": [ + { + "attach_id": "f8e75054-35ab-4f7a-83f8-0f6638a865b0", + "format": "anoncreds/proof-request@v1.0", + }, + ], + "present_multiple": false, + "request_presentations~attach": [ + { + "@id": "f8e75054-35ab-4f7a-83f8-0f6638a865b0", + "data": { + "base64": "eyJuYW1lIjoidGVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjExMTUyNTQ2NTQwNjU3ODkyNDY4OTA3MDUiLCJyZXF1ZXN0ZWRfYXR0cmlidXRlcyI6eyJ0ZXN0Ijp7Im5hbWUiOiJ0ZXN0IiwicmVzdHJpY3Rpb25zIjpbeyJjcmVkX2RlZl9pZCI6ImRpZDppbmR5OmJjb3ZyaW46dGVzdDo2TEhxZFVlV0RXc0w5NHpSYzFVTEV4L2Fub25jcmVkcy92MC9DTEFJTV9ERUYvNDAwODMyL3Rlc3QifV19fSwicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOnt9fQ==", + }, + "mime-type": "application/json", + }, + ], + "will_confirm": true, + "~thread": { + "thid": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2024-02-28T09:36:44.806Z", + }, + }, + "423586ec-1f01-458c-bc83-7080c7c90173": { + "id": "423586ec-1f01-458c-bc83-7080c7c90173", + "tags": { + "associatedRecordId": "6cea02c6-8a02-480d-be63-598e4dd7287a", + "messageId": "b62d95a7-076c-4e38-ba1f-9d0d8d51677d", + "messageName": "presentation", + "messageType": "https://didcomm.org/present-proof/2.0/presentation", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "present-proof", + "role": "sender", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "6cea02c6-8a02-480d-be63-598e4dd7287a", + "createdAt": "2024-02-28T09:37:01.712Z", + "id": "423586ec-1f01-458c-bc83-7080c7c90173", + "message": { + "@id": "b62d95a7-076c-4e38-ba1f-9d0d8d51677d", + "@type": "https://didcomm.org/present-proof/2.0/presentation", + "formats": [ + { + "attach_id": "21f8cb0f-084c-4741-8815-026914a92ce1", + "format": "anoncreds/proof@v1.0", + }, + ], + "last_presentation": true, + "presentations~attach": [ + { + "@id": "21f8cb0f-084c-4741-8815-026914a92ce1", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsidGVzdCI6IjcyMTU1OTM5NDg2ODQ2ODQ5NTA5NzU5MzY5NzMzMjY2NDg2OTgyODIxNzk1ODEwNDQ4MjQ1NDIzMTY4OTU3MzkwNjA3NjQ0MzYzMjcyIn0sImFfcHJpbWUiOiIyNDc2MzE0OTMxMzE1OTU5ODY1MzU1MjgzMjM1MjkxMDc4MDM0NjAzMjcxMjQzMTEwOTQ3OTM0NDU3MzAyMTUwMjY5NDIzODkyMDYzODk2OTkzNzgxMDQwMDQ2NzI1MzIyNjE5Nzg1MzU2MTgxMjU0NDAxOTIzMzk4NjYyNzMyNjI3MjU3MzA5MDgxMjA3NjY2ODI5NDc0MDY5MDA2NjY5NDk3MTI4NzI1ODA4NjExOTAwMzQ0MTIxOTUwODc0NjYzMTEwNjE0NTc2ODc2NjQ2NTM2NTcxOTkwMjM5MDg4NTIwNjAyODY1NzM5MDM5MTAxMjI3NDY3Mzk5NzE3ODc3MDc4ODA0NzM5NDYyOTkzMjg5OTc4NjM2NDQxNzg2MTE2OTM4MzkzMzIzMTMwNDQwNDgzNDI5NjY1ODczNTE2NTA4ODA2MzA5MTMzODMxMjkxMzM5NDg1NDIyMzg1MDE1MzY3Nzc1NDc3ODkwNjQzMzg0MTYyODk5MDA1NzA0MjMxNzM1MTg2ODE5OTEwNTc2MDU0NjIzODkzOTk0MDkyNTA4MjU4ODgzODQ0MjkzMTkxNzY2ODQyNzg1ODA4NzY3MDQ1NDE3MTQ3NDc3NDQwNTk4MTk2NjM5OTA0NTQxNjE2MDY2MTYyMjYxNzE5MTUyMTYzNDk5ODEzNDc3NjM2MzE2NDgwNzQxMzI3OTg3OTk2NTk5MTc2OTc1MzQzNzUzOTgyOTMyMzA2NTA4MzQ3ODMxMjM2NjA1ODc2NTkwMDI3NDI5MDY0ODg0NzU5NTc4OTY2MjY5NzcxODQxNDUzMTI1NjYzMjgxOCIsImUiOiIxMzI4MDA2MjE3OTg5NjIzMjUyNTQ2MzQwNDE2MTAxMTcwODY0NTM4NDQwNzEzNzczNDE4NjA5ODU2NDE3MjI2MTg1NTQxMzE3NTk4MTQ0NzQ2Nzg1MDQ2NDcxMjA2MTUzNzY2NjU3NjQwNzEyNDc1ODY3NjMwOTE4NTIwMzY4MTE1MDQ3NTEzNDQiLCJ2IjoiNTgyMDc2NDI4NzI3NjQwNjY3ODA5ODkxNTI4MDIyMzQ0NjYwMDU4Njg1MTkxOTUwODIyMTExMzgwMjU1MDcxMTAwNzE3NzM5MzE0MTQxMzkxMTYxOTU5NTg2NDk0ODE2Njg5OTg2NTc0NDk1NzExOTI4OTEzODcwNjEwMDE1NjE3NTYxOTI5NjQzMjY5NjM2ODEzNTk4NjgwMTAyMDQ4NDYzNjk1NjgxMDIzNDI5MTM2NDI2NDgwNDEzNzI2MzEyNzMxMTczODE2NjI3MjExNzEyMTQzNTg4OTgyMDM5NTk5Mjg0NTgzNDI1MjE3MjI1OTg0NjcxNjYxODcwNzY4NzMxNjYyODE2MjMzOTUzMDg2MzYyMDA5NDA0OTQ3NTQxOTY5OTAwNzcxODA1NzI0NTUwNTQ1MTczOTg5MDI2NjAwNzk3ODkwMzc1MDE5OTQ4MjM3OTA2NjY1MTA5MTY0NDIxNDQ1MTEwMTQ0ODYyNzEzNjY1NjA0MTkwMzY1NTQzNjM3ODY4NTc1MjI1OTA2MTMyNjk5MTc0NzcxMjMzNDkzOTUzODc5MTQwMjQwNDIxMzY5NTA3MDU3MTgwMjA5NTM1NzEyODI1NzI2NDkwODkxMDE2OTUyMDkzMzc5MTMyMjI3MzMzNjg2OTEyMDE3MTEwMDM5ODcwNTc3MTMwNTA4ODQ2MDM2Mzc0MzYxMzE1MTQ3ODgwODA4NzkyNjQ0MzU2Mjg4NzgwNjQ4OTUyNDQxNzkyMDU0NDY4ODU2NjY1ODg0MTg1MzYyNDM4Mjk1NTcxNjAyMjk5MTA5MjA4ODg5NTU2NzE3ODc5NzI4NDUzMjk5NDI0MTQwMzc1NTU1OTQwNjUzOTAxOTIzMDE5NjQyNjU4MzI0NDU0MzU0MDk5MjU4ODczODY2NjY2MDg2MTYwODA5NTExNDYwOTQzMTAzNzEwMDk5MTU3MzExMDM3OTAwODIwOTI1ODYwNTYyMzE4NDc0MDc3NTQ3NTg2MTUxODg0NzEwOTU2Mzc0MzUxNDI1MTQzMjA2MjI3ODk0MzE5NjM2MDc4MDQyODU5NTMyOTU3MTk0MzYwNDYzNTIyODg5MDM1NjUyODAyNjk5MjUyNDkzMDcwNjA0ODgwMjY0NzA4MDg1MDAwNzczODUzOTQyMzg0ODQ2MDY5NTYwMTYzMjE0MzUwODM0MTcwODU4OTU3OTUzOTc5NDg3MTk3NDY0NjY0MDU1IiwibSI6eyJtYXN0ZXJfc2VjcmV0IjoiMzg0ODM0OTE0MjE1Mjg5MjAyNTM1MTM4ODg1MzgzNjAwNzAyNjMyNDQzODE4NjkzMzA2OTYzMjM4ODMwMDE1ODgwMzU2MDQ2NjMwMjMwNTc3MDU3NTQ2MjY0MzkwNzk2NjQxODg5NjMzMTkwMzUzMTcxOTUzNjAwMDAxOTM2NDE1NzM5NDcxODExNjQyMDQ2Mzk0MjEyODk3OTE3Nzg4NDQ4MzMxNTU0NjA5MTA1ODU3NSJ9LCJtMiI6Ijc3OTk1NzIwMTQ1NDU5ODMxODM2MjI3NTA2NTM4NDQxNjUxMDI3NDA0NDM5MTcyMDQwMDc5ODc3NDUyMDI2NDI5NTgxNjMyNDYxNjg5NzIzMzkwMjU4NDc5NDY5MTg4Mjk4NzgwMTgyMDkzNTMxMDc3NzkzNjEzODUwMjAwNzc1NjIzMDcxMjk4NTI5ODExOTk3OTkyMTEzODUxODM5MjcyMjExMTQ5NDEzNTY3OTEyMzIifSwiZ2VfcHJvb2ZzIjpbXX0sIm5vbl9yZXZvY19wcm9vZiI6bnVsbH1dLCJhZ2dyZWdhdGVkX3Byb29mIjp7ImNfaGFzaCI6IjQ1NTgyNDIzNzE0NzIyNTkwODQ4NDgxOTM4NTYxNDU3MTcwNjI4NzAzODk4OTI1NTU2OTE0Mjc3NDU5MTU4OTM3Nzc0NDA3NDgxNzIyIiwiY19saXN0IjpbWzE5Niw0MSwxMTYsMzIsNDIsMTgsMjUsMTA3LDE1MCwyMzcsMTMsNjksMTIyLDEyOCwyMDcsNDYsMjQsMjE2LDYxLDg4LDE1OSwyMjMsMTQyLDE2MiwxMDksMTMwLDIyOSwyNDQsNjIsMTEzLDUsMTIxLDU5LDE1NiwyMTEsMTUwLDEsOTMsNTUsMTI4LDI0MywyMzgsODAsMTgxLDEyMSwyMjcsNzgsNzQsMzAsODIsMTQ0LDIwNSwxNzIsODEsMjQxLDE3NSwxNCwxNjIsNzMsMTk0LDY0LDE3NSwyMzEsMTM3LDI0OSwyMzcsMjMyLDE1MiwxOTMsMTAzLDEyNiwyNTMsMTI1LDE4MSwyMzUsMjMzLDEwOSwxNzQsMTcwLDE2OSw0MiwyMzEsMTYsMjI0LDIwNiwxNywxMzMsODEsNDYsMzUsNzAsMjE1LDM3LDI0MCwxNTUsMTQzLDE2MCw2OCwxOTksNzgsODgsMTksMTE0LDEwNywxMDYsMTUzLDE0MywxNjcsNDUsMTE2LDE2Myw1Myw2MSwxMTAsMTg5LDIzMCwxNTUsMjM1LDIzMywxNSwyNDEsNzUsMTM4LDEwNyw3MCwyLDk1LDE4NSwxMzEsMTI5LDEwMSwyMzksMTk1LDY3LDE4NCwzOSwxMDMsNDcsMTYwLDEzMSwyMTUsMjQ3LDIwNywyMywxMDYsMTkwLDE0NCwwLDEwLDM3LDEyNSw5MSwxMTQsMTI0LDEyNSwxNDgsMTU0LDE1NSwxNzUsMjI1LDI1MywxMzgsMjA5LDE2OCw3NywxOTYsNDIsMTgwLDEzMywxMTEsMzgsMTUzLDQzLDE1OSwxOTUsOTMsODAsNDMsMTMzLDg3LDE5NCwyNDcsMzgsNDAsMjE1LDE3OSwxOSwyMSwxMTcsNywxNDAsMjE3LDQwLDM5LDY2LDE3LDkzLDM3LDE1Miw5MCwyNDQsMzcsMTQ4LDk3LDE3NCw1OCw2NCwxNjYsNzAsOTAsMzYsNTUsMTEzLDIxMCwxODQsMjE0LDk0LDQ1LDEyNCwyNDEsMjM5LDE0OSwzOSwzMyw4OCwxNSwxNSwyNDksNTcsMjQ0LDUyLDIzMSwxOTUsNzUsNjUsNjUsMjUyLDEyMiwzMywyNTUsMjQ0LDE4MiwyLDExOSwyMjAsNDIsNzIsNzQsMTU3LDE1OCwyMTMsMTEyLDg3LDExLDE5NywyNDJdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsidGVzdCI6eyJzdWJfcHJvb2ZfaW5kZXgiOjAsInJhdyI6InRlc3QiLCJlbmNvZGVkIjoiNzIxNTU5Mzk0ODY4NDY4NDk1MDk3NTkzNjk3MzMyNjY0ODY5ODI4MjE3OTU4MTA0NDgyNDU0MjMxNjg5NTczOTA2MDc2NDQzNjMyNzIifX0sInNlbGZfYXR0ZXN0ZWRfYXR0cnMiOnt9LCJ1bnJldmVhbGVkX2F0dHJzIjp7fSwicHJlZGljYXRlcyI6e319LCJpZGVudGlmaWVycyI6W3sic2NoZW1hX2lkIjoiZGlkOmluZHk6YmNvdnJpbjp0ZXN0OjZMSHFkVWVXRFdzTDk0elJjMVVMRXgvYW5vbmNyZWRzL3YwL1NDSEVNQS90ZXN0MC4xNTk5MjIxODcyMzA4MDAxLzEuMCIsImNyZWRfZGVmX2lkIjoiZGlkOmluZHk6YmNvdnJpbjp0ZXN0OjZMSHFkVWVXRFdzTDk0elJjMVVMRXgvYW5vbmNyZWRzL3YwL0NMQUlNX0RFRi80MDA4MzIvdGVzdCIsInJldl9yZWdfaWQiOm51bGwsInRpbWVzdGFtcCI6bnVsbH1dfQ==", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": { + "on": [ + "RECEIPT", + ], + }, + "~service": { + "recipientKeys": [ + "4jXwJs8iWhNoQWoNhugUuFAKHo6Lodr983s6gHDtHNSX", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001", + }, + "~thread": { + "pthid": "2b2e1468-d90f-43d8-a30c-6a494b9f4420", + "thid": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2024-02-28T09:37:03.716Z", + }, + }, + "6cea02c6-8a02-480d-be63-598e4dd7287a": { + "id": "6cea02c6-8a02-480d-be63-598e4dd7287a", + "tags": { + "connectionId": undefined, + "parentThreadId": "2b2e1468-d90f-43d8-a30c-6a494b9f4420", + "role": "prover", + "state": "done", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + }, + "type": "ProofRecord", + "value": { + "createdAt": "2024-02-28T09:36:53.850Z", + "id": "6cea02c6-8a02-480d-be63-598e4dd7287a", + "metadata": {}, + "parentThreadId": "2b2e1468-d90f-43d8-a30c-6a494b9f4420", + "protocolVersion": "v2", + "role": "prover", + "state": "done", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + "updatedAt": "2024-02-05T22:50:20.522Z", + }, + }, + "76ad893a-f582-4bd6-be47-27e88a7ebc53": { + "id": "76ad893a-f582-4bd6-be47-27e88a7ebc53", + "tags": { + "associatedRecordId": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "messageId": "b62d95a7-076c-4e38-ba1f-9d0d8d51677d", + "messageName": "presentation", + "messageType": "https://didcomm.org/present-proof/2.0/presentation", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "present-proof", + "role": "receiver", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "createdAt": "2024-02-28T09:37:13.397Z", + "id": "76ad893a-f582-4bd6-be47-27e88a7ebc53", + "message": { + "@id": "b62d95a7-076c-4e38-ba1f-9d0d8d51677d", + "@type": "https://didcomm.org/present-proof/2.0/presentation", + "formats": [ + { + "attach_id": "21f8cb0f-084c-4741-8815-026914a92ce1", + "format": "anoncreds/proof@v1.0", + }, + ], + "last_presentation": true, + "presentations~attach": [ + { + "@id": "21f8cb0f-084c-4741-8815-026914a92ce1", + "data": { + "base64": "eyJwcm9vZiI6eyJwcm9vZnMiOlt7InByaW1hcnlfcHJvb2YiOnsiZXFfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsidGVzdCI6IjcyMTU1OTM5NDg2ODQ2ODQ5NTA5NzU5MzY5NzMzMjY2NDg2OTgyODIxNzk1ODEwNDQ4MjQ1NDIzMTY4OTU3MzkwNjA3NjQ0MzYzMjcyIn0sImFfcHJpbWUiOiIyNDc2MzE0OTMxMzE1OTU5ODY1MzU1MjgzMjM1MjkxMDc4MDM0NjAzMjcxMjQzMTEwOTQ3OTM0NDU3MzAyMTUwMjY5NDIzODkyMDYzODk2OTkzNzgxMDQwMDQ2NzI1MzIyNjE5Nzg1MzU2MTgxMjU0NDAxOTIzMzk4NjYyNzMyNjI3MjU3MzA5MDgxMjA3NjY2ODI5NDc0MDY5MDA2NjY5NDk3MTI4NzI1ODA4NjExOTAwMzQ0MTIxOTUwODc0NjYzMTEwNjE0NTc2ODc2NjQ2NTM2NTcxOTkwMjM5MDg4NTIwNjAyODY1NzM5MDM5MTAxMjI3NDY3Mzk5NzE3ODc3MDc4ODA0NzM5NDYyOTkzMjg5OTc4NjM2NDQxNzg2MTE2OTM4MzkzMzIzMTMwNDQwNDgzNDI5NjY1ODczNTE2NTA4ODA2MzA5MTMzODMxMjkxMzM5NDg1NDIyMzg1MDE1MzY3Nzc1NDc3ODkwNjQzMzg0MTYyODk5MDA1NzA0MjMxNzM1MTg2ODE5OTEwNTc2MDU0NjIzODkzOTk0MDkyNTA4MjU4ODgzODQ0MjkzMTkxNzY2ODQyNzg1ODA4NzY3MDQ1NDE3MTQ3NDc3NDQwNTk4MTk2NjM5OTA0NTQxNjE2MDY2MTYyMjYxNzE5MTUyMTYzNDk5ODEzNDc3NjM2MzE2NDgwNzQxMzI3OTg3OTk2NTk5MTc2OTc1MzQzNzUzOTgyOTMyMzA2NTA4MzQ3ODMxMjM2NjA1ODc2NTkwMDI3NDI5MDY0ODg0NzU5NTc4OTY2MjY5NzcxODQxNDUzMTI1NjYzMjgxOCIsImUiOiIxMzI4MDA2MjE3OTg5NjIzMjUyNTQ2MzQwNDE2MTAxMTcwODY0NTM4NDQwNzEzNzczNDE4NjA5ODU2NDE3MjI2MTg1NTQxMzE3NTk4MTQ0NzQ2Nzg1MDQ2NDcxMjA2MTUzNzY2NjU3NjQwNzEyNDc1ODY3NjMwOTE4NTIwMzY4MTE1MDQ3NTEzNDQiLCJ2IjoiNTgyMDc2NDI4NzI3NjQwNjY3ODA5ODkxNTI4MDIyMzQ0NjYwMDU4Njg1MTkxOTUwODIyMTExMzgwMjU1MDcxMTAwNzE3NzM5MzE0MTQxMzkxMTYxOTU5NTg2NDk0ODE2Njg5OTg2NTc0NDk1NzExOTI4OTEzODcwNjEwMDE1NjE3NTYxOTI5NjQzMjY5NjM2ODEzNTk4NjgwMTAyMDQ4NDYzNjk1NjgxMDIzNDI5MTM2NDI2NDgwNDEzNzI2MzEyNzMxMTczODE2NjI3MjExNzEyMTQzNTg4OTgyMDM5NTk5Mjg0NTgzNDI1MjE3MjI1OTg0NjcxNjYxODcwNzY4NzMxNjYyODE2MjMzOTUzMDg2MzYyMDA5NDA0OTQ3NTQxOTY5OTAwNzcxODA1NzI0NTUwNTQ1MTczOTg5MDI2NjAwNzk3ODkwMzc1MDE5OTQ4MjM3OTA2NjY1MTA5MTY0NDIxNDQ1MTEwMTQ0ODYyNzEzNjY1NjA0MTkwMzY1NTQzNjM3ODY4NTc1MjI1OTA2MTMyNjk5MTc0NzcxMjMzNDkzOTUzODc5MTQwMjQwNDIxMzY5NTA3MDU3MTgwMjA5NTM1NzEyODI1NzI2NDkwODkxMDE2OTUyMDkzMzc5MTMyMjI3MzMzNjg2OTEyMDE3MTEwMDM5ODcwNTc3MTMwNTA4ODQ2MDM2Mzc0MzYxMzE1MTQ3ODgwODA4NzkyNjQ0MzU2Mjg4NzgwNjQ4OTUyNDQxNzkyMDU0NDY4ODU2NjY1ODg0MTg1MzYyNDM4Mjk1NTcxNjAyMjk5MTA5MjA4ODg5NTU2NzE3ODc5NzI4NDUzMjk5NDI0MTQwMzc1NTU1OTQwNjUzOTAxOTIzMDE5NjQyNjU4MzI0NDU0MzU0MDk5MjU4ODczODY2NjY2MDg2MTYwODA5NTExNDYwOTQzMTAzNzEwMDk5MTU3MzExMDM3OTAwODIwOTI1ODYwNTYyMzE4NDc0MDc3NTQ3NTg2MTUxODg0NzEwOTU2Mzc0MzUxNDI1MTQzMjA2MjI3ODk0MzE5NjM2MDc4MDQyODU5NTMyOTU3MTk0MzYwNDYzNTIyODg5MDM1NjUyODAyNjk5MjUyNDkzMDcwNjA0ODgwMjY0NzA4MDg1MDAwNzczODUzOTQyMzg0ODQ2MDY5NTYwMTYzMjE0MzUwODM0MTcwODU4OTU3OTUzOTc5NDg3MTk3NDY0NjY0MDU1IiwibSI6eyJtYXN0ZXJfc2VjcmV0IjoiMzg0ODM0OTE0MjE1Mjg5MjAyNTM1MTM4ODg1MzgzNjAwNzAyNjMyNDQzODE4NjkzMzA2OTYzMjM4ODMwMDE1ODgwMzU2MDQ2NjMwMjMwNTc3MDU3NTQ2MjY0MzkwNzk2NjQxODg5NjMzMTkwMzUzMTcxOTUzNjAwMDAxOTM2NDE1NzM5NDcxODExNjQyMDQ2Mzk0MjEyODk3OTE3Nzg4NDQ4MzMxNTU0NjA5MTA1ODU3NSJ9LCJtMiI6Ijc3OTk1NzIwMTQ1NDU5ODMxODM2MjI3NTA2NTM4NDQxNjUxMDI3NDA0NDM5MTcyMDQwMDc5ODc3NDUyMDI2NDI5NTgxNjMyNDYxNjg5NzIzMzkwMjU4NDc5NDY5MTg4Mjk4NzgwMTgyMDkzNTMxMDc3NzkzNjEzODUwMjAwNzc1NjIzMDcxMjk4NTI5ODExOTk3OTkyMTEzODUxODM5MjcyMjExMTQ5NDEzNTY3OTEyMzIifSwiZ2VfcHJvb2ZzIjpbXX0sIm5vbl9yZXZvY19wcm9vZiI6bnVsbH1dLCJhZ2dyZWdhdGVkX3Byb29mIjp7ImNfaGFzaCI6IjQ1NTgyNDIzNzE0NzIyNTkwODQ4NDgxOTM4NTYxNDU3MTcwNjI4NzAzODk4OTI1NTU2OTE0Mjc3NDU5MTU4OTM3Nzc0NDA3NDgxNzIyIiwiY19saXN0IjpbWzE5Niw0MSwxMTYsMzIsNDIsMTgsMjUsMTA3LDE1MCwyMzcsMTMsNjksMTIyLDEyOCwyMDcsNDYsMjQsMjE2LDYxLDg4LDE1OSwyMjMsMTQyLDE2MiwxMDksMTMwLDIyOSwyNDQsNjIsMTEzLDUsMTIxLDU5LDE1NiwyMTEsMTUwLDEsOTMsNTUsMTI4LDI0MywyMzgsODAsMTgxLDEyMSwyMjcsNzgsNzQsMzAsODIsMTQ0LDIwNSwxNzIsODEsMjQxLDE3NSwxNCwxNjIsNzMsMTk0LDY0LDE3NSwyMzEsMTM3LDI0OSwyMzcsMjMyLDE1MiwxOTMsMTAzLDEyNiwyNTMsMTI1LDE4MSwyMzUsMjMzLDEwOSwxNzQsMTcwLDE2OSw0MiwyMzEsMTYsMjI0LDIwNiwxNywxMzMsODEsNDYsMzUsNzAsMjE1LDM3LDI0MCwxNTUsMTQzLDE2MCw2OCwxOTksNzgsODgsMTksMTE0LDEwNywxMDYsMTUzLDE0MywxNjcsNDUsMTE2LDE2Myw1Myw2MSwxMTAsMTg5LDIzMCwxNTUsMjM1LDIzMywxNSwyNDEsNzUsMTM4LDEwNyw3MCwyLDk1LDE4NSwxMzEsMTI5LDEwMSwyMzksMTk1LDY3LDE4NCwzOSwxMDMsNDcsMTYwLDEzMSwyMTUsMjQ3LDIwNywyMywxMDYsMTkwLDE0NCwwLDEwLDM3LDEyNSw5MSwxMTQsMTI0LDEyNSwxNDgsMTU0LDE1NSwxNzUsMjI1LDI1MywxMzgsMjA5LDE2OCw3NywxOTYsNDIsMTgwLDEzMywxMTEsMzgsMTUzLDQzLDE1OSwxOTUsOTMsODAsNDMsMTMzLDg3LDE5NCwyNDcsMzgsNDAsMjE1LDE3OSwxOSwyMSwxMTcsNywxNDAsMjE3LDQwLDM5LDY2LDE3LDkzLDM3LDE1Miw5MCwyNDQsMzcsMTQ4LDk3LDE3NCw1OCw2NCwxNjYsNzAsOTAsMzYsNTUsMTEzLDIxMCwxODQsMjE0LDk0LDQ1LDEyNCwyNDEsMjM5LDE0OSwzOSwzMyw4OCwxNSwxNSwyNDksNTcsMjQ0LDUyLDIzMSwxOTUsNzUsNjUsNjUsMjUyLDEyMiwzMywyNTUsMjQ0LDE4MiwyLDExOSwyMjAsNDIsNzIsNzQsMTU3LDE1OCwyMTMsMTEyLDg3LDExLDE5NywyNDJdXX19LCJyZXF1ZXN0ZWRfcHJvb2YiOnsicmV2ZWFsZWRfYXR0cnMiOnsidGVzdCI6eyJzdWJfcHJvb2ZfaW5kZXgiOjAsInJhdyI6InRlc3QiLCJlbmNvZGVkIjoiNzIxNTU5Mzk0ODY4NDY4NDk1MDk3NTkzNjk3MzMyNjY0ODY5ODI4MjE3OTU4MTA0NDgyNDU0MjMxNjg5NTczOTA2MDc2NDQzNjMyNzIifX0sInNlbGZfYXR0ZXN0ZWRfYXR0cnMiOnt9LCJ1bnJldmVhbGVkX2F0dHJzIjp7fSwicHJlZGljYXRlcyI6e319LCJpZGVudGlmaWVycyI6W3sic2NoZW1hX2lkIjoiZGlkOmluZHk6YmNvdnJpbjp0ZXN0OjZMSHFkVWVXRFdzTDk0elJjMVVMRXgvYW5vbmNyZWRzL3YwL1NDSEVNQS90ZXN0MC4xNTk5MjIxODcyMzA4MDAxLzEuMCIsImNyZWRfZGVmX2lkIjoiZGlkOmluZHk6YmNvdnJpbjp0ZXN0OjZMSHFkVWVXRFdzTDk0elJjMVVMRXgvYW5vbmNyZWRzL3YwL0NMQUlNX0RFRi80MDA4MzIvdGVzdCIsInJldl9yZWdfaWQiOm51bGwsInRpbWVzdGFtcCI6bnVsbH1dfQ==", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": { + "on": [ + "RECEIPT", + ], + }, + "~service": { + "recipientKeys": [ + "4jXwJs8iWhNoQWoNhugUuFAKHo6Lodr983s6gHDtHNSX", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:3001", + }, + "~thread": { + "pthid": "2b2e1468-d90f-43d8-a30c-6a494b9f4420", + "thid": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + }, + "~transport": { + "return_route": "all", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2024-02-28T09:37:13.397Z", + }, + }, + "9bf15c4a-8876-4878-85f6-a33eca8e8842": { + "id": "9bf15c4a-8876-4878-85f6-a33eca8e8842", + "tags": { + "associatedRecordId": "6cea02c6-8a02-480d-be63-598e4dd7287a", + "messageId": "9e1568e0-7c3e-4112-b76e-869ea9d12f06", + "messageName": "request-presentation", + "messageType": "https://didcomm.org/present-proof/2.0/request-presentation", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "present-proof", + "role": "receiver", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "6cea02c6-8a02-480d-be63-598e4dd7287a", + "createdAt": "2024-02-28T09:36:54.124Z", + "id": "9bf15c4a-8876-4878-85f6-a33eca8e8842", + "message": { + "@id": "9e1568e0-7c3e-4112-b76e-869ea9d12f06", + "@type": "https://didcomm.org/present-proof/2.0/request-presentation", + "formats": [ + { + "attach_id": "f8e75054-35ab-4f7a-83f8-0f6638a865b0", + "format": "anoncreds/proof-request@v1.0", + }, + ], + "present_multiple": false, + "request_presentations~attach": [ + { + "@id": "f8e75054-35ab-4f7a-83f8-0f6638a865b0", + "data": { + "base64": "eyJuYW1lIjoidGVzdCIsInZlcnNpb24iOiIxLjAiLCJub25jZSI6IjExMTUyNTQ2NTQwNjU3ODkyNDY4OTA3MDUiLCJyZXF1ZXN0ZWRfYXR0cmlidXRlcyI6eyJ0ZXN0Ijp7Im5hbWUiOiJ0ZXN0IiwicmVzdHJpY3Rpb25zIjpbeyJjcmVkX2RlZl9pZCI6ImRpZDppbmR5OmJjb3ZyaW46dGVzdDo2TEhxZFVlV0RXc0w5NHpSYzFVTEV4L2Fub25jcmVkcy92MC9DTEFJTV9ERUYvNDAwODMyL3Rlc3QifV19fSwicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOnt9fQ==", + }, + "mime-type": "application/json", + }, + ], + "will_confirm": true, + "~thread": { + "pthid": "2b2e1468-d90f-43d8-a30c-6a494b9f4420", + "thid": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + }, + }, + "metadata": {}, + "role": "receiver", + "updatedAt": "2024-02-28T09:36:54.124Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2023-03-18T18:35:02.888Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.5", + "updatedAt": "2024-02-05T22:50:20.522Z", + }, + }, + "a190fdb4-161d-41ea-bfe6-219c5cf63b59": { + "id": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "tags": { + "connectionId": undefined, + "parentThreadId": undefined, + "role": "verifier", + "state": "done", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + }, + "type": "ProofRecord", + "value": { + "createdAt": "2024-02-28T09:36:43.889Z", + "id": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "isVerified": true, + "metadata": {}, + "protocolVersion": "v2", + "role": "verifier", + "state": "done", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + "updatedAt": "2024-02-05T22:50:20.522Z", + }, + }, + "cee2243c-d02d-4e1a-b97c-6befcb768ced": { + "id": "cee2243c-d02d-4e1a-b97c-6befcb768ced", + "tags": { + "associatedRecordId": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "messageId": "ae6f67d0-21b6-4b12-b039-7b9dbd530d30", + "messageName": "ack", + "messageType": "https://didcomm.org/present-proof/2.0/ack", + "protocolMajorVersion": "2", + "protocolMinorVersion": "0", + "protocolName": "present-proof", + "role": "sender", + "threadId": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + }, + "type": "DidCommMessageRecord", + "value": { + "associatedRecordId": "a190fdb4-161d-41ea-bfe6-219c5cf63b59", + "createdAt": "2024-02-28T09:37:15.884Z", + "id": "cee2243c-d02d-4e1a-b97c-6befcb768ced", + "message": { + "@id": "ae6f67d0-21b6-4b12-b039-7b9dbd530d30", + "@type": "https://didcomm.org/present-proof/2.0/ack", + "status": "OK", + "~service": { + "recipientKeys": [ + "GTzoFyznwPBdprbgXcZ7LJyzMia921w8v1ykcCvud5hX", + ], + "routingKeys": [], + "serviceEndpoint": "http://localhost:2001", + }, + "~thread": { + "pthid": "2b2e1468-d90f-43d8-a30c-6a494b9f4420", + "thid": "a2f120ce-ff0f-498a-a092-97225e5d5f68", + }, + }, + "metadata": {}, + "role": "sender", + "updatedAt": "2024-02-28T09:37:15.884Z", + }, + }, +} +`; diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/backup-askar.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/backup-askar.test.ts.snap new file mode 100644 index 0000000000..e0f7f189cc --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/backup-askar.test.ts.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UpdateAssistant | Backup | Aries Askar should create a backup 1`] = ` +[ + { + "_tags": { + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "role": "issuer", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "autoAcceptCredential": "contentApproved", + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "createdAt": "2022-03-21T22:50:20.522Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [], + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "protocolVersion": "v1", + "role": "issuer", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-03-22T22:50:20.522Z", + }, + { + "_tags": { + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "credentialIds": [ + "a77114e1-c812-4bff-a53c-3d5003fcc278", + ], + "role": "holder", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "autoAcceptCredential": "contentApproved", + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "createdAt": "2022-03-21T22:50:20.535Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [ + { + "credentialRecordId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "credentialRecordType": "indy", + }, + ], + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": { + "master_secret_blinding_data": { + "v_prime": "36456944381549782028917743247126995038265466209293312755125557271456380841610111892515020379470931691048072348420844231863825225515560265358581756565441268878364665494094789024845049226122885121039335781567964878826549149370097276812152226343824116049855825405977949749345353074025294938300401262824951638782220004732873597724698990420932910079362747837952520524827009393981876443737452031919055976088763615615890946142630576421462920865811255312740184209214306243871230276622595183415487741608569800898909023830922654063814555128779494528740438076748829436757078504882332589744263200806138145494157659396691564807976032319024007464003538934", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "373984270150786864433163", + }, + }, + "protocolVersion": "v1", + "role": "holder", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-03-22T22:50:20.522Z", + }, + { + "_tags": { + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "role": "issuer", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "autoAcceptCredential": "contentApproved", + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "createdAt": "2022-03-21T22:50:20.740Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [], + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "protocolVersion": "v1", + "role": "issuer", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-03-22T22:50:20.522Z", + }, + { + "_tags": { + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "credentialIds": [ + "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + ], + "role": "holder", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "autoAcceptCredential": "contentApproved", + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "createdAt": "2022-03-21T22:50:20.746Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [ + { + "credentialRecordId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "credentialRecordType": "indy", + }, + ], + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": { + "master_secret_blinding_data": { + "v_prime": "24405223168730122709164916892481085040205443709643249329100687534344659826655374235392514476392517756663433844139774514430993889493707631169979521764390851593418941181409704266182779162417466204970949168472702858363964258641437554267668466400711344128132909691514606077477555576087059339291048485225394874964325220472232903203038212033940680060605090839733163438385288769519855418153181511119637865605476043416048121313638627002888436809192752657860306784733123742838413845299796745569824223645588826964796075250758249133953560017373025169692866449286962430731916293683231375510684692358406054381559324718715654332979447698704161714028193478", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "698370616023883730498375", + }, + }, + "protocolVersion": "v1", + "role": "holder", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-03-22T22:50:20.522Z", + }, +] +`; diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/backup.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/backup.test.ts.snap new file mode 100644 index 0000000000..3b2cf87249 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/backup.test.ts.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UpdateAssistant | Backup should create a backup 1`] = ` +[ + { + "_tags": { + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "role": "issuer", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "autoAcceptCredential": "contentApproved", + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "createdAt": "2022-03-21T22:50:20.522Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [], + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "protocolVersion": "v1", + "role": "issuer", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-03-21T22:50:20.522Z", + }, + { + "_tags": { + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "credentialIds": [ + "a77114e1-c812-4bff-a53c-3d5003fcc278", + ], + "role": "holder", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "autoAcceptCredential": "contentApproved", + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "createdAt": "2022-03-21T22:50:20.535Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [ + { + "credentialRecordId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "credentialRecordType": "indy", + }, + ], + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": { + "master_secret_blinding_data": { + "v_prime": "36456944381549782028917743247126995038265466209293312755125557271456380841610111892515020379470931691048072348420844231863825225515560265358581756565441268878364665494094789024845049226122885121039335781567964878826549149370097276812152226343824116049855825405977949749345353074025294938300401262824951638782220004732873597724698990420932910079362747837952520524827009393981876443737452031919055976088763615615890946142630576421462920865811255312740184209214306243871230276622595183415487741608569800898909023830922654063814555128779494528740438076748829436757078504882332589744263200806138145494157659396691564807976032319024007464003538934", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "373984270150786864433163", + }, + }, + "protocolVersion": "v1", + "role": "holder", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-03-21T22:50:20.522Z", + }, + { + "_tags": { + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "role": "issuer", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "autoAcceptCredential": "contentApproved", + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "createdAt": "2022-03-21T22:50:20.740Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [], + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "protocolVersion": "v1", + "role": "issuer", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-03-21T22:50:20.522Z", + }, + { + "_tags": { + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "credentialIds": [ + "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + ], + "role": "holder", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "autoAcceptCredential": "contentApproved", + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "createdAt": "2022-03-21T22:50:20.746Z", + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentials": [ + { + "credentialRecordId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "credentialRecordType": "indy", + }, + ], + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": { + "master_secret_blinding_data": { + "v_prime": "24405223168730122709164916892481085040205443709643249329100687534344659826655374235392514476392517756663433844139774514430993889493707631169979521764390851593418941181409704266182779162417466204970949168472702858363964258641437554267668466400711344128132909691514606077477555576087059339291048485225394874964325220472232903203038212033940680060605090839733163438385288769519855418153181511119637865605476043416048121313638627002888436809192752657860306784733123742838413845299796745569824223645588826964796075250758249133953560017373025169692866449286962430731916293683231375510684692358406054381559324718715654332979447698704161714028193478", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "698370616023883730498375", + }, + }, + "protocolVersion": "v1", + "role": "holder", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-03-21T22:50:20.522Z", + }, +] +`; diff --git a/packages/core/src/storage/migration/__tests__/backup-askar.test.ts b/packages/core/src/storage/migration/__tests__/backup-askar.test.ts new file mode 100644 index 0000000000..a8fdc3a618 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/backup-askar.test.ts @@ -0,0 +1,168 @@ +import type { FileSystem } from '../../FileSystem' +import type { StorageUpdateError } from '../error/StorageUpdateError' + +import { readFileSync, unlinkSync } from 'fs' +import path from 'path' + +import { askarModule } from '../../../../../askar/tests/helpers' +import { getAgentOptions, getAskarWalletConfig } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { InjectionSymbols } from '../../../constants' +import { CredoError } from '../../../error' +import { CredentialExchangeRecord, CredentialRepository } from '../../../modules/credentials' +import { JsonTransformer } from '../../../utils' +import { StorageUpdateService } from '../StorageUpdateService' +import { UpdateAssistant } from '../UpdateAssistant' + +const agentOptions = getAgentOptions( + 'UpdateAssistant | Backup | Aries Askar', + { + walletConfig: getAskarWalletConfig('UpdateAssistant | Backup | Aries Askar', { inMemory: false }), + }, + { + askar: askarModule, + } +) + +const aliceCredentialRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/alice-4-credentials-0.1.json'), + 'utf8' +) + +const backupDate = new Date('2022-03-22T22:50:20.522Z') +jest.useFakeTimers().setSystemTime(backupDate) +const backupIdentifier = backupDate.getTime() + +describe('UpdateAssistant | Backup | Aries Askar', () => { + let updateAssistant: UpdateAssistant + let agent: Agent + let backupPath: string + + beforeEach(async () => { + agent = new Agent(agentOptions) + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) + backupPath = `${fileSystem.dataPath}/migration/backup/${backupIdentifier}` + + // If tests fail it's possible the cleanup has been skipped. So remove before running tests + const doesFileSystemExist = await fileSystem.exists(backupPath) + if (doesFileSystemExist) { + unlinkSync(backupPath) + } + const doesbackupFileSystemExist = await fileSystem.exists(`${backupPath}-error`) + if (doesbackupFileSystemExist) { + unlinkSync(`${backupPath}-error`) + } + + updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'allMediator', + }, + }) + + await updateAssistant.initialize() + }) + + afterEach(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + it('should create a backup', async () => { + const aliceCredentialRecordsJson = JSON.parse(aliceCredentialRecordsString) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const aliceCredentialRecords = Object.values(aliceCredentialRecordsJson).map((data: any) => { + const record = JsonTransformer.fromJSON(data.value, CredentialExchangeRecord) + + record.setTags(data.tags) + return record + }) + + const credentialRepository = agent.dependencyManager.resolve(CredentialRepository) + const storageUpdateService = agent.dependencyManager.resolve(StorageUpdateService) + + // Add 0.1 data and set version to 0.1 + for (const credentialRecord of aliceCredentialRecords) { + await credentialRepository.save(agent.context, credentialRecord) + } + await storageUpdateService.setCurrentStorageVersion(agent.context, '0.1') + + // Expect an update is needed + expect(await updateAssistant.isUpToDate()).toBe(false) + + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) + // Backup should not exist before update + expect(await fileSystem.exists(backupPath)).toBe(false) + + const walletSpy = jest.spyOn(agent.wallet, 'export') + + // Create update + await updateAssistant.update() + + // A wallet export should have been initiated + expect(walletSpy).toHaveBeenCalledWith({ key: agent.wallet.walletConfig?.key, path: backupPath }) + + // Backup should be cleaned after update + expect(await fileSystem.exists(backupPath)).toBe(false) + + expect( + (await credentialRepository.getAll(agent.context)).sort((a, b) => a.id.localeCompare(b.id)) + ).toMatchSnapshot() + }) + + it('should restore the backup if an error occurs during the update', async () => { + const aliceCredentialRecordsJson = JSON.parse(aliceCredentialRecordsString) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const aliceCredentialRecords = Object.values(aliceCredentialRecordsJson).map((data: any) => { + const record = JsonTransformer.fromJSON(data.value, CredentialExchangeRecord) + + record.setTags(data.tags) + return record + }) + + const credentialRepository = agent.dependencyManager.resolve(CredentialRepository) + const storageUpdateService = agent.dependencyManager.resolve(StorageUpdateService) + + // Add 0.1 data and set version to 0.1 + for (const credentialRecord of aliceCredentialRecords) { + await credentialRepository.save(agent.context, credentialRecord) + } + await storageUpdateService.setCurrentStorageVersion(agent.context, '0.1') + + // Expect an update is needed + expect(await updateAssistant.isUpToDate()).toBe(false) + jest.spyOn(updateAssistant, 'getNeededUpdates').mockResolvedValue([ + { + fromVersion: '0.1', + toVersion: '0.2', + doUpdate: async () => { + throw new CredoError("Uh oh I'm broken") + }, + }, + ]) + + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) + // Backup should not exist before update + expect(await fileSystem.exists(backupPath)).toBe(false) + + let updateError: StorageUpdateError | undefined = undefined + + try { + await updateAssistant.update() + } catch (error) { + updateError = error + } + + expect(updateError?.cause?.message).toEqual("Uh oh I'm broken") + + // Only backup error should exist after update + expect(await fileSystem.exists(backupPath)).toBe(false) + expect(await fileSystem.exists(`${backupPath}-error`)).toBe(true) + + // Wallet should be same as when we started because of backup + expect((await credentialRepository.getAll(agent.context)).sort((a, b) => a.id.localeCompare(b.id))).toEqual( + aliceCredentialRecords.sort((a, b) => a.id.localeCompare(b.id)) + ) + }) +}) diff --git a/packages/core/src/storage/migration/__tests__/backup.test.ts b/packages/core/src/storage/migration/__tests__/backup.test.ts new file mode 100644 index 0000000000..5e3b09e78d --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/backup.test.ts @@ -0,0 +1,170 @@ +import type { FileSystem } from '../../FileSystem' +import type { StorageUpdateError } from '../error/StorageUpdateError' + +import { readFileSync, unlinkSync } from 'fs' +import path from 'path' + +import { askarModule } from '../../../../../askar/tests/helpers' +import { getAgentOptions, getAskarWalletConfig } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { InjectionSymbols } from '../../../constants' +import { CredoError } from '../../../error' +import { CredentialExchangeRecord, CredentialRepository } from '../../../modules/credentials' +import { JsonTransformer } from '../../../utils' +import { StorageUpdateService } from '../StorageUpdateService' +import { UpdateAssistant } from '../UpdateAssistant' + +const agentOptions = getAgentOptions( + 'UpdateAssistant | Backup', + { + walletConfig: getAskarWalletConfig('UpdateAssistant | Backup', { + inMemory: false, + }), + }, + { askar: askarModule } +) +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion +agentOptions.config.walletConfig!.storage!.inMemory = false + +const aliceCredentialRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/alice-4-credentials-0.1.json'), + 'utf8' +) + +const backupDate = new Date('2022-03-21T22:50:20.522Z') +jest.useFakeTimers().setSystemTime(backupDate) +const backupIdentifier = backupDate.getTime() + +describe('UpdateAssistant | Backup', () => { + let updateAssistant: UpdateAssistant + let agent: Agent + let backupPath: string + + beforeEach(async () => { + agent = new Agent(agentOptions) + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) + backupPath = `${fileSystem.dataPath}/migration/backup/${backupIdentifier}` + + // If tests fail it's possible the cleanup has been skipped. So remove before running tests + const doesFileSystemExist = await fileSystem.exists(backupPath) + if (doesFileSystemExist) { + unlinkSync(backupPath) + } + const doesbackupFileSystemExist = await fileSystem.exists(`${backupPath}-error`) + if (doesbackupFileSystemExist) { + unlinkSync(`${backupPath}-error`) + } + + updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'allMediator', + }, + }) + + await updateAssistant.initialize() + }) + + afterEach(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + it('should create a backup', async () => { + const aliceCredentialRecordsJson = JSON.parse(aliceCredentialRecordsString) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const aliceCredentialRecords = Object.values(aliceCredentialRecordsJson).map((data: any) => { + const record = JsonTransformer.fromJSON(data.value, CredentialExchangeRecord) + + record.setTags(data.tags) + return record + }) + + const credentialRepository = agent.dependencyManager.resolve(CredentialRepository) + const storageUpdateService = agent.dependencyManager.resolve(StorageUpdateService) + + // Add 0.1 data and set version to 0.1 + for (const credentialRecord of aliceCredentialRecords) { + await credentialRepository.save(agent.context, credentialRecord) + } + await storageUpdateService.setCurrentStorageVersion(agent.context, '0.1') + + // Expect an update is needed + expect(await updateAssistant.isUpToDate()).toBe(false) + + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) + // Backup should not exist before update + expect(await fileSystem.exists(backupPath)).toBe(false) + + const walletSpy = jest.spyOn(agent.wallet, 'export') + + // Create update + await updateAssistant.update() + + // A wallet export should have been initiated + expect(walletSpy).toHaveBeenCalledWith({ key: agent.wallet.walletConfig?.key, path: backupPath }) + + // Backup should be cleaned after update + expect(await fileSystem.exists(backupPath)).toBe(false) + + expect( + (await credentialRepository.getAll(agent.context)).sort((a, b) => a.id.localeCompare(b.id)) + ).toMatchSnapshot() + }) + + it('should restore the backup if an error occurs during the update', async () => { + const aliceCredentialRecordsJson = JSON.parse(aliceCredentialRecordsString) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const aliceCredentialRecords = Object.values(aliceCredentialRecordsJson).map((data: any) => { + const record = JsonTransformer.fromJSON(data.value, CredentialExchangeRecord) + + record.setTags(data.tags) + return record + }) + + const credentialRepository = agent.dependencyManager.resolve(CredentialRepository) + const storageUpdateService = agent.dependencyManager.resolve(StorageUpdateService) + + // Add 0.1 data and set version to 0.1 + for (const credentialRecord of aliceCredentialRecords) { + await credentialRepository.save(agent.context, credentialRecord) + } + await storageUpdateService.setCurrentStorageVersion(agent.context, '0.1') + + // Expect an update is needed + expect(await updateAssistant.isUpToDate()).toBe(false) + jest.spyOn(updateAssistant, 'getNeededUpdates').mockResolvedValue([ + { + fromVersion: '0.1', + toVersion: '0.2', + doUpdate: async () => { + throw new CredoError("Uh oh I'm broken") + }, + }, + ]) + + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) + // Backup should not exist before update + expect(await fileSystem.exists(backupPath)).toBe(false) + + let updateError: StorageUpdateError | undefined = undefined + + try { + await updateAssistant.update() + } catch (error) { + updateError = error + } + + expect(updateError?.cause?.message).toEqual("Uh oh I'm broken") + + // Only backup error should exist after update + expect(await fileSystem.exists(backupPath)).toBe(false) + expect(await fileSystem.exists(`${backupPath}-error`)).toBe(true) + + // Wallet should be same as when we started because of backup + expect((await credentialRepository.getAll(agent.context)).sort((a, b) => a.id.localeCompare(b.id))).toEqual( + aliceCredentialRecords.sort((a, b) => a.id.localeCompare(b.id)) + ) + }) +}) diff --git a/packages/core/src/storage/migration/__tests__/updates.ts b/packages/core/src/storage/migration/__tests__/updates.ts new file mode 100644 index 0000000000..520f4518ab --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/updates.ts @@ -0,0 +1,16 @@ +import { supportedUpdates } from '../updates' +import { updateV0_1ToV0_2 } from '../updates/0.1-0.2' + +describe('supportedUpdates', () => { + // This test is intentional to be bumped explicitly when a new upgrade is added + it('supports 1 update(s)', () => { + expect(supportedUpdates.length).toBe(1) + }) + + it('supports an update from 0.1 to 0.2', () => { + const upgrade = supportedUpdates[0] + expect(upgrade.fromVersion).toBe('0.1') + expect(upgrade.toVersion).toBe('0.2') + expect(upgrade.doUpdate).toBe(updateV0_1ToV0_2) + }) +}) diff --git a/packages/core/src/storage/migration/error/StorageUpdateError.ts b/packages/core/src/storage/migration/error/StorageUpdateError.ts new file mode 100644 index 0000000000..c597a2a36a --- /dev/null +++ b/packages/core/src/storage/migration/error/StorageUpdateError.ts @@ -0,0 +1,7 @@ +import { CredoError } from '../../../error/CredoError' + +export class StorageUpdateError extends CredoError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/core/src/storage/migration/index.ts b/packages/core/src/storage/migration/index.ts new file mode 100644 index 0000000000..4358c05472 --- /dev/null +++ b/packages/core/src/storage/migration/index.ts @@ -0,0 +1,6 @@ +export * from './repository/StorageVersionRecord' +export * from './repository/StorageVersionRepository' +export * from './StorageUpdateService' +export * from './UpdateAssistant' +export { Update } from './updates' +export * from './isUpToDate' diff --git a/packages/core/src/storage/migration/isUpToDate.ts b/packages/core/src/storage/migration/isUpToDate.ts new file mode 100644 index 0000000000..393d9c3eb4 --- /dev/null +++ b/packages/core/src/storage/migration/isUpToDate.ts @@ -0,0 +1,17 @@ +import type { UpdateToVersion } from './updates' +import type { VersionString } from '../../utils/version' + +import { isFirstVersionEqualToSecond, isFirstVersionHigherThanSecond, parseVersionString } from '../../utils/version' + +import { CURRENT_FRAMEWORK_STORAGE_VERSION } from './updates' + +export function isStorageUpToDate(storageVersion: VersionString, updateToVersion?: UpdateToVersion) { + const currentStorageVersion = parseVersionString(storageVersion) + const compareToVersion = parseVersionString(updateToVersion ?? CURRENT_FRAMEWORK_STORAGE_VERSION) + + const isUpToDate = + isFirstVersionEqualToSecond(currentStorageVersion, compareToVersion) || + isFirstVersionHigherThanSecond(currentStorageVersion, compareToVersion) + + return isUpToDate +} diff --git a/packages/core/src/storage/migration/repository/StorageVersionRecord.ts b/packages/core/src/storage/migration/repository/StorageVersionRecord.ts new file mode 100644 index 0000000000..3d39b652af --- /dev/null +++ b/packages/core/src/storage/migration/repository/StorageVersionRecord.ts @@ -0,0 +1,31 @@ +import type { VersionString } from '../../../utils/version' + +import { uuid } from '../../../utils/uuid' +import { BaseRecord } from '../../BaseRecord' + +export interface StorageVersionRecordProps { + id?: string + createdAt?: Date + storageVersion: VersionString +} + +export class StorageVersionRecord extends BaseRecord { + public storageVersion!: VersionString + + public static readonly type = 'StorageVersionRecord' + public readonly type = StorageVersionRecord.type + + public constructor(props: StorageVersionRecordProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.storageVersion = props.storageVersion + } + } + + public getTags() { + return this._tags + } +} diff --git a/packages/core/src/storage/migration/repository/StorageVersionRepository.ts b/packages/core/src/storage/migration/repository/StorageVersionRepository.ts new file mode 100644 index 0000000000..b32edec8cb --- /dev/null +++ b/packages/core/src/storage/migration/repository/StorageVersionRepository.ts @@ -0,0 +1,17 @@ +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../Repository' +import { StorageService } from '../../StorageService' + +import { StorageVersionRecord } from './StorageVersionRecord' + +@injectable() +export class StorageVersionRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(StorageVersionRecord, storageService, eventEmitter) + } +} diff --git a/packages/core/src/storage/migration/updates.ts b/packages/core/src/storage/migration/updates.ts new file mode 100644 index 0000000000..ad06ad3178 --- /dev/null +++ b/packages/core/src/storage/migration/updates.ts @@ -0,0 +1,64 @@ +import type { V0_1ToV0_2UpdateConfig } from './updates/0.1-0.2' +import type { BaseAgent } from '../../agent/BaseAgent' +import type { VersionString } from '../../utils/version' + +import { updateV0_1ToV0_2 } from './updates/0.1-0.2' +import { updateV0_2ToV0_3 } from './updates/0.2-0.3' +import { updateV0_3ToV0_3_1 } from './updates/0.3-0.3.1' +import { updateV0_3_1ToV0_4 } from './updates/0.3.1-0.4' +import { updateV0_4ToV0_5 } from './updates/0.4-0.5' + +export const INITIAL_STORAGE_VERSION = '0.1' + +export interface Update { + fromVersion: VersionString + toVersion: VersionString + doUpdate: (agent: Agent, updateConfig: UpdateConfig) => Promise +} + +export interface UpdateConfig { + v0_1ToV0_2: V0_1ToV0_2UpdateConfig +} + +export const DEFAULT_UPDATE_CONFIG: UpdateConfig = { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'recipientIfEndpoint', + }, +} + +export const supportedUpdates = [ + { + fromVersion: '0.1', + toVersion: '0.2', + doUpdate: updateV0_1ToV0_2, + }, + { + fromVersion: '0.2', + toVersion: '0.3', + doUpdate: updateV0_2ToV0_3, + }, + { + fromVersion: '0.3', + toVersion: '0.3.1', + doUpdate: updateV0_3ToV0_3_1, + }, + { + fromVersion: '0.3.1', + toVersion: '0.4', + doUpdate: updateV0_3_1ToV0_4, + }, + { + fromVersion: '0.4', + toVersion: '0.5', + doUpdate: updateV0_4ToV0_5, + }, +] as const + +// Current version is last toVersion from the supported updates +export const CURRENT_FRAMEWORK_STORAGE_VERSION = supportedUpdates[supportedUpdates.length - 1].toVersion as LastItem< + typeof supportedUpdates +>['toVersion'] + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type LastItem = T extends readonly [...infer _, infer U] ? U : T[0] | undefined +export type UpdateToVersion = (typeof supportedUpdates)[number]['toVersion'] diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/__fixtures__/didPeer4kgVt6CidfKgo1MoWMqsQX.json b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/__fixtures__/didPeer4kgVt6CidfKgo1MoWMqsQX.json new file mode 100644 index 0000000000..2d5c1298a6 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/__fixtures__/didPeer4kgVt6CidfKgo1MoWMqsQX.json @@ -0,0 +1,31 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "verificationMethod": [ + { + "id": "#5sD8ttxn", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "5sD8ttxn9Bd9a1HmueLirJ4HNhs4Q8qzAqDd1UCR9iqD" + } + ], + "service": [ + { + "id": "#7", + "serviceEndpoint": "https://agent.com/did-comm", + "type": "did-communication", + "priority": 10, + "recipientKeys": ["#5sD8ttxn"], + "routingKeys": [] + }, + { + "id": "#6", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "type": "IndyAgent", + "priority": 5, + "recipientKeys": ["5sD8ttxn9Bd9a1HmueLirJ4HNhs4Q8qzAqDd1UCR9iqD"], + "routingKeys": [] + } + ], + "authentication": ["#5sD8ttxn"], + "id": "did:peer:1zQmcP5YaLwnwfYto5eFGmrV4nFMPVz7LPHHi63zSk7U9CL7" +} diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/__fixtures__/didPeerR1xKJw17sUoXhejEpugMYJ.json b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/__fixtures__/didPeerR1xKJw17sUoXhejEpugMYJ.json new file mode 100644 index 0000000000..ff38887fe0 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/__fixtures__/didPeerR1xKJw17sUoXhejEpugMYJ.json @@ -0,0 +1,30 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "verificationMethod": [ + { + "id": "#E6D1m3eE", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" + } + ], + "service": [ + { + "id": "#7", + "serviceEndpoint": "https://agent.com/did-comm", + "type": "did-communication", + "priority": 10, + "recipientKeys": ["#E6D1m3eE"], + "routingKeys": [] + }, + { + "id": "#6", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "type": "IndyAgent", + "priority": 5, + "recipientKeys": ["E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu"], + "routingKeys": [] + } + ], + "id": "did:peer:1zQmU1AYVxXvPeQujvUxnSVGUMsqvFNQHN2TMMMQz6TbXor8" +} diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/__fixtures__/legacyDidPeer4kgVt6CidfKgo1MoWMqsQX.json b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/__fixtures__/legacyDidPeer4kgVt6CidfKgo1MoWMqsQX.json new file mode 100644 index 0000000000..1fc537664e --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/__fixtures__/legacyDidPeer4kgVt6CidfKgo1MoWMqsQX.json @@ -0,0 +1,35 @@ +{ + "@context": "https://w3id.org/did/v1", + "id": "4kgVt6CidfKgo1MoWMqsQX", + "service": [ + { + "id": "0", + "type": "Mediator", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" + }, + { + "id": "6", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["5sD8ttxn9Bd9a1HmueLirJ4HNhs4Q8qzAqDd1UCR9iqD"], + "routingKeys": [], + "priority": 5 + }, + { + "id": "7", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["4kgVt6CidfKgo1MoWMqsQX#4"], + "routingKeys": [], + "priority": 10 + } + ], + "authentication": [ + { + "id": "4kgVt6CidfKgo1MoWMqsQX#4", + "type": "Ed25519VerificationKey2018", + "controller": "4kgVt6CidfKgo1MoWMqsQX", + "publicKeyBase58": "5sD8ttxn9Bd9a1HmueLirJ4HNhs4Q8qzAqDd1UCR9iqD" + } + ] +} diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/__fixtures__/legacyDidPeerR1xKJw17sUoXhejEpugMYJ.json b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/__fixtures__/legacyDidPeerR1xKJw17sUoXhejEpugMYJ.json new file mode 100644 index 0000000000..a9c34bdf48 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/__fixtures__/legacyDidPeerR1xKJw17sUoXhejEpugMYJ.json @@ -0,0 +1,49 @@ +{ + "@context": "https://w3id.org/did/v1", + "id": "R1xKJw17sUoXhejEpugMYJ", + "publicKey": [ + { + "id": "R1xKJw17sUoXhejEpugMYJ#4", + "type": "Ed25519VerificationKey2018", + "controller": "R1xKJw17sUoXhejEpugMYJ", + "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" + } + ], + "service": [ + { + "id": "0", + "type": "Mediator", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" + }, + { + "id": "6", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu"], + "routingKeys": [], + "priority": 5 + }, + { + "id": "7", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["R1xKJw17sUoXhejEpugMYJ#4"], + "routingKeys": [], + "priority": 10 + } + ], + "keyAgreement": [ + { + "id": "R1xKJw17sUoXhejEpugMYJ#key-agreement-1", + "type": "X25519KeyAgreementKey2019", + "controller": "R1xKJw17sUoXhejEpugMYJ", + "publicKeyBase58": "Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt" + } + ], + "authentication": [ + { + "type": "Ed25519SignatureAuthentication2018", + "publicKey": "R1xKJw17sUoXhejEpugMYJ#4" + } + ] +} diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts new file mode 100644 index 0000000000..7002d39535 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts @@ -0,0 +1,692 @@ +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { Agent } from '../../../../../agent/Agent' +import { + ConnectionRecord, + ConnectionRole, + ConnectionState, + DidExchangeRole, + DidExchangeState, +} from '../../../../../modules/connections' +import { ConnectionRepository } from '../../../../../modules/connections/repository/ConnectionRepository' +import { DidDocumentRole } from '../../../../../modules/dids/domain/DidDocumentRole' +import { DidRecord } from '../../../../../modules/dids/repository' +import { DidRepository } from '../../../../../modules/dids/repository/DidRepository' +import { OutOfBandRole } from '../../../../../modules/oob/domain/OutOfBandRole' +import { OutOfBandState } from '../../../../../modules/oob/domain/OutOfBandState' +import { OutOfBandRecord } from '../../../../../modules/oob/repository' +import { OutOfBandRepository } from '../../../../../modules/oob/repository/OutOfBandRepository' +import { JsonTransformer } from '../../../../../utils' +import * as testModule from '../connection' + +import didPeer4kgVt6CidfKgo1MoWMqsQX from './__fixtures__/didPeer4kgVt6CidfKgo1MoWMqsQX.json' +import didPeerR1xKJw17sUoXhejEpugMYJ from './__fixtures__/didPeerR1xKJw17sUoXhejEpugMYJ.json' +import legacyDidPeer4kgVt6CidfKgo1MoWMqsQX from './__fixtures__/legacyDidPeer4kgVt6CidfKgo1MoWMqsQX.json' +import legacyDidPeerR1xKJw17sUoXhejEpugMYJ from './__fixtures__/legacyDidPeerR1xKJw17sUoXhejEpugMYJ.json' + +const agentConfig = getAgentConfig('Migration ConnectionRecord 0.1-0.2') +const agentContext = getAgentContext() + +jest.mock('../../../../../modules/connections/repository/ConnectionRepository') +const ConnectionRepositoryMock = ConnectionRepository as jest.Mock +const connectionRepository = new ConnectionRepositoryMock() + +jest.mock('../../../../../modules/dids/repository/DidRepository') +const DidRepositoryMock = DidRepository as jest.Mock +const didRepository = new DidRepositoryMock() + +jest.mock('../../../../../modules/oob/repository/OutOfBandRepository') +const OutOfBandRepositoryMock = OutOfBandRepository as jest.Mock +const outOfBandRepository = new OutOfBandRepositoryMock() + +jest.mock('../../../../../agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn((cls) => { + if (cls === ConnectionRepository) { + return connectionRepository + } else if (cls === DidRepository) { + return didRepository + } else if (cls === OutOfBandRepository) { + return outOfBandRepository + } + + throw new Error(`No instance found for ${cls}`) + }), + }, + })), + } +}) + +const connectionJson = { + role: 'inviter', + state: 'invited', + did: legacyDidPeerR1xKJw17sUoXhejEpugMYJ.id, + didDoc: legacyDidPeerR1xKJw17sUoXhejEpugMYJ, + theirDid: legacyDidPeer4kgVt6CidfKgo1MoWMqsQX.id, + theirDidDoc: legacyDidPeer4kgVt6CidfKgo1MoWMqsQX, + invitation: { + '@type': 'https://didcomm.org/connections/1.0/invitation', + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeys: ['E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu'], + serviceEndpoint: 'https://example.com', + label: 'test', + }, + createdAt: '2020-04-08T15:51:43.819Z', +} + +const connectionJsonNewDidStateRole = { + role: 'responder', + state: 'invitation-sent', + did: didPeerR1xKJw17sUoXhejEpugMYJ.id, + theirDid: didPeer4kgVt6CidfKgo1MoWMqsQX.id, + invitation: { + '@type': 'https://didcomm.org/connections/1.0/invitation', + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeys: ['E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu'], + serviceEndpoint: 'https://example.com', + label: 'test', + }, + createdAt: '2020-04-08T15:51:43.819Z', + autoAcceptConnection: true, + multiUseInvitation: false, + mediatorId: 'a-mediator-id', +} + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.1-0.2 | Connection', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('migrateCredentialRecordToV0_2()', () => { + it('should fetch all records and apply the needed updates', async () => { + const input = JsonTransformer.fromJSON(connectionJson, ConnectionRecord) + const records = [input] + + mockFunction(connectionRepository.getAll).mockResolvedValue(records) + + // Not records exist yet + mockFunction(outOfBandRepository.findByQuery).mockResolvedValue([]) + mockFunction(didRepository.findById).mockResolvedValue(null) + + await testModule.migrateConnectionRecordToV0_2(agent) + + expect(connectionRepository.getAll).toHaveBeenCalledTimes(1) + expect(connectionRepository.update).toHaveBeenCalledTimes(records.length) + const [[, updatedConnectionRecord]] = mockFunction(connectionRepository.update).mock.calls + + // Check first object is transformed correctly. + // - removed invitation, theirDidDoc, didDoc + // - Added invitationDid + // - Updated did, theirDid + expect(updatedConnectionRecord.toJSON()).toEqual({ + _tags: {}, + metadata: {}, + createdAt: '2020-04-08T15:51:43.819Z', + role: 'responder', + state: 'invitation-sent', + did: didPeerR1xKJw17sUoXhejEpugMYJ.id, + invitationDid: + 'did:peer:2.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3NZVTRNSHRmbU5oTm0xdUdNdkFOcjlqNENCdjJGeW1qaUp0UmdBMzZiU1ZII3o2TWtzWVU0TUh0Zm1OaE5tMXVHTXZBTnI5ajRDQnYyRnltamlKdFJnQTM2YlNWSCJdfQ', + theirDid: didPeer4kgVt6CidfKgo1MoWMqsQX.id, + outOfBandId: expect.any(String), + connectionTypes: [], + previousDids: [], + previousTheirDids: [], + }) + }) + }) + + describe('updateConnectionRoleAndState', () => { + it('should update the connection role and state to did exchange values', async () => { + const connectionRecord = JsonTransformer.fromJSON( + { ...connectionJson, state: 'requested', role: 'invitee' }, + ConnectionRecord + ) + + await testModule.updateConnectionRoleAndState(agent, connectionRecord) + + expect(connectionRecord.toJSON()).toEqual({ + _tags: {}, + metadata: {}, + createdAt: '2020-04-08T15:51:43.819Z', + role: 'requester', + state: 'request-sent', + did: legacyDidPeerR1xKJw17sUoXhejEpugMYJ.id, + didDoc: legacyDidPeerR1xKJw17sUoXhejEpugMYJ, + theirDid: legacyDidPeer4kgVt6CidfKgo1MoWMqsQX.id, + theirDidDoc: legacyDidPeer4kgVt6CidfKgo1MoWMqsQX, + invitation: { + '@type': 'https://didcomm.org/connections/1.0/invitation', + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeys: ['E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu'], + serviceEndpoint: 'https://example.com', + label: 'test', + }, + connectionTypes: [], + previousDids: [], + previousTheirDids: [], + }) + }) + }) + + describe('extractDidDocument', () => { + it('should extract the did document from the connection record and update the did to a did:peer did', async () => { + const connectionRecord = JsonTransformer.fromJSON(connectionJson, ConnectionRecord) + + // No did record exists yet + mockFunction(didRepository.findById).mockResolvedValue(null) + + await testModule.extractDidDocument(agent, connectionRecord) + + expect(connectionRecord.toJSON()).toEqual({ + _tags: {}, + metadata: {}, + createdAt: '2020-04-08T15:51:43.819Z', + role: 'inviter', + state: 'invited', + did: didPeerR1xKJw17sUoXhejEpugMYJ.id, + theirDid: didPeer4kgVt6CidfKgo1MoWMqsQX.id, + invitation: { + '@type': 'https://didcomm.org/connections/1.0/invitation', + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeys: ['E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu'], + serviceEndpoint: 'https://example.com', + label: 'test', + }, + connectionTypes: [], + previousDids: [], + previousTheirDids: [], + }) + }) + + it('should create a DidRecord for didDoc and theirDidDoc', async () => { + const connectionRecord = JsonTransformer.fromJSON(connectionJson, ConnectionRecord) + + // No did record exists yet + mockFunction(didRepository.findById).mockResolvedValue(null) + + await testModule.extractDidDocument(agent, connectionRecord) + + expect(didRepository.save).toHaveBeenCalledTimes(2) + + const [[, didRecord], [, theirDidRecord]] = mockFunction(didRepository.save).mock.calls + + expect(didRecord.toJSON()).toMatchObject({ + id: didPeerR1xKJw17sUoXhejEpugMYJ.id, + role: DidDocumentRole.Created, + didDocument: didPeerR1xKJw17sUoXhejEpugMYJ, + createdAt: connectionRecord.createdAt.toISOString(), + metadata: { + '_internal/legacyDid': { + unqualifiedDid: legacyDidPeerR1xKJw17sUoXhejEpugMYJ.id, + didDocumentString: JSON.stringify(legacyDidPeerR1xKJw17sUoXhejEpugMYJ), + }, + }, + }) + expect(didRecord.getTags()).toMatchObject({ + recipientKeyFingerprints: [ + 'z6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH', + 'z6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH', + ], + }) + + expect(theirDidRecord.toJSON()).toMatchObject({ + id: didPeer4kgVt6CidfKgo1MoWMqsQX.id, + role: DidDocumentRole.Received, + didDocument: didPeer4kgVt6CidfKgo1MoWMqsQX, + createdAt: connectionRecord.createdAt.toISOString(), + metadata: { + '_internal/legacyDid': { + unqualifiedDid: legacyDidPeer4kgVt6CidfKgo1MoWMqsQX.id, + didDocumentString: JSON.stringify(legacyDidPeer4kgVt6CidfKgo1MoWMqsQX), + }, + }, + }) + expect(theirDidRecord.getTags()).toMatchObject({ + recipientKeyFingerprints: [ + 'z6MkjKUBV9DDUj7cgW8UbDJZhPcHCH8up26Lrr8YqkAS4wcb', + 'z6MkjKUBV9DDUj7cgW8UbDJZhPcHCH8up26Lrr8YqkAS4wcb', + ], + }) + }) + + it('should not extract the did document if it does not exist on the connection record', async () => { + const connectionRecord = JsonTransformer.fromJSON( + { ...connectionJson, didDoc: undefined, theirDidDoc: undefined }, + ConnectionRecord + ) + + await testModule.extractDidDocument(agent, connectionRecord) + + expect(didRepository.findById).not.toHaveBeenCalled() + expect(didRepository.save).not.toHaveBeenCalled() + + // Should be the same as the input + expect(connectionRecord.toJSON()).toEqual({ + ...connectionJson, + didDoc: undefined, + theirDidDoc: undefined, + metadata: {}, + _tags: {}, + connectionTypes: [], + previousDids: [], + previousTheirDids: [], + }) + }) + + it('should not create a did record if a did record for the did already exists', async () => { + const connectionRecord = JsonTransformer.fromJSON(connectionJson, ConnectionRecord) + + const didRecord = JsonTransformer.fromJSON( + { + id: didPeerR1xKJw17sUoXhejEpugMYJ.id, + role: DidDocumentRole.Created, + didDocument: didPeerR1xKJw17sUoXhejEpugMYJ, + createdAt: connectionRecord.createdAt.toISOString(), + metadata: { + '_internal/legacyDid': { + unqualifiedDid: legacyDidPeerR1xKJw17sUoXhejEpugMYJ.id, + didDocumentString: JSON.stringify(legacyDidPeerR1xKJw17sUoXhejEpugMYJ), + }, + }, + _tags: { + recipientKeys: ['R1xKJw17sUoXhejEpugMYJ#4', 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu'], + }, + }, + DidRecord + ) + + const theirDidRecord = JsonTransformer.fromJSON( + { + id: didPeer4kgVt6CidfKgo1MoWMqsQX.id, + role: DidDocumentRole.Received, + didDocument: didPeer4kgVt6CidfKgo1MoWMqsQX, + createdAt: connectionRecord.createdAt.toISOString(), + metadata: { + '_internal/legacyDid': { + unqualifiedDid: legacyDidPeer4kgVt6CidfKgo1MoWMqsQX.id, + didDocumentString: JSON.stringify(legacyDidPeer4kgVt6CidfKgo1MoWMqsQX), + }, + }, + _tags: { + recipientKeys: ['4kgVt6CidfKgo1MoWMqsQX#4', '5sD8ttxn9Bd9a1HmueLirJ4HNhs4Q8qzAqDd1UCR9iqD'], + }, + }, + DidRecord + ) + + // Both did records already exist + mockFunction(didRepository.findById).mockImplementation((_, id) => + Promise.resolve(id === didPeerR1xKJw17sUoXhejEpugMYJ.id ? didRecord : theirDidRecord) + ) + + await testModule.extractDidDocument(agent, connectionRecord) + + expect(didRepository.save).not.toHaveBeenCalled() + expect(didRepository.findById).toHaveBeenNthCalledWith(1, agentContext, didPeerR1xKJw17sUoXhejEpugMYJ.id) + expect(didRepository.findById).toHaveBeenNthCalledWith(2, agentContext, didPeer4kgVt6CidfKgo1MoWMqsQX.id) + + expect(connectionRecord.toJSON()).toEqual({ + _tags: {}, + metadata: {}, + createdAt: '2020-04-08T15:51:43.819Z', + role: 'inviter', + state: 'invited', + did: didPeerR1xKJw17sUoXhejEpugMYJ.id, + theirDid: didPeer4kgVt6CidfKgo1MoWMqsQX.id, + invitation: { + '@type': 'https://didcomm.org/connections/1.0/invitation', + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeys: ['E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu'], + serviceEndpoint: 'https://example.com', + label: 'test', + }, + connectionTypes: [], + previousDids: [], + previousTheirDids: [], + }) + }) + }) + + describe('migrateToOobRecord', () => { + it('should extract the invitation from the connection record and generate an invitation did', async () => { + const connectionRecord = JsonTransformer.fromJSON(connectionJsonNewDidStateRole, ConnectionRecord) + + // No did record exists yet + mockFunction(outOfBandRepository.findByQuery).mockResolvedValue([]) + + await testModule.migrateToOobRecord(agent, connectionRecord) + + expect(connectionRecord.toJSON()).toEqual({ + _tags: {}, + metadata: {}, + createdAt: '2020-04-08T15:51:43.819Z', + role: 'responder', + state: 'invitation-sent', + did: didPeerR1xKJw17sUoXhejEpugMYJ.id, + theirDid: didPeer4kgVt6CidfKgo1MoWMqsQX.id, + invitationDid: + 'did:peer:2.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3NZVTRNSHRmbU5oTm0xdUdNdkFOcjlqNENCdjJGeW1qaUp0UmdBMzZiU1ZII3o2TWtzWVU0TUh0Zm1OaE5tMXVHTXZBTnI5ajRDQnYyRnltamlKdFJnQTM2YlNWSCJdfQ', + outOfBandId: expect.any(String), + autoAcceptConnection: true, + mediatorId: 'a-mediator-id', + connectionTypes: [], + previousDids: [], + previousTheirDids: [], + }) + }) + + it('should create an OutOfBandRecord from the invitation and store the outOfBandId in the connection record', async () => { + const connectionRecord = JsonTransformer.fromJSON(connectionJsonNewDidStateRole, ConnectionRecord) + + // No did record exists yet + mockFunction(outOfBandRepository.findByQuery).mockResolvedValue([]) + + await testModule.migrateToOobRecord(agent, connectionRecord) + + const [[, outOfBandRecord]] = mockFunction(outOfBandRepository.save).mock.calls + + expect(outOfBandRepository.save).toHaveBeenCalledTimes(1) + expect(connectionRecord.outOfBandId).toEqual(outOfBandRecord.id) + + expect(outOfBandRecord.toJSON()).toEqual({ + id: expect.any(String), + _tags: { recipientKeyFingerprints: ['z6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH'] }, + metadata: {}, + // Checked below + outOfBandInvitation: { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + services: [ + { + id: '#inline', + serviceEndpoint: 'https://example.com', + type: 'did-communication', + recipientKeys: ['did:key:z6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH'], + }, + ], + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + label: 'test', + accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], + handshake_protocols: ['https://didcomm.org/connections/1.0'], + }, + role: OutOfBandRole.Sender, + state: OutOfBandState.AwaitResponse, + autoAcceptConnection: true, + reusable: false, + mediatorId: 'a-mediator-id', + createdAt: connectionRecord.createdAt.toISOString(), + }) + }) + + it('should create an OutOfBandRecord if an OutOfBandRecord with the invitation id already exists, but the recipientKeys are different', async () => { + const connectionRecord = JsonTransformer.fromJSON(connectionJsonNewDidStateRole, ConnectionRecord) + + // Out of band record does not exist yet + mockFunction(outOfBandRepository.findByQuery).mockResolvedValueOnce([]) + + await testModule.migrateToOobRecord(agent, connectionRecord) + + expect(outOfBandRepository.findByQuery).toHaveBeenCalledTimes(1) + expect(outOfBandRepository.findByQuery).toHaveBeenNthCalledWith(1, agentContext, { + invitationId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeyFingerprints: ['z6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH'], + role: OutOfBandRole.Sender, + }) + + // Expect the out of band record to be created + expect(outOfBandRepository.save).toHaveBeenCalled() + }) + + it('should not create an OutOfBandRecord if an OutOfBandRecord with the invitation id and recipientKeys already exists', async () => { + const connectionRecord = JsonTransformer.fromJSON(connectionJsonNewDidStateRole, ConnectionRecord) + + const outOfBandRecord = JsonTransformer.fromJSON( + { + id: '3c52cc26-577d-4200-8753-05f1f425c342', + _tags: {}, + metadata: {}, + // Checked below + outOfBandInvitation: { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + services: [ + { + id: '#inline', + serviceEndpoint: 'https://example.com', + type: 'did-communication', + recipientKeys: ['did:key:z6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH'], + }, + ], + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + label: 'test', + accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], + handshake_protocols: ['https://didcomm.org/connections/1.0'], + }, + role: OutOfBandRole.Sender, + state: OutOfBandState.AwaitResponse, + autoAcceptConnection: true, + did: didPeerR1xKJw17sUoXhejEpugMYJ.id, + reusable: false, + mediatorId: 'a-mediator-id', + createdAt: connectionRecord.createdAt.toISOString(), + }, + OutOfBandRecord + ) + + // Out of band record does not exist yet + mockFunction(outOfBandRepository.findByQuery).mockResolvedValueOnce([outOfBandRecord]) + + await testModule.migrateToOobRecord(agent, connectionRecord) + + expect(outOfBandRepository.findByQuery).toHaveBeenCalledTimes(1) + expect(outOfBandRepository.findByQuery).toHaveBeenNthCalledWith(1, agentContext, { + invitationId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeyFingerprints: ['z6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH'], + role: OutOfBandRole.Sender, + }) + expect(outOfBandRepository.save).not.toHaveBeenCalled() + + expect(connectionRecord.toJSON()).toEqual({ + _tags: {}, + metadata: {}, + createdAt: '2020-04-08T15:51:43.819Z', + role: 'responder', + state: 'invitation-sent', + did: didPeerR1xKJw17sUoXhejEpugMYJ.id, + theirDid: didPeer4kgVt6CidfKgo1MoWMqsQX.id, + invitationDid: + 'did:peer:2.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3NZVTRNSHRmbU5oTm0xdUdNdkFOcjlqNENCdjJGeW1qaUp0UmdBMzZiU1ZII3o2TWtzWVU0TUh0Zm1OaE5tMXVHTXZBTnI5ajRDQnYyRnltamlKdFJnQTM2YlNWSCJdfQ', + autoAcceptConnection: true, + mediatorId: 'a-mediator-id', + outOfBandId: outOfBandRecord.id, + connectionTypes: [], + previousDids: [], + previousTheirDids: [], + }) + }) + + it('should update the existing out of band record to reusable and state await response if the connection record is a multiUseInvitation', async () => { + const connectionRecord = JsonTransformer.fromJSON( + { ...connectionJsonNewDidStateRole, multiUseInvitation: true }, + ConnectionRecord + ) + + const outOfBandRecord = JsonTransformer.fromJSON( + { + id: '3c52cc26-577d-4200-8753-05f1f425c342', + _tags: {}, + metadata: {}, + // Checked below + outOfBandInvitation: { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + services: [ + { + id: '#inline', + serviceEndpoint: 'https://example.com', + type: 'did-communication', + recipientKeys: ['did:key:z6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH'], + }, + ], + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + label: 'test', + accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], + handshake_protocols: ['https://didcomm.org/connections/1.0'], + }, + role: OutOfBandRole.Sender, + state: OutOfBandState.AwaitResponse, + autoAcceptConnection: true, + did: didPeerR1xKJw17sUoXhejEpugMYJ.id, + reusable: false, + mediatorId: 'a-mediator-id', + createdAt: connectionRecord.createdAt.toISOString(), + }, + OutOfBandRecord + ) + + // Out of band record already exists + mockFunction(outOfBandRepository.findByQuery).mockResolvedValueOnce([outOfBandRecord]) + + await testModule.migrateToOobRecord(agent, connectionRecord) + + expect(outOfBandRepository.findByQuery).toHaveBeenCalledTimes(1) + expect(outOfBandRepository.findByQuery).toHaveBeenNthCalledWith(1, agentContext, { + invitationId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeyFingerprints: ['z6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH'], + role: OutOfBandRole.Sender, + }) + expect(outOfBandRepository.save).not.toHaveBeenCalled() + expect(outOfBandRepository.update).toHaveBeenCalledWith(agentContext, outOfBandRecord) + expect(connectionRepository.delete).toHaveBeenCalledWith(agentContext, connectionRecord) + + expect(outOfBandRecord.toJSON()).toEqual({ + id: '3c52cc26-577d-4200-8753-05f1f425c342', + _tags: {}, + metadata: {}, + outOfBandInvitation: { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + services: [ + { + id: '#inline', + serviceEndpoint: 'https://example.com', + type: 'did-communication', + recipientKeys: ['did:key:z6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH'], + }, + ], + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + label: 'test', + accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], + handshake_protocols: ['https://didcomm.org/connections/1.0'], + }, + role: OutOfBandRole.Sender, + state: OutOfBandState.AwaitResponse, + autoAcceptConnection: true, + did: didPeerR1xKJw17sUoXhejEpugMYJ.id, + reusable: true, + mediatorId: 'a-mediator-id', + createdAt: connectionRecord.createdAt.toISOString(), + }) + }) + }) + + describe('oobStateFromDidExchangeRoleAndState', () => { + it('should return the correct state for all connection role and state combinations', () => { + expect( + testModule.oobStateFromDidExchangeRoleAndState(DidExchangeRole.Responder, DidExchangeState.InvitationSent) + ).toEqual(OutOfBandState.AwaitResponse) + expect( + testModule.oobStateFromDidExchangeRoleAndState(DidExchangeRole.Responder, DidExchangeState.RequestReceived) + ).toEqual(OutOfBandState.Done) + expect( + testModule.oobStateFromDidExchangeRoleAndState(DidExchangeRole.Responder, DidExchangeState.ResponseSent) + ).toEqual(OutOfBandState.Done) + expect( + testModule.oobStateFromDidExchangeRoleAndState(DidExchangeRole.Responder, DidExchangeState.Completed) + ).toEqual(OutOfBandState.Done) + expect( + testModule.oobStateFromDidExchangeRoleAndState(DidExchangeRole.Responder, DidExchangeState.Abandoned) + ).toEqual(OutOfBandState.Done) + expect(testModule.oobStateFromDidExchangeRoleAndState(DidExchangeRole.Requester, DidExchangeState.Start)).toEqual( + OutOfBandState.PrepareResponse + ) + + expect( + testModule.oobStateFromDidExchangeRoleAndState(DidExchangeRole.Requester, DidExchangeState.InvitationReceived) + ).toEqual(OutOfBandState.PrepareResponse) + expect( + testModule.oobStateFromDidExchangeRoleAndState(DidExchangeRole.Requester, DidExchangeState.RequestSent) + ).toEqual(OutOfBandState.Done) + expect( + testModule.oobStateFromDidExchangeRoleAndState(DidExchangeRole.Requester, DidExchangeState.ResponseReceived) + ).toEqual(OutOfBandState.Done) + expect( + testModule.oobStateFromDidExchangeRoleAndState(DidExchangeRole.Requester, DidExchangeState.Completed) + ).toEqual(OutOfBandState.Done) + expect( + testModule.oobStateFromDidExchangeRoleAndState(DidExchangeRole.Requester, DidExchangeState.Abandoned) + ).toEqual(OutOfBandState.Done) + expect(testModule.oobStateFromDidExchangeRoleAndState(DidExchangeRole.Responder, DidExchangeState.Start)).toEqual( + OutOfBandState.AwaitResponse + ) + }) + }) + + describe('didExchangeStateAndRoleFromRoleAndState', () => { + it('should return the correct state for all connection role and state combinations', () => { + expect( + testModule.didExchangeStateAndRoleFromRoleAndState(ConnectionRole.Inviter, ConnectionState.Invited) + ).toEqual([DidExchangeRole.Responder, DidExchangeState.InvitationSent]) + expect( + testModule.didExchangeStateAndRoleFromRoleAndState(ConnectionRole.Inviter, ConnectionState.Requested) + ).toEqual([DidExchangeRole.Responder, DidExchangeState.RequestReceived]) + expect( + testModule.didExchangeStateAndRoleFromRoleAndState(ConnectionRole.Inviter, ConnectionState.Responded) + ).toEqual([DidExchangeRole.Responder, DidExchangeState.ResponseSent]) + expect( + testModule.didExchangeStateAndRoleFromRoleAndState(ConnectionRole.Inviter, ConnectionState.Complete) + ).toEqual([DidExchangeRole.Responder, DidExchangeState.Completed]) + + expect( + testModule.didExchangeStateAndRoleFromRoleAndState(ConnectionRole.Invitee, ConnectionState.Invited) + ).toEqual([DidExchangeRole.Requester, DidExchangeState.InvitationReceived]) + expect( + testModule.didExchangeStateAndRoleFromRoleAndState(ConnectionRole.Invitee, ConnectionState.Requested) + ).toEqual([DidExchangeRole.Requester, DidExchangeState.RequestSent]) + expect( + testModule.didExchangeStateAndRoleFromRoleAndState(ConnectionRole.Invitee, ConnectionState.Responded) + ).toEqual([DidExchangeRole.Requester, DidExchangeState.ResponseReceived]) + expect( + testModule.didExchangeStateAndRoleFromRoleAndState(ConnectionRole.Invitee, ConnectionState.Complete) + ).toEqual([DidExchangeRole.Requester, DidExchangeState.Completed]) + }) + + it('should return did exchange role if role is already did exchange role', () => { + expect( + testModule.didExchangeStateAndRoleFromRoleAndState(DidExchangeRole.Responder, DidExchangeState.RequestSent) + ).toEqual([DidExchangeRole.Responder, expect.anything()]) + + expect( + testModule.didExchangeStateAndRoleFromRoleAndState(DidExchangeRole.Requester, ConnectionState.Requested) + ).toEqual([DidExchangeRole.Requester, expect.anything()]) + }) + + it('should return the input state if state is not a valid connection state', () => { + expect( + testModule.didExchangeStateAndRoleFromRoleAndState( + DidExchangeRole.Responder, + 'something-weird' as ConnectionState + ) + ).toEqual([DidExchangeRole.Responder, 'something-weird']) + }) + }) +}) diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/credential.test.ts b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/credential.test.ts new file mode 100644 index 0000000000..d7eefe2be7 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/credential.test.ts @@ -0,0 +1,527 @@ +import type { CredentialRecordBinding } from '../../../../../../src/modules/credentials' + +import { CredentialExchangeRecord, CredentialState } from '../../../../../../src/modules/credentials' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { Agent } from '../../../../../agent/Agent' +import { CredentialRepository } from '../../../../../modules/credentials/repository/CredentialRepository' +import { JsonTransformer } from '../../../../../utils' +import { DidCommMessageRole } from '../../../../didcomm' +import { DidCommMessageRepository } from '../../../../didcomm/DidCommMessageRepository' +import * as testModule from '../credential' + +const agentConfig = getAgentConfig('Migration CredentialRecord 0.1-0.2') +const agentContext = getAgentContext() + +jest.mock('../../../../../modules/credentials/repository/CredentialRepository') +const CredentialRepositoryMock = CredentialRepository as jest.Mock +const credentialRepository = new CredentialRepositoryMock() + +jest.mock('../../../../didcomm/DidCommMessageRepository') +const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock +const didCommMessageRepository = new DidCommMessageRepositoryMock() + +jest.mock('../../../../../agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn((token) => + token === CredentialRepositoryMock ? credentialRepository : didCommMessageRepository + ), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.1-0.2 | Credential', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + afterEach(() => { + mockFunction(didCommMessageRepository.save).mockReset() + }) + + describe('migrateCredentialRecordToV0_2()', () => { + it('should fetch all records and apply the needed updates ', async () => { + const records: CredentialExchangeRecord[] = [ + getCredential({ + credentialId: 'credentialId1', + metadata: { + schemaId: 'schemaId', + credentialDefinitionId: 'credentialDefinitionId', + anotherObject: { + someNested: 'value', + }, + requestMetadata: { + the: { + indy: { + meta: 'data', + }, + }, + }, + }, + }), + ] + + mockFunction(credentialRepository.getAll).mockResolvedValue(records) + + await testModule.migrateCredentialRecordToV0_2(agent) + + expect(credentialRepository.getAll).toHaveBeenCalledTimes(1) + expect(credentialRepository.update).toHaveBeenCalledTimes(records.length) + + const updatedRecord = mockFunction(credentialRepository.update).mock.calls[0][1] + + // Check first object is transformed correctly + expect(updatedRecord.toJSON()).toMatchObject({ + credentials: [ + { + credentialRecordId: 'credentialId1', + credentialRecordType: 'indy', + }, + ], + protocolVersion: 'v1', + metadata: { + '_internal/indyCredential': { + schemaId: 'schemaId', + credentialDefinitionId: 'credentialDefinitionId', + }, + anotherObject: { + someNested: 'value', + }, + '_internal/indyRequest': { + the: { + indy: { + meta: 'data', + }, + }, + }, + }, + }) + }) + }) + + describe('updateIndyMetadata()', () => { + it('should correctly update the old top-level keys into the nested structure', async () => { + const credentialRecord = getCredential({ + metadata: { + schemaId: 'schemaId', + credentialDefinitionId: 'schemaId', + anotherObject: { + someNested: 'value', + }, + requestMetadata: { + the: { + indy: { + meta: 'data', + }, + }, + }, + }, + }) + + await testModule.updateIndyMetadata(agent, credentialRecord) + + expect(credentialRecord).toMatchObject({ + metadata: { + data: { + '_internal/indyCredential': { + schemaId: 'schemaId', + credentialDefinitionId: 'schemaId', + }, + anotherObject: { + someNested: 'value', + }, + '_internal/indyRequest': { + the: { + indy: { + meta: 'data', + }, + }, + }, + }, + }, + }) + }) + + it('should not fail if some the top-level metadata keys do not exist', async () => { + const credentialRecord = getCredential({ + metadata: { + schemaId: 'schemaId', + anotherObject: { + someNested: 'value', + }, + }, + }) + + await testModule.updateIndyMetadata(agent, credentialRecord) + + expect(credentialRecord).toMatchObject({ + metadata: { + data: { + '_internal/indyCredential': { + schemaId: 'schemaId', + }, + anotherObject: { + someNested: 'value', + }, + }, + }, + }) + }) + + it('should not fail if all of the top-level metadata keys do not exist', async () => { + const credentialRecord = getCredential({ + metadata: { + anotherObject: { + someNested: 'value', + }, + }, + }) + + await testModule.updateIndyMetadata(agent, credentialRecord) + + expect(credentialRecord).toMatchObject({ + metadata: { + data: { + anotherObject: { + someNested: 'value', + }, + }, + }, + }) + }) + }) + + describe('migrateInternalCredentialRecordProperties()', () => { + it('should set the protocol version to v1 if not set on the record', async () => { + const credentialRecord = getCredential({}) + + await testModule.migrateInternalCredentialRecordProperties(agent, credentialRecord) + + expect(credentialRecord).toMatchObject({ + protocolVersion: 'v1', + }) + }) + + it('should not set the protocol version if a value is already set', async () => { + const credentialRecord = getCredential({ + protocolVersion: 'v2', + }) + + await testModule.migrateInternalCredentialRecordProperties(agent, credentialRecord) + + expect(credentialRecord).toMatchObject({ + protocolVersion: 'v2', + }) + }) + + it('should migrate the credentialId to credentials array if present', async () => { + const credentialRecord = getCredential({ + credentialId: 'theCredentialId', + }) + + await testModule.migrateInternalCredentialRecordProperties(agent, credentialRecord) + + expect(credentialRecord.toJSON()).toMatchObject({ + protocolVersion: 'v1', + credentials: [ + { + credentialRecordId: 'theCredentialId', + credentialRecordType: 'indy', + }, + ], + }) + }) + + it('should migrate the credentialId if not present', async () => { + const credentialRecord = getCredential({}) + + await testModule.migrateInternalCredentialRecordProperties(agent, credentialRecord) + + expect(credentialRecord.toJSON()).toMatchObject({ + protocolVersion: 'v1', + credentialId: undefined, + credentials: [], + }) + }) + }) + + describe('moveDidCommMessages()', () => { + it('should move the proposalMessage, offerMessage, requestMessage and credentialMessage to the didCommMessageRepository', async () => { + const proposalMessage = { '@type': 'ProposalMessage' } + const offerMessage = { '@type': 'OfferMessage' } + const requestMessage = { '@type': 'RequestMessage' } + const credentialMessage = { '@type': 'CredentialMessage' } + + const credentialRecord = getCredential({ + id: 'theCredentialId', + state: CredentialState.Done, + credentials: [ + { + credentialRecordId: 'theCredentialRecordId', + credentialRecordType: 'indy', + }, + ], + proposalMessage, + offerMessage, + requestMessage, + credentialMessage, + }) + + await testModule.moveDidCommMessages(agent, credentialRecord) + + expect(didCommMessageRepository.save).toHaveBeenCalledTimes(4) + const [[, proposalMessageRecord], [, offerMessageRecord], [, requestMessageRecord], [, credentialMessageRecord]] = + mockFunction(didCommMessageRepository.save).mock.calls + + expect(proposalMessageRecord).toMatchObject({ + role: DidCommMessageRole.Sender, + associatedRecordId: 'theCredentialId', + message: proposalMessage, + }) + + expect(offerMessageRecord).toMatchObject({ + role: DidCommMessageRole.Receiver, + associatedRecordId: 'theCredentialId', + message: offerMessage, + }) + + expect(requestMessageRecord).toMatchObject({ + role: DidCommMessageRole.Sender, + associatedRecordId: 'theCredentialId', + message: requestMessage, + }) + + expect(credentialMessageRecord).toMatchObject({ + role: DidCommMessageRole.Receiver, + associatedRecordId: 'theCredentialId', + message: credentialMessage, + }) + + expect(credentialRecord.toJSON()).toEqual({ + _tags: {}, + credentialId: undefined, + metadata: {}, + protocolVersion: undefined, + id: 'theCredentialId', + state: CredentialState.Done, + credentials: [ + { + credentialRecordId: 'theCredentialRecordId', + credentialRecordType: 'indy', + }, + ], + }) + }) + + it('should only move the messages which exist in the record', async () => { + const proposalMessage = { '@type': 'ProposalMessage' } + const offerMessage = { '@type': 'OfferMessage' } + + const credentialRecord = getCredential({ + id: 'theCredentialId', + state: CredentialState.Done, + credentials: [ + { + credentialRecordId: 'theCredentialRecordId', + credentialRecordType: 'indy', + }, + ], + proposalMessage, + offerMessage, + }) + + await testModule.moveDidCommMessages(agent, credentialRecord) + + expect(didCommMessageRepository.save).toHaveBeenCalledTimes(2) + const [[, proposalMessageRecord], [, offerMessageRecord]] = mockFunction(didCommMessageRepository.save).mock.calls + + expect(proposalMessageRecord).toMatchObject({ + role: DidCommMessageRole.Sender, + associatedRecordId: 'theCredentialId', + message: proposalMessage, + }) + + expect(offerMessageRecord).toMatchObject({ + role: DidCommMessageRole.Receiver, + associatedRecordId: 'theCredentialId', + message: offerMessage, + }) + + expect(credentialRecord.toJSON()).toEqual({ + _tags: {}, + credentialId: undefined, + metadata: {}, + protocolVersion: undefined, + id: 'theCredentialId', + state: CredentialState.Done, + credentials: [ + { + credentialRecordId: 'theCredentialRecordId', + credentialRecordType: 'indy', + }, + ], + }) + }) + + it('should determine the correct DidCommMessageRole for each message', async () => { + const proposalMessage = { '@type': 'ProposalMessage' } + const offerMessage = { '@type': 'OfferMessage' } + const requestMessage = { '@type': 'RequestMessage' } + const credentialMessage = { '@type': 'CredentialMessage' } + + const credentialRecord = getCredential({ + id: 'theCredentialId', + state: CredentialState.Done, + proposalMessage, + offerMessage, + requestMessage, + credentialMessage, + }) + + await testModule.moveDidCommMessages(agent, credentialRecord) + + expect(didCommMessageRepository.save).toHaveBeenCalledTimes(4) + const [[, proposalMessageRecord], [, offerMessageRecord], [, requestMessageRecord], [, credentialMessageRecord]] = + mockFunction(didCommMessageRepository.save).mock.calls + + expect(proposalMessageRecord).toMatchObject({ + role: DidCommMessageRole.Receiver, + associatedRecordId: 'theCredentialId', + message: proposalMessage, + }) + + expect(offerMessageRecord).toMatchObject({ + role: DidCommMessageRole.Sender, + associatedRecordId: 'theCredentialId', + message: offerMessage, + }) + + expect(requestMessageRecord).toMatchObject({ + role: DidCommMessageRole.Receiver, + associatedRecordId: 'theCredentialId', + message: requestMessage, + }) + + expect(credentialMessageRecord).toMatchObject({ + role: DidCommMessageRole.Sender, + associatedRecordId: 'theCredentialId', + message: credentialMessage, + }) + + expect(credentialRecord.toJSON()).toEqual({ + _tags: {}, + credentialId: undefined, + metadata: {}, + protocolVersion: undefined, + id: 'theCredentialId', + credentials: [], + state: CredentialState.Done, + }) + }) + }) + + describe('getCredentialRole', () => { + it('should return CredentialRole.Holder if the credentials array is not empty', () => { + const credentialRecord = getCredential({ + credentials: [ + { + credentialRecordId: 'theCredentialRecordId', + credentialRecordType: 'indy', + }, + ], + }) + + expect(testModule.getCredentialRole(credentialRecord)).toBe(testModule.V01_02MigrationCredentialRole.Holder) + }) + + it('should return CredentialRole.Issuer if state is Done and credentials array is empty', () => { + const credentialRecord = getCredential({ + state: CredentialState.Done, + credentials: [], + }) + + expect(testModule.getCredentialRole(credentialRecord)).toBe(testModule.V01_02MigrationCredentialRole.Issuer) + }) + + it('should return CredentialRole.Holder if the value is a holder state', () => { + const holderStates = [ + CredentialState.Declined, + CredentialState.ProposalSent, + CredentialState.OfferReceived, + CredentialState.RequestSent, + CredentialState.CredentialReceived, + ] + + for (const holderState of holderStates) { + expect( + testModule.getCredentialRole( + getCredential({ + state: holderState, + }) + ) + ).toBe(testModule.V01_02MigrationCredentialRole.Holder) + } + }) + + it('should return CredentialRole.Issuer if the state is not a holder state no credentials are in the array and the state is not Done', () => { + expect( + testModule.getCredentialRole( + getCredential({ + state: CredentialState.CredentialIssued, + }) + ) + ).toBe(testModule.V01_02MigrationCredentialRole.Issuer) + }) + }) +}) + +function getCredential({ + metadata, + credentialId, + protocolVersion, + proposalMessage, + offerMessage, + requestMessage, + credentialMessage, + state, + credentials, + id, +}: { + metadata?: Record + credentialId?: string + protocolVersion?: string + /* eslint-disable @typescript-eslint/no-explicit-any */ + proposalMessage?: any + offerMessage?: any + requestMessage?: any + credentialMessage?: any + /* eslint-enable @typescript-eslint/no-explicit-any */ + state?: CredentialState + credentials?: CredentialRecordBinding[] + id?: string +}) { + return JsonTransformer.fromJSON( + { + protocolVersion, + credentialId, + metadata, + proposalMessage, + offerMessage, + requestMessage, + credentialMessage, + state, + credentials, + id, + }, + CredentialExchangeRecord + ) +} diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/mediation.test.ts b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/mediation.test.ts new file mode 100644 index 0000000000..b5616578e2 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/mediation.test.ts @@ -0,0 +1,184 @@ +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { Agent } from '../../../../../agent/Agent' +import { MediationRole, MediationRecord } from '../../../../../modules/routing' +import { MediationRepository } from '../../../../../modules/routing/repository/MediationRepository' +import { JsonTransformer } from '../../../../../utils' +import * as testModule from '../mediation' + +const agentConfig = getAgentConfig('Migration MediationRecord 0.1-0.2') +const agentContext = getAgentContext() + +jest.mock('../../../../../modules/routing/repository/MediationRepository') +const MediationRepositoryMock = MediationRepository as jest.Mock +const mediationRepository = new MediationRepositoryMock() + +jest.mock('../../../../../agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn(() => mediationRepository), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.1-0.2 | Mediation', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + describe('migrateMediationRecordToV0_2()', () => { + it('should fetch all records and apply the needed updates ', async () => { + const records: MediationRecord[] = [ + getMediationRecord({ + role: MediationRole.Mediator, + endpoint: 'firstEndpoint', + }), + getMediationRecord({ + role: MediationRole.Recipient, + endpoint: 'secondEndpoint', + }), + ] + + mockFunction(mediationRepository.getAll).mockResolvedValue(records) + + await testModule.migrateMediationRecordToV0_2(agent, { + mediationRoleUpdateStrategy: 'allMediator', + }) + + expect(mediationRepository.getAll).toHaveBeenCalledTimes(1) + expect(mediationRepository.update).toHaveBeenCalledTimes(records.length) + + // Check second object is transformed correctly + expect(mediationRepository.update).toHaveBeenNthCalledWith( + 2, + agentContext, + getMediationRecord({ + role: MediationRole.Mediator, + endpoint: 'secondEndpoint', + }) + ) + + expect(records).toMatchObject([ + { + role: MediationRole.Mediator, + endpoint: 'firstEndpoint', + }, + { + role: MediationRole.Mediator, + endpoint: 'secondEndpoint', + }, + ]) + }) + }) + + describe('updateMediationRole()', () => { + it(`should update the role to ${MediationRole.Mediator} if no endpoint exists on the record and mediationRoleUpdateStrategy is 'recipientIfEndpoint'`, async () => { + const mediationRecord = getMediationRecord({ + role: MediationRole.Recipient, + }) + + await testModule.updateMediationRole(agent, mediationRecord, { + mediationRoleUpdateStrategy: 'recipientIfEndpoint', + }) + + expect(mediationRecord).toMatchObject({ + role: MediationRole.Mediator, + }) + }) + + it(`should update the role to ${MediationRole.Recipient} if an endpoint exists on the record and mediationRoleUpdateStrategy is 'recipientIfEndpoint'`, async () => { + const mediationRecord = getMediationRecord({ + role: MediationRole.Mediator, + endpoint: 'something', + }) + + await testModule.updateMediationRole(agent, mediationRecord, { + mediationRoleUpdateStrategy: 'recipientIfEndpoint', + }) + + expect(mediationRecord).toMatchObject({ + role: MediationRole.Recipient, + endpoint: 'something', + }) + }) + + it(`should not update the role if mediationRoleUpdateStrategy is 'doNotChange'`, async () => { + const mediationRecordMediator = getMediationRecord({ + role: MediationRole.Mediator, + endpoint: 'something', + }) + const mediationRecordRecipient = getMediationRecord({ + role: MediationRole.Recipient, + endpoint: 'something', + }) + + await testModule.updateMediationRole(agent, mediationRecordMediator, { + mediationRoleUpdateStrategy: 'doNotChange', + }) + + expect(mediationRecordMediator).toMatchObject({ + role: MediationRole.Mediator, + endpoint: 'something', + }) + + await testModule.updateMediationRole(agent, mediationRecordRecipient, { + mediationRoleUpdateStrategy: 'doNotChange', + }) + + expect(mediationRecordRecipient).toMatchObject({ + role: MediationRole.Recipient, + endpoint: 'something', + }) + }) + + it(`should update the role to ${MediationRole.Recipient} if mediationRoleUpdateStrategy is 'allRecipient'`, async () => { + const mediationRecord = getMediationRecord({ + role: MediationRole.Mediator, + endpoint: 'something', + }) + + await testModule.updateMediationRole(agent, mediationRecord, { + mediationRoleUpdateStrategy: 'allRecipient', + }) + + expect(mediationRecord).toMatchObject({ + role: MediationRole.Recipient, + endpoint: 'something', + }) + }) + + it(`should update the role to ${MediationRole.Mediator} if mediationRoleUpdateStrategy is 'allMediator'`, async () => { + const mediationRecord = getMediationRecord({ + role: MediationRole.Recipient, + endpoint: 'something', + }) + + await testModule.updateMediationRole(agent, mediationRecord, { + mediationRoleUpdateStrategy: 'allMediator', + }) + + expect(mediationRecord).toMatchObject({ + role: MediationRole.Mediator, + endpoint: 'something', + }) + }) + }) +}) + +function getMediationRecord({ role, endpoint }: { role: MediationRole; endpoint?: string }) { + return JsonTransformer.fromJSON( + { + role, + endpoint, + }, + MediationRecord + ) +} diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/connection.ts b/packages/core/src/storage/migration/updates/0.1-0.2/connection.ts new file mode 100644 index 0000000000..266ccce315 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/connection.ts @@ -0,0 +1,482 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' +import type { ConnectionRecord } from '../../../../modules/connections' +import type { JsonObject } from '../../../../types' + +import { outOfBandServiceToInlineKeysNumAlgo2Did } from '../../../..//modules/dids/methods/peer/peerDidNumAlgo2' +import { + DidExchangeState, + ConnectionState, + ConnectionInvitationMessage, + ConnectionRole, + DidDoc, + ConnectionRepository, + DidExchangeRole, +} from '../../../../modules/connections' +import { convertToNewDidDocument } from '../../../../modules/connections/services/helpers' +import { DidKey } from '../../../../modules/dids' +import { DidDocumentRole } from '../../../../modules/dids/domain/DidDocumentRole' +import { DidRecord, DidRepository } from '../../../../modules/dids/repository' +import { DidRecordMetadataKeys } from '../../../../modules/dids/repository/didRecordMetadataTypes' +import { OutOfBandRole } from '../../../../modules/oob/domain/OutOfBandRole' +import { OutOfBandState } from '../../../../modules/oob/domain/OutOfBandState' +import { convertToNewInvitation } from '../../../../modules/oob/helpers' +import { OutOfBandRecord, OutOfBandRepository } from '../../../../modules/oob/repository' +import { JsonEncoder, JsonTransformer } from '../../../../utils' + +/** + * Migrates the {@link ConnectionRecord} to 0.2 compatible format. It fetches all records from storage + * and applies the needed updates to the records. After a record has been transformed, it is updated + * in storage and the next record will be transformed. + * + * The following transformations are applied: + * - {@link updateConnectionRoleAndState} + * - {@link extractDidDocument} + * - {@link migrateToOobRecord} + */ +export async function migrateConnectionRecordToV0_2(agent: Agent) { + agent.config.logger.info('Migrating connection records to storage version 0.2') + const connectionRepository = agent.dependencyManager.resolve(ConnectionRepository) + + agent.config.logger.debug(`Fetching all connection records from storage`) + const allConnections = await connectionRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${allConnections.length} connection records to update.`) + for (const connectionRecord of allConnections) { + agent.config.logger.debug(`Migrating connection record with id ${connectionRecord.id} to storage version 0.2`) + await updateConnectionRoleAndState(agent, connectionRecord) + await extractDidDocument(agent, connectionRecord) + + // migration of oob record MUST run after extracting the did document as it relies on the updated did + // it also MUST run after update the connection role and state as it assumes the values are + // did exchange roles and states + const _connectionRecord = await migrateToOobRecord(agent, connectionRecord) + + // migrateToOobRecord will return the connection record if it has not been deleted. When using multiUseInvitation the connection record + // will be removed after processing, in which case the update method will throw an error. + if (_connectionRecord) { + await connectionRepository.update(agent.context, connectionRecord) + } + + agent.config.logger.debug( + `Successfully migrated connection record with id ${connectionRecord.id} to storage version 0.2` + ) + } +} + +/** + * With the addition of the did exchange protocol there are now two states and roles related to the connection record; for the did exchange protocol and for the connection protocol. + * To keep it easy to work with the connection record, all state and role values are updated to those of the {@link DidExchangeRole} and {@link DidExchangeState}. + * + * This migration method transforms all connection record state and role values to their respective values of the {@link DidExchangeRole} and {@link DidExchangeState}. For convenience a getter + * property `rfc0160ConnectionState` is added to the connection record which returns the {@link ConnectionState} value. + * + * The following 0.1.0 connection record structure (unrelated keys omitted): + * + * ```json + * { + * "state": "invited", + * "role": "inviter" + * } + * ``` + * + * Will be transformed into the following 0.2.0 structure (unrelated keys omitted): + * + * ```json + * { + * "state": "invitation-sent", + * "role": "responder", + * } + * ``` + */ +export async function updateConnectionRoleAndState( + agent: Agent, + connectionRecord: ConnectionRecord +) { + agent.config.logger.debug( + `Extracting 'didDoc' and 'theirDidDoc' from connection record into separate DidRecord and updating unqualified dids to did:peer dids` + ) + + const oldState = connectionRecord.state + const oldRole = connectionRecord.role + + const [didExchangeRole, didExchangeState] = didExchangeStateAndRoleFromRoleAndState( + connectionRecord.role, + connectionRecord.state + ) + + connectionRecord.role = didExchangeRole + connectionRecord.state = didExchangeState + + agent.config.logger.debug( + `Updated connection record state from ${oldState} to ${connectionRecord.state} and role from ${oldRole} to ${connectionRecord.role}` + ) +} + +/** + * The connection record previously stored both did documents from a connection in the connection record itself. Version 0.2.0 added a generic did storage that can be used for numerous usages, one of which + * is the storage of did documents for connection records. + * + * This migration method extracts the did documents from the `didDoc` and `theirDidDoc` properties from the connection record, updates them to did documents compliant with the DID Core spec, and stores them + * in the did repository. By doing so it also updates the unqualified dids in the `did` and `theirDid` fields generated by the indy-sdk to fully qualified `did:peer` dids compliant with + * the [Peer DID Method Specification](https://identity.foundation/peer-did-method-spec/). + * + * To account for the fact that the mechanism to migrate legacy did document to peer did documents is not defined yet, the legacy did and did document are stored in the did record metadata. + * This will be deleted later if we can be certain the did doc conversion to a did:peer did document is correct. + * + * The following 0.1.0 connection record structure (unrelated keys omitted): + * + * ```json + * { + * "did": "BBPoJqRKatdcfLEAFL7exC", + * "theirDid": "N8NQHLtCKfPmWMgCSdfa7h", + * "didDoc": , + * "theirDidDoc": , + * "verkey": "GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa" + * } + * ``` + * + * Will be transformed into the following 0.2.0 structure (unrelated keys omitted): + * + * ```json + * { + * "did": "did:peer:1zQmXUaPPhPCbUVZ3hGYmQmGxWTwyDfhqESXCpMFhKaF9Y2A", + * "theirDid": "did:peer:1zQmZMygzYqNwU6Uhmewx5Xepf2VLp5S4HLSwwgf2aiKZuwa" + * } + * ``` + */ +export async function extractDidDocument(agent: Agent, connectionRecord: ConnectionRecord) { + agent.config.logger.debug( + `Extracting 'didDoc' and 'theirDidDoc' from connection record into separate DidRecord and updating unqualified dids to did:peer dids` + ) + + const didRepository = agent.dependencyManager.resolve(DidRepository) + + const untypedConnectionRecord = connectionRecord as unknown as JsonObject + const oldOurDidDocJson = untypedConnectionRecord.didDoc as JsonObject | undefined + const oldTheirDidDocJson = untypedConnectionRecord.theirDidDoc as JsonObject | undefined + + if (oldOurDidDocJson) { + const oldOurDidDoc = JsonTransformer.fromJSON(oldOurDidDocJson, DidDoc) + + agent.config.logger.debug( + `Found a legacy did document for did ${oldOurDidDoc.id} in connection record didDoc. Converting it to a peer did document.` + ) + + const newOurDidDocument = convertToNewDidDocument(oldOurDidDoc) + + // Maybe we already have a record for this did because the migration failed previously + // NOTE: in 0.3.0 the id property was updated to be a uuid, and a new did property was added. As this is the update from 0.1 to 0.2, + // the `id` property of the record is still the did here. + let ourDidRecord = await didRepository.findById(agent.context, newOurDidDocument.id) + + if (!ourDidRecord) { + agent.config.logger.debug(`Creating did record for our did ${newOurDidDocument.id}`) + ourDidRecord = new DidRecord({ + // NOTE: in 0.3.0 the id property was updated to be a uuid, and a new did property was added. Here we make the id and did property both the did. + // In the 0.3.0 update the `id` property will be updated to an uuid. + id: newOurDidDocument.id, + did: newOurDidDocument.id, + role: DidDocumentRole.Created, + didDocument: newOurDidDocument, + createdAt: connectionRecord.createdAt, + }) + + ourDidRecord.metadata.set(DidRecordMetadataKeys.LegacyDid, { + unqualifiedDid: oldOurDidDoc.id, + didDocumentString: JsonEncoder.toString(oldOurDidDocJson), + }) + + await didRepository.save(agent.context, ourDidRecord) + + agent.config.logger.debug(`Successfully saved did record for did ${newOurDidDocument.id}`) + } else { + agent.config.logger.debug(`Found existing did record for did ${newOurDidDocument.id}, not creating did record.`) + } + + agent.config.logger.debug(`Deleting old did document from connection record and storing new did:peer did`) + // Remove didDoc and assign the new did:peer did to did + delete untypedConnectionRecord.didDoc + connectionRecord.did = newOurDidDocument.id + } else { + agent.config.logger.debug( + `Did not find a did document in connection record didDoc. Not converting it to a peer did document.` + ) + } + + if (oldTheirDidDocJson) { + const oldTheirDidDoc = JsonTransformer.fromJSON(oldTheirDidDocJson, DidDoc) + + agent.config.logger.debug( + `Found a legacy did document for theirDid ${oldTheirDidDoc.id} in connection record theirDidDoc. Converting it to a peer did document.` + ) + + const newTheirDidDocument = convertToNewDidDocument(oldTheirDidDoc) + + // Maybe we already have a record for this did because the migration failed previously + // NOTE: in 0.3.0 the id property was updated to be a uuid, and a new did property was added. As this is the update from 0.1 to 0.2, + // the `id` property of the record is still the did here. + let theirDidRecord = await didRepository.findById(agent.context, newTheirDidDocument.id) + + if (!theirDidRecord) { + agent.config.logger.debug(`Creating did record for theirDid ${newTheirDidDocument.id}`) + + theirDidRecord = new DidRecord({ + // NOTE: in 0.3.0 the id property was updated to be a uuid, and a new did property was added. Here we make the id and did property both the did. + // In the 0.3.0 update the `id` property will be updated to an uuid. + id: newTheirDidDocument.id, + did: newTheirDidDocument.id, + role: DidDocumentRole.Received, + didDocument: newTheirDidDocument, + createdAt: connectionRecord.createdAt, + }) + + theirDidRecord.metadata.set(DidRecordMetadataKeys.LegacyDid, { + unqualifiedDid: oldTheirDidDoc.id, + didDocumentString: JsonEncoder.toString(oldTheirDidDocJson), + }) + + await didRepository.save(agent.context, theirDidRecord) + + agent.config.logger.debug(`Successfully saved did record for theirDid ${newTheirDidDocument.id}`) + } else { + agent.config.logger.debug( + `Found existing did record for theirDid ${newTheirDidDocument.id}, not creating did record.` + ) + } + + agent.config.logger.debug(`Deleting old theirDidDoc from connection record and storing new did:peer theirDid`) + // Remove theirDidDoc and assign the new did:peer did to theirDid + delete untypedConnectionRecord.theirDidDoc + connectionRecord.theirDid = newTheirDidDocument.id + } else { + agent.config.logger.debug( + `Did not find a did document in connection record theirDidDoc. Not converting it to a peer did document.` + ) + } + + // Delete legacy verkey property + delete untypedConnectionRecord.verkey +} + +/** + * With the addition of the out of band protocol, invitations are now stored in the {@link OutOfBandRecord}. In addition a new field `invitationDid` is added to the connection record that + * is generated based on the invitation service or did. This allows to reuse existing connections. + * + * This migration method extracts the invitation and other relevant data into a separate {@link OutOfBandRecord}. By doing so it converts the old connection protocol invitation into the new + * Out of band invitation message. Based on the service or did of the invitation, the `invitationDid` is populated. + * + * Previously when creating a multi use invitation, a connection record would be created with the `multiUseInvitation` set to true. The connection record would always be in state `invited`. + * If a request for the multi use invitation came in, a new connection record would be created. With the addition of the out of band module, no connection records are created until a request + * is received. So for multi use invitation this means that the connection record with multiUseInvitation=true will be deleted, and instead all connections created using that out of band invitation + * will contain the `outOfBandId` of the multi use invitation. + * + * The following 0.1.0 connection record structure (unrelated keys omitted): + * + * ```json + * { + * "invitation": { + * "@type": "https://didcomm.org/connections/1.0/invitation", + * "@id": "04a2c382-999e-4de9-a1d2-9dec0b2fa5e4", + * "recipientKeys": ["E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu"], + * "serviceEndpoint": "https://example.com", + * "label": "test", + * }, + * "multiUseInvitation": false + * } + * ``` + * + * Will be transformed into the following 0.2.0 structure (unrelated keys omitted): + * + * ```json + * { + * "invitationDid": "did:peer:2.Ez6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSJ9", + * "outOfBandId": "04a2c382-999e-4de9-a1d2-9dec0b2fa5e4" + * } + * ``` + */ +export async function migrateToOobRecord( + agent: Agent, + connectionRecord: ConnectionRecord +): Promise { + agent.config.logger.debug( + `Migrating properties from connection record with id ${connectionRecord.id} to out of band record` + ) + + const oobRepository = agent.dependencyManager.resolve(OutOfBandRepository) + const connectionRepository = agent.dependencyManager.resolve(ConnectionRepository) + + const untypedConnectionRecord = connectionRecord as unknown as JsonObject + const oldInvitationJson = untypedConnectionRecord.invitation as JsonObject | undefined + const oldMultiUseInvitation = untypedConnectionRecord.multiUseInvitation as boolean | undefined + + // Only migrate if there is an invitation stored + if (oldInvitationJson) { + const oldInvitation = JsonTransformer.fromJSON(oldInvitationJson, ConnectionInvitationMessage) + + agent.config.logger.debug(`Found a legacy invitation in connection record. Migrating it to an out of band record.`) + + const outOfBandInvitation = convertToNewInvitation(oldInvitation) + + // If both the recipientKeys, the @id and the role match we assume the connection was created using the same invitation. + const recipientKeyFingerprints = outOfBandInvitation + .getInlineServices() + .map((s) => s.recipientKeys) + .reduce((acc, curr) => [...acc, ...curr], []) + .map((didKey) => DidKey.fromDid(didKey).key.fingerprint) + + const oobRole = connectionRecord.role === DidExchangeRole.Responder ? OutOfBandRole.Sender : OutOfBandRole.Receiver + const oobRecords = await oobRepository.findByQuery(agent.context, { + invitationId: oldInvitation.id, + recipientKeyFingerprints, + role: oobRole, + }) + + let oobRecord: OutOfBandRecord | undefined = oobRecords[0] + + if (!oobRecord) { + agent.config.logger.debug(`Create out of band record.`) + + const connectionRole = connectionRecord.role as DidExchangeRole + const connectionState = connectionRecord.state as DidExchangeState + const oobState = oobStateFromDidExchangeRoleAndState(connectionRole, connectionState) + + oobRecord = new OutOfBandRecord({ + role: oobRole, + state: oobState, + alias: connectionRecord.alias, + autoAcceptConnection: connectionRecord.autoAcceptConnection, + outOfBandInvitation, + reusable: oldMultiUseInvitation, + mediatorId: connectionRecord.mediatorId, + createdAt: connectionRecord.createdAt, + tags: { recipientKeyFingerprints }, + }) + + await oobRepository.save(agent.context, oobRecord) + agent.config.logger.debug(`Successfully saved out of band record for invitation @id ${oldInvitation.id}`) + } else { + agent.config.logger.debug( + `Found existing out of band record for invitation @id ${oldInvitation.id} and did ${connectionRecord.did}, not creating a new out of band record.` + ) + } + + // We need to update the oob record with the reusable data. We don't know initially if an oob record is reusable or not, as there can be 1..n connections for each invitation + // only when we find the multiUseInvitation we can update it. + if (oldMultiUseInvitation) { + oobRecord.reusable = true + oobRecord.state = OutOfBandState.AwaitResponse + oobRecord.mediatorId = connectionRecord.mediatorId + oobRecord.autoAcceptConnection = connectionRecord.autoAcceptConnection + + await oobRepository.update(agent.context, oobRecord) + await connectionRepository.delete(agent.context, connectionRecord) + agent.config.logger.debug( + `Set reusable=true for out of band record with invitation @id ${oobRecord.outOfBandInvitation.id} and role ${oobRole}.` + ) + + return + } + + agent.config.logger.debug(`Setting invitationDid and outOfBand Id, and removing invitation from connection record`) + // All connections have been made using the connection protocol, which means we can be certain + // that there was only one service, thus we can use the first oob message service + // Note: since this is an update from 0.1 to 0.2, we use former way of calculating numAlgo2Dids + const [invitationDid] = [ + ...oobRecord.outOfBandInvitation.getDidServices(), + ...oobRecord.outOfBandInvitation.getInlineServices().map(outOfBandServiceToInlineKeysNumAlgo2Did), + ] + connectionRecord.invitationDid = invitationDid + + // Remove invitation and assign the oob id to the connection record + delete untypedConnectionRecord.invitation + connectionRecord.outOfBandId = oobRecord.id + } + + agent.config.logger.debug('Removing multiUseInvitation property from connection record') + // multiUseInvitation is now stored as reusable in the out of band record + delete untypedConnectionRecord.multiUseInvitation + + return connectionRecord +} + +/** + * Determine the out of band state based on the did exchange role and state. + */ +export function oobStateFromDidExchangeRoleAndState(role: DidExchangeRole, state: DidExchangeState) { + const stateMapping = { + [DidExchangeState.InvitationReceived]: OutOfBandState.PrepareResponse, + [DidExchangeState.InvitationSent]: OutOfBandState.AwaitResponse, + + [DidExchangeState.RequestReceived]: OutOfBandState.Done, + [DidExchangeState.RequestSent]: OutOfBandState.Done, + + [DidExchangeState.ResponseReceived]: OutOfBandState.Done, + [DidExchangeState.ResponseSent]: OutOfBandState.Done, + + [DidExchangeState.Completed]: OutOfBandState.Done, + [DidExchangeState.Abandoned]: OutOfBandState.Done, + } + + if (state === DidExchangeState.Start) { + return role === DidExchangeRole.Requester ? OutOfBandState.PrepareResponse : OutOfBandState.AwaitResponse + } + + return stateMapping[state] +} + +/** + * Determine the did exchange state based on the connection/did-exchange role and state. + */ +export function didExchangeStateAndRoleFromRoleAndState( + role: ConnectionRole | DidExchangeRole, + state: ConnectionState | DidExchangeState +): [DidExchangeRole, DidExchangeState] { + const roleMapping = { + // Responder / Inviter + [DidExchangeRole.Responder]: DidExchangeRole.Responder, + [ConnectionRole.Inviter]: DidExchangeRole.Responder, + + // Request / Invitee + [DidExchangeRole.Requester]: DidExchangeRole.Requester, + [ConnectionRole.Invitee]: DidExchangeRole.Requester, + } + + const roleStateMapping = { + [DidExchangeRole.Requester]: { + // DidExchangeRole.Requester + [ConnectionState.Invited]: DidExchangeState.InvitationReceived, + [ConnectionState.Requested]: DidExchangeState.RequestSent, + [ConnectionState.Responded]: DidExchangeState.ResponseReceived, + [ConnectionState.Complete]: DidExchangeState.Completed, + [ConnectionState.Null]: DidExchangeState.Start, + }, + [DidExchangeRole.Responder]: { + // DidExchangeRole.Responder + [ConnectionState.Invited]: DidExchangeState.InvitationSent, + [ConnectionState.Requested]: DidExchangeState.RequestReceived, + [ConnectionState.Responded]: DidExchangeState.ResponseSent, + [ConnectionState.Complete]: DidExchangeState.Completed, + [ConnectionState.Null]: DidExchangeState.Start, + }, + } + + // Map the role to did exchange role. Can handle did exchange roles to make the function re-runnable + const didExchangeRole = roleMapping[role] + + // Take into account possibility that the record state was already updated to + // didExchange state and roles. This makes the script re-runnable and + // adds some resiliency to the script. + const stateMapping = roleStateMapping[didExchangeRole] + + // Check if state is a valid connection state + if (isConnectionState(state)) { + return [didExchangeRole, stateMapping[state]] + } + + // If state is not a valid state we assume the state is already a did exchange state + return [didExchangeRole, state] +} + +function isConnectionState(state: string): state is ConnectionState { + return Object.values(ConnectionState).includes(state as ConnectionState) +} diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/credential.ts b/packages/core/src/storage/migration/updates/0.1-0.2/credential.ts new file mode 100644 index 0000000000..9003f38456 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/credential.ts @@ -0,0 +1,255 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' +import type { CredentialExchangeRecord } from '../../../../modules/credentials' +import type { JsonObject, PlaintextMessage } from '../../../../types' + +import { CredentialState } from '../../../../modules/credentials/models/CredentialState' +import { CredentialRepository } from '../../../../modules/credentials/repository/CredentialRepository' +import { Metadata } from '../../../Metadata' +import { DidCommMessageRepository, DidCommMessageRecord, DidCommMessageRole } from '../../../didcomm' + +/** + * Migrates the {@link CredentialRecord} to 0.2 compatible format. It fetches all records from storage + * and applies the needed updates to the records. After a record has been transformed, it is updated + * in storage and the next record will be transformed. + * + * The following transformations are applied: + * - {@link updateIndyMetadata} + */ +export async function migrateCredentialRecordToV0_2(agent: Agent) { + agent.config.logger.info('Migrating credential records to storage version 0.2') + const credentialRepository = agent.dependencyManager.resolve(CredentialRepository) + + agent.config.logger.debug(`Fetching all credential records from storage`) + const allCredentials = await credentialRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${allCredentials.length} credential records to update.`) + for (const credentialRecord of allCredentials) { + agent.config.logger.debug(`Migrating credential record with id ${credentialRecord.id} to storage version 0.2`) + + await updateIndyMetadata(agent, credentialRecord) + await migrateInternalCredentialRecordProperties(agent, credentialRecord) + await moveDidCommMessages(agent, credentialRecord) + + await credentialRepository.update(agent.context, credentialRecord) + + agent.config.logger.debug( + `Successfully migrated credential record with id ${credentialRecord.id} to storage version 0.2` + ) + } +} + +export enum V01_02MigrationCredentialRole { + Issuer, + Holder, +} + +const holderCredentialStates = [ + CredentialState.Declined, + CredentialState.ProposalSent, + CredentialState.OfferReceived, + CredentialState.RequestSent, + CredentialState.CredentialReceived, +] + +const didCommMessageRoleMapping = { + [V01_02MigrationCredentialRole.Issuer]: { + proposalMessage: DidCommMessageRole.Receiver, + offerMessage: DidCommMessageRole.Sender, + requestMessage: DidCommMessageRole.Receiver, + credentialMessage: DidCommMessageRole.Sender, + }, + [V01_02MigrationCredentialRole.Holder]: { + proposalMessage: DidCommMessageRole.Sender, + offerMessage: DidCommMessageRole.Receiver, + requestMessage: DidCommMessageRole.Sender, + credentialMessage: DidCommMessageRole.Receiver, + }, +} + +const credentialRecordMessageKeys = ['proposalMessage', 'offerMessage', 'requestMessage', 'credentialMessage'] as const + +export function getCredentialRole(credentialRecord: CredentialExchangeRecord) { + // Credentials will only have a value when a credential is received, meaning we're the holder + if (credentialRecord.credentials.length > 0) { + return V01_02MigrationCredentialRole.Holder + } + // If credentialRecord.credentials doesn't have any values, and we're also not in state done it means we're the issuer. + else if (credentialRecord.state === CredentialState.Done) { + return V01_02MigrationCredentialRole.Issuer + } + // For these states we know for certain that we're the holder + else if (holderCredentialStates.includes(credentialRecord.state)) { + return V01_02MigrationCredentialRole.Holder + } + + // For all other states we can be certain we're the issuer + return V01_02MigrationCredentialRole.Issuer +} + +/** + * The credential record had a custom `metadata` property in pre-0.1.0 storage that contained the `requestMetadata`, `schemaId` and `credentialDefinition` + * properties. Later a generic metadata API was added that only allows objects to be stored. Therefore the properties were moved into a different structure. + * + * This migration method updates the top level properties to the new nested metadata structure. + * + * The following pre-0.1.0 structure: + * + * ```json + * { + * "requestMetadata": "", + * "schemaId": "", + * "credentialDefinitionId": "" + * } + * ``` + * + * Will be transformed into the following 0.2.0 structure: + * + * ```json + * { + * "_internal/indyRequest": , + * "_internal/indyCredential": { + * "schemaId": "", + * "credentialDefinitionId": "" + * } + * } + * ``` + */ +export async function updateIndyMetadata( + agent: Agent, + credentialRecord: CredentialExchangeRecord +) { + agent.config.logger.debug(`Updating indy metadata to use the generic metadata api available to records.`) + + const { requestMetadata, schemaId, credentialDefinitionId, ...rest } = credentialRecord.metadata.data + const metadata = new Metadata>(rest) + + const indyRequestMetadataKey = '_internal/indyRequest' + const indyCredentialMetadataKey = '_internal/indyCredential' + if (requestMetadata) { + agent.config.logger.trace(`Found top-level 'requestMetadata' key, moving to '${indyRequestMetadataKey}'`) + metadata.add(indyRequestMetadataKey, { ...requestMetadata }) + } + + if (schemaId && typeof schemaId === 'string') { + agent.config.logger.trace(`Found top-level 'schemaId' key, moving to '${indyCredentialMetadataKey}.schemaId'`) + metadata.add(indyCredentialMetadataKey, { schemaId }) + } + + if (credentialDefinitionId && typeof credentialDefinitionId === 'string') { + agent.config.logger.trace( + `Found top-level 'credentialDefinitionId' key, moving to '${indyCredentialMetadataKey}.credentialDefinitionId'` + ) + metadata.add(indyCredentialMetadataKey, { credentialDefinitionId }) + } + + credentialRecord.metadata = metadata +} + +/** + * With the addition of support for different protocol versions the credential record now stores the protocol version. + * With the addition of issue credential v2 support, other credential formats than indy can be used, and multiple credentials can be issued at once. To + * account for this the `credentialId` has been replaced by the `credentials` array. This is an array of objects containing the `credentialRecordId` and + * the `credentialRecordType`. For all current credentials the `credentialRecordType` will always be `indy`. + * + * The following 0.1.0 credential record structure (unrelated keys omitted): + * + * ```json + * { + * "credentialId": "09e46da9-a575-4909-b016-040e96c3c539" + * } + * ``` + * + * Will be transformed into the following 0.2.0 structure (unrelated keys omitted): + * + * ```json + * { + * "protocolVersion: "v1", + * "credentials": [ + * { + * "credentialRecordId": "09e46da9-a575-4909-b016-040e96c3c539", + * "credentialRecordType": "anoncreds" + * } + * ] + * } + * ``` + */ +export async function migrateInternalCredentialRecordProperties( + agent: Agent, + credentialRecord: CredentialExchangeRecord +) { + agent.config.logger.debug( + `Migrating internal credential record ${credentialRecord.id} properties to storage version 0.2` + ) + + if (!credentialRecord.protocolVersion) { + agent.config.logger.debug(`Setting protocolVersion to v1`) + credentialRecord.protocolVersion = 'v1' + } + + const untypedCredentialRecord = credentialRecord as unknown as JsonObject + + if (untypedCredentialRecord.credentialId) { + agent.config.logger.debug(`Migrating indy credentialId ${untypedCredentialRecord.id} to credentials array`) + credentialRecord.credentials = [ + { + credentialRecordId: untypedCredentialRecord.credentialId as string, + credentialRecordType: 'indy', + }, + ] + + delete untypedCredentialRecord.credentialId + } + + agent.config.logger.debug( + `Successfully migrated internal credential record ${credentialRecord.id} properties to storage version 0.2` + ) +} + +/** + * In 0.2.0 the v1 didcomm messages have been moved out of the credential record into separate record using the DidCommMessageRepository. + * This migration scripts extracts all message (proposalMessage, offerMessage, requestMessage, credentialMessage) and moves + * them into the DidCommMessageRepository. + */ +export async function moveDidCommMessages( + agent: Agent, + credentialRecord: CredentialExchangeRecord +) { + agent.config.logger.debug( + `Moving didcomm messages from credential record with id ${credentialRecord.id} to DidCommMessageRecord` + ) + const didCommMessageRepository = agent.dependencyManager.resolve(DidCommMessageRepository) + + for (const messageKey of credentialRecordMessageKeys) { + agent.config.logger.debug( + `Starting move of ${messageKey} from credential record with id ${credentialRecord.id} to DIDCommMessageRecord` + ) + const credentialRecordJson = credentialRecord as unknown as JsonObject + const message = credentialRecordJson[messageKey] as PlaintextMessage | undefined + + if (message) { + const credentialRole = getCredentialRole(credentialRecord) + const didCommMessageRole = didCommMessageRoleMapping[credentialRole][messageKey] + + const didCommMessageRecord = new DidCommMessageRecord({ + role: didCommMessageRole, + associatedRecordId: credentialRecord.id, + message, + }) + await didCommMessageRepository.save(agent.context, didCommMessageRecord) + + agent.config.logger.debug( + `Successfully moved ${messageKey} from credential record with id ${credentialRecord.id} to DIDCommMessageRecord` + ) + + delete credentialRecordJson[messageKey] + } else { + agent.config.logger.debug( + `Credential record with id ${credentialRecord.id} does not have a ${messageKey}. Not creating a DIDCommMessageRecord` + ) + } + } + + agent.config.logger.debug( + `Successfully moved didcomm messages from credential record with id ${credentialRecord.id} to DIDCommMessageRecord` + ) +} diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/index.ts b/packages/core/src/storage/migration/updates/0.1-0.2/index.ts new file mode 100644 index 0000000000..17065705f3 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/index.ts @@ -0,0 +1,16 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' +import type { UpdateConfig } from '../../updates' + +import { migrateConnectionRecordToV0_2 } from './connection' +import { migrateCredentialRecordToV0_2 } from './credential' +import { migrateMediationRecordToV0_2 } from './mediation' + +export interface V0_1ToV0_2UpdateConfig { + mediationRoleUpdateStrategy: 'allMediator' | 'allRecipient' | 'recipientIfEndpoint' | 'doNotChange' +} + +export async function updateV0_1ToV0_2(agent: Agent, config: UpdateConfig): Promise { + await migrateCredentialRecordToV0_2(agent) + await migrateMediationRecordToV0_2(agent, config.v0_1ToV0_2) + await migrateConnectionRecordToV0_2(agent) +} diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/mediation.ts b/packages/core/src/storage/migration/updates/0.1-0.2/mediation.ts new file mode 100644 index 0000000000..c131646507 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/mediation.ts @@ -0,0 +1,82 @@ +import type { V0_1ToV0_2UpdateConfig } from './index' +import type { BaseAgent } from '../../../../agent/BaseAgent' +import type { MediationRecord } from '../../../../modules/routing' + +import { MediationRepository, MediationRole } from '../../../../modules/routing' + +/** + * Migrates the {@link MediationRecord} to 0.2 compatible format. It fetches all records from storage + * and applies the needed updates to the records. After a record has been transformed, it is updated + * in storage and the next record will be transformed. + * + * The following transformations are applied: + * - {@link updateMediationRole} + */ +export async function migrateMediationRecordToV0_2( + agent: Agent, + upgradeConfig: V0_1ToV0_2UpdateConfig +) { + agent.config.logger.info('Migrating mediation records to storage version 0.2') + const mediationRepository = agent.dependencyManager.resolve(MediationRepository) + + agent.config.logger.debug(`Fetching all mediation records from storage`) + const allMediationRecords = await mediationRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${allMediationRecords.length} mediation records to update.`) + for (const mediationRecord of allMediationRecords) { + agent.config.logger.debug(`Migrating mediation record with id ${mediationRecord.id} to storage version 0.2`) + + await updateMediationRole(agent, mediationRecord, upgradeConfig) + + await mediationRepository.update(agent.context, mediationRecord) + + agent.config.logger.debug( + `Successfully migrated mediation record with id ${mediationRecord.id} to storage version 0.2` + ) + } +} + +/** + * The role in the mediation record was always being set to {@link MediationRole.Mediator} for both mediators and recipients. This didn't cause any issues, but would return the wrong role for recipients. + * + * In 0.2 a check is added to make sure the role of a mediation record matches with actions (e.g. a recipient can't grant mediation), which means it will throw an error if the role is not set correctly. + * + * Because it's not always possible detect whether the role should actually be mediator or recipient, a number of configuration options are provided on how the role should be updated: + * + * - `allMediator`: The role is set to {@link MediationRole.Mediator} for both mediators and recipients + * - `allRecipient`: The role is set to {@link MediationRole.Recipient} for both mediators and recipients + * - `recipientIfEndpoint`: The role is set to {@link MediationRole.Recipient} if their is an `endpoint` configured on the record otherwise it is set to {@link MediationRole.Mediator}. + * The endpoint is not set when running as a mediator, so in theory this allows to determine the role of the record. + * There is one case where this could be problematic when the role should be recipient, if the mediation grant hasn't actually occurred (meaning the endpoint is not set). + * - `doNotChange`: The role is not changed + * + * Most agents only act as either the role of mediator or recipient, in which case the `allMediator` or `allRecipient` configuration is the most appropriate. If your agent acts as both a recipient and mediator, the `recipientIfEndpoint` configuration is the most appropriate. The `doNotChange` options is not recommended and can lead to errors if the role is not set correctly. + * + */ +export async function updateMediationRole( + agent: Agent, + mediationRecord: MediationRecord, + { mediationRoleUpdateStrategy }: V0_1ToV0_2UpdateConfig +) { + agent.config.logger.debug(`Updating mediation record role using strategy '${mediationRoleUpdateStrategy}'`) + + switch (mediationRoleUpdateStrategy) { + case 'allMediator': + mediationRecord.role = MediationRole.Mediator + break + case 'allRecipient': + mediationRecord.role = MediationRole.Recipient + break + case 'recipientIfEndpoint': + if (mediationRecord.endpoint) { + agent.config.logger.debug('Mediation record endpoint is set, setting mediation role to recipient') + mediationRecord.role = MediationRole.Recipient + } else { + agent.config.logger.debug('Mediation record endpoint is not set, setting mediation role to mediator') + mediationRecord.role = MediationRole.Mediator + } + break + case 'doNotChange': + break + } +} diff --git a/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/connection.test.ts b/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/connection.test.ts new file mode 100644 index 0000000000..64ada8c5d2 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/connection.test.ts @@ -0,0 +1,196 @@ +import type { ConnectionRecordProps, CustomConnectionTags } from '../../../../../modules/connections' +import type { MediationRecordProps } from '../../../../../modules/routing' + +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { Agent } from '../../../../../agent/Agent' +import { + ConnectionRecord, + ConnectionRepository, + ConnectionType, + DidExchangeRole, + DidExchangeState, +} from '../../../../../modules/connections' +import { MediationRecord, MediationState, MediationRepository, MediationRole } from '../../../../../modules/routing' +import { JsonTransformer } from '../../../../../utils' +import * as testModule from '../connection' + +const agentConfig = getAgentConfig('Migration ConnectionRecord 0.2-0.3') +const agentContext = getAgentContext() + +jest.mock('../../../../../modules/connections/repository/ConnectionRepository') +const ConnectionRepositoryMock = ConnectionRepository as jest.Mock +const connectionRepository = new ConnectionRepositoryMock() + +jest.mock('../../../../../modules/routing/repository/MediationRepository') +const MediationRepositoryMock = MediationRepository as jest.Mock +const mediationRepository = new MediationRepositoryMock() + +jest.mock('../../../../../agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn((token) => (token === ConnectionRepositoryMock ? connectionRepository : mediationRepository)), + }, + })), + } +}) + +const AgentMock = Agent as jest.Mock + +describe('0.2-0.3 | Connection', () => { + let agent: Agent + + beforeEach(() => { + agent = AgentMock() + }) + + describe('migrateConnectionRecordToV0_3', () => { + it('should fetch all records and apply the needed updates', async () => { + const connectionRecordsProps = [ + getConnection({ + state: DidExchangeState.Completed, + role: DidExchangeRole.Responder, + id: 'theConnectionId', + }), + getConnection({ + state: DidExchangeState.Completed, + role: DidExchangeRole.Responder, + id: 'theConnectionId2', + }), + ] + + const mediationRecordsProps = [ + getMediator({ + state: MediationState.Granted, + role: MediationRole.Recipient, + connectionId: 'theConnectionId', + threadId: 'theThreadId', + }), + ] + + const connectionRecords: ConnectionRecord[] = connectionRecordsProps + + mockFunction(connectionRepository.getAll).mockResolvedValue(connectionRecords) + + const mediationRecords: MediationRecord[] = mediationRecordsProps + + mockFunction(mediationRepository.getAll).mockResolvedValue(mediationRecords) + + await testModule.migrateConnectionRecordToV0_3(agent) + + expect(connectionRepository.getAll).toBeCalledTimes(1) + expect(mediationRepository.getAll).toBeCalledTimes(1) + expect(connectionRepository.update).toBeCalledTimes(connectionRecords.length) + }) + }) + + describe('migrateConnectionRecordMediatorTags', () => { + it('should set the mediator connection type on the record, connection type tags should be undefined', async () => { + const connectionRecordProps = { + state: DidExchangeState.Completed, + role: DidExchangeRole.Responder, + id: 'theConnectionId', + } + + const connectionRecord = getConnection(connectionRecordProps) + + await testModule.migrateConnectionRecordTags(agent, connectionRecord, new Set(['theConnectionId'])) + + expect(connectionRecord.toJSON()).toEqual({ + ...connectionRecordProps, + connectionTypes: [ConnectionType.Mediator], + _tags: { + connectionType: undefined, + }, + previousDids: [], + previousTheirDids: [], + metadata: {}, + }) + }) + + it('should add the mediator connection type to existing types on the record, connection type tags should be undefined', async () => { + const connectionRecordProps = { + state: DidExchangeState.Completed, + role: DidExchangeRole.Responder, + id: 'theConnectionId', + _tags: { + connectionType: ['theConnectionType'], + }, + } + + const connectionRecord = getConnection(connectionRecordProps) + + await testModule.migrateConnectionRecordTags(agent, connectionRecord, new Set(['theConnectionId'])) + + expect(connectionRecord.toJSON()).toEqual({ + ...connectionRecordProps, + connectionTypes: ['theConnectionType', ConnectionType.Mediator], + _tags: { + connectionType: undefined, + }, + previousDids: [], + previousTheirDids: [], + metadata: {}, + }) + }) + + it('should not set the mediator connection type on the record, connection type tags should be undefined', async () => { + const connectionRecordProps = { + state: DidExchangeState.Completed, + role: DidExchangeRole.Responder, + id: 'theConnectionId', + } + + const connectionRecord = getConnection(connectionRecordProps) + + await testModule.migrateConnectionRecordTags(agent, connectionRecord) + + expect(connectionRecord.toJSON()).toEqual({ + ...connectionRecordProps, + connectionTypes: [], + previousDids: [], + previousTheirDids: [], + _tags: { + connectionType: undefined, + }, + metadata: {}, + }) + }) + + it('should not add the mediator connection type to existing types on the record, connection type tags should be undefined', async () => { + const connectionRecordProps = { + state: DidExchangeState.Completed, + role: DidExchangeRole.Responder, + id: 'theConnectionId', + _tags: { + connectionType: ['theConnectionType'], + }, + } + + const connectionRecord = getConnection(connectionRecordProps) + + await testModule.migrateConnectionRecordTags(agent, connectionRecord) + + expect(connectionRecord.toJSON()).toEqual({ + ...connectionRecordProps, + connectionTypes: ['theConnectionType'], + _tags: { + connectionType: undefined, + }, + metadata: {}, + previousDids: [], + previousTheirDids: [], + }) + }) + }) +}) + +function getConnection({ state, role, id, _tags }: ConnectionRecordProps & { _tags?: CustomConnectionTags }) { + return JsonTransformer.fromJSON({ state, role, id, _tags }, ConnectionRecord) +} + +function getMediator({ state, role, connectionId, threadId }: MediationRecordProps) { + return JsonTransformer.fromJSON({ state, role, connectionId, threadId }, MediationRecord) +} diff --git a/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/proof.test.ts b/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/proof.test.ts new file mode 100644 index 0000000000..e35af14c16 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/proof.test.ts @@ -0,0 +1,310 @@ +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { Agent } from '../../../../../agent/Agent' +import { ProofExchangeRecord, ProofState } from '../../../../../modules/proofs' +import { ProofRepository } from '../../../../../modules/proofs/repository/ProofRepository' +import { JsonTransformer } from '../../../../../utils' +import { DidCommMessageRole } from '../../../../didcomm' +import { DidCommMessageRepository } from '../../../../didcomm/DidCommMessageRepository' +import * as testModule from '../proof' + +const agentConfig = getAgentConfig('Migration ProofExchangeRecord 0.2-0.3') +const agentContext = getAgentContext() + +jest.mock('../../../../../modules/proofs/repository/ProofRepository') +const ProofRepositoryMock = ProofRepository as jest.Mock +const proofRepository = new ProofRepositoryMock() + +jest.mock('../../../../didcomm/DidCommMessageRepository') +const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock +const didCommMessageRepository = new DidCommMessageRepositoryMock() + +jest.mock('../../../../../agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn((token) => (token === ProofRepositoryMock ? proofRepository : didCommMessageRepository)), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.2-0.3 | Proof', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + afterEach(() => { + mockFunction(didCommMessageRepository.save).mockReset() + }) + + describe('migrateProofExchangeRecordToV0_3()', () => { + it('should fetch all records and apply the needed updates ', async () => { + const records: ProofExchangeRecord[] = [getProof({})] + + mockFunction(proofRepository.getAll).mockResolvedValue(records) + + await testModule.migrateProofExchangeRecordToV0_3(agent) + + expect(proofRepository.getAll).toHaveBeenCalledTimes(1) + expect(proofRepository.update).toHaveBeenCalledTimes(records.length) + + const updatedRecord = mockFunction(proofRepository.update).mock.calls[0][1] + + // Check first object is transformed correctly + expect(updatedRecord.toJSON()).toMatchObject({ + protocolVersion: 'v1', + }) + }) + }) + + describe('migrateInternalProofExchangeRecordProperties()', () => { + it('should set the protocol version to v1 if not set on the record', async () => { + const proofRecord = getProof({}) + + await testModule.migrateInternalProofExchangeRecordProperties(agent, proofRecord) + + expect(proofRecord).toMatchObject({ + protocolVersion: 'v1', + }) + }) + + it('should not set the protocol version if a value is already set', async () => { + const proofRecord = getProof({ + protocolVersion: 'v2', + }) + + await testModule.migrateInternalProofExchangeRecordProperties(agent, proofRecord) + + expect(proofRecord).toMatchObject({ + protocolVersion: 'v2', + }) + }) + }) + + describe('moveDidCommMessages()', () => { + it('should move the proposalMessage, requestMessage and presentationMessage to the didCommMessageRepository', async () => { + const proposalMessage = { '@type': 'ProposalMessage' } + const requestMessage = { '@type': 'RequestMessage' } + const presentationMessage = { '@type': 'ProofMessage' } + + const proofRecord = getProof({ + id: 'theProofId', + state: ProofState.Done, + proposalMessage, + requestMessage, + presentationMessage, + }) + + await testModule.moveDidCommMessages(agent, proofRecord) + + expect(didCommMessageRepository.save).toHaveBeenCalledTimes(3) + const [[, proposalMessageRecord], [, requestMessageRecord], [, presentationMessageRecord]] = mockFunction( + didCommMessageRepository.save + ).mock.calls + + expect(proposalMessageRecord).toMatchObject({ + role: DidCommMessageRole.Sender, + associatedRecordId: 'theProofId', + message: proposalMessage, + }) + + expect(requestMessageRecord).toMatchObject({ + role: DidCommMessageRole.Receiver, + associatedRecordId: 'theProofId', + message: requestMessage, + }) + + expect(presentationMessageRecord).toMatchObject({ + role: DidCommMessageRole.Sender, + associatedRecordId: 'theProofId', + message: presentationMessage, + }) + + expect(proofRecord.toJSON()).toEqual({ + _tags: {}, + protocolVersion: undefined, + id: 'theProofId', + state: ProofState.Done, + metadata: {}, + isVerified: undefined, + }) + }) + + it('should only move the messages which exist in the record', async () => { + const proposalMessage = { '@type': 'ProposalMessage' } + + const proofRecord = getProof({ + id: 'theProofId', + state: ProofState.Done, + proposalMessage, + isVerified: true, + }) + + await testModule.moveDidCommMessages(agent, proofRecord) + + expect(didCommMessageRepository.save).toHaveBeenCalledTimes(1) + const [[, proposalMessageRecord]] = mockFunction(didCommMessageRepository.save).mock.calls + + expect(proposalMessageRecord).toMatchObject({ + role: DidCommMessageRole.Receiver, + associatedRecordId: 'theProofId', + message: proposalMessage, + }) + + expect(proofRecord.toJSON()).toEqual({ + _tags: {}, + protocolVersion: undefined, + id: 'theProofId', + state: ProofState.Done, + metadata: {}, + isVerified: true, + presentationMessage: undefined, + requestMessage: undefined, + }) + }) + + it('should determine the correct DidCommMessageRole for each message', async () => { + const proposalMessage = { '@type': 'ProposalMessage' } + const requestMessage = { '@type': 'RequestMessage' } + const presentationMessage = { '@type': 'ProofMessage' } + + const proofRecord = getProof({ + id: 'theProofId', + state: ProofState.Done, + proposalMessage, + requestMessage, + presentationMessage, + }) + + await testModule.moveDidCommMessages(agent, proofRecord) + + expect(didCommMessageRepository.save).toHaveBeenCalledTimes(3) + const [[, proposalMessageRecord], [, requestMessageRecord], [, presentationMessageRecord]] = mockFunction( + didCommMessageRepository.save + ).mock.calls + + expect(proposalMessageRecord).toMatchObject({ + role: DidCommMessageRole.Sender, + associatedRecordId: 'theProofId', + message: proposalMessage, + }) + + expect(requestMessageRecord).toMatchObject({ + role: DidCommMessageRole.Receiver, + associatedRecordId: 'theProofId', + message: requestMessage, + }) + + expect(presentationMessageRecord).toMatchObject({ + role: DidCommMessageRole.Sender, + associatedRecordId: 'theProofId', + message: presentationMessage, + }) + + expect(proofRecord.toJSON()).toEqual({ + _tags: {}, + metadata: {}, + protocolVersion: undefined, + id: 'theProofId', + state: ProofState.Done, + }) + }) + }) + + describe('getProofRole', () => { + it('should return ProofRole.Verifier if isVerified is set', () => { + expect( + testModule.getProofRole( + getProof({ + isVerified: true, + }) + ) + ).toBe(testModule.V02_03MigrationProofRole.Verifier) + + expect( + testModule.getProofRole( + getProof({ + isVerified: false, + }) + ) + ).toBe(testModule.V02_03MigrationProofRole.Verifier) + }) + + it('should return ProofRole.Prover if state is Done and isVerified is not set', () => { + const proofRecord = getProof({ + state: ProofState.Done, + }) + + expect(testModule.getProofRole(proofRecord)).toBe(testModule.V02_03MigrationProofRole.Prover) + }) + + it('should return ProofRole.Prover if the value is a prover state', () => { + const holderStates = [ + ProofState.Declined, + ProofState.ProposalSent, + ProofState.RequestReceived, + ProofState.PresentationSent, + ] + + for (const holderState of holderStates) { + expect( + testModule.getProofRole( + getProof({ + state: holderState, + }) + ) + ).toBe(testModule.V02_03MigrationProofRole.Prover) + } + }) + + it('should return ProofRole.Verifier if the state is not a prover state, isVerified is not set and the state is not Done', () => { + expect( + testModule.getProofRole( + getProof({ + state: ProofState.PresentationReceived, + }) + ) + ).toBe(testModule.V02_03MigrationProofRole.Verifier) + }) + }) +}) + +function getProof({ + protocolVersion, + proposalMessage, + requestMessage, + presentationMessage, + state, + isVerified, + id, +}: { + protocolVersion?: string + /* eslint-disable @typescript-eslint/no-explicit-any */ + proposalMessage?: any + requestMessage?: any + presentationMessage?: any + /* eslint-enable @typescript-eslint/no-explicit-any */ + state?: ProofState + isVerified?: boolean + id?: string +}) { + return JsonTransformer.fromJSON( + { + protocolVersion, + proposalMessage, + requestMessage, + presentationMessage, + state, + isVerified, + id, + }, + ProofExchangeRecord + ) +} diff --git a/packages/core/src/storage/migration/updates/0.2-0.3/connection.ts b/packages/core/src/storage/migration/updates/0.2-0.3/connection.ts new file mode 100644 index 0000000000..80ec2fcfae --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.2-0.3/connection.ts @@ -0,0 +1,67 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' +import type { ConnectionRecord } from '../../../../modules/connections' + +import { ConnectionType, ConnectionRepository } from '../../../../modules/connections' +import { MediationRepository } from '../../../../modules/routing' + +/** + * Migrate the {@link ConnectionRecord} to a 0.3 compatible format. + * + * @param agent + */ +export async function migrateConnectionRecordToV0_3(agent: Agent) { + agent.config.logger.info('Migrating connection records to storage version 0.3') + const connectionRepository = agent.dependencyManager.resolve(ConnectionRepository) + const mediationRepository = agent.dependencyManager.resolve(MediationRepository) + + agent.config.logger.debug('Fetching all connection records from storage') + const allConnections = await connectionRepository.getAll(agent.context) + agent.config.logger.debug(`Found a total of ${allConnections.length} connection records to update`) + + agent.config.logger.debug('Fetching all mediation records from storage') + const allMediators = await mediationRepository.getAll(agent.context) + agent.config.logger.debug(`Found a total of ${allMediators.length} mediation records`) + + const mediatorConnectionIds = new Set(allMediators.map((mediator) => mediator.connectionId)) + + for (const connectionRecord of allConnections) { + agent.config.logger.debug(`Migrating connection record with id ${connectionRecord.id} to storage version 0.3`) + + await migrateConnectionRecordTags(agent, connectionRecord, mediatorConnectionIds) + await connectionRepository.update(agent.context, connectionRecord) + + agent.config.logger.debug( + `Successfully migrated connection record with id ${connectionRecord.id} to storage version 0.3` + ) + } +} + +/** + * + * @param agent + * @param connectionRecord + */ +export async function migrateConnectionRecordTags( + agent: Agent, + connectionRecord: ConnectionRecord, + mediatorConnectionIds: Set = new Set() +) { + agent.config.logger.debug( + `Migrating internal connection record type tags ${connectionRecord.id} to storage version 0.3` + ) + + // Old connection records will have tags set in the 'connectionType' property + const connectionTypeTags = (connectionRecord.getTags().connectionType || []) as [string] + const connectionTypes = [...connectionTypeTags] + + if (mediatorConnectionIds.has(connectionRecord.id) && !connectionTypes.includes(ConnectionType.Mediator)) { + connectionTypes.push(ConnectionType.Mediator) + } + + connectionRecord.connectionTypes = connectionTypes + connectionRecord.setTag('connectionType', undefined) + + agent.config.logger.debug( + `Successfully migrated internal connection record type tags ${connectionRecord.id} to storage version 0.3` + ) +} diff --git a/packages/core/src/storage/migration/updates/0.2-0.3/index.ts b/packages/core/src/storage/migration/updates/0.2-0.3/index.ts new file mode 100644 index 0000000000..60a56fa546 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.2-0.3/index.ts @@ -0,0 +1,9 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' + +import { migrateConnectionRecordToV0_3 } from './connection' +import { migrateProofExchangeRecordToV0_3 } from './proof' + +export async function updateV0_2ToV0_3(agent: Agent): Promise { + await migrateProofExchangeRecordToV0_3(agent) + await migrateConnectionRecordToV0_3(agent) +} diff --git a/packages/core/src/storage/migration/updates/0.2-0.3/proof.ts b/packages/core/src/storage/migration/updates/0.2-0.3/proof.ts new file mode 100644 index 0000000000..e92cd73649 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.2-0.3/proof.ts @@ -0,0 +1,162 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' +import type { ProofExchangeRecord } from '../../../../modules/proofs' +import type { JsonObject, PlaintextMessage } from '../../../../types' + +import { ProofState } from '../../../../modules/proofs/models' +import { ProofRepository } from '../../../../modules/proofs/repository/ProofRepository' +import { DidCommMessageRepository, DidCommMessageRecord, DidCommMessageRole } from '../../../didcomm' + +/** + * Migrates the {@link ProofExchangeRecord} to 0.3 compatible format. It fetches all records from storage + * and applies the needed updates to the records. After a record has been transformed, it is updated + * in storage and the next record will be transformed. + * + * The following transformations are applied: + * - {@link migrateInternalProofExchangeRecordProperties} + * - {@link moveDidCommMessages} + */ +export async function migrateProofExchangeRecordToV0_3(agent: Agent) { + agent.config.logger.info('Migrating proof records to storage version 0.3') + const proofRepository = agent.dependencyManager.resolve(ProofRepository) + + agent.config.logger.debug(`Fetching all proof records from storage`) + const allProofs = await proofRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${allProofs.length} proof records to update.`) + for (const proofRecord of allProofs) { + agent.config.logger.debug(`Migrating proof record with id ${proofRecord.id} to storage version 0.3`) + + await migrateInternalProofExchangeRecordProperties(agent, proofRecord) + await moveDidCommMessages(agent, proofRecord) + + await proofRepository.update(agent.context, proofRecord) + + agent.config.logger.debug(`Successfully migrated proof record with id ${proofRecord.id} to storage version 0.3`) + } +} + +export enum V02_03MigrationProofRole { + Verifier, + Prover, +} + +const proverProofStates = [ + ProofState.Declined, + ProofState.ProposalSent, + ProofState.RequestReceived, + ProofState.PresentationSent, + ProofState.Done, +] + +const didCommMessageRoleMapping = { + [V02_03MigrationProofRole.Verifier]: { + proposalMessage: DidCommMessageRole.Receiver, + requestMessage: DidCommMessageRole.Sender, + presentationMessage: DidCommMessageRole.Receiver, + }, + [V02_03MigrationProofRole.Prover]: { + proposalMessage: DidCommMessageRole.Sender, + requestMessage: DidCommMessageRole.Receiver, + presentationMessage: DidCommMessageRole.Sender, + }, +} + +const proofRecordMessageKeys = ['proposalMessage', 'requestMessage', 'presentationMessage'] as const + +export function getProofRole(proofRecord: ProofExchangeRecord) { + // Proofs will only have an isVerified value when a presentation is verified, meaning we're the verifier + if (proofRecord.isVerified !== undefined) { + return V02_03MigrationProofRole.Verifier + } + // If proofRecord.isVerified doesn't have any value, and we're also not in state done it means we're the prover. + else if (proofRecord.state === ProofState.Done) { + return V02_03MigrationProofRole.Prover + } + // For these states we know for certain that we're the prover + else if (proverProofStates.includes(proofRecord.state)) { + return V02_03MigrationProofRole.Prover + } + + // For all other states we can be certain we're the verifier + return V02_03MigrationProofRole.Verifier +} + +/** + * With the addition of support for different protocol versions the proof record now stores the protocol version. + * + * The following 0.2.0 proof record structure (unrelated keys omitted): + * + * ```json + * { + * } + * ``` + * + * Will be transformed into the following 0.3.0 structure (unrelated keys omitted): + * + * ```json + * { + * "protocolVersion: "v1" + * } + * ``` + */ +export async function migrateInternalProofExchangeRecordProperties( + agent: Agent, + proofRecord: ProofExchangeRecord +) { + agent.config.logger.debug(`Migrating internal proof record ${proofRecord.id} properties to storage version 0.3`) + + if (!proofRecord.protocolVersion) { + agent.config.logger.debug(`Setting protocolVersion to v1`) + proofRecord.protocolVersion = 'v1' + } + + agent.config.logger.debug( + `Successfully migrated internal proof record ${proofRecord.id} properties to storage version 0.3` + ) +} + +/** + * In 0.3.0 the v1 didcomm messages have been moved out of the proof record into separate record using the DidCommMessageRepository. + * This migration scripts extracts all message (proposalMessage, requestMessage, presentationMessage) and moves + * them into the DidCommMessageRepository. + */ +export async function moveDidCommMessages(agent: Agent, proofRecord: ProofExchangeRecord) { + agent.config.logger.debug( + `Moving didcomm messages from proof record with id ${proofRecord.id} to DidCommMessageRecord` + ) + const didCommMessageRepository = agent.dependencyManager.resolve(DidCommMessageRepository) + + for (const messageKey of proofRecordMessageKeys) { + agent.config.logger.debug( + `Starting move of ${messageKey} from proof record with id ${proofRecord.id} to DIDCommMessageRecord` + ) + const proofRecordJson = proofRecord as unknown as JsonObject + const message = proofRecordJson[messageKey] as PlaintextMessage | undefined + + if (message) { + const proofRole = getProofRole(proofRecord) + const didCommMessageRole = didCommMessageRoleMapping[proofRole][messageKey] + + const didCommMessageRecord = new DidCommMessageRecord({ + role: didCommMessageRole, + associatedRecordId: proofRecord.id, + message, + }) + await didCommMessageRepository.save(agent.context, didCommMessageRecord) + + agent.config.logger.debug( + `Successfully moved ${messageKey} from proof record with id ${proofRecord.id} to DIDCommMessageRecord` + ) + + delete proofRecordJson[messageKey] + } else { + agent.config.logger.debug( + `Proof record with id ${proofRecord.id} does not have a ${messageKey}. Not creating a DIDCommMessageRecord` + ) + } + } + + agent.config.logger.debug( + `Successfully moved didcomm messages from proof record with id ${proofRecord.id} to DIDCommMessageRecord` + ) +} diff --git a/packages/core/src/storage/migration/updates/0.3-0.3.1/__tests__/did.test.ts b/packages/core/src/storage/migration/updates/0.3-0.3.1/__tests__/did.test.ts new file mode 100644 index 0000000000..7ce585b93a --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.3-0.3.1/__tests__/did.test.ts @@ -0,0 +1,74 @@ +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { AgentContext } from '../../../../../agent' +import { Agent } from '../../../../../agent/Agent' +import { DidDocumentRole, DidRecord } from '../../../../../modules/dids' +import { DidRepository } from '../../../../../modules/dids/repository/DidRepository' +import { JsonTransformer } from '../../../../../utils' +import { Metadata } from '../../../../Metadata' +import * as testModule from '../did' + +const agentConfig = getAgentConfig('Migration DidRecord 0.3-0.3.1') +const agentContext = getAgentContext() + +jest.mock('../../../../../modules/dids/repository/DidRepository') +const DidRepositoryMock = DidRepository as jest.Mock +const didRepository = new DidRepositoryMock() + +jest.mock('../../../../../agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn(() => didRepository), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.3-0.3.1 | Did', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + describe('migrateDidRecordToV0_3_1()', () => { + it('should fetch all records and apply the needed updates ', async () => { + const records: DidRecord[] = [getDid({ id: 'did:peer:123' })] + + mockFunction(didRepository.getAll).mockResolvedValue(records) + + await testModule.migrateDidRecordToV0_3_1(agent) + + expect(didRepository.getAll).toHaveBeenCalledTimes(1) + expect(didRepository.save).toHaveBeenCalledTimes(1) + + const [, didRecord] = mockFunction(didRepository.save).mock.calls[0] + expect(didRecord).toEqual({ + type: 'DidRecord', + id: expect.any(String), + did: 'did:peer:123', + metadata: expect.any(Metadata), + role: DidDocumentRole.Created, + _tags: {}, + }) + + expect(didRepository.deleteById).toHaveBeenCalledTimes(1) + expect(didRepository.deleteById).toHaveBeenCalledWith(expect.any(AgentContext), 'did:peer:123') + }) + }) +}) + +function getDid({ id }: { id: string }) { + return JsonTransformer.fromJSON( + { + role: DidDocumentRole.Created, + id, + }, + DidRecord + ) +} diff --git a/packages/core/src/storage/migration/updates/0.3-0.3.1/did.ts b/packages/core/src/storage/migration/updates/0.3-0.3.1/did.ts new file mode 100644 index 0000000000..4b8d0571f6 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.3-0.3.1/did.ts @@ -0,0 +1,42 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' + +import { DidRepository } from '../../../../modules/dids' +import { uuid } from '../../../../utils/uuid' + +/** + * Migrates the {@link DidRecord} to 0.3 compatible format. It fetches all records from storage + * and applies the needed updates to the records. After a record has been transformed, it is updated + * in storage and the next record will be transformed. + * + * The following transformations are applied: + * - {@link extractDidAsSeparateProperty} + */ +export async function migrateDidRecordToV0_3_1(agent: Agent) { + agent.config.logger.info('Migrating did records to storage version 0.3.1') + const didRepository = agent.dependencyManager.resolve(DidRepository) + + agent.config.logger.debug(`Fetching all did records from storage`) + const allDids = await didRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${allDids.length} did records to update.`) + for (const didRecord of allDids) { + agent.config.logger.debug(`Migrating did record with id ${didRecord.id} to storage version 0.3.1`) + + const newId = uuid() + + agent.config.logger.debug(`Updating id ${didRecord.id} to ${newId} for did record`) + // The id of the didRecord was previously the did itself. This prevented us from connecting to ourselves + didRecord.did = didRecord.id + didRecord.id = newId + + // Save new did record + await didRepository.save(agent.context, didRecord) + + // Delete old did record + await didRepository.deleteById(agent.context, didRecord.did) + + agent.config.logger.debug( + `Successfully migrated did record with old id ${didRecord.did} to new id ${didRecord.id} to storage version 0.3.1` + ) + } +} diff --git a/packages/core/src/storage/migration/updates/0.3-0.3.1/index.ts b/packages/core/src/storage/migration/updates/0.3-0.3.1/index.ts new file mode 100644 index 0000000000..6d9e6b40ab --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.3-0.3.1/index.ts @@ -0,0 +1,7 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' + +import { migrateDidRecordToV0_3_1 } from './did' + +export async function updateV0_3ToV0_3_1(agent: Agent): Promise { + await migrateDidRecordToV0_3_1(agent) +} diff --git a/packages/core/src/storage/migration/updates/0.3.1-0.4/__tests__/cache.test.ts b/packages/core/src/storage/migration/updates/0.3.1-0.4/__tests__/cache.test.ts new file mode 100644 index 0000000000..477cdb0ffa --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.3.1-0.4/__tests__/cache.test.ts @@ -0,0 +1,53 @@ +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { Agent } from '../../../../../agent/Agent' +import * as testModule from '../cache' + +const agentConfig = getAgentConfig('Migration Cache 0.3.1-0.4') +const agentContext = getAgentContext() + +const storageService = { + getAll: jest.fn(), + deleteById: jest.fn(), +} + +jest.mock('../../../../../agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn(() => storageService), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.3.1-0.4 | Cache', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + describe('migrateCacheToV0_4()', () => { + it('should fetch all cache records and remove them ', async () => { + const records = [{ id: 'first' }, { id: 'second' }] + + mockFunction(storageService.getAll).mockResolvedValue(records) + + await testModule.migrateCacheToV0_4(agent) + + expect(storageService.getAll).toHaveBeenCalledTimes(1) + expect(storageService.getAll).toHaveBeenCalledWith(agent.context, expect.anything()) + expect(storageService.deleteById).toHaveBeenCalledTimes(2) + + const [, , firstId] = mockFunction(storageService.deleteById).mock.calls[0] + const [, , secondId] = mockFunction(storageService.deleteById).mock.calls[1] + expect(firstId).toEqual('first') + expect(secondId).toEqual('second') + }) + }) +}) diff --git a/packages/core/src/storage/migration/updates/0.3.1-0.4/__tests__/did.test.ts b/packages/core/src/storage/migration/updates/0.3.1-0.4/__tests__/did.test.ts new file mode 100644 index 0000000000..fce2f75fb3 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.3.1-0.4/__tests__/did.test.ts @@ -0,0 +1,81 @@ +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { Agent } from '../../../../../agent/Agent' +import { DidDocumentRole, DidRecord } from '../../../../../modules/dids' +import { DidRepository } from '../../../../../modules/dids/repository/DidRepository' +import { JsonTransformer } from '../../../../../utils' +import { uuid } from '../../../../../utils/uuid' +import { Metadata } from '../../../../Metadata' +import * as testModule from '../did' + +const agentConfig = getAgentConfig('Migration DidRecord 0.3.1-0.4') +const agentContext = getAgentContext() + +jest.mock('../../../../../modules/dids/repository/DidRepository') +const DidRepositoryMock = DidRepository as jest.Mock +const didRepository = new DidRepositoryMock() + +jest.mock('../../../../../agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn(() => didRepository), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.3.1-0.4 | Did', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + describe('migrateDidRecordToV0_4()', () => { + it('should fetch all records and apply the needed updates ', async () => { + const records: DidRecord[] = [getDid({ did: 'did:sov:123', qualifiedIndyDid: 'did:indy:local:123' })] + + mockFunction(didRepository.findByQuery).mockResolvedValue(records) + + await testModule.migrateDidRecordToV0_4(agent) + + expect(didRepository.findByQuery).toHaveBeenCalledTimes(1) + expect(didRepository.findByQuery).toHaveBeenCalledWith(agent.context, { + method: 'sov', + role: DidDocumentRole.Created, + }) + expect(didRepository.findByQuery).toHaveBeenCalledTimes(1) + + const [, didRecord] = mockFunction(didRepository.update).mock.calls[0] + expect(didRecord).toEqual({ + type: 'DidRecord', + id: expect.any(String), + did: 'did:indy:local:123', + metadata: expect.any(Metadata), + role: DidDocumentRole.Created, + _tags: { + qualifiedIndyDid: undefined, + }, + }) + }) + }) +}) + +function getDid({ did, qualifiedIndyDid }: { did: string; qualifiedIndyDid: string }) { + return JsonTransformer.fromJSON( + { + role: DidDocumentRole.Created, + id: uuid(), + did, + _tags: { + qualifiedIndyDid, + }, + }, + DidRecord + ) +} diff --git a/packages/core/src/storage/migration/updates/0.3.1-0.4/__tests__/w3cCredentialRecord.test.ts b/packages/core/src/storage/migration/updates/0.3.1-0.4/__tests__/w3cCredentialRecord.test.ts new file mode 100644 index 0000000000..4dd7339b8c --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.3.1-0.4/__tests__/w3cCredentialRecord.test.ts @@ -0,0 +1,63 @@ +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { Agent } from '../../../../../agent/Agent' +import { W3cCredentialRecord, W3cJsonLdVerifiableCredential } from '../../../../../modules/vc' +import { Ed25519Signature2018Fixtures } from '../../../../../modules/vc/data-integrity/__tests__/fixtures' +import { JsonTransformer } from '../../../../../utils' +import * as testModule from '../w3cCredentialRecord' + +const agentConfig = getAgentConfig('Migration W3cCredentialRecord 0.3.1-0.4') +const agentContext = getAgentContext() + +const repository = { + getAll: jest.fn(), + update: jest.fn(), +} + +jest.mock('../../../../../agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn(() => repository), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.3.1-0.4 | W3cCredentialRecord', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + describe('migrateW3cCredentialRecordToV0_4()', () => { + it('should fetch all w3c credential records and re-save them', async () => { + const records = [ + new W3cCredentialRecord({ + tags: {}, + id: '3b3cf6ca-fa09-4498-b891-e280fbbb7fa7', + credential: JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ), + }), + ] + + mockFunction(repository.getAll).mockResolvedValue(records) + + await testModule.migrateW3cCredentialRecordToV0_4(agent) + + expect(repository.getAll).toHaveBeenCalledTimes(1) + expect(repository.getAll).toHaveBeenCalledWith(agent.context) + expect(repository.update).toHaveBeenCalledTimes(1) + + const [, record] = mockFunction(repository.update).mock.calls[0] + expect(record.getTags().claimFormat).toEqual('ldp_vc') + }) + }) +}) diff --git a/packages/core/src/storage/migration/updates/0.3.1-0.4/cache.ts b/packages/core/src/storage/migration/updates/0.3.1-0.4/cache.ts new file mode 100644 index 0000000000..5ee3174e3b --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.3.1-0.4/cache.ts @@ -0,0 +1,32 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' +import type { StorageService } from '../../../StorageService' + +import { InjectionSymbols } from '../../../../constants' +import { BaseRecord } from '../../../BaseRecord' + +/** + * removes the all cache records as used in 0.3.0, as they have been updated to use the new cache interface. + */ +export async function migrateCacheToV0_4(agent: Agent) { + agent.config.logger.info('Removing 0.3 cache records from storage') + + const storageService = agent.dependencyManager.resolve>(InjectionSymbols.StorageService) + + agent.config.logger.debug(`Fetching all cache records`) + const records = await storageService.getAll(agent.context, CacheRecord) + + for (const record of records) { + agent.config.logger.debug(`Removing cache record with id ${record.id}`) + await storageService.deleteById(agent.context, CacheRecord, record.id) + agent.config.logger.debug(`Successfully removed cache record with id ${record.id}`) + } +} + +class CacheRecord extends BaseRecord { + public static readonly type = 'CacheRecord' + public readonly type = CacheRecord.type + + public getTags() { + return this._tags + } +} diff --git a/packages/core/src/storage/migration/updates/0.3.1-0.4/did.ts b/packages/core/src/storage/migration/updates/0.3.1-0.4/did.ts new file mode 100644 index 0000000000..f9844b8c1c --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.3.1-0.4/did.ts @@ -0,0 +1,51 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' +import type { DidRecord } from '../../../../modules/dids' + +import { DidDocumentRole, DidRepository } from '../../../../modules/dids' + +/** + * Migrates the {@link DidRecord} to 0.4 compatible format. It fetches all did records from storage + * with method sov and applies the needed updates to the records. After a record has been transformed, it is updated + * in storage and the next record will be transformed. + * + * The following transformations are applied: + * - {@link migrateSovDidToIndyDid} + */ +export async function migrateDidRecordToV0_4(agent: Agent) { + agent.config.logger.info('Migrating did records to storage version 0.4') + const didRepository = agent.dependencyManager.resolve(DidRepository) + + agent.config.logger.debug(`Fetching all did records with did method did:sov from storage`) + const allSovDids = await didRepository.findByQuery(agent.context, { + method: 'sov', + role: DidDocumentRole.Created, + }) + + agent.config.logger.debug(`Found a total of ${allSovDids.length} did:sov did records to update.`) + for (const sovDidRecord of allSovDids) { + agent.config.logger.debug(`Migrating did:sov did record with id ${sovDidRecord.id} to storage version 0.4`) + + const oldDid = sovDidRecord.did + migrateSovDidToIndyDid(agent, sovDidRecord) + + // Save updated did record + await didRepository.update(agent.context, sovDidRecord) + + agent.config.logger.debug( + `Successfully migrated did:sov did record with old did ${oldDid} to new did ${sovDidRecord.did} for storage version 0.4` + ) + } +} + +export function migrateSovDidToIndyDid(agent: Agent, didRecord: DidRecord) { + agent.config.logger.debug( + `Migrating did record with id ${didRecord.id} and did ${didRecord.did} to indy did for version 0.4` + ) + + const qualifiedIndyDid = didRecord.getTag('qualifiedIndyDid') as string + + didRecord.did = qualifiedIndyDid + + // Unset qualifiedIndyDid tag + didRecord.setTag('qualifiedIndyDid', undefined) +} diff --git a/packages/core/src/storage/migration/updates/0.3.1-0.4/index.ts b/packages/core/src/storage/migration/updates/0.3.1-0.4/index.ts new file mode 100644 index 0000000000..0dc939dbed --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.3.1-0.4/index.ts @@ -0,0 +1,11 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' + +import { migrateCacheToV0_4 } from './cache' +import { migrateDidRecordToV0_4 } from './did' +import { migrateW3cCredentialRecordToV0_4 } from './w3cCredentialRecord' + +export async function updateV0_3_1ToV0_4(agent: Agent): Promise { + await migrateDidRecordToV0_4(agent) + await migrateCacheToV0_4(agent) + await migrateW3cCredentialRecordToV0_4(agent) +} diff --git a/packages/core/src/storage/migration/updates/0.3.1-0.4/w3cCredentialRecord.ts b/packages/core/src/storage/migration/updates/0.3.1-0.4/w3cCredentialRecord.ts new file mode 100644 index 0000000000..945083bab0 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.3.1-0.4/w3cCredentialRecord.ts @@ -0,0 +1,28 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' + +import { W3cCredentialRepository } from '../../../../modules/vc/repository' + +/** + * Re-saves the w3c credential records to add the new claimFormat tag. + */ +export async function migrateW3cCredentialRecordToV0_4(agent: Agent) { + agent.config.logger.info('Migration w3c credential records records to storage version 0.4') + + const w3cCredentialRepository = agent.dependencyManager.resolve(W3cCredentialRepository) + + agent.config.logger.debug(`Fetching all w3c credential records from storage`) + const records = await w3cCredentialRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${records.length} w3c credential records to update.`) + + for (const record of records) { + agent.config.logger.debug( + `Re-saving w3c credential record with id ${record.id} to add claimFormat tag for storage version 0.4` + ) + + // Save updated record + await w3cCredentialRepository.update(agent.context, record) + + agent.config.logger.debug(`Successfully migrated w3c credential record with id ${record.id} to storage version 0.4`) + } +} diff --git a/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/credentialExchangeRecord.test.ts b/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/credentialExchangeRecord.test.ts new file mode 100644 index 0000000000..90895c6075 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/credentialExchangeRecord.test.ts @@ -0,0 +1,184 @@ +import type { CredentialRecordBinding } from '../../../../../modules/credentials' + +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests' +import { Agent } from '../../../../../agent/Agent' +import { CredentialRole, CredentialState, CredentialExchangeRecord } from '../../../../../modules/credentials' +import { CredentialRepository } from '../../../../../modules/credentials/repository/CredentialRepository' +import { JsonTransformer } from '../../../../../utils' +import { DidCommMessageRecord, DidCommMessageRole } from '../../../../didcomm' +import { DidCommMessageRepository } from '../../../../didcomm/DidCommMessageRepository' +import * as testModule from '../credentialExchangeRecord' + +const agentConfig = getAgentConfig('Migration - Credential Exchange Record - 0.4-0.5') +const agentContext = getAgentContext() + +jest.mock('../../../../../modules/credentials/repository/CredentialRepository') +const CredentialRepositoryMock = CredentialRepository as jest.Mock +const credentialRepository = new CredentialRepositoryMock() + +jest.mock('../../../../didcomm/DidCommMessageRepository') +const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock +const didCommMessageRepository = new DidCommMessageRepositoryMock() + +jest.mock('../../../../../agent/Agent', () => ({ + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn((injectionToken) => + injectionToken === CredentialRepository ? credentialRepository : didCommMessageRepository + ), + }, + })), +})) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.4-0.5 | Migration | Credential Exchange Record', () => { + let agent: Agent + + beforeAll(() => { + agent = new AgentMock() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('migrateCredentialExchangeRecordToV0_5()', () => { + it('should fetch all records and apply the needed updates ', async () => { + const records: CredentialExchangeRecord[] = [getCredentialRecord({ state: CredentialState.OfferSent })] + + mockFunction(credentialRepository.getAll).mockResolvedValue(records) + + await testModule.migrateCredentialExchangeRecordToV0_5(agent) + + expect(credentialRepository.getAll).toHaveBeenCalledTimes(1) + expect(credentialRepository.update).toHaveBeenCalledTimes(1) + + const [, credentialRecord] = mockFunction(credentialRepository.update).mock.calls[0] + expect(credentialRecord.toJSON()).toMatchObject({ + role: CredentialRole.Issuer, + }) + }) + }) + + describe('migrateRole()', () => { + // according to: https://github.com/hyperledger/aries-rfcs/blob/main/features/0036-issue-credential/README.md#states + genMigrateRoleTests(CredentialState.ProposalReceived, CredentialRole.Issuer) + genMigrateRoleTests(CredentialState.OfferSent, CredentialRole.Issuer) + genMigrateRoleTests(CredentialState.RequestReceived, CredentialRole.Issuer) + genMigrateRoleTests(CredentialState.CredentialIssued, CredentialRole.Issuer) + genMigrateRoleTests(CredentialState.Done, CredentialRole.Issuer, { doneStateWithCredentials: false }) + + genMigrateRoleTests(CredentialState.ProposalSent, CredentialRole.Holder) + genMigrateRoleTests(CredentialState.OfferReceived, CredentialRole.Holder) + genMigrateRoleTests(CredentialState.RequestSent, CredentialRole.Holder) + genMigrateRoleTests(CredentialState.CredentialReceived, CredentialRole.Holder) + genMigrateRoleTests(CredentialState.Done, CredentialRole.Holder, { doneStateWithCredentials: true }) + genMigrateRoleTests(CredentialState.Declined, CredentialRole.Holder) + + genMigrateRoleTests(CredentialState.Abandoned, CredentialRole.Issuer, { + didCommMessage: { messageName: 'propose-credential', didCommMessageRole: DidCommMessageRole.Receiver }, + }) + genMigrateRoleTests(CredentialState.Abandoned, CredentialRole.Holder, { + didCommMessage: { messageName: 'propose-credential', didCommMessageRole: DidCommMessageRole.Sender }, + }) + + genMigrateRoleTests(CredentialState.Abandoned, CredentialRole.Holder, { + didCommMessage: { messageName: 'offer-credential', didCommMessageRole: DidCommMessageRole.Receiver }, + }) + genMigrateRoleTests(CredentialState.Abandoned, CredentialRole.Issuer, { + didCommMessage: { messageName: 'offer-credential', didCommMessageRole: DidCommMessageRole.Sender }, + }) + + genMigrateRoleTests(CredentialState.Abandoned, CredentialRole.Issuer, { + didCommMessage: { messageName: 'request-credential', didCommMessageRole: DidCommMessageRole.Receiver }, + }) + genMigrateRoleTests(CredentialState.Abandoned, CredentialRole.Holder, { + didCommMessage: { messageName: 'request-credential', didCommMessageRole: DidCommMessageRole.Sender }, + }) + }) + + function genMigrateRoleTests( + state: CredentialState, + expectedRole: CredentialRole, + { + doneStateWithCredentials, + didCommMessage, + }: { + doneStateWithCredentials?: boolean + didCommMessage?: { + messageName: 'propose-credential' | 'offer-credential' | 'request-credential' + didCommMessageRole: DidCommMessageRole + } + } = {} + ) { + it(`Should migrate state: '${state}' to role: '${expectedRole}'${ + doneStateWithCredentials !== undefined + ? ` when record ${doneStateWithCredentials ? 'has' : 'does not have'} credentials property` + : '' + }`, async () => { + const record = getCredentialRecord({ + state, + credentials: doneStateWithCredentials + ? [{ credentialRecordId: 'some-id', credentialRecordType: 'some-record' }] + : undefined, + }) + + if (didCommMessage) { + mockFunction(didCommMessageRepository.findByQuery).mockResolvedValueOnce([ + new DidCommMessageRecord({ + message: { + '@id': '123', + '@type': `https://didcomm.org/issue-credential/1.0/${didCommMessage.messageName}`, + }, + role: didCommMessage.didCommMessageRole, + associatedRecordId: record.id, + }), + ]) + } + + await testModule.migrateRole(agent, record) + + expect(record.toJSON()).toMatchObject({ + role: expectedRole, + }) + + if (didCommMessage) { + expect(didCommMessageRepository.findByQuery).toHaveBeenCalledTimes(1) + expect(didCommMessageRepository.findByQuery).toHaveBeenCalledWith(agent.context, { + associatedRecordId: record.id, + $or: [ + { messageName: 'offer-credential' }, + { messageName: 'propose-credential' }, + { messageName: 'request-credential' }, + ], + }) + } + }) + } +}) + +function getCredentialRecord({ + id, + metadata, + credentials, + state, +}: { + id?: string + metadata?: Record + credentials?: CredentialRecordBinding[] + state?: CredentialState +}) { + return JsonTransformer.fromJSON( + { + id: id ?? 'credential-id', + metadata, + credentials, + state, + }, + CredentialExchangeRecord + ) +} diff --git a/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/proofExchangeRecord.test.ts b/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/proofExchangeRecord.test.ts new file mode 100644 index 0000000000..be397ffe96 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/proofExchangeRecord.test.ts @@ -0,0 +1,161 @@ +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests' +import { Agent } from '../../../../../agent/Agent' +import { ProofExchangeRecord, ProofRepository, ProofRole, ProofState } from '../../../../../modules/proofs' +import { JsonTransformer } from '../../../../../utils' +import { DidCommMessageRecord, DidCommMessageRepository, DidCommMessageRole } from '../../../../didcomm' +import * as testModule from '../proofExchangeRecord' + +const agentConfig = getAgentConfig('Migration - Proof Exchange Record - 0.4-0.5') +const agentContext = getAgentContext() + +jest.mock('../../../../../modules/proofs/repository/ProofRepository') +const ProofRepositoryMock = ProofRepository as jest.Mock +const proofRepository = new ProofRepositoryMock() + +jest.mock('../../../../didcomm/DidCommMessageRepository') +const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock +const didCommMessageRepository = new DidCommMessageRepositoryMock() + +jest.mock('../../../../../agent/Agent', () => ({ + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn((injectionToken) => + injectionToken === ProofRepository ? proofRepository : didCommMessageRepository + ), + }, + })), +})) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.4-0.5 | Migration | Proof Exchange Record', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('migrateProofExchangeRecordToV0_5()', () => { + it('should fetch all records and apply the needed updates ', async () => { + const records: ProofExchangeRecord[] = [getProofRecord({})] + + mockFunction(proofRepository.getAll).mockResolvedValue(records) + + await testModule.migrateProofExchangeRecordToV0_5(agent) + + expect(proofRepository.getAll).toHaveBeenCalledTimes(1) + expect(proofRepository.update).toHaveBeenCalledTimes(1) + }) + }) + + /* + * + * Does not cover the `Abandoned` and `Done` state. + * These are covered in the integration tests as they required more state setup in the walletj + * + */ + describe('migrateRole()', () => { + // according to: https://github.com/hyperledger/aries-rfcs/blob/main/features/0037-present-proof/README.md#states + genMigrateRoleTests(ProofState.RequestSent, ProofRole.Verifier) + genMigrateRoleTests(ProofState.ProposalReceived, ProofRole.Verifier) + genMigrateRoleTests(ProofState.PresentationReceived, ProofRole.Verifier) + + genMigrateRoleTests(ProofState.RequestReceived, ProofRole.Prover) + genMigrateRoleTests(ProofState.Declined, ProofRole.Prover) + genMigrateRoleTests(ProofState.ProposalSent, ProofRole.Prover) + genMigrateRoleTests(ProofState.PresentationSent, ProofRole.Prover) + + genMigrateRoleTests(ProofState.Done, ProofRole.Prover, { + messageName: 'propose-presentation', + didCommMessageRole: DidCommMessageRole.Sender, + }) + genMigrateRoleTests(ProofState.Abandoned, ProofRole.Prover, { + messageName: 'propose-presentation', + didCommMessageRole: DidCommMessageRole.Sender, + }) + + genMigrateRoleTests(ProofState.Done, ProofRole.Verifier, { + messageName: 'propose-presentation', + didCommMessageRole: DidCommMessageRole.Receiver, + }) + genMigrateRoleTests(ProofState.Abandoned, ProofRole.Verifier, { + messageName: 'propose-presentation', + didCommMessageRole: DidCommMessageRole.Receiver, + }) + + genMigrateRoleTests(ProofState.Done, ProofRole.Verifier, { + messageName: 'request-presentation', + didCommMessageRole: DidCommMessageRole.Sender, + }) + genMigrateRoleTests(ProofState.Abandoned, ProofRole.Verifier, { + messageName: 'request-presentation', + didCommMessageRole: DidCommMessageRole.Sender, + }) + + genMigrateRoleTests(ProofState.Done, ProofRole.Prover, { + messageName: 'request-presentation', + didCommMessageRole: DidCommMessageRole.Receiver, + }) + genMigrateRoleTests(ProofState.Abandoned, ProofRole.Prover, { + messageName: 'request-presentation', + didCommMessageRole: DidCommMessageRole.Receiver, + }) + }) + + function genMigrateRoleTests( + state: ProofState, + role: ProofRole, + didCommMessage?: { + messageName: 'propose-presentation' | 'request-presentation' + didCommMessageRole: DidCommMessageRole + } + ) { + it(`Should migrate state: '${state}' to role: '${role}'`, async () => { + const record = getProofRecord({ state }) + + if (didCommMessage) { + mockFunction(didCommMessageRepository.findByQuery).mockResolvedValueOnce([ + new DidCommMessageRecord({ + message: { + '@id': '123', + '@type': `https://didcomm.org/present-proof/1.0/${didCommMessage.messageName}`, + }, + role: didCommMessage.didCommMessageRole, + associatedRecordId: record.id, + }), + ]) + } + + await testModule.migrateRole(agent, record) + + expect(record.toJSON()).toMatchObject({ + role, + }) + + if (didCommMessage) { + expect(didCommMessageRepository.findByQuery).toHaveBeenCalledTimes(1) + expect(didCommMessageRepository.findByQuery).toHaveBeenCalledWith(agent.context, { + associatedRecordId: record.id, + $or: [{ messageName: 'propose-presentation' }, { messageName: 'request-presentation' }], + }) + } + }) + } +}) + +function getProofRecord({ id, state }: { id?: string; state?: ProofState }) { + return JsonTransformer.fromJSON( + { + id: id ?? 'proof-id', + state: state ?? ProofState.ProposalSent, + }, + ProofExchangeRecord + ) +} diff --git a/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts b/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts new file mode 100644 index 0000000000..41b1bd1a02 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts @@ -0,0 +1,121 @@ +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { Agent } from '../../../../../agent/Agent' +import { AgentConfig } from '../../../../../agent/AgentConfig' +import { W3cCredentialRecord, W3cCredentialRepository, W3cJsonLdVerifiableCredential } from '../../../../../modules/vc' +import { W3cJsonLdCredentialService } from '../../../../../modules/vc/data-integrity/W3cJsonLdCredentialService' +import { Ed25519Signature2018Fixtures } from '../../../../../modules/vc/data-integrity/__tests__/fixtures' +import { JsonTransformer } from '../../../../../utils' +import * as testModule from '../w3cCredentialRecord' + +const dependencyManager = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + resolve: (_injectionToken: unknown) => { + // no-op + }, +} + +const agentConfig = getAgentConfig('Migration W3cCredentialRecord 0.4-0.5') +const agentContext = getAgentContext({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dependencyManager: dependencyManager as any, +}) + +const repository = { + getAll: jest.fn(), + update: jest.fn(), +} + +const w3cJsonLdCredentialService = { + getExpandedTypesForCredential: jest.fn().mockResolvedValue(['https://example.com#example']), +} + +dependencyManager.resolve = (injectionToken: unknown) => { + if (injectionToken === W3cJsonLdCredentialService) { + return w3cJsonLdCredentialService + } else if (injectionToken === W3cCredentialRepository) { + return repository + } else if (injectionToken === AgentConfig) { + return agentConfig + } + + throw new Error('unknown injection token') +} + +jest.mock('../../../../../agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.4-0.5 | W3cCredentialRecord', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('migrateW3cCredentialRecordToV0_5()', () => { + it('should fetch all w3c credential records and re-save them', async () => { + const records = [ + new W3cCredentialRecord({ + tags: { + expandedTypes: ['https://example.com'], + }, + id: '3b3cf6ca-fa09-4498-b891-e280fbbb7fa7', + credential: JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ), + }), + ] + + mockFunction(repository.getAll).mockResolvedValue(records) + + await testModule.migrateW3cCredentialRecordToV0_5(agent) + + expect(repository.getAll).toHaveBeenCalledTimes(1) + expect(repository.getAll).toHaveBeenCalledWith(agent.context) + expect(repository.update).toHaveBeenCalledTimes(1) + + const [, record] = mockFunction(repository.update).mock.calls[0] + expect(record.getTags().types).toEqual(['VerifiableCredential', 'UniversityDegreeCredential']) + }) + + it("should re-calculate the expandedTypes if it contains 'https' values", async () => { + const records = [ + new W3cCredentialRecord({ + tags: { + expandedTypes: ['https'], + }, + id: '3b3cf6ca-fa09-4498-b891-e280fbbb7fa7', + credential: JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ), + }), + ] + + mockFunction(repository.getAll).mockResolvedValue(records) + + await testModule.migrateW3cCredentialRecordToV0_5(agent) + + expect(repository.getAll).toHaveBeenCalledTimes(1) + expect(repository.getAll).toHaveBeenCalledWith(agent.context) + expect(repository.update).toHaveBeenCalledTimes(1) + + const [, record] = mockFunction(repository.update).mock.calls[0] + expect(record.getTags().expandedTypes).toEqual(['https://example.com#example']) + }) + }) +}) diff --git a/packages/core/src/storage/migration/updates/0.4-0.5/credentialExchangeRecord.ts b/packages/core/src/storage/migration/updates/0.4-0.5/credentialExchangeRecord.ts new file mode 100644 index 0000000000..e91b4be8f1 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.4-0.5/credentialExchangeRecord.ts @@ -0,0 +1,136 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' +import type { CredentialExchangeRecord } from '../../../../modules/credentials' + +import { CredoError } from '../../../../error' +import { + V2RequestCredentialMessage, + V2ProposeCredentialMessage, + V2OfferCredentialMessage, + CredentialRole, + CredentialRepository, + CredentialState, +} from '../../../../modules/credentials' +import { parseMessageType } from '../../../../utils/messageType' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../didcomm' + +/** + * Migrates the {@link CredentialExchangeRecord} to 0.5 compatible format. It fetches all credential exchange records from + * storage and applies the needed updates to the records. After a record has been transformed, it is updated + * in storage and the next record will be transformed. + * + * The following transformations are applied: + * - {@link migrateRole} + */ +export async function migrateCredentialExchangeRecordToV0_5(agent: Agent) { + agent.config.logger.info('Migrating credential exchange records to storage version 0.5') + const credentialRepository = agent.dependencyManager.resolve(CredentialRepository) + + agent.config.logger.debug(`Fetching all credential records from storage`) + const credentialRecords = await credentialRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${credentialRecords.length} credential exchange records to update.`) + for (const credentialRecord of credentialRecords) { + agent.config.logger.debug( + `Migrating credential exchange record with id ${credentialRecord.id} to storage version 0.5` + ) + + await migrateRole(agent, credentialRecord) + + // Save updated record + await credentialRepository.update(agent.context, credentialRecord) + + agent.config.logger.debug( + `Successfully migrated credential exchange record with id ${credentialRecord.id} to storage version 0.5` + ) + } +} + +const holderCredentialStates = [ + CredentialState.Declined, + CredentialState.ProposalSent, + CredentialState.OfferReceived, + CredentialState.RequestSent, + CredentialState.CredentialReceived, +] + +const issuerCredentialStates = [ + CredentialState.ProposalReceived, + CredentialState.OfferSent, + CredentialState.RequestReceived, + CredentialState.CredentialIssued, +] + +export async function getCredentialRole(agent: BaseAgent, credentialRecord: CredentialExchangeRecord) { + // Credentials will only have a value when a credential is received, meaning we're the holder + if (credentialRecord.credentials.length > 0) { + return CredentialRole.Holder + } + // If credentialRecord.credentials doesn't have any values, and we're also not in state done it means we're the issuer. + else if (credentialRecord.state === CredentialState.Done) { + return CredentialRole.Issuer + } + // For these states we know for certain that we're the holder + else if (holderCredentialStates.includes(credentialRecord.state)) { + return CredentialRole.Holder + } + // For these states we know for certain that we're the issuer + else if (issuerCredentialStates.includes(credentialRecord.state)) { + return CredentialRole.Issuer + } + + // We now need to determine the role based on the didcomm message. Only the Abandoned state remains + // and we can't be certain of the role based on the state alone. + + // Fetch any of the associated credential messages that we can use to determine the role + // Either one of these MUST be present or we can't determine the role. + const didCommMessageRepository = agent.dependencyManager.resolve(DidCommMessageRepository) + const [didCommMessageRecord] = await didCommMessageRepository.findByQuery(agent.context, { + associatedRecordId: credentialRecord.id, + $or: [ + // We can't be certain which messages will be present. + { messageName: V2OfferCredentialMessage.type.messageName }, + { messageName: V2ProposeCredentialMessage.type.messageName }, + { messageName: V2RequestCredentialMessage.type.messageName }, + ], + }) + + if (!didCommMessageRecord) { + throw new CredoError( + `Unable to determine the role of the credential exchange record with id ${credentialRecord.id} without any didcomm messages and state abandoned` + ) + } + + // Maps the message name and the didcomm message role to the respective credential role + const roleStateMapping = { + [V2OfferCredentialMessage.type.messageName]: { + [DidCommMessageRole.Sender]: CredentialRole.Issuer, + [DidCommMessageRole.Receiver]: CredentialRole.Holder, + }, + [V2ProposeCredentialMessage.type.messageName]: { + [DidCommMessageRole.Sender]: CredentialRole.Holder, + [DidCommMessageRole.Receiver]: CredentialRole.Issuer, + }, + [V2RequestCredentialMessage.type.messageName]: { + [DidCommMessageRole.Sender]: CredentialRole.Holder, + [DidCommMessageRole.Receiver]: CredentialRole.Issuer, + }, + } + + const messageName = parseMessageType(didCommMessageRecord.message['@type']).messageName + const credentialRole = roleStateMapping[messageName][didCommMessageRecord.role] + + return credentialRole +} + +/** + * Add a role to the credential record. + */ +export async function migrateRole(agent: Agent, credentialRecord: CredentialExchangeRecord) { + agent.config.logger.debug(`Adding role to record with id ${credentialRecord.id} to for version 0.4`) + + credentialRecord.role = await getCredentialRole(agent, credentialRecord) + + agent.config.logger.debug( + `Successfully updated role to '${credentialRecord.role}' on credential record with id ${credentialRecord.id} to for version 0.4` + ) +} diff --git a/packages/core/src/storage/migration/updates/0.4-0.5/index.ts b/packages/core/src/storage/migration/updates/0.4-0.5/index.ts new file mode 100644 index 0000000000..7a612c2815 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.4-0.5/index.ts @@ -0,0 +1,11 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' + +import { migrateCredentialExchangeRecordToV0_5 } from './credentialExchangeRecord' +import { migrateProofExchangeRecordToV0_5 } from './proofExchangeRecord' +import { migrateW3cCredentialRecordToV0_5 } from './w3cCredentialRecord' + +export async function updateV0_4ToV0_5(agent: Agent): Promise { + await migrateW3cCredentialRecordToV0_5(agent) + await migrateCredentialExchangeRecordToV0_5(agent) + await migrateProofExchangeRecordToV0_5(agent) +} diff --git a/packages/core/src/storage/migration/updates/0.4-0.5/proofExchangeRecord.ts b/packages/core/src/storage/migration/updates/0.4-0.5/proofExchangeRecord.ts new file mode 100644 index 0000000000..c275d592fc --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.4-0.5/proofExchangeRecord.ts @@ -0,0 +1,113 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' +import type { ProofExchangeRecord } from '../../../../modules/proofs' + +import { CredoError } from '../../../../error' +import { + ProofRole, + ProofRepository, + ProofState, + V2RequestPresentationMessage, + V2ProposePresentationMessage, +} from '../../../../modules/proofs' +import { parseMessageType } from '../../../../utils/messageType' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../didcomm' + +/** + * Migrates the {@link ProofExchangeExchangeRecord} to 0.5 compatible format. It fetches all proof exchange records from + * storage and applies the needed updates to the records. After a record has been transformed, it is updated + * in storage and the next record will be transformed. + * + * The following transformations are applied: + * - {@link migrateRole} + */ +export async function migrateProofExchangeRecordToV0_5(agent: Agent) { + agent.config.logger.info('Migrating proof exchange records to storage version 0.5') + const proofRepository = agent.dependencyManager.resolve(ProofRepository) + + agent.config.logger.debug(`Fetching all proof records from storage`) + const proofRecords = await proofRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${proofRecords.length} proof exchange records to update.`) + for (const proofRecord of proofRecords) { + agent.config.logger.debug(`Migrating proof exchange record with id ${proofRecord.id} to storage version 0.5`) + + await migrateRole(agent, proofRecord) + + // Save updated record + await proofRepository.update(agent.context, proofRecord) + + agent.config.logger.debug( + `Successfully migrated proof exchange record with id ${proofRecord.id} to storage version 0.5` + ) + } +} + +const proverProofStates = [ + ProofState.RequestReceived, + ProofState.ProposalSent, + ProofState.PresentationSent, + ProofState.Declined, +] +const verifierProofStates = [ProofState.RequestSent, ProofState.ProposalReceived, ProofState.PresentationReceived] + +export async function getProofRole(agent: BaseAgent, proofRecord: ProofExchangeRecord) { + // For these states we know for certain that we're the prover + if (proverProofStates.includes(proofRecord.state)) { + return ProofRole.Prover + } + // For these states we know for certain that we're the verifier + else if (verifierProofStates.includes(proofRecord.state)) { + return ProofRole.Verifier + } + + // We now need to determine the role based on the didcomm message. Only the Done and Abandoned states + // remain and we can't be certain of the role based on the state alone. + + // Fetch any of the associated proof messages that we can use to determine the role + // Either one of these MUST be present or we can't determine the role. + const didCommMessageRepository = agent.dependencyManager.resolve(DidCommMessageRepository) + const [didCommMessageRecord] = await didCommMessageRepository.findByQuery(agent.context, { + associatedRecordId: proofRecord.id, + $or: [ + // We can't be certain which messages will be present. + { messageName: V2ProposePresentationMessage.type.messageName }, + { messageName: V2RequestPresentationMessage.type.messageName }, + ], + }) + + if (!didCommMessageRecord) { + throw new CredoError( + `Unable to determine the role of the proof exchange record with id ${proofRecord.id} without any didcomm messages and state abandoned/done` + ) + } + + // Maps the message name and the didcomm message role to the respective proof role + const roleStateMapping = { + [V2ProposePresentationMessage.type.messageName]: { + [DidCommMessageRole.Sender]: ProofRole.Prover, + [DidCommMessageRole.Receiver]: ProofRole.Verifier, + }, + [V2RequestPresentationMessage.type.messageName]: { + [DidCommMessageRole.Sender]: ProofRole.Verifier, + [DidCommMessageRole.Receiver]: ProofRole.Prover, + }, + } + + const messageName = parseMessageType(didCommMessageRecord.message['@type']).messageName + const proofRole = roleStateMapping[messageName][didCommMessageRecord.role] + + return proofRole +} + +/** + * Add a role to the proof record. + */ +export async function migrateRole(agent: Agent, proofRecord: ProofExchangeRecord) { + agent.config.logger.debug(`Adding role to record with id ${proofRecord.id} to for version 0.5`) + + proofRecord.role = await getProofRole(agent, proofRecord) + + agent.config.logger.debug( + `Successfully updated role to '${proofRecord.role}' on proof record with id ${proofRecord.id} to for version 0.5` + ) +} diff --git a/packages/core/src/storage/migration/updates/0.4-0.5/w3cCredentialRecord.ts b/packages/core/src/storage/migration/updates/0.4-0.5/w3cCredentialRecord.ts new file mode 100644 index 0000000000..0555904ec4 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.4-0.5/w3cCredentialRecord.ts @@ -0,0 +1,88 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' +import type { W3cCredentialRecord } from '../../../../modules/vc/repository' + +import { W3cJsonLdVerifiableCredential } from '../../../../modules/vc' +import { W3cJsonLdCredentialService } from '../../../../modules/vc/data-integrity/W3cJsonLdCredentialService' +import { W3cCredentialRepository } from '../../../../modules/vc/repository' + +/** + * Re-saves the w3c credential records to add the new 'types' tag. + */ +export async function migrateW3cCredentialRecordToV0_5(agent: Agent) { + agent.config.logger.info('Migration w3c credential records records to storage version 0.5') + + const w3cCredentialRepository = agent.dependencyManager.resolve(W3cCredentialRepository) + + agent.config.logger.debug(`Fetching all w3c credential records from storage`) + const records = await w3cCredentialRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${records.length} w3c credential records to update.`) + + for (const record of records) { + agent.config.logger.debug( + `Updating w3c credential record with id ${record.id} to add 'types' tag and fix 'expandedTypes' tag for storage version 0.5` + ) + + await fixIncorrectExpandedTypesWithAskarStorage(agent, record) + + // Save updated record + await w3cCredentialRepository.update(agent.context, record) + + agent.config.logger.debug(`Successfully migrated w3c credential record with id ${record.id} to storage version 0.5`) + } +} + +/** + * Up until 0.5.0 the AskarStorageService contained a bug where a non-computed (so manually set on record) array tag values that contained a : in the value + * would be incorrectly parsed back from an askar tag to a tag on a record. This would only cause problems for the storage if the record was re-saved and not + * computed. The following would happen: + * - Create record with non-computed tag, e.g. expandedTypes that contains a value with a : in it + * - Save record. The tag is correctly set in Askar as `expandedTypes:https://example.com' + * - Read record. The tag is correctly read from Askar as `expandedTypes:https://example.com'. However the transformation would result in the tag value on the record being set to `https'. + * - Save record. The non-computed (important, as otherwise the correct computed value would overwrite the incorrect value before storing) tag value is now set to `https' instead of `https://example.com' + * + * This function checks if any of the values for expandedTypes is `https` and if so, re-calculates the correct value and sets it on the record. + * + * NOTE: This function needs to resolve the context of a W3cCredentialRecord to be able to correctly calculate the expanded types. + * To not brick a wallet that has no internet when updating, the storage update will allow the resolving of the expanded types to fail. + * If this is the case, at a later point the expanded types will need to be recalculated and set on the record. + * + * If w3c credential records are never re-saved this shouldn't be a problem though. By default w3c credential records are not re-saved, + * and so it only applies if you have implemented a custom flow that re-saves w3c credential records (e.g. if you add metadata). + */ +export async function fixIncorrectExpandedTypesWithAskarStorage( + agent: Agent, + w3cCredentialRecord: W3cCredentialRecord +) { + // We don't store the expanded types for JWT credentials (should we? As you can have jwt_vc with json-ld) + if (!(w3cCredentialRecord.credential instanceof W3cJsonLdVerifiableCredential)) return + + const expandedTypes = (w3cCredentialRecord.getTag('expandedTypes') ?? []) as string[] + + // Check if one of the values is `https` + const hasInvalidType = expandedTypes.some((type) => type === 'https') + + if (!hasInvalidType) return + + agent.context.config.logger.info( + `W3c credential record with id '${w3cCredentialRecord.id}' contains invalid expanded types. Recalculating...` + ) + const w3cJsonLdCredentialService = agent.dependencyManager.resolve(W3cJsonLdCredentialService) + + try { + // JsonLd credentials need expanded types to be stored. + const newExpandedTypes = await w3cJsonLdCredentialService.getExpandedTypesForCredential( + agent.context, + w3cCredentialRecord.credential + ) + + w3cCredentialRecord.setTag('expandedTypes', newExpandedTypes) + agent.context.config.logger.info( + `Successfully recalculated expanded types for w3c credential record with id ${w3cCredentialRecord.id} to ${newExpandedTypes} and set it on the record.` + ) + } catch (error) { + agent.context.config.logger.error( + `Retrieving expandedTypes fro w3c credential record with id ${w3cCredentialRecord.id} failed. To not brick the wallet, the storage update will not fail. Make sure to recalculate the expanded types at a later point. This is probably due to a missing internet connection. See https://credo.js.org/guides/updating/versions/0.4-to-0.5 for more information.` + ) + } +} diff --git a/packages/core/src/transport/HttpOutboundTransport.ts b/packages/core/src/transport/HttpOutboundTransport.ts new file mode 100644 index 0000000000..72d34d11bb --- /dev/null +++ b/packages/core/src/transport/HttpOutboundTransport.ts @@ -0,0 +1,145 @@ +import type { OutboundTransport } from './OutboundTransport' +import type { Agent } from '../agent/Agent' +import type { AgentMessageReceivedEvent } from '../agent/Events' +import type { Logger } from '../logger' +import type { OutboundPackage } from '../types' + +import { AbortController } from 'abort-controller' +import { Subject } from 'rxjs' + +import { AgentEventTypes } from '../agent/Events' +import { CredoError } from '../error/CredoError' +import { isValidJweStructure, JsonEncoder } from '../utils' + +export class HttpOutboundTransport implements OutboundTransport { + private agent!: Agent + private logger!: Logger + private fetch!: typeof fetch + private isActive = false + + private outboundSessionCount = 0 + private outboundSessionsObservable = new Subject() + + public supportedSchemes = ['http', 'https'] + + public async start(agent: Agent): Promise { + this.agent = agent + this.logger = this.agent.config.logger + this.fetch = this.agent.config.agentDependencies.fetch + this.isActive = true + this.outboundSessionCount = 0 + + this.logger.debug('Starting HTTP outbound transport') + } + + public async stop(): Promise { + this.logger.debug('Stopping HTTP outbound transport') + this.isActive = false + + if (this.outboundSessionCount === 0) { + this.agent.config.logger.debug('No open outbound HTTP sessions. Immediately stopping HttpOutboundTransport') + return + } + + this.agent.config.logger.debug( + `Still ${this.outboundSessionCount} open outbound HTTP sessions. Waiting for sessions to close before stopping HttpOutboundTransport` + ) + // Track all 'closed' sessions + // TODO: add timeout? -> we have a timeout on the request + return new Promise((resolve) => + this.outboundSessionsObservable.subscribe(() => { + this.agent.config.logger.debug(`${this.outboundSessionCount} HttpOutboundTransport sessions still active`) + if (this.outboundSessionCount === 0) resolve() + }) + ) + } + + public async sendMessage(outboundPackage: OutboundPackage) { + const { payload, endpoint } = outboundPackage + + if (!this.isActive) { + throw new CredoError('Outbound transport is not active. Not sending message.') + } + + if (!endpoint) { + throw new CredoError(`Missing endpoint. I don't know how and where to send the message.`) + } + + this.logger.debug(`Sending outbound message to endpoint '${outboundPackage.endpoint}'`, { + payload: outboundPackage.payload, + }) + + try { + const abortController = new AbortController() + const id = setTimeout(() => abortController.abort(), 15000) + this.outboundSessionCount++ + + let response + let responseMessage + try { + response = await this.fetch(endpoint, { + method: 'POST', + body: JSON.stringify(payload), + headers: { 'Content-Type': this.agent.config.didCommMimeType }, + signal: abortController.signal as NonNullable, + }) + clearTimeout(id) + responseMessage = await response.text() + } catch (error) { + // Request is aborted after 15 seconds, but that doesn't necessarily mean the request + // went wrong. ACA-Py keeps the socket alive until it has a response message. So we assume + // that if the error was aborted and we had return routing enabled, we should ignore the error. + if (error.name == 'AbortError' && outboundPackage.responseRequested) { + this.logger.debug( + 'Request was aborted due to timeout. Not throwing error due to return routing on sent message' + ) + } else { + throw error + } + } + + // TODO: do we just want to ignore messages that were returned if we didn't request it? + // TODO: check response header type (and also update inbound transports to use the correct headers types) + if (response && responseMessage) { + this.logger.debug(`Response received`, { responseMessage, status: response.status }) + + // This should not happen + if (!this.isActive) { + this.logger.error('Received response message over HttpOutboundTransport while transport was not active.') + } + + try { + const encryptedMessage = JsonEncoder.fromString(responseMessage) + if (!isValidJweStructure(encryptedMessage)) { + this.logger.error( + `Received a response from the other agent but the structure of the incoming message is not a DIDComm message: ${responseMessage}` + ) + return + } + // Emit event with the received agent message. + this.agent.events.emit(this.agent.context, { + type: AgentEventTypes.AgentMessageReceived, + payload: { + message: encryptedMessage, + }, + }) + } catch (error) { + this.logger.debug('Unable to parse response message') + } + } else { + this.logger.debug(`No response received.`) + } + } catch (error) { + this.logger.error(`Error sending message to ${endpoint}: ${error.message}`, { + error, + message: error.message, + body: payload, + didCommMimeType: this.agent.config.didCommMimeType, + }) + throw new CredoError(`Error sending message to ${endpoint}: ${error.message}`, { cause: error }) + } finally { + this.outboundSessionCount-- + this.outboundSessionsObservable.next(undefined) + } + } +} diff --git a/packages/core/src/transport/InboundTransport.ts b/packages/core/src/transport/InboundTransport.ts new file mode 100644 index 0000000000..fd744bfcfa --- /dev/null +++ b/packages/core/src/transport/InboundTransport.ts @@ -0,0 +1,7 @@ +import type { Agent } from '../agent/Agent' + +export interface InboundTransport { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + start(agent: Agent): Promise + stop(): Promise +} diff --git a/packages/core/src/transport/OutboundTransport.ts b/packages/core/src/transport/OutboundTransport.ts new file mode 100644 index 0000000000..0fa33bbe61 --- /dev/null +++ b/packages/core/src/transport/OutboundTransport.ts @@ -0,0 +1,12 @@ +import type { Agent } from '../agent/Agent' +import type { OutboundPackage } from '../types' + +export interface OutboundTransport { + supportedSchemes: string[] + + sendMessage(outboundPackage: OutboundPackage): Promise + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + start(agent: Agent): Promise + stop(): Promise +} diff --git a/packages/core/src/transport/TransportEventTypes.ts b/packages/core/src/transport/TransportEventTypes.ts new file mode 100644 index 0000000000..1dd34674d3 --- /dev/null +++ b/packages/core/src/transport/TransportEventTypes.ts @@ -0,0 +1,39 @@ +import type { BaseEvent } from '../agent/Events' +import type { TransportSession } from '../agent/TransportService' + +export enum TransportEventTypes { + OutboundWebSocketClosedEvent = 'OutboundWebSocketClosedEvent', + OutboundWebSocketOpenedEvent = 'OutboundWebSocketOpenedEvent', + TransportSessionSaved = 'TransportSessionSaved', + TransportSessionRemoved = 'TransportSessionRemoved', +} + +export interface OutboundWebSocketClosedEvent extends BaseEvent { + type: TransportEventTypes.OutboundWebSocketClosedEvent + payload: { + socketId: string + connectionId?: string + } +} + +export interface OutboundWebSocketOpenedEvent extends BaseEvent { + type: TransportEventTypes.OutboundWebSocketOpenedEvent + payload: { + socketId: string + connectionId?: string + } +} + +export interface TransportSessionSavedEvent extends BaseEvent { + type: typeof TransportEventTypes.TransportSessionSaved + payload: { + session: TransportSession + } +} + +export interface TransportSessionRemovedEvent extends BaseEvent { + type: typeof TransportEventTypes.TransportSessionRemoved + payload: { + session: TransportSession + } +} diff --git a/packages/core/src/transport/WsOutboundTransport.ts b/packages/core/src/transport/WsOutboundTransport.ts new file mode 100644 index 0000000000..8d77a8744f --- /dev/null +++ b/packages/core/src/transport/WsOutboundTransport.ts @@ -0,0 +1,190 @@ +import type { OutboundTransport } from './OutboundTransport' +import type { OutboundWebSocketClosedEvent, OutboundWebSocketOpenedEvent } from './TransportEventTypes' +import type { Agent } from '../agent/Agent' +import type { AgentMessageReceivedEvent } from '../agent/Events' +import type { Logger } from '../logger' +import type { OutboundPackage } from '../types' +import type { WebSocket } from 'ws' + +import { AgentEventTypes } from '../agent/Events' +import { CredoError } from '../error/CredoError' +import { isValidJweStructure, JsonEncoder } from '../utils' +import { Buffer } from '../utils/buffer' + +import { TransportEventTypes } from './TransportEventTypes' + +export class WsOutboundTransport implements OutboundTransport { + private transportTable: Map = new Map() + private agent!: Agent + private logger!: Logger + private WebSocketClass!: typeof WebSocket + public supportedSchemes = ['ws', 'wss'] + private isActive = false + + public async start(agent: Agent): Promise { + this.agent = agent + + this.logger = agent.config.logger + + this.logger.debug('Starting WS outbound transport') + this.WebSocketClass = agent.config.agentDependencies.WebSocketClass + + this.isActive = true + } + + public async stop() { + this.logger.debug('Stopping WS outbound transport') + this.isActive = false + + const stillOpenSocketClosingPromises: Array> = [] + + this.transportTable.forEach((socket) => { + socket.removeEventListener('message', this.handleMessageEvent) + if (socket.readyState !== this.WebSocketClass.CLOSED) { + stillOpenSocketClosingPromises.push(new Promise((resolve) => socket.once('close', resolve))) + socket.close() + } + }) + + // Wait for all open websocket connections to have been closed + await Promise.all(stillOpenSocketClosingPromises) + } + + public async sendMessage(outboundPackage: OutboundPackage) { + const { payload, endpoint, connectionId } = outboundPackage + this.logger.debug(`Sending outbound message to endpoint '${endpoint}' over WebSocket transport.`, { + payload, + }) + + if (!this.isActive) { + throw new CredoError('Outbound transport is not active. Not sending message.') + } + + if (!endpoint) { + throw new CredoError("Missing connection or endpoint. I don't know how and where to send the message.") + } + + const socketId = `${endpoint}-${connectionId}` + const isNewSocket = !this.hasOpenSocket(socketId) + const socket = await this.resolveSocket({ socketId, endpoint, connectionId }) + + // If the socket was created for this message and we don't have return routing enabled + // We can close the socket as it shouldn't return messages anymore + // make sure to use the socket in a manner that is compliant with the https://developer.mozilla.org/en-US/docs/Web/API/WebSocket + // (React Native) and https://github.com/websockets/ws (NodeJs) + socket.send(Buffer.from(JSON.stringify(payload))) + if (isNewSocket && !outboundPackage.responseRequested) { + socket.close() + } + } + + private hasOpenSocket(socketId: string) { + return this.transportTable.get(socketId) !== undefined + } + + private async resolveSocket({ + socketId, + endpoint, + connectionId, + }: { + socketId: string + endpoint?: string + connectionId?: string + }) { + // If we already have a socket connection use it + let socket = this.transportTable.get(socketId) + + if (!socket || socket.readyState === this.WebSocketClass.CLOSING) { + if (!endpoint) { + throw new CredoError(`Missing endpoint. I don't know how and where to send the message.`) + } + socket = await this.createSocketConnection({ + endpoint, + socketId, + connectionId, + }) + this.transportTable.set(socketId, socket) + this.listenOnWebSocketMessages(socket) + } + + if (socket.readyState !== this.WebSocketClass.OPEN) { + throw new CredoError('Socket is not open.') + } + + return socket + } + + // NOTE: Because this method is passed to the event handler this must be a lambda method + // so 'this' is scoped to the 'WsOutboundTransport' class instance + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private handleMessageEvent = (event: any) => { + this.logger.trace('WebSocket message event received.', { url: event.target.url }) + const payload = JsonEncoder.fromBuffer(event.data) + if (!isValidJweStructure(payload)) { + throw new Error( + `Received a response from the other agent but the structure of the incoming message is not a DIDComm message: ${payload}` + ) + } + this.logger.debug('Payload received from mediator:', payload) + + this.agent.events.emit(this.agent.context, { + type: AgentEventTypes.AgentMessageReceived, + payload: { + message: payload, + }, + }) + } + + private listenOnWebSocketMessages(socket: WebSocket) { + socket.addEventListener('message', this.handleMessageEvent) + } + + private createSocketConnection({ + socketId, + endpoint, + connectionId, + }: { + socketId: string + endpoint: string + connectionId?: string + }): Promise { + return new Promise((resolve, reject) => { + this.logger.debug(`Connecting to WebSocket ${endpoint}`) + const socket = new this.WebSocketClass(endpoint) + + socket.onopen = () => { + this.logger.debug(`Successfully connected to WebSocket ${endpoint}`) + resolve(socket) + + this.agent.events.emit(this.agent.context, { + type: TransportEventTypes.OutboundWebSocketOpenedEvent, + payload: { + socketId, + connectionId: connectionId, + }, + }) + } + + socket.onerror = (error) => { + this.logger.debug(`Error while connecting to WebSocket ${endpoint}`, { + error, + }) + reject(error) + } + + socket.onclose = async () => { + this.logger.debug(`WebSocket closing to ${endpoint}`) + socket.removeEventListener('message', this.handleMessageEvent) + this.transportTable.delete(socketId) + + this.agent.events.emit(this.agent.context, { + type: TransportEventTypes.OutboundWebSocketClosedEvent, + payload: { + socketId, + connectionId: connectionId, + }, + }) + } + }) + } +} diff --git a/packages/core/src/transport/index.ts b/packages/core/src/transport/index.ts new file mode 100644 index 0000000000..9ab7390fae --- /dev/null +++ b/packages/core/src/transport/index.ts @@ -0,0 +1,5 @@ +export * from './InboundTransport' +export * from './OutboundTransport' +export * from './HttpOutboundTransport' +export * from './WsOutboundTransport' +export * from './TransportEventTypes' diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 0000000000..cd00c8706b --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,132 @@ +import type { Logger } from './logger' + +export enum KeyDerivationMethod { + /** default value in indy-sdk. Will be used when no value is provided */ + Argon2IMod = 'ARGON2I_MOD', + /** less secure, but faster */ + Argon2IInt = 'ARGON2I_INT', + /** raw wallet master key */ + Raw = 'RAW', +} + +export interface WalletStorageConfig { + type: string + [key: string]: unknown +} + +export interface WalletConfig { + id: string + key: string + keyDerivationMethod?: KeyDerivationMethod + storage?: WalletStorageConfig +} + +export interface WalletConfigRekey { + id: string + key: string + rekey: string + keyDerivationMethod?: KeyDerivationMethod + rekeyDerivationMethod?: KeyDerivationMethod +} + +export interface WalletExportImportConfig { + key: string + path: string +} + +export type EncryptedMessage = { + /** + * The "protected" member MUST be present and contain the value + * BASE64URL(UTF8(JWE Protected Header)) when the JWE Protected + * Header value is non-empty; otherwise, it MUST be absent. These + * Header Parameter values are integrity protected. + */ + protected: string + + /** + * The "iv" member MUST be present and contain the value + * BASE64URL(JWE Initialization Vector) when the JWE Initialization + * Vector value is non-empty; otherwise, it MUST be absent. + */ + iv: string + + /** + * The "ciphertext" member MUST be present and contain the value + * BASE64URL(JWE Ciphertext). + */ + ciphertext: string + + /** + * The "tag" member MUST be present and contain the value + * BASE64URL(JWE Authentication Tag) when the JWE Authentication Tag + * value is non-empty; otherwise, it MUST be absent. + */ + tag: string +} + +export enum DidCommMimeType { + V0 = 'application/ssi-agent-wire', + V1 = 'application/didcomm-envelope-enc', +} + +export interface InitConfig { + /** + * Agent public endpoints, sorted by priority (higher priority first) + */ + endpoints?: string[] + label: string + walletConfig?: WalletConfig + logger?: Logger + didCommMimeType?: DidCommMimeType + useDidKeyInProtocols?: boolean + useDidSovPrefixWhereAllowed?: boolean + connectionImageUrl?: string + autoUpdateStorageOnStartup?: boolean + backupBeforeStorageUpdate?: boolean +} + +export type ProtocolVersion = `${number}.${number}` +export interface PlaintextMessage { + '@type': string + '@id': string + '~thread'?: { + thid?: string + pthid?: string + } + [key: string]: unknown +} + +export interface OutboundPackage { + payload: EncryptedMessage + responseRequested?: boolean + endpoint?: string + connectionId?: string +} + +export type JsonValue = string | number | boolean | null | JsonObject | JsonArray +export type JsonArray = Array +export interface JsonObject { + [property: string]: JsonValue +} + +/** + * Flatten an array of arrays + * @example + * ``` + * type Flattened = FlatArray<[[1], [2]]> + * + * // is the same as + * type Flattened = 1 | 2 + * ``` + */ +export type FlatArray = Arr extends ReadonlyArray ? FlatArray : Arr + +/** + * Get the awaited (resolved promise) type of Promise type. + */ +export type Awaited = T extends Promise ? U : never + +/** + * Type util that returns `true` or `false` based on whether the input type `T` is of type `any` + */ +export type IsAny = unknown extends T ? ([keyof T] extends [never] ? false : true) : false diff --git a/packages/core/src/utils/Hasher.ts b/packages/core/src/utils/Hasher.ts new file mode 100644 index 0000000000..4c9af1ac0c --- /dev/null +++ b/packages/core/src/utils/Hasher.ts @@ -0,0 +1,25 @@ +import { hash as sha256 } from '@stablelib/sha256' + +import { TypedArrayEncoder } from './TypedArrayEncoder' + +export type HashName = 'sha-256' + +type HashingMap = { + [key in HashName]: (data: Uint8Array) => Uint8Array +} + +const hashingMap: HashingMap = { + 'sha-256': (data) => sha256(data), +} + +export class Hasher { + public static hash(data: Uint8Array | string, hashName: HashName | string): Uint8Array { + const dataAsUint8Array = typeof data === 'string' ? TypedArrayEncoder.fromString(data) : data + if (hashName in hashingMap) { + const hashFn = hashingMap[hashName as HashName] + return hashFn(dataAsUint8Array) + } + + throw new Error(`Unsupported hash name '${hashName}'`) + } +} diff --git a/packages/core/src/utils/HashlinkEncoder.ts b/packages/core/src/utils/HashlinkEncoder.ts new file mode 100644 index 0000000000..abb8728bf5 --- /dev/null +++ b/packages/core/src/utils/HashlinkEncoder.ts @@ -0,0 +1,133 @@ +import type { HashName } from './Hasher' +import type { BaseName } from './MultiBaseEncoder' +import type { Buffer } from './buffer' + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore ts is giving me headaches because this package has no types +import cbor from 'borc' + +import { MultiBaseEncoder } from './MultiBaseEncoder' +import { MultiHashEncoder } from './MultiHashEncoder' + +type Metadata = { + urls?: string[] + contentType?: string +} + +export type HashlinkData = { + checksum: string + metadata?: Metadata +} + +const hexTable = { + urls: 0x0f, + contentType: 0x0e, +} + +export class HashlinkEncoder { + /** + * Encodes a buffer, with optional metadata, into a hashlink + * + * @param buffer the buffer to encode into a hashlink + * @param hashAlgorithm the name of the hashing algorithm 'sha-256' + * @param baseEncoding the name of the base encoding algorithm 'base58btc' + * @param metadata the optional metadata in the hashlink + * + * @returns hashlink hashlink with optional metadata + */ + public static encode( + buffer: Buffer | Uint8Array, + hashAlgorithm: HashName, + baseEncoding: BaseName = 'base58btc', + metadata?: Metadata + ) { + const checksum = this.encodeMultiHash(buffer, hashAlgorithm, baseEncoding) + const mbMetadata = metadata && Object.keys(metadata).length > 0 ? this.encodeMetadata(metadata, baseEncoding) : null + return mbMetadata ? `hl:${checksum}:${mbMetadata}` : `hl:${checksum}` + } + + /** + * Decodes a hashlink into HashlinkData object + * + * @param hashlink the hashlink that needs decoding + * + * @returns object the decoded hashlink + */ + public static decode(hashlink: string): HashlinkData { + if (this.isValid(hashlink)) { + const hashlinkList = hashlink.split(':') + const [, checksum, encodedMetadata] = hashlinkList + return encodedMetadata ? { checksum, metadata: this.decodeMetadata(encodedMetadata) } : { checksum } + } else { + throw new Error(`Invalid hashlink: ${hashlink}`) + } + } + + /** + * Validates a hashlink + * + * @param hashlink the hashlink that needs validating + * + * @returns a boolean whether the hashlink is valid + * + * */ + public static isValid(hashlink: string): boolean { + const hashlinkList = hashlink.split(':') + const validMultiBase = MultiBaseEncoder.isValid(hashlinkList[1]) + if (!validMultiBase) { + return false + } + const { data } = MultiBaseEncoder.decode(hashlinkList[1]) + const validMultiHash = MultiHashEncoder.isValid(data) + return validMultiHash ? true : false + } + + private static encodeMultiHash( + data: Buffer | Uint8Array, + hashName: HashName, + baseEncoding: BaseName = 'base58btc' + ): string { + const mh = MultiHashEncoder.encode(data, hashName) + const mb = MultiBaseEncoder.encode(mh, baseEncoding) + return mb + } + + private static encodeMetadata(metadata: Metadata, baseEncoding: BaseName): string { + const metadataMap = new Map() + + for (const key of Object.keys(metadata)) { + if (key === 'urls' || key === 'contentType') { + metadataMap.set(hexTable[key], metadata[key]) + } else { + throw new Error(`Invalid metadata: ${metadata}`) + } + } + + const cborData = cbor.encode(metadataMap) + + const multibaseMetadata = MultiBaseEncoder.encode(cborData, baseEncoding) + + return multibaseMetadata + } + + private static decodeMetadata(mb: string): Metadata { + const obj = { urls: [] as string[], contentType: '' } + const { data } = MultiBaseEncoder.decode(mb) + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cborData: Map = cbor.decode(data) + cborData.forEach((value, key) => { + if (key === hexTable.urls) { + obj.urls = value + } else if (key === hexTable.contentType) { + obj.contentType = value + } else { + throw new Error(`Invalid metadata property: ${key}:${value}`) + } + }) + return obj + } catch (error) { + throw new Error(`Invalid metadata: ${mb}, ${error}`) + } + } +} diff --git a/packages/core/src/utils/JWE.ts b/packages/core/src/utils/JWE.ts new file mode 100644 index 0000000000..f0c7c6049f --- /dev/null +++ b/packages/core/src/utils/JWE.ts @@ -0,0 +1,14 @@ +import type { EncryptedMessage } from '../types' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isValidJweStructure(message: any): message is EncryptedMessage { + return Boolean( + message && + typeof message === 'object' && + message !== null && + typeof message.protected === 'string' && + message.iv && + message.ciphertext && + message.tag + ) +} diff --git a/packages/core/src/utils/JsonEncoder.ts b/packages/core/src/utils/JsonEncoder.ts new file mode 100644 index 0000000000..f8b4f0e42e --- /dev/null +++ b/packages/core/src/utils/JsonEncoder.ts @@ -0,0 +1,67 @@ +import { base64ToBase64URL } from './base64' +import { Buffer } from './buffer' + +export class JsonEncoder { + /** + * Encode json object into base64 string. + * + * @param json the json object to encode into base64 string + */ + public static toBase64(json: unknown) { + return JsonEncoder.toBuffer(json).toString('base64') + } + + /** + * Encode json object into base64url string. + * + * @param json the json object to encode into base64url string + */ + public static toBase64URL(json: unknown) { + return base64ToBase64URL(JsonEncoder.toBase64(json)) + } + + /** + * Decode base64 string into json object. Also supports base64url + * + * @param base64 the base64 or base64url string to decode into json + */ + public static fromBase64(base64: string) { + return JsonEncoder.fromBuffer(Buffer.from(base64, 'base64')) + } + + /** + * Encode json object into string + * + * @param json the json object to encode into string + */ + public static toString(json: unknown) { + return JSON.stringify(json) + } + + /** + * Decode string into json object + * + * @param string the string to decode into json + */ + public static fromString(string: string) { + return JSON.parse(string) + } + + /** + * Encode json object into buffer + * + * @param json the json object to encode into buffer format + */ + public static toBuffer(json: unknown) { + return Buffer.from(JsonEncoder.toString(json)) + } + + /** + * Decode buffer into json object + * + * @param buffer the buffer to decode into json + */ + public static fromBuffer(buffer: Buffer | Uint8Array) { + return JsonEncoder.fromString(Buffer.from(buffer).toString('utf-8')) + } +} diff --git a/packages/core/src/utils/JsonTransformer.ts b/packages/core/src/utils/JsonTransformer.ts new file mode 100644 index 0000000000..eb65999ca3 --- /dev/null +++ b/packages/core/src/utils/JsonTransformer.ts @@ -0,0 +1,61 @@ +import type { Validate } from 'class-validator' + +import { instanceToPlain, plainToInstance, instanceToInstance } from 'class-transformer' + +import { ClassValidationError } from '../error/ClassValidationError' + +import { MessageValidator } from './MessageValidator' + +interface Validate { + validate?: boolean +} + +export class JsonTransformer { + public static toJSON(classInstance: T) { + return instanceToPlain(classInstance, { + exposeDefaultValues: true, + }) + } + + public static fromJSON( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + cls: { new (...args: any[]): T }, + { validate = true }: Validate = {} + ): T { + const instance = plainToInstance(cls, json, { exposeDefaultValues: true }) + + // Skip validation + if (!validate) return instance + + if (!instance) { + throw new ClassValidationError('Cannot validate instance of ', { classType: Object.getPrototypeOf(cls).name }) + } + MessageValidator.validateSync(instance) + + return instance + } + + public static clone(classInstance: T): T { + return instanceToInstance(classInstance, { + exposeDefaultValues: true, + enableCircularCheck: true, + enableImplicitConversion: true, + ignoreDecorators: true, + }) + } + + public static serialize(classInstance: T): string { + return JSON.stringify(this.toJSON(classInstance)) + } + + public static deserialize( + jsonString: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + cls: { new (...args: any[]): T }, + { validate = true }: Validate = {} + ): T { + return this.fromJSON(JSON.parse(jsonString), cls, { validate }) + } +} diff --git a/packages/core/src/utils/LinkedAttachment.ts b/packages/core/src/utils/LinkedAttachment.ts new file mode 100644 index 0000000000..2c03accbf7 --- /dev/null +++ b/packages/core/src/utils/LinkedAttachment.ts @@ -0,0 +1,43 @@ +import { Type } from 'class-transformer' +import { IsString } from 'class-validator' + +import { Attachment } from '../decorators/attachment/Attachment' + +import { encodeAttachment } from './attachment' + +export interface LinkedAttachmentOptions { + name: string + attachment: Attachment +} + +export class LinkedAttachment { + public constructor(options: LinkedAttachmentOptions) { + this.attributeName = options.name + this.attachment = options.attachment + this.attachment.id = this.getId(options.attachment) + } + + /** + * The name that will be used to generate the linked credential + */ + @IsString() + public attributeName: string + + /** + * The attachment that needs to be linked to the credential + */ + @Type(() => Attachment) + public attachment: Attachment + + /** + * Generates an ID based on the data in the attachment + * + * @param attachment the attachment that requires a hashlink + * @returns the id + */ + private getId(attachment: Attachment): string { + // Take the second element since the id property + // of a decorator MUST not contain a colon and has a maximum size of 64 characters + return encodeAttachment(attachment).split(':')[1].substring(0, 64) + } +} diff --git a/packages/core/src/utils/MessageValidator.ts b/packages/core/src/utils/MessageValidator.ts new file mode 100644 index 0000000000..386cb49377 --- /dev/null +++ b/packages/core/src/utils/MessageValidator.ts @@ -0,0 +1,29 @@ +import { validateSync } from 'class-validator' + +import { ClassValidationError } from '../error' +import { isValidationErrorArray } from '../error/ValidationErrorUtils' + +export class MessageValidator { + /** + * + * @param classInstance the class instance to validate + * @returns void + * @throws array of validation errors {@link ValidationError} + */ + // eslint-disable-next-line @typescript-eslint/ban-types + public static validateSync(classInstance: T & {}) { + // NOTE: validateSync returns an Array of errors. + // We have to transform that into an error of choice and throw that. + const errors = validateSync(classInstance) + if (isValidationErrorArray(errors)) { + throw new ClassValidationError('Failed to validate class.', { + classType: classInstance.constructor.name, + validationErrors: errors, + }) + } else if (errors.length !== 0) { + throw new ClassValidationError('An unknown validation error occurred.', { + classType: Object.prototype.constructor(classInstance).name, + }) + } + } +} diff --git a/packages/core/src/utils/MultiBaseEncoder.ts b/packages/core/src/utils/MultiBaseEncoder.ts new file mode 100644 index 0000000000..2a1e19e125 --- /dev/null +++ b/packages/core/src/utils/MultiBaseEncoder.ts @@ -0,0 +1,66 @@ +import { decodeFromBase58, encodeToBase58 } from './base58' + +export type BaseName = 'base58btc' + +type EncodingMap = { + [key in BaseName]: (data: Uint8Array) => string +} + +type DecodingMap = { + [key: string]: (data: string) => { data: Uint8Array; baseName: BaseName } +} + +const multibaseEncodingMap: EncodingMap = { + base58btc: (data) => `z${encodeToBase58(data)}`, +} + +const multibaseDecodingMap: DecodingMap = { + z: (data) => ({ data: decodeFromBase58(data.substring(1)), baseName: 'base58btc' }), +} + +export class MultiBaseEncoder { + /** + * + * Encodes a buffer into a multibase + * + * @param buffer the buffer that has to be encoded + * @param baseName the encoding algorithm + */ + public static encode(buffer: Uint8Array, baseName: BaseName) { + const encode = multibaseEncodingMap[baseName] + + if (!encode) { + throw new Error(`Unsupported encoding '${baseName}'`) + } + + return encode(buffer) + } + + /** + * + * Decodes a multibase into a Uint8Array + * + * @param data the multibase that has to be decoded + * + * @returns decoded data and the multi base name + */ + public static decode(data: string): { data: Uint8Array; baseName: string } { + const prefix = data[0] + const decode = multibaseDecodingMap[prefix] + + if (!decode) { + throw new Error(`No decoder found for multibase prefix '${prefix}'`) + } + + return decode(data) + } + + public static isValid(data: string): boolean { + try { + MultiBaseEncoder.decode(data) + return true + } catch (error) { + return false + } + } +} diff --git a/packages/core/src/utils/MultiHashEncoder.ts b/packages/core/src/utils/MultiHashEncoder.ts new file mode 100644 index 0000000000..d6742981b3 --- /dev/null +++ b/packages/core/src/utils/MultiHashEncoder.ts @@ -0,0 +1,87 @@ +import type { HashName } from './Hasher' + +import { Hasher } from './Hasher' +import { VarintEncoder } from './VarintEncoder' +import { Buffer } from './buffer' + +type MultiHashNameMap = { + [key in HashName]: number +} + +type MultiHashCodeMap = { + [key: number]: HashName +} + +const multiHashNameMap: MultiHashNameMap = { + 'sha-256': 0x12, +} + +const multiHashCodeMap: MultiHashCodeMap = Object.entries(multiHashNameMap).reduce( + (map, [hashName, hashCode]) => ({ ...map, [hashCode]: hashName }), + {} +) + +export class MultiHashEncoder { + /** + * + * Encodes a buffer into a hash + * + * @param buffer the buffer that has to be encoded + * @param hashName the hashing algorithm, 'sha-256' + * + * @returns a multihash + */ + public static encode(data: Uint8Array, hashName: HashName): Buffer { + const hash = Hasher.hash(data, hashName) + const hashCode = multiHashNameMap[hashName] + + const hashPrefix = VarintEncoder.encode(hashCode) + const hashLengthPrefix = VarintEncoder.encode(hash.length) + + return Buffer.concat([hashPrefix, hashLengthPrefix, hash]) + } + + /** + * + * Decodes the multihash + * + * @param data the multihash that has to be decoded + * + * @returns object with the data and the hashing algorithm + */ + public static decode(data: Uint8Array): { data: Buffer; hashName: string } { + const [hashPrefix, hashPrefixByteLength] = VarintEncoder.decode(data) + const withoutHashPrefix = data.slice(hashPrefixByteLength) + + const [, lengthPrefixByteLength] = VarintEncoder.decode(withoutHashPrefix) + const withoutLengthPrefix = withoutHashPrefix.slice(lengthPrefixByteLength) + + const hashName = multiHashCodeMap[hashPrefix] + + if (!hashName) { + throw new Error(`Unsupported hash code 0x${hashPrefix.toString(16)}`) + } + + return { + data: Buffer.from(withoutLengthPrefix), + hashName: multiHashCodeMap[hashPrefix], + } + } + + /** + * + * Validates if it is a valid mulithash + * + * @param data the multihash that needs to be validated + * + * @returns a boolean whether the multihash is valid + */ + public static isValid(data: Uint8Array): boolean { + try { + MultiHashEncoder.decode(data) + return true + } catch (e) { + return false + } + } +} diff --git a/packages/core/src/utils/TypedArrayEncoder.ts b/packages/core/src/utils/TypedArrayEncoder.ts new file mode 100644 index 0000000000..b98a5350c2 --- /dev/null +++ b/packages/core/src/utils/TypedArrayEncoder.ts @@ -0,0 +1,95 @@ +import { decodeFromBase58, encodeToBase58 } from './base58' +import { base64ToBase64URL } from './base64' +import { Buffer } from './buffer' + +export class TypedArrayEncoder { + /** + * Encode buffer into base64 string. + * + * @param buffer the buffer to encode into base64 string + */ + public static toBase64(buffer: Buffer | Uint8Array) { + return Buffer.from(buffer).toString('base64') + } + + /** + * Encode buffer into base64url string. + * + * @param buffer the buffer to encode into base64url string + */ + public static toBase64URL(buffer: Buffer | Uint8Array) { + return base64ToBase64URL(TypedArrayEncoder.toBase64(buffer)) + } + + /** + * Encode buffer into base58 string. + * + * @param buffer the buffer to encode into base58 string + */ + public static toBase58(buffer: Buffer | Uint8Array) { + return encodeToBase58(buffer) + } + + /** + * Decode base64 string into buffer. Also supports base64url + * + * @param base64 the base64 or base64url string to decode into buffer format + */ + public static fromBase64(base64: string) { + return Buffer.from(base64, 'base64') + } + + /** + * Decode base58 string into buffer + * + * @param base58 the base58 string to decode into buffer format + */ + public static fromBase58(base58: string) { + return Buffer.from(decodeFromBase58(base58)) + } + + /** + * Encode buffer into base64 string. + * + * @param buffer the buffer to encode into base64 string + */ + public static toHex(buffer: Buffer | Uint8Array) { + return Buffer.from(buffer).toString('hex') + } + + /** + * Decode hex string into buffer + * + * @param hex the hex string to decode into buffer format + */ + public static fromHex(hex: string) { + return Buffer.from(hex, 'hex') + } + + /** + * Decode string into buffer. + * + * @param str the string to decode into buffer format + */ + public static fromString(str: string): Buffer { + return Buffer.from(str) + } + + public static toUtf8String(buffer: Buffer | Uint8Array) { + return Buffer.from(buffer).toString() + } + + /** + * Check whether an array is byte, or typed, array + * + * @param array unknown The array that has to be checked + * + * @returns A boolean if the array is a byte array + */ + public static isTypedArray(array: unknown): boolean { + // Checks whether the static property 'BYTES_PER_ELEMENT' exists on the provided array. + // This has to be done, since the TypedArrays, e.g. Uint8Array and Float32Array, do not + // extend a single base class + return 'BYTES_PER_ELEMENT' in (array as Record) + } +} diff --git a/packages/core/src/utils/VarintEncoder.ts b/packages/core/src/utils/VarintEncoder.ts new file mode 100644 index 0000000000..d8a16699da --- /dev/null +++ b/packages/core/src/utils/VarintEncoder.ts @@ -0,0 +1,20 @@ +import { decode, encode, encodingLength } from 'varint' + +import { Buffer } from './buffer' + +export class VarintEncoder { + public static decode(data: Uint8Array | number[] | Buffer) { + const code = decode(data) + return [code, decode.bytes] as const + } + + public static encode(int: number) { + const target = new Buffer(VarintEncoder.encodingLength(int)) + encode(int, target) + return target + } + + public static encodingLength(int: number) { + return encodingLength(int) + } +} diff --git a/packages/core/src/utils/__tests__/HashlinkEncoder.test.ts b/packages/core/src/utils/__tests__/HashlinkEncoder.test.ts new file mode 100644 index 0000000000..1beee86740 --- /dev/null +++ b/packages/core/src/utils/__tests__/HashlinkEncoder.test.ts @@ -0,0 +1,78 @@ +import { HashlinkEncoder } from '../HashlinkEncoder' +import { Buffer } from '../buffer' + +const validData = { + data: Buffer.from('Hello World!'), + metadata: { + urls: ['https://example.org/hw.txt'], + contentType: 'text/plain', + }, +} + +const invalidData = { + data: Buffer.from('Hello World!'), + metadata: { + unknownKey: 'unkownValue', + contentType: 'image/png', + }, +} + +const validHashlink = + 'hl:zQmWvQxTqbG2Z9HPJgG57jjwR154cKhbtJenbyYTWkjgF3e:zCwPSdabLuj3jue1qYujzunnKwpL4myKdyeqySyFhnzZ8qeLeC1U6N5eycFD' + +const invalidHashlink = + 'hl:gQmWvQxTqbqwlkhhhhh9w8e7rJenbyYTWkjgF3e:z51a94WAQfNv1KEcPeoV3V2isZFPFqSzE9ghNFQ8DuQu4hTHtFRug8SDgug14Ff' + +const invalidMetadata = + 'hl:zQmWvQxTqbG2Z9HPJgG57jjwR154cKhbtJenbyYTWkjgF3e:zHCwSqQisPgCc2sMSNmHWyQtCKu4kgQVD6Q1Nhxff7uNRqN6r' + +describe('HashlinkEncoder', () => { + describe('encode()', () => { + it('Encodes string to hashlink', () => { + const hashlink = HashlinkEncoder.encode(validData.data, 'sha-256') + expect(hashlink).toEqual('hl:zQmWvQxTqbG2Z9HPJgG57jjwR154cKhbtJenbyYTWkjgF3e') + }) + + it('Encodes string and metadata to hashlink', () => { + const hashlink = HashlinkEncoder.encode(validData.data, 'sha-256', 'base58btc', validData.metadata) + expect(hashlink).toEqual(validHashlink) + }) + + it('Encodes invalid metadata in hashlink', () => { + expect(() => { + HashlinkEncoder.encode(validData.data, 'sha-256', 'base58btc', invalidData.metadata) + }).toThrow(/^Invalid metadata: /) + }) + }) + + describe('decode()', () => { + it('Decodes hashlink', () => { + const decodedHashlink = HashlinkEncoder.decode(validHashlink) + expect(decodedHashlink).toEqual({ + checksum: 'zQmWvQxTqbG2Z9HPJgG57jjwR154cKhbtJenbyYTWkjgF3e', + metadata: { contentType: 'text/plain', urls: ['https://example.org/hw.txt'] }, + }) + }) + + it('Decodes invalid hashlink', () => { + expect(() => { + HashlinkEncoder.decode(invalidHashlink) + }).toThrow(/^Invalid hashlink: /) + }) + + it('Decodes invalid metadata in hashlink', () => { + expect(() => { + HashlinkEncoder.decode(invalidMetadata) + }).toThrow(/^Invalid metadata: /) + }) + }) + + describe('isValid()', () => { + it('Validate hashlink', () => { + expect(HashlinkEncoder.isValid(validHashlink)).toEqual(true) + }) + it('Validate invalid hashlink', () => { + expect(HashlinkEncoder.isValid(invalidHashlink)).toEqual(false) + }) + }) +}) diff --git a/packages/core/src/utils/__tests__/JWE.test.ts b/packages/core/src/utils/__tests__/JWE.test.ts new file mode 100644 index 0000000000..02ed3b5a55 --- /dev/null +++ b/packages/core/src/utils/__tests__/JWE.test.ts @@ -0,0 +1,19 @@ +import { isValidJweStructure } from '../JWE' + +describe('ValidJWEStructure', () => { + test('throws error when the response message has an invalid JWE structure', async () => { + const responseMessage = 'invalid JWE structure' + expect(isValidJweStructure(responseMessage)).toBe(false) + }) + + test('valid JWE structure', async () => { + const responseMessage = { + protected: + 'eyJlbmMiOiJ4Y2hhY2hhMjBwb2x5MTMwNV9pZXRmIiwidHlwIjoiSldNLzEuMCIsImFsZyI6IkF1dGhjcnlwdCIsInJlY2lwaWVudHMiOlt7ImVuY3J5cHRlZF9rZXkiOiJNYUNKa3B1YzltZWxnblEtUk8teWtsQWRBWWxzY21GdFEzd1hjZ3R0R0dlSmVsZDBEc2pmTUpSWUtYUDA0cTQ2IiwiaGVhZGVyIjp7ImtpZCI6IkJid2ZCaDZ3bWdZUnJ1TlozZXhFelk2RXBLS2g4cGNob211eDJQUjg5bURlIiwiaXYiOiJOWVJGb0xoUG1EZlFhQ3czUzQ2RmM5M1lucWhDUnhKbiIsInNlbmRlciI6IkRIQ0lsdE5tcEgwRlRrd3NuVGNSWXgwZmYzTHBQTlF6VG1jbUdhRW83aGU5d19ERkFmemNTWFdhOEFnNzRHVEpfdnBpNWtzQkQ3MWYwYjI2VF9mVHBfV2FscTBlWUhmeTE4ZEszejhUTkJFQURpZ1VPWi1wR21pV3FrUT0ifX1dfQ==', + iv: 'KNezOOt7JJtuU2q1', + ciphertext: 'mwRMpVg9wkF4rIZcBeWLcc0fWhs=', + tag: '0yW0Lx8-vWevj3if91R06g==', + } + expect(isValidJweStructure(responseMessage)).toBe(true) + }) +}) diff --git a/src/lib/utils/__tests__/JsonEncoder.test.ts b/packages/core/src/utils/__tests__/JsonEncoder.test.ts similarity index 95% rename from src/lib/utils/__tests__/JsonEncoder.test.ts rename to packages/core/src/utils/__tests__/JsonEncoder.test.ts index ffd23e9293..21e688adfc 100644 --- a/src/lib/utils/__tests__/JsonEncoder.test.ts +++ b/packages/core/src/utils/__tests__/JsonEncoder.test.ts @@ -1,12 +1,12 @@ -import { JsonEncoder } from '../JsonEncoder'; +import { JsonEncoder } from '../JsonEncoder' +import { Buffer } from '../buffer' describe('JsonEncoder', () => { const mockCredentialRequest = { prover_did: 'did:sov:4xRwQoKEBcLMR3ni1uEVxo', cred_def_id: 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:132:TAG', blinded_ms: { - u: - '29923358933378594884016949116015048362197910313115517615886712002247549877203754161040662113193681067971700889005059954680562568543707874455462734614770719857476517583732118175472117258262243524371047244612027670795341517678404538774311254888125372395478117859977957781545203614422057872425388379038174161933077364160507888216751250173474993459002409808169413785534283242566104648944508528553665015907898791106766199966107619949762668272339931067050394002255637771035855365582098862561250576470504742036107864292062117797625825433248517924504550308968312780301031964645548333248088593015937359889688860141757860414963', + u: '29923358933378594884016949116015048362197910313115517615886712002247549877203754161040662113193681067971700889005059954680562568543707874455462734614770719857476517583732118175472117258262243524371047244612027670795341517678404538774311254888125372395478117859977957781545203614422057872425388379038174161933077364160507888216751250173474993459002409808169413785534283242566104648944508528553665015907898791106766199966107619949762668272339931067050394002255637771035855365582098862561250576470504742036107864292062117797625825433248517924504550308968312780301031964645548333248088593015937359889688860141757860414963', ur: null, hidden_attributes: ['master_secret'], committed_attributes: {}, @@ -22,23 +22,23 @@ describe('JsonEncoder', () => { r_caps: {}, }, nonce: '1050445344368089902090762', - }; + } describe('toBase64', () => { test('encodes JSON object to Base64 string', () => { expect(JsonEncoder.toBase64(mockCredentialRequest)).toEqual( 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0=' - ); - }); - }); + ) + }) + }) describe('toBase64URL', () => { test('encodes JSON object to Base64URL string', () => { expect(JsonEncoder.toBase64URL(mockCredentialRequest)).toEqual( 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0' - ); - }); - }); + ) + }) + }) describe('fromBase64', () => { test('decodes Base64 string to JSON object', () => { @@ -46,25 +46,25 @@ describe('JsonEncoder', () => { JsonEncoder.fromBase64( 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0=' ) - ).toEqual(mockCredentialRequest); - }); + ).toEqual(mockCredentialRequest) + }) test('decodes Base64URL string to JSON object', () => { expect( JsonEncoder.fromBase64( 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0' ) - ).toEqual(mockCredentialRequest); - }); - }); + ).toEqual(mockCredentialRequest) + }) + }) describe('toString', () => { test('encodes JSON object to string', () => { expect(JsonEncoder.toString(mockCredentialRequest)).toEqual( '{"prover_did":"did:sov:4xRwQoKEBcLMR3ni1uEVxo","cred_def_id":"TL1EaPFCZ8Si5aUrqScBDt:3:CL:132:TAG","blinded_ms":{"u":"29923358933378594884016949116015048362197910313115517615886712002247549877203754161040662113193681067971700889005059954680562568543707874455462734614770719857476517583732118175472117258262243524371047244612027670795341517678404538774311254888125372395478117859977957781545203614422057872425388379038174161933077364160507888216751250173474993459002409808169413785534283242566104648944508528553665015907898791106766199966107619949762668272339931067050394002255637771035855365582098862561250576470504742036107864292062117797625825433248517924504550308968312780301031964645548333248088593015937359889688860141757860414963","ur":null,"hidden_attributes":["master_secret"],"committed_attributes":{}},"blinded_ms_correctness_proof":{"c":"75472844799889714957212252604198654959564254049476575366093619008804723782477","v_dash_cap":"241658605435359439784797922191819649424225260343012968028448862434367856520024882808883591559121551472624638941147168471260306900890395947200746462759366134826652687587855065931934069125722728256802714393333799201395529881991611738749782316620019450872378759017790239991970563038638122599532027158669448906627074717578703201192531300159510207969537644782742047266895171574655304024820059682220314444862709893029720461916722147413329108444569762285168314593698192502335045295322386283022390355151395113112616277162501380456415321942055143111557003766356007622317275215637810918710682103087743057471347095588547791117729784209946546040216520440891760010822467637114796927573279512910382694631674566232457443287483928629","m_caps":{"master_secret":"10958352359087386063400811574253208318437641929870855399316173807950198557972017190892697250360879224208078813463868425727031560613450102005398401915135973584419891172590586358799"},"r_caps":{}},"nonce":"1050445344368089902090762"}' - ); - }); - }); + ) + }) + }) describe('fromString', () => { test('decodes string to JSON object', () => { @@ -72,27 +72,27 @@ describe('JsonEncoder', () => { JsonEncoder.fromString( '{"prover_did":"did:sov:4xRwQoKEBcLMR3ni1uEVxo","cred_def_id":"TL1EaPFCZ8Si5aUrqScBDt:3:CL:132:TAG","blinded_ms":{"u":"29923358933378594884016949116015048362197910313115517615886712002247549877203754161040662113193681067971700889005059954680562568543707874455462734614770719857476517583732118175472117258262243524371047244612027670795341517678404538774311254888125372395478117859977957781545203614422057872425388379038174161933077364160507888216751250173474993459002409808169413785534283242566104648944508528553665015907898791106766199966107619949762668272339931067050394002255637771035855365582098862561250576470504742036107864292062117797625825433248517924504550308968312780301031964645548333248088593015937359889688860141757860414963","ur":null,"hidden_attributes":["master_secret"],"committed_attributes":{}},"blinded_ms_correctness_proof":{"c":"75472844799889714957212252604198654959564254049476575366093619008804723782477","v_dash_cap":"241658605435359439784797922191819649424225260343012968028448862434367856520024882808883591559121551472624638941147168471260306900890395947200746462759366134826652687587855065931934069125722728256802714393333799201395529881991611738749782316620019450872378759017790239991970563038638122599532027158669448906627074717578703201192531300159510207969537644782742047266895171574655304024820059682220314444862709893029720461916722147413329108444569762285168314593698192502335045295322386283022390355151395113112616277162501380456415321942055143111557003766356007622317275215637810918710682103087743057471347095588547791117729784209946546040216520440891760010822467637114796927573279512910382694631674566232457443287483928629","m_caps":{"master_secret":"10958352359087386063400811574253208318437641929870855399316173807950198557972017190892697250360879224208078813463868425727031560613450102005398401915135973584419891172590586358799"},"r_caps":{}},"nonce":"1050445344368089902090762"}' ) - ).toEqual(mockCredentialRequest); - }); - }); + ).toEqual(mockCredentialRequest) + }) + }) describe('toBuffer', () => { test('encodes buffer to JSON object', () => { const expected = Buffer.from( '{"prover_did":"did:sov:4xRwQoKEBcLMR3ni1uEVxo","cred_def_id":"TL1EaPFCZ8Si5aUrqScBDt:3:CL:132:TAG","blinded_ms":{"u":"29923358933378594884016949116015048362197910313115517615886712002247549877203754161040662113193681067971700889005059954680562568543707874455462734614770719857476517583732118175472117258262243524371047244612027670795341517678404538774311254888125372395478117859977957781545203614422057872425388379038174161933077364160507888216751250173474993459002409808169413785534283242566104648944508528553665015907898791106766199966107619949762668272339931067050394002255637771035855365582098862561250576470504742036107864292062117797625825433248517924504550308968312780301031964645548333248088593015937359889688860141757860414963","ur":null,"hidden_attributes":["master_secret"],"committed_attributes":{}},"blinded_ms_correctness_proof":{"c":"75472844799889714957212252604198654959564254049476575366093619008804723782477","v_dash_cap":"241658605435359439784797922191819649424225260343012968028448862434367856520024882808883591559121551472624638941147168471260306900890395947200746462759366134826652687587855065931934069125722728256802714393333799201395529881991611738749782316620019450872378759017790239991970563038638122599532027158669448906627074717578703201192531300159510207969537644782742047266895171574655304024820059682220314444862709893029720461916722147413329108444569762285168314593698192502335045295322386283022390355151395113112616277162501380456415321942055143111557003766356007622317275215637810918710682103087743057471347095588547791117729784209946546040216520440891760010822467637114796927573279512910382694631674566232457443287483928629","m_caps":{"master_secret":"10958352359087386063400811574253208318437641929870855399316173807950198557972017190892697250360879224208078813463868425727031560613450102005398401915135973584419891172590586358799"},"r_caps":{}},"nonce":"1050445344368089902090762"}' - ); + ) - expect(JsonEncoder.toBuffer(mockCredentialRequest).equals(expected)).toBe(true); - }); - }); + expect(JsonEncoder.toBuffer(mockCredentialRequest).equals(expected)).toBe(true) + }) + }) describe('fromBuffer', () => { test('decodes JSON object to buffer', () => { const buffer = Buffer.from( '{"prover_did":"did:sov:4xRwQoKEBcLMR3ni1uEVxo","cred_def_id":"TL1EaPFCZ8Si5aUrqScBDt:3:CL:132:TAG","blinded_ms":{"u":"29923358933378594884016949116015048362197910313115517615886712002247549877203754161040662113193681067971700889005059954680562568543707874455462734614770719857476517583732118175472117258262243524371047244612027670795341517678404538774311254888125372395478117859977957781545203614422057872425388379038174161933077364160507888216751250173474993459002409808169413785534283242566104648944508528553665015907898791106766199966107619949762668272339931067050394002255637771035855365582098862561250576470504742036107864292062117797625825433248517924504550308968312780301031964645548333248088593015937359889688860141757860414963","ur":null,"hidden_attributes":["master_secret"],"committed_attributes":{}},"blinded_ms_correctness_proof":{"c":"75472844799889714957212252604198654959564254049476575366093619008804723782477","v_dash_cap":"241658605435359439784797922191819649424225260343012968028448862434367856520024882808883591559121551472624638941147168471260306900890395947200746462759366134826652687587855065931934069125722728256802714393333799201395529881991611738749782316620019450872378759017790239991970563038638122599532027158669448906627074717578703201192531300159510207969537644782742047266895171574655304024820059682220314444862709893029720461916722147413329108444569762285168314593698192502335045295322386283022390355151395113112616277162501380456415321942055143111557003766356007622317275215637810918710682103087743057471347095588547791117729784209946546040216520440891760010822467637114796927573279512910382694631674566232457443287483928629","m_caps":{"master_secret":"10958352359087386063400811574253208318437641929870855399316173807950198557972017190892697250360879224208078813463868425727031560613450102005398401915135973584419891172590586358799"},"r_caps":{}},"nonce":"1050445344368089902090762"}' - ); + ) - expect(JsonEncoder.fromBuffer(buffer)).toEqual(mockCredentialRequest); - }); - }); -}); + expect(JsonEncoder.fromBuffer(buffer)).toEqual(mockCredentialRequest) + }) + }) +}) diff --git a/packages/core/src/utils/__tests__/JsonTransformer.test.ts b/packages/core/src/utils/__tests__/JsonTransformer.test.ts new file mode 100644 index 0000000000..0d7158fb80 --- /dev/null +++ b/packages/core/src/utils/__tests__/JsonTransformer.test.ts @@ -0,0 +1,84 @@ +import { ConnectionInvitationMessage } from '../../modules/connections' +import { DidDocument, VerificationMethod } from '../../modules/dids' +import { JsonTransformer } from '../JsonTransformer' + +describe('JsonTransformer', () => { + describe('toJSON', () => { + it('transforms class instance to JSON object', () => { + const invitation = new ConnectionInvitationMessage({ + did: 'did:sov:test1234', + id: 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', + label: 'test-label', + }) + + const json = { + '@type': 'https://didcomm.org/connections/1.0/invitation', + '@id': 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', + label: 'test-label', + did: 'did:sov:test1234', + } + + expect(JsonTransformer.toJSON(invitation)).toEqual(json) + }) + }) + + describe('fromJSON', () => { + it('transforms JSON object to class instance', () => { + const json = { + '@type': 'https://didcomm.org/connections/1.0/invitation', + '@id': 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', + label: 'test-label', + did: 'did:sov:test1234', + } + + const invitation = new ConnectionInvitationMessage({ + did: 'did:sov:test1234', + id: 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', + label: 'test-label', + }) + + expect(JsonTransformer.fromJSON(json, ConnectionInvitationMessage)).toEqual(invitation) + }) + }) + + describe('serialize', () => { + it('transforms class instance to JSON string', () => { + const invitation = new ConnectionInvitationMessage({ + did: 'did:sov:test1234', + id: 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', + label: 'test-label', + }) + + const jsonString = + '{"@type":"https://didcomm.org/connections/1.0/invitation","@id":"afe2867e-58c3-4a8d-85b2-23370dd9c9f0","label":"test-label","did":"did:sov:test1234"}' + + expect(JsonTransformer.serialize(invitation)).toEqual(jsonString) + }) + }) + + describe('deserialize', () => { + it('transforms JSON string to class instance', () => { + const jsonString = + '{"@type":"https://didcomm.org/connections/1.0/invitation","@id":"afe2867e-58c3-4a8d-85b2-23370dd9c9f0","label":"test-label","did":"did:sov:test1234"}' + + const invitation = new ConnectionInvitationMessage({ + did: 'did:sov:test1234', + id: 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', + label: 'test-label', + }) + + expect(JsonTransformer.deserialize(jsonString, ConnectionInvitationMessage)).toEqual(invitation) + }) + + it('transforms JSON string to nested class instance', () => { + const didDocumentString = + '{"@context":["https://w3id.org/did/v1"],"id":"did:peer:1zQmRYBx1pL86DrsxoJ2ZD3w42d7Ng92ErPgFsCSqg8Q1h4i","controller": "nowYouAreUnderMyControl", "keyAgreement":[{"id":"#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V", "controller": "#id", "type":"Ed25519VerificationKey2018","publicKeyBase58":"ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7"}],"service":[{"id":"#service-0","type":"did-communication","serviceEndpoint":"https://example.com/endpoint","recipientKeys":["#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V"],"routingKeys":["did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH#z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH"],"accept":["didcomm/v2","didcomm/aip2;env=rfc587"]}]}' + + const didDocument = JsonTransformer.deserialize(didDocumentString, DidDocument) + + const keyAgreement = didDocument.keyAgreement ?? [] + + expect(keyAgreement[0]).toBeInstanceOf(VerificationMethod) + }) + }) +}) diff --git a/packages/core/src/utils/__tests__/MessageValidator.test.ts b/packages/core/src/utils/__tests__/MessageValidator.test.ts new file mode 100644 index 0000000000..b0c15e2491 --- /dev/null +++ b/packages/core/src/utils/__tests__/MessageValidator.test.ts @@ -0,0 +1,27 @@ +import { ClassValidationError } from '../../error/ClassValidationError' +import { ConnectionInvitationMessage } from '../../modules/connections' +import { MessageValidator } from '../MessageValidator' + +describe('MessageValidator', () => { + describe('validateSync', () => { + it('validates a class instance correctly', () => { + const invitation = new ConnectionInvitationMessage({ + did: 'did:sov:test1234', + id: 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', + label: 'test-label', + }) + + expect(MessageValidator.validateSync(invitation)).toBeUndefined() + }) + it('throws an error for invalid class instance', () => { + const invitation = new ConnectionInvitationMessage({ + did: 'did:sov:test1234', + id: 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', + label: 'test-label', + }) + invitation.did = undefined + + expect(() => MessageValidator.validateSync(invitation)).toThrow(ClassValidationError) + }) + }) +}) diff --git a/packages/core/src/utils/__tests__/MultibaseEncoder.test.ts b/packages/core/src/utils/__tests__/MultibaseEncoder.test.ts new file mode 100644 index 0000000000..302bac9c99 --- /dev/null +++ b/packages/core/src/utils/__tests__/MultibaseEncoder.test.ts @@ -0,0 +1,40 @@ +import { MultiBaseEncoder } from '../MultiBaseEncoder' +import { TypedArrayEncoder } from '../TypedArrayEncoder' +import { Buffer } from '../buffer' + +const validData = Buffer.from('Hello World!') +const validMultiBase = 'zKWfinQuRQ3ekD1danFHqvKRg9koFp8vpokUeREEgjSyHwweeKDFaxVHi' +const invalidMultiBase = 'gKWfinQuRQ3ekD1danFHqvKRg9koFp8vpokUeREEgjSyHwweeKDFaxVHi' + +describe('MultiBaseEncoder', () => { + describe('encode()', () => { + it('Encodes valid multibase', () => { + const multibase = MultiBaseEncoder.encode(validData, 'base58btc') + expect(multibase).toEqual('z2NEpo7TZRRrLZSi2U') + }) + }) + + describe('Decodes()', () => { + it('Decodes multibase', () => { + const { data, baseName } = MultiBaseEncoder.decode(validMultiBase) + expect(TypedArrayEncoder.toUtf8String(data)).toEqual('This is a valid base58btc encoded string!') + expect(baseName).toEqual('base58btc') + }) + + it('Decodes invalid multibase', () => { + expect(() => { + MultiBaseEncoder.decode(invalidMultiBase) + }).toThrow(/^No decoder found for multibase prefix/) + }) + }) + + describe('isValid()', () => { + it('Validates valid multibase', () => { + expect(MultiBaseEncoder.isValid(validMultiBase)).toEqual(true) + }) + + it('Validates invalid multibase', () => { + expect(MultiBaseEncoder.isValid(invalidMultiBase)).toEqual(false) + }) + }) +}) diff --git a/packages/core/src/utils/__tests__/MultihashEncoder.test.ts b/packages/core/src/utils/__tests__/MultihashEncoder.test.ts new file mode 100644 index 0000000000..d60a2a1e2c --- /dev/null +++ b/packages/core/src/utils/__tests__/MultihashEncoder.test.ts @@ -0,0 +1,44 @@ +import { Hasher } from '../Hasher' +import { MultiHashEncoder } from '../MultiHashEncoder' +import { Buffer } from '../buffer' + +const validData = Buffer.from('Hello World!') +const validMultiHash = new Uint8Array([ + 18, 32, 127, 131, 177, 101, 127, 241, 252, 83, 185, 45, 193, 129, 72, 161, 214, 93, 252, 45, 75, 31, 163, 214, 119, + 40, 74, 221, 210, 0, 18, 109, 144, 105, +]) +const validHash = Hasher.hash(validData, 'sha-256') +const invalidMultiHash = new Uint8Array([99, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33]) + +describe('MultiHashEncoder', () => { + describe('encode()', () => { + it('encodes multihash', () => { + const multihash = MultiHashEncoder.encode(validData, 'sha-256') + expect(multihash.equals(Buffer.from(validMultiHash))).toBe(true) + }) + }) + + describe('decode()', () => { + it('Decodes multihash', () => { + const { data, hashName } = MultiHashEncoder.decode(validMultiHash) + expect(hashName).toEqual('sha-256') + expect(data.equals(Buffer.from(validHash))).toBe(true) + }) + + it('Decodes invalid multihash', () => { + expect(() => { + MultiHashEncoder.decode(invalidMultiHash) + }).toThrow() + }) + }) + + describe('isValid()', () => { + it('Validates valid multihash', () => { + expect(MultiHashEncoder.isValid(validMultiHash)).toEqual(true) + }) + + it('Validates invalid multihash', () => { + expect(MultiHashEncoder.isValid(invalidMultiHash)).toEqual(false) + }) + }) +}) diff --git a/packages/core/src/utils/__tests__/TypedArrayEncoder.test.ts b/packages/core/src/utils/__tests__/TypedArrayEncoder.test.ts new file mode 100644 index 0000000000..925bf97f82 --- /dev/null +++ b/packages/core/src/utils/__tests__/TypedArrayEncoder.test.ts @@ -0,0 +1,79 @@ +import { TypedArrayEncoder } from '../TypedArrayEncoder' +import { Buffer } from '../buffer' + +describe('TypedArrayEncoder', () => { + const mockCredentialRequestBuffer = Buffer.from( + JSON.stringify({ + prover_did: 'did:sov:4xRwQoKEBcLMR3ni1uEVxo', + cred_def_id: 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:132:TAG', + blinded_ms: { + u: '29923358933378594884016949116015048362197910313115517615886712002247549877203754161040662113193681067971700889005059954680562568543707874455462734614770719857476517583732118175472117258262243524371047244612027670795341517678404538774311254888125372395478117859977957781545203614422057872425388379038174161933077364160507888216751250173474993459002409808169413785534283242566104648944508528553665015907898791106766199966107619949762668272339931067050394002255637771035855365582098862561250576470504742036107864292062117797625825433248517924504550308968312780301031964645548333248088593015937359889688860141757860414963', + ur: null, + hidden_attributes: ['master_secret'], + committed_attributes: {}, + }, + blinded_ms_correctness_proof: { + c: '75472844799889714957212252604198654959564254049476575366093619008804723782477', + v_dash_cap: + '241658605435359439784797922191819649424225260343012968028448862434367856520024882808883591559121551472624638941147168471260306900890395947200746462759366134826652687587855065931934069125722728256802714393333799201395529881991611738749782316620019450872378759017790239991970563038638122599532027158669448906627074717578703201192531300159510207969537644782742047266895171574655304024820059682220314444862709893029720461916722147413329108444569762285168314593698192502335045295322386283022390355151395113112616277162501380456415321942055143111557003766356007622317275215637810918710682103087743057471347095588547791117729784209946546040216520440891760010822467637114796927573279512910382694631674566232457443287483928629', + m_caps: { + master_secret: + '10958352359087386063400811574253208318437641929870855399316173807950198557972017190892697250360879224208078813463868425727031560613450102005398401915135973584419891172590586358799', + }, + r_caps: {}, + }, + nonce: '1050445344368089902090762', + }) + ) + + describe('isTypedArray', () => { + test('is array of type typedArray', () => { + const mockArray = [0, 1, 2] + expect(TypedArrayEncoder.isTypedArray(mockArray)).toStrictEqual(false) + }) + + test('is Uint8Array of type typedArray', () => { + const mockArray = new Uint8Array([0, 1, 2]) + expect(TypedArrayEncoder.isTypedArray(mockArray)).toStrictEqual(true) + }) + + test('is Buffer of type typedArray', () => { + const mockArray = new Buffer([0, 1, 2]) + expect(TypedArrayEncoder.isTypedArray(mockArray)).toStrictEqual(true) + }) + }) + + describe('toBase64', () => { + test('encodes buffer to Base64 string', () => { + expect(TypedArrayEncoder.toBase64(mockCredentialRequestBuffer)).toEqual( + 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0=' + ) + }) + }) + + describe('toBase64URL', () => { + test('encodes buffer to Base64URL string', () => { + expect(TypedArrayEncoder.toBase64URL(mockCredentialRequestBuffer)).toEqual( + 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0' + ) + }) + }) + + describe('fromBase64', () => { + test('decodes Base64 string to buffer object', () => { + expect( + TypedArrayEncoder.fromBase64( + 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0=' + ).equals(mockCredentialRequestBuffer) + ).toEqual(true) + }) + + test('decodes Base64URL string to buffer object', () => { + expect( + TypedArrayEncoder.fromBase64( + 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0' + ).equals(mockCredentialRequestBuffer) + ).toEqual(true) + }) + }) +}) diff --git a/packages/core/src/utils/__tests__/deepEquality.test.ts b/packages/core/src/utils/__tests__/deepEquality.test.ts new file mode 100644 index 0000000000..645a35e329 --- /dev/null +++ b/packages/core/src/utils/__tests__/deepEquality.test.ts @@ -0,0 +1,82 @@ +import { Metadata } from '../../storage/Metadata' +import { deepEquality } from '../deepEquality' + +describe('deepEquality', () => { + test('evaluates to true with equal maps', () => { + const a = new Map([ + ['foo', 1], + ['bar', 2], + ['baz', 3], + ]) + const b = new Map([ + ['foo', 1], + ['bar', 2], + ['baz', 3], + ]) + expect(deepEquality(a, b)).toBe(true) + }) + test('evaluates to false with unequal maps', () => { + const c = new Map([ + ['foo', 1], + ['baz', 3], + ['bar', 2], + ]) + + const d = new Map([ + ['foo', 1], + ['bar', 2], + ['qux', 3], + ]) + expect(deepEquality(c, d)).toBe(false) + }) + + test('evaluates to true with equal maps with different order', () => { + const a = new Map([ + ['baz', 3], + ['bar', 2], + ['foo', 1], + ]) + const b = new Map([ + ['foo', 1], + ['bar', 2], + ['baz', 3], + ]) + expect(deepEquality(a, b)).toBe(true) + }) + test('evaluates to true with equal primitives', () => { + expect(deepEquality(1, 1)).toBe(true) + expect(deepEquality(true, true)).toBe(true) + expect(deepEquality('a', 'a')).toBe(true) + }) + + test('evaluates to false with unequal primitives', () => { + expect(deepEquality(1, 2)).toBe(false) + expect(deepEquality(true, false)).toBe(false) + expect(deepEquality('a', 'b')).toBe(false) + }) + + test('evaluates to true with equal complex types', () => { + const fn = () => 'hello World!' + expect(deepEquality(fn, fn)).toBe(true) + expect(deepEquality({}, {})).toBe(true) + expect(deepEquality({ foo: 'bar' }, { foo: 'bar' })).toBe(true) + expect(deepEquality({ foo: 'baz', bar: 'bar' }, { bar: 'bar', foo: 'baz' })).toBe(true) + expect(deepEquality(Metadata, Metadata)).toBe(true) + expect(deepEquality(new Metadata({}), new Metadata({}))).toBe(true) + }) + + test('evaluates to false with unequal complex types', () => { + const fn = () => 'hello World!' + const fnTwo = () => 'Goodbye World!' + class Bar { + public constructor() { + 'yes' + } + } + expect(deepEquality(fn, fnTwo)).toBe(false) + expect(deepEquality({ bar: 'foo' }, { a: 'b' })).toBe(false) + expect(deepEquality({ b: 'a' }, { b: 'a', c: 'd' })).toBe(false) + expect(deepEquality(Metadata, Bar)).toBe(false) + expect(deepEquality(new Metadata({}), new Bar())).toBe(false) + }) +}) diff --git a/packages/core/src/utils/__tests__/messageType.test.ts b/packages/core/src/utils/__tests__/messageType.test.ts new file mode 100644 index 0000000000..904e035eb7 --- /dev/null +++ b/packages/core/src/utils/__tests__/messageType.test.ts @@ -0,0 +1,349 @@ +import { AgentMessage } from '../../agent/AgentMessage' +import { + canHandleMessageType, + parseDidCommProtocolUri, + parseMessageType, + replaceLegacyDidSovPrefix, + replaceLegacyDidSovPrefixOnMessage, + replaceNewDidCommPrefixWithLegacyDidSov, + replaceNewDidCommPrefixWithLegacyDidSovOnMessage, + supportsIncomingDidCommProtocolUri, + supportsIncomingMessageType, +} from '../messageType' + +export class TestMessage extends AgentMessage { + public constructor() { + super() + + this.id = this.generateId() + } + + public readonly type = TestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/fake-protocol/1.5/invitation') +} + +describe('messageType', () => { + describe('replaceLegacyDidSovPrefixOnMessage()', () => { + it('should replace the message type prefix with https://didcomm.org if it starts with did:sov:BzCbsNYhMrjHiqZDTUASHg;spec', () => { + const message = { + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/basicmessage/1.0/message', + } + + replaceLegacyDidSovPrefixOnMessage(message) + + expect(message['@type']).toBe('https://didcomm.org/basicmessage/1.0/message') + }) + + it("should not replace the message type prefix with https://didcomm.org if it doesn't start with did:sov:BzCbsNYhMrjHiqZDTUASHg;spec", () => { + const messageOtherDidSov = { + '@type': 'did:sov:another_did;spec/basicmessage/1.0/message', + } + replaceLegacyDidSovPrefixOnMessage(messageOtherDidSov) + expect(messageOtherDidSov['@type']).toBe('did:sov:another_did;spec/basicmessage/1.0/message') + + const messageDidComm = { + '@type': 'https://didcomm.org/basicmessage/1.0/message', + } + replaceLegacyDidSovPrefixOnMessage(messageDidComm) + expect(messageDidComm['@type']).toBe('https://didcomm.org/basicmessage/1.0/message') + }) + }) + + describe('replaceLegacyDidSovPrefix()', () => { + it('should replace the message type prefix with https://didcomm.org if it starts with did:sov:BzCbsNYhMrjHiqZDTUASHg;spec', () => { + const type = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/basicmessage/1.0/message' + + expect(replaceLegacyDidSovPrefix(type)).toBe('https://didcomm.org/basicmessage/1.0/message') + }) + + it("should not replace the message type prefix with https://didcomm.org if it doesn't start with did:sov:BzCbsNYhMrjHiqZDTUASHg;spec", () => { + const messageTypeOtherDidSov = 'did:sov:another_did;spec/basicmessage/1.0/message' + + expect(replaceLegacyDidSovPrefix(messageTypeOtherDidSov)).toBe( + 'did:sov:another_did;spec/basicmessage/1.0/message' + ) + + const messageTypeDidComm = 'https://didcomm.org/basicmessage/1.0/message' + + expect(replaceLegacyDidSovPrefix(messageTypeDidComm)).toBe('https://didcomm.org/basicmessage/1.0/message') + }) + }) + + describe('replaceNewDidCommPrefixWithLegacyDidSovOnMessage()', () => { + it('should replace the message type prefix with did:sov:BzCbsNYhMrjHiqZDTUASHg;spec if it starts with https://didcomm.org', () => { + const message = { + '@type': 'https://didcomm.org/basicmessage/1.0/message', + } + + replaceNewDidCommPrefixWithLegacyDidSovOnMessage(message) + + expect(message['@type']).toBe('did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/basicmessage/1.0/message') + }) + }) + + describe('replaceNewDidCommPrefixWithLegacyDidSov()', () => { + it('should replace the message type prefix with did:sov:BzCbsNYhMrjHiqZDTUASHg;spec if it starts with https://didcomm.org', () => { + const type = 'https://didcomm.org/basicmessage/1.0/message' + + expect(replaceNewDidCommPrefixWithLegacyDidSov(type)).toBe( + 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/basicmessage/1.0/message' + ) + }) + + it("should not replace the message type prefix with did:sov:BzCbsNYhMrjHiqZDTUASHg;spec if it doesn't start with https://didcomm.org", () => { + const messageTypeOtherDidSov = 'did:sov:another_did;spec/basicmessage/1.0/message' + + expect(replaceNewDidCommPrefixWithLegacyDidSov(messageTypeOtherDidSov)).toBe( + 'did:sov:another_did;spec/basicmessage/1.0/message' + ) + }) + }) + + describe('parseMessageType()', () => { + test('correctly parses the message type', () => { + expect(parseMessageType('https://didcomm.org/connections/1.0/request')).toEqual({ + documentUri: 'https://didcomm.org', + protocolName: 'connections', + protocolVersion: '1.0', + protocolMajorVersion: 1, + protocolMinorVersion: 0, + messageName: 'request', + protocolUri: `https://didcomm.org/connections/1.0`, + messageTypeUri: 'https://didcomm.org/connections/1.0/request', + }) + + expect(parseMessageType('https://didcomm.org/issue-credential/4.5/propose-credential')).toEqual({ + documentUri: 'https://didcomm.org', + protocolName: 'issue-credential', + protocolVersion: '4.5', + protocolMajorVersion: 4, + protocolMinorVersion: 5, + messageName: 'propose-credential', + protocolUri: `https://didcomm.org/issue-credential/4.5`, + messageTypeUri: 'https://didcomm.org/issue-credential/4.5/propose-credential', + }) + }) + + test('throws error when invalid message type is passed', () => { + expect(() => parseMessageType('https://didcomm.org/connections/1.0/message-type/and-else')).toThrow() + }) + }) + + describe('parseDidCommProtocolUri()', () => { + test('correctly parses the protocol uri', () => { + expect(parseDidCommProtocolUri('https://didcomm.org/connections/1.0')).toEqual({ + documentUri: 'https://didcomm.org', + protocolName: 'connections', + protocolVersion: '1.0', + protocolMajorVersion: 1, + protocolMinorVersion: 0, + protocolUri: 'https://didcomm.org/connections/1.0', + }) + + expect(parseDidCommProtocolUri('https://didcomm.org/issue-credential/4.5')).toEqual({ + documentUri: 'https://didcomm.org', + protocolName: 'issue-credential', + protocolVersion: '4.5', + protocolMajorVersion: 4, + protocolMinorVersion: 5, + protocolUri: `https://didcomm.org/issue-credential/4.5`, + }) + }) + + test('throws error when message type is passed', () => { + expect(() => parseDidCommProtocolUri('https://didcomm.org/connections/1.0/message-type')).toThrow() + }) + }) + + describe('supportsIncomingDidCommProtocolUri()', () => { + test('returns true when the document uri, protocol name, major version all match and the minor version is lower than the expected minor version', () => { + const incomingProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.0') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(true) + }) + + test('returns true when the document uri, protocol name, major version all match and the minor version is higher than the expected minor version', () => { + const incomingProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.8') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(true) + }) + + test('returns true when the document uri, protocol name, major version and minor version all match', () => { + const incomingProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(true) + }) + + test('returns true when the protocol name, major version and minor version all match and the incoming protocol uri is using the legacy did sov prefix', () => { + const incomingProtocolUri = parseDidCommProtocolUri('did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.4') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(true) + }) + + test('returns false when the protocol name, major version and minor version all match and the incoming protocol uri is using the legacy did sov prefix but allowLegacyDidSovPrefixMismatch is set to false', () => { + const incomingProtocolUri = parseDidCommProtocolUri('did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.4') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect( + supportsIncomingDidCommProtocolUri(expectedProtocolUri, incomingProtocolUri, { + allowLegacyDidSovPrefixMismatch: false, + }) + ).toBe(false) + }) + + test('returns false when the major version does not match', () => { + const incomingProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/2.4') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(false) + + const incomingProtocolUri2 = parseDidCommProtocolUri('https://didcomm.org/connections/2.0') + const expectedProtocolUri2 = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri2, expectedProtocolUri2)).toBe(false) + }) + + test('returns false when the protocol name does not match', () => { + const incomingProtocolUri = parseDidCommProtocolUri('https://didcomm.org/issue-credential/1.4') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(false) + }) + + test('returns false when the document uri does not match', () => { + const incomingProtocolUri = parseDidCommProtocolUri('https://my-protocol.org/connections/1.4') + const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + + expect(supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri)).toBe(false) + }) + }) + + describe('supportsIncomingMessageType()', () => { + test('returns true when the document uri, protocol name, major version all match and the minor version is lower than the expected minor version', () => { + const incomingMessageType = parseMessageType('https://didcomm.org/connections/1.0/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(incomingMessageType, expectedMessageType)).toBe(true) + }) + + test('returns true when the document uri, protocol name, major version all match and the minor version is higher than the expected minor version', () => { + const incomingMessageType = parseMessageType('https://didcomm.org/connections/1.8/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(incomingMessageType, expectedMessageType)).toBe(true) + }) + + test('returns true when the document uri, protocol name, major version and minor version all match', () => { + const incomingMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(incomingMessageType, expectedMessageType)).toBe(true) + }) + + test('returns true when the protocol name, major version and minor version all match and the incoming message type is using the legacy did sov prefix', () => { + const incomingMessageType = parseMessageType('did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.4/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(incomingMessageType, expectedMessageType)).toBe(true) + }) + + test('returns false when the protocol name, major version and minor version all match and the incoming message type is using the legacy did sov prefix but allowLegacyDidSovPrefixMismatch is set to false', () => { + const incomingMessageType = parseMessageType('did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.4/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect( + supportsIncomingMessageType(expectedMessageType, incomingMessageType, { + allowLegacyDidSovPrefixMismatch: false, + }) + ).toBe(false) + }) + + test('returns false when the major version does not match', () => { + const incomingMessageType = parseMessageType('https://didcomm.org/connections/2.4/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(incomingMessageType, expectedMessageType)).toBe(false) + + const incomingMessageType2 = parseMessageType('https://didcomm.org/connections/2.0/request') + const expectedMessageType2 = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(incomingMessageType2, expectedMessageType2)).toBe(false) + }) + + test('returns false when the message name does not match', () => { + const incomingMessageType = parseMessageType('https://didcomm.org/connections/1.4/proposal') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(incomingMessageType, expectedMessageType)).toBe(false) + }) + + test('returns false when the protocol name does not match', () => { + const incomingMessageType = parseMessageType('https://didcomm.org/issue-credential/1.4/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(incomingMessageType, expectedMessageType)).toBe(false) + }) + + test('returns false when the document uri does not match', () => { + const incomingMessageType = parseMessageType('https://my-protocol.org/connections/1.4/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(incomingMessageType, expectedMessageType)).toBe(false) + }) + }) + + describe('canHandleMessageType()', () => { + test('returns true when the document uri, protocol name, major version all match and the minor version is lower than the expected minor version', () => { + expect( + canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/fake-protocol/1.0/invitation')) + ).toBe(true) + }) + + test('returns true when the document uri, protocol name, major version all match and the minor version is higher than the expected minor version', () => { + expect( + canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/fake-protocol/1.8/invitation')) + ).toBe(true) + }) + + test('returns true when the document uri, protocol name, major version and minor version all match', () => { + expect( + canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/fake-protocol/1.5/invitation')) + ).toBe(true) + }) + + test('returns false when the major version does not match', () => { + expect( + canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/fake-protocol/2.5/invitation')) + ).toBe(false) + + expect( + canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/fake-protocol/2.0/invitation')) + ).toBe(false) + }) + + test('returns false when the message name does not match', () => { + expect(canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/fake-protocol/1.5/request'))).toBe( + false + ) + }) + + test('returns false when the protocol name does not match', () => { + expect( + canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/another-fake-protocol/1.5/invitation')) + ).toBe(false) + }) + + test('returns false when the document uri does not match', () => { + expect( + canHandleMessageType( + TestMessage, + parseMessageType('https://another-didcomm-site.org/fake-protocol/1.5/invitation') + ) + ).toBe(false) + }) + }) +}) diff --git a/packages/core/src/utils/__tests__/parseInvitation.test.ts b/packages/core/src/utils/__tests__/parseInvitation.test.ts new file mode 100644 index 0000000000..b9966f52cd --- /dev/null +++ b/packages/core/src/utils/__tests__/parseInvitation.test.ts @@ -0,0 +1,223 @@ +import { agentDependencies } from '../../../tests' +import { ConnectionInvitationMessage } from '../../modules/connections' +import { InvitationType, OutOfBandInvitation } from '../../modules/oob' +import { convertToNewInvitation } from '../../modules/oob/helpers' +import { JsonEncoder } from '../JsonEncoder' +import { JsonTransformer } from '../JsonTransformer' +import { MessageValidator } from '../MessageValidator' +import { oobInvitationFromShortUrl, parseInvitationShortUrl } from '../parseInvitation' + +const mockOobInvite = { + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/invitation', + '@id': '764af259-8bb4-4546-b91a-924c912d0bb8', + label: 'Alice', + handshake_protocols: ['did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0'], + services: ['did:sov:MvTqVXCEmJ87usL9uQTo7v'], +} + +const mockConnectionInvite = { + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation', + '@id': '20971ef0-1029-46db-a25b-af4c465dd16b', + label: 'test', + serviceEndpoint: 'http://sour-cow-15.tun1.indiciotech.io', + recipientKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], +} + +const mockLegacyConnectionless = { + '@id': '035b6404-f496-4cb6-a2b5-8bd09e8c92c1', + '@type': 'https://didcomm.org/some-protocol/1.0/some-message', + '~service': { + recipientKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], + routingKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], + serviceEndpoint: 'https://example.com/endpoint', + }, +} + +const header = new Headers() + +const dummyHeader = new Headers() + +header.append('Content-Type', 'application/json') + +const mockedResponseOobJson = { + status: 200, + ok: true, + headers: header, + json: async () => ({ + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/invitation', + '@id': '764af259-8bb4-4546-b91a-924c912d0bb8', + label: 'Alice', + handshake_protocols: ['did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0'], + services: ['did:sov:MvTqVXCEmJ87usL9uQTo7v'], + }), +} as Response + +const mockedResponseOobUrl = { + status: 200, + ok: true, + headers: dummyHeader, + url: 'https://wonderful-rabbit-5.tun2.indiciotech.io?oob=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9vdXQtb2YtYmFuZC8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiNzY0YWYyNTktOGJiNC00NTQ2LWI5MWEtOTI0YzkxMmQwYmI4IiwgImxhYmVsIjogIkFsaWNlIiwgImhhbmRzaGFrZV9wcm90b2NvbHMiOiBbImRpZDpzb3Y6QnpDYnNOWWhNcmpIaXFaRFRVQVNIZztzcGVjL2Nvbm5lY3Rpb25zLzEuMCJdLCAic2VydmljZXMiOiBbImRpZDpzb3Y6TXZUcVZYQ0VtSjg3dXNMOXVRVG83diJdfQ====', +} as Response + +dummyHeader.forEach(mockedResponseOobUrl.headers.append) + +const mockedLegacyConnectionlessInvitationJson = { + status: 200, + ok: true, + json: async () => mockLegacyConnectionless, + headers: header, +} as Response + +const mockedResponseConnectionJson = { + status: 200, + ok: true, + json: async () => ({ + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation', + '@id': '20971ef0-1029-46db-a25b-af4c465dd16b', + label: 'test', + serviceEndpoint: 'http://sour-cow-15.tun1.indiciotech.io', + recipientKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], + }), + headers: header, +} as Response + +const mockedResponseConnectionUrl = { + status: 200, + ok: true, + url: 'http://sour-cow-15.tun1.indiciotech.io?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiMjA5NzFlZjAtMTAyOS00NmRiLWEyNWItYWY0YzQ2NWRkMTZiIiwgImxhYmVsIjogInRlc3QiLCAic2VydmljZUVuZHBvaW50IjogImh0dHA6Ly9zb3VyLWNvdy0xNS50dW4xLmluZGljaW90ZWNoLmlvIiwgInJlY2lwaWVudEtleXMiOiBbIjVHdnBmOU00ajd2V3BIeWVUeXZCS2JqWWU3cVdjNzJrR282cVphTEhrTHJkIl19', + headers: dummyHeader, +} as Response + +let outOfBandInvitationMock: OutOfBandInvitation +let connectionInvitationMock: ConnectionInvitationMessage +let connectionInvitationToNew: OutOfBandInvitation + +beforeAll(async () => { + outOfBandInvitationMock = JsonTransformer.fromJSON(mockOobInvite, OutOfBandInvitation) + outOfBandInvitationMock.invitationType = InvitationType.OutOfBand + MessageValidator.validateSync(outOfBandInvitationMock) + connectionInvitationMock = JsonTransformer.fromJSON(mockConnectionInvite, ConnectionInvitationMessage) + MessageValidator.validateSync(connectionInvitationMock) + connectionInvitationToNew = convertToNewInvitation(connectionInvitationMock) +}) + +describe('shortened urls resolving to oob invitations', () => { + test('Resolve a mocked response in the form of a oob invitation as a json object', async () => { + const short = await oobInvitationFromShortUrl(mockedResponseOobJson) + expect(short).toEqual(outOfBandInvitationMock) + }) + test('Resolve a mocked response in the form of a oob invitation encoded in an url', async () => { + const short = await oobInvitationFromShortUrl(mockedResponseOobUrl) + expect(short).toEqual(outOfBandInvitationMock) + }) + + test("Resolve a mocked response in the form of a oob invitation as a json object with header 'application/json; charset=utf-8'", async () => { + const short = await oobInvitationFromShortUrl({ + ...mockedResponseOobJson, + headers: new Headers({ + 'content-type': 'application/json; charset=utf-8', + }), + } as Response) + expect(short).toEqual(outOfBandInvitationMock) + }) +}) + +describe('legacy connectionless', () => { + test('parse url containing d_m ', async () => { + const parsed = await parseInvitationShortUrl( + `https://example.com?d_m=${JsonEncoder.toBase64URL(mockLegacyConnectionless)}`, + agentDependencies + ) + expect(parsed.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + label: undefined, + 'requests~attach': [ + { + '@id': expect.any(String), + data: { + base64: + 'eyJAaWQiOiIwMzViNjQwNC1mNDk2LTRjYjYtYTJiNS04YmQwOWU4YzkyYzEiLCJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvc29tZS1wcm90b2NvbC8xLjAvc29tZS1tZXNzYWdlIn0=', + }, + 'mime-type': 'application/json', + }, + ], + services: [ + { + id: expect.any(String), + recipientKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + routingKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + ], + }) + }) + + test('parse short url returning legacy connectionless invitation to out of band invitation', async () => { + const parsed = await oobInvitationFromShortUrl(mockedLegacyConnectionlessInvitationJson) + expect(parsed.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + label: undefined, + 'requests~attach': [ + { + '@id': expect.any(String), + data: { + base64: + 'eyJAaWQiOiIwMzViNjQwNC1mNDk2LTRjYjYtYTJiNS04YmQwOWU4YzkyYzEiLCJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvc29tZS1wcm90b2NvbC8xLjAvc29tZS1tZXNzYWdlIn0=', + }, + 'mime-type': 'application/json', + }, + ], + services: [ + { + id: expect.any(String), + recipientKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + routingKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + ], + }) + }) +}) + +describe('shortened urls resolving to connection invitations', () => { + test('Resolve a mocked response in the form of a connection invitation as a json object', async () => { + const short = await oobInvitationFromShortUrl(mockedResponseConnectionJson) + expect(short).toEqual(connectionInvitationToNew) + }) + + test('Resolve a mocked Response in the form of a connection invitation encoded in an url c_i query parameter', async () => { + const short = await oobInvitationFromShortUrl(mockedResponseConnectionUrl) + expect(short).toEqual(connectionInvitationToNew) + }) + + test('Resolve a mocked Response in the form of a connection invitation encoded in an url oob query parameter', async () => { + const mockedResponseConnectionInOobUrl = { + status: 200, + ok: true, + headers: dummyHeader, + url: 'https://oob.lissi.io/ssi?oob=eyJAdHlwZSI6ImRpZDpzb3Y6QnpDYnNOWWhNcmpIaXFaRFRVQVNIZztzcGVjL2Nvbm5lY3Rpb25zLzEuMC9pbnZpdGF0aW9uIiwiQGlkIjoiMGU0NmEzYWEtMzUyOC00OTIxLWJmYjItN2JjYjk0NjVjNjZjIiwibGFiZWwiOiJTdGFkdCB8IExpc3NpLURlbW8iLCJzZXJ2aWNlRW5kcG9pbnQiOiJodHRwczovL2RlbW8tYWdlbnQuaW5zdGl0dXRpb25hbC1hZ2VudC5saXNzaS5pZC9kaWRjb21tLyIsImltYWdlVXJsIjoiaHR0cHM6Ly9yb3V0aW5nLmxpc3NpLmlvL2FwaS9JbWFnZS9kZW1vTXVzdGVyaGF1c2VuIiwicmVjaXBpZW50S2V5cyI6WyJEZlcxbzM2ekxuczlVdGlDUGQyalIyS2pvcnRvZkNhcFNTWTdWR2N2WEF6aCJdfQ', + } as Response + + dummyHeader.forEach(mockedResponseConnectionInOobUrl.headers.append) + + const expectedOobMessage = convertToNewInvitation( + JsonTransformer.fromJSON( + { + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation', + '@id': '0e46a3aa-3528-4921-bfb2-7bcb9465c66c', + label: 'Stadt | Lissi-Demo', + serviceEndpoint: 'https://demo-agent.institutional-agent.lissi.id/didcomm/', + imageUrl: 'https://routing.lissi.io/api/Image/demoMusterhausen', + recipientKeys: ['DfW1o36zLns9UtiCPd2jR2KjortofCapSSY7VGcvXAzh'], + }, + ConnectionInvitationMessage + ) + ) + const short = await oobInvitationFromShortUrl(mockedResponseConnectionInOobUrl) + expect(short).toEqual(expectedOobMessage) + }) +}) diff --git a/packages/core/src/utils/__tests__/transformers.test.ts b/packages/core/src/utils/__tests__/transformers.test.ts new file mode 100644 index 0000000000..d72d862062 --- /dev/null +++ b/packages/core/src/utils/__tests__/transformers.test.ts @@ -0,0 +1,37 @@ +import { JsonTransformer } from '../JsonTransformer' +import { DateTransformer } from '../transformers' + +class TestDateTransformer { + @DateTransformer() + public date: Date + + public constructor(date: Date) { + this.date = date + } +} + +describe('transformers', () => { + describe('DateTransformer', () => { + it('converts ISO date string to Date when using fromJSON', () => { + const testDate = JsonTransformer.fromJSON({ date: '2020-01-01T00:00:00.000Z' }, TestDateTransformer) + + expect(testDate.date).toBeInstanceOf(Date) + expect(testDate.date.getTime()).toEqual(1577836800000) + }) + + it('converts Date to ISO string when using toJSON', () => { + const testDateJson = JsonTransformer.toJSON(new TestDateTransformer(new Date('2020-01-01T00:00:00.000Z'))) + + expect(testDateJson.date).toBe('2020-01-01T00:00:00.000Z') + }) + + it('clones the Date to a new Date instance when using clone', () => { + const oldDate = new Date('2020-01-01T00:00:00.000Z') + const date = JsonTransformer.clone(new TestDateTransformer(oldDate)) + + expect(date.date).not.toBe(oldDate) + expect(date.date.getTime()).toEqual(oldDate.getTime()) + expect(date.date.toISOString()).toBe('2020-01-01T00:00:00.000Z') + }) + }) +}) diff --git a/packages/core/src/utils/__tests__/version.test.ts b/packages/core/src/utils/__tests__/version.test.ts new file mode 100644 index 0000000000..77c1bbe808 --- /dev/null +++ b/packages/core/src/utils/__tests__/version.test.ts @@ -0,0 +1,82 @@ +import { isFirstVersionEqualToSecond, isFirstVersionHigherThanSecond, parseVersionString } from '../version' + +describe('version', () => { + describe('parseVersionString()', () => { + it('parses a version string to a tuple', () => { + expect(parseVersionString('1.0')).toStrictEqual([1, 0, 0]) + expect(parseVersionString('2.12')).toStrictEqual([2, 12, 0]) + expect(parseVersionString('2.3.1')).toStrictEqual([2, 3, 1]) + expect(parseVersionString('0.2.1')).toStrictEqual([0, 2, 1]) + expect(parseVersionString('0.0')).toStrictEqual([0, 0, 0]) + }) + }) + + describe('isFirstVersionHigherThanSecond()', () => { + it('returns true if the major version digit of the first version is higher than the second', () => { + expect(isFirstVersionHigherThanSecond([2, 0, 0], [1, 0, 0])).toBe(true) + expect(isFirstVersionHigherThanSecond([2, 1, 0], [1, 1, 1])).toBe(true) + }) + + it('returns false if the major version digit of the first version is lower than the second', () => { + expect(isFirstVersionHigherThanSecond([1, 0, 0], [2, 0, 0])).toBe(false) + expect(isFirstVersionHigherThanSecond([1, 10, 2], [2, 1, 0])).toBe(false) + }) + + it('returns true if the major version digit of both versions are equal, but the minor version of the first version is higher', () => { + expect(isFirstVersionHigherThanSecond([1, 10, 0], [1, 0, 0])).toBe(true) + expect(isFirstVersionHigherThanSecond([2, 11, 0], [2, 10, 0])).toBe(true) + }) + + it('returns false if the major version digit of both versions are equal, but the minor version of the second version is higher', () => { + expect(isFirstVersionHigherThanSecond([1, 0, 0], [1, 10, 0])).toBe(false) + expect(isFirstVersionHigherThanSecond([2, 10, 0], [2, 11, 0])).toBe(false) + }) + + it('returns false if the major, minor and patch version digit of both versions are equal', () => { + expect(isFirstVersionHigherThanSecond([1, 0, 0], [1, 0, 0])).toBe(false) + expect(isFirstVersionHigherThanSecond([2, 10, 0], [2, 10, 0])).toBe(false) + }) + + it('returns true if the major and minor version digit of both versions are equal but patch version is higher', () => { + expect(isFirstVersionHigherThanSecond([1, 0, 1], [1, 0, 0])).toBe(true) + expect(isFirstVersionHigherThanSecond([2, 10, 3], [2, 10, 2])).toBe(true) + }) + + it('returns false if the major and minor version digit of both versions are equal but patch version is lower', () => { + expect(isFirstVersionHigherThanSecond([1, 0, 0], [1, 0, 1])).toBe(false) + expect(isFirstVersionHigherThanSecond([2, 10, 2], [2, 10, 3])).toBe(false) + }) + }) + + describe('isFirstVersionEqualToSecond()', () => { + it('returns false if the major version digit of the first version is lower than the second', () => { + expect(isFirstVersionEqualToSecond([2, 0, 0], [1, 0, 0])).toBe(false) + expect(isFirstVersionEqualToSecond([2, 1, 0], [1, 10, 0])).toBe(false) + }) + + it('returns false if the major version digit of the first version is higher than the second', () => { + expect(isFirstVersionEqualToSecond([1, 0, 0], [2, 0, 0])).toBe(false) + expect(isFirstVersionEqualToSecond([1, 10, 0], [2, 1, 0])).toBe(false) + }) + + it('returns false if the major version digit of both versions are equal, but the minor version of the first version is lower', () => { + expect(isFirstVersionEqualToSecond([1, 10, 0], [1, 0, 0])).toBe(false) + expect(isFirstVersionEqualToSecond([2, 11, 0], [2, 10, 0])).toBe(false) + }) + + it('returns false if the major version digit of both versions are equal, but the minor version of the second version is lower', () => { + expect(isFirstVersionEqualToSecond([1, 0, 0], [1, 10, 0])).toBe(false) + expect(isFirstVersionEqualToSecond([2, 10, 0], [2, 11, 0])).toBe(false) + }) + + it('returns true if the major, minor and patch version digit of both versions are equal', () => { + expect(isFirstVersionEqualToSecond([1, 0, 0], [1, 0, 0])).toBe(true) + expect(isFirstVersionEqualToSecond([2, 10, 0], [2, 10, 0])).toBe(true) + }) + + it('returns false if the patch version digit of both versions are different', () => { + expect(isFirstVersionEqualToSecond([1, 0, 1], [1, 0, 0])).toBe(false) + expect(isFirstVersionEqualToSecond([2, 10, 0], [2, 10, 4])).toBe(false) + }) + }) +}) diff --git a/packages/core/src/utils/array.ts b/packages/core/src/utils/array.ts new file mode 100644 index 0000000000..032324ea7d --- /dev/null +++ b/packages/core/src/utils/array.ts @@ -0,0 +1,19 @@ +import type { SingleOrArray } from './type' + +export const asArray = (val?: SingleOrArray): Array => { + if (!val) return [] + if (Array.isArray(val)) return val + return [val] +} + +type ExtractValueFromSingleOrArray = V extends SingleOrArray ? Value : never + +export const mapSingleOrArray = , Return>( + value: Wrapper, + fn: (value: ExtractValueFromSingleOrArray) => Return +): SingleOrArray => { + const mapped = asArray>(value as []).map(fn) + + // We need to return a single or array value based on the input type + return Array.isArray(value) ? mapped : mapped[0] +} diff --git a/packages/core/src/utils/attachment.ts b/packages/core/src/utils/attachment.ts new file mode 100644 index 0000000000..0cfdea098c --- /dev/null +++ b/packages/core/src/utils/attachment.ts @@ -0,0 +1,42 @@ +import type { HashName } from './Hasher' +import type { BaseName } from './MultiBaseEncoder' +import type { Attachment } from '../decorators/attachment/Attachment' + +import { CredoError } from '../error/CredoError' + +import { HashlinkEncoder } from './HashlinkEncoder' +import { TypedArrayEncoder } from './TypedArrayEncoder' + +/** + * Encodes an attachment based on the `data` property + * + * @param attachment The attachment that needs to be encoded + * @param hashAlgorithm The hashing algorithm that is going to be used + * @param baseName The base encoding name that is going to be used + * @returns A hashlink based on the attachment data + */ +export function encodeAttachment( + attachment: Attachment, + hashAlgorithm: HashName = 'sha-256', + baseName: BaseName = 'base58btc' +) { + if (attachment.data.sha256) { + return `hl:${attachment.data.sha256}` + } else if (attachment.data.base64) { + return HashlinkEncoder.encode(TypedArrayEncoder.fromBase64(attachment.data.base64), hashAlgorithm, baseName) + } else if (attachment.data.json) { + throw new CredoError(`Attachment: (${attachment.id}) has json encoded data. This is currently not supported`) + } else { + throw new CredoError(`Attachment: (${attachment.id}) has no data to create a link with`) + } +} + +/** + * Checks if an attachment is a linked Attachment + * + * @param attachment the attachment that has to be validated + * @returns a boolean whether the attachment is a linkedAttachment + */ +export function isLinkedAttachment(attachment: Attachment) { + return HashlinkEncoder.isValid(`hl:${attachment.id}`) +} diff --git a/packages/core/src/utils/base58.ts b/packages/core/src/utils/base58.ts new file mode 100644 index 0000000000..4b3b0d4338 --- /dev/null +++ b/packages/core/src/utils/base58.ts @@ -0,0 +1,13 @@ +import base from '@multiformats/base-x' + +const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + +const base58Converter = base(BASE58_ALPHABET) + +export function decodeFromBase58(base58: string) { + return base58Converter.decode(base58) +} + +export function encodeToBase58(buffer: Uint8Array) { + return base58Converter.encode(buffer) +} diff --git a/src/lib/utils/base64.ts b/packages/core/src/utils/base64.ts similarity index 91% rename from src/lib/utils/base64.ts rename to packages/core/src/utils/base64.ts index a0b2c3599e..b66d6ad5db 100644 --- a/src/lib/utils/base64.ts +++ b/packages/core/src/utils/base64.ts @@ -1,3 +1,3 @@ export function base64ToBase64URL(base64: string) { - return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') } diff --git a/packages/core/src/utils/buffer.ts b/packages/core/src/utils/buffer.ts new file mode 100644 index 0000000000..8ed596442f --- /dev/null +++ b/packages/core/src/utils/buffer.ts @@ -0,0 +1,3 @@ +import { Buffer } from 'buffer/' + +export { Buffer } diff --git a/packages/core/src/utils/deepEquality.ts b/packages/core/src/utils/deepEquality.ts new file mode 100644 index 0000000000..b2f2ac7aad --- /dev/null +++ b/packages/core/src/utils/deepEquality.ts @@ -0,0 +1,55 @@ +import { areObjectsEqual } from './objectEquality' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function deepEquality(x: any, y: any): boolean { + // We do a simple equals here to check primitives, functions, regex, etc. + // This will only happen if the typing of the function is ignored + const isXSimpleEqualY = simpleEqual(x, y) + if (isXSimpleEqualY !== undefined) return isXSimpleEqualY + + if (!(x instanceof Map) || !(y instanceof Map)) return areObjectsEqual(x, y) + + const xMap = x as Map + const yMap = y as Map + + // At this point we are sure we have two instances of a Map + const xKeys = Array.from(xMap.keys()) + const yKeys = Array.from(yMap.keys()) + + // Keys from both maps are not equal, content has not been verified, yet + if (!equalsIgnoreOrder(xKeys, yKeys)) return false + + // Here we recursively check whether the value of xMap is equals to the value of yMap + return Array.from(xMap.entries()).every(([key, xVal]) => deepEquality(xVal, yMap.get(key))) +} + +/** + * @note This will only work for primitive array equality + */ +export function equalsIgnoreOrder(a: Array, b: Array): boolean { + if (a.length !== b.length) return false + return a.every((k) => b.includes(k)) +} + +// We take any here as we have to check some properties, they will be undefined if they do not exist +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function simpleEqual(x: any, y: any) { + // short circuit for easy equality + if (x === y) return true + + if ((x === null || x === undefined) && (y === null || y === undefined)) return x === y + + // after this just checking type of one would be enough + if (x.constructor !== y.constructor) return false + + // if they are functions, they should exactly refer to same one (because of closures) + if (x instanceof Function) return x === y + + // if they are regexps, they should exactly refer to same one (it is hard to better equality check on current ES) + if (x instanceof RegExp) return x === y + + if (x.valueOf && y.valueOf && x.valueOf() === y.valueOf()) return true + + // if they are dates, they must had equal valueOf + if (x instanceof Date || y instanceof Date) return false +} diff --git a/packages/core/src/utils/did.ts b/packages/core/src/utils/did.ts new file mode 100644 index 0000000000..8f64e8ac83 --- /dev/null +++ b/packages/core/src/utils/did.ts @@ -0,0 +1,20 @@ +import { TypedArrayEncoder } from './TypedArrayEncoder' + +export function indyDidFromPublicKeyBase58(publicKeyBase58: string): string { + const buffer = TypedArrayEncoder.fromBase58(publicKeyBase58) + + const did = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + + return did +} + +/** + * Checks whether `potentialDid` is a valid DID. You can optionally provide a `method` to + * check whether the did is for that specific method. + * + * Note: the check in this method is very simple and just check whether the did starts with + * `did:` or `did::`. It does not do an advanced regex check on the did. + */ +export function isDid(potentialDid: string, method?: string) { + return method ? potentialDid.startsWith(`did:${method}:`) : potentialDid.startsWith('did:') +} diff --git a/packages/core/src/utils/error.ts b/packages/core/src/utils/error.ts new file mode 100644 index 0000000000..530264240a --- /dev/null +++ b/packages/core/src/utils/error.ts @@ -0,0 +1 @@ +export const isError = (value: unknown): value is Error => value instanceof Error diff --git a/packages/core/src/utils/fetch.ts b/packages/core/src/utils/fetch.ts new file mode 100644 index 0000000000..1c2f842de3 --- /dev/null +++ b/packages/core/src/utils/fetch.ts @@ -0,0 +1,28 @@ +import type { AgentDependencies } from '../agent/AgentDependencies' + +import { AbortController } from 'abort-controller' + +export async function fetchWithTimeout( + fetch: AgentDependencies['fetch'], + url: string, + init?: Omit & { + /** + * @default 5000 + */ + timeoutMs?: number + } +) { + const abortController = new AbortController() + const timeoutMs = init?.timeoutMs ?? 5000 + + const timeout = setTimeout(() => abortController.abort(), timeoutMs) + + try { + return await fetch(url, { + ...init, + signal: abortController.signal as NonNullable, + }) + } finally { + clearTimeout(timeout) + } +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts new file mode 100644 index 0000000000..42cb459fba --- /dev/null +++ b/packages/core/src/utils/index.ts @@ -0,0 +1,18 @@ +export * from './TypedArrayEncoder' +export * from './JsonEncoder' +export * from './JsonTransformer' +export * from './MultiBaseEncoder' +export * from './buffer' +export * from './MultiHashEncoder' +export * from './JWE' +export * from './VarintEncoder' +export * from './Hasher' +export * from './validators' +export * from './type' +export * from './deepEquality' +export * from './objectEquality' +export * from './MessageValidator' +export * from './did' +export * from './array' +export * from './timestamp' +export { DateTransformer } from './transformers' diff --git a/packages/core/src/utils/messageType.ts b/packages/core/src/utils/messageType.ts new file mode 100644 index 0000000000..a76903d504 --- /dev/null +++ b/packages/core/src/utils/messageType.ts @@ -0,0 +1,280 @@ +import type { PlaintextMessage } from '../types' +import type { ValidationOptions, ValidationArguments } from 'class-validator' + +import { ValidateBy, buildMessage } from 'class-validator' + +const PROTOCOL_URI_REGEX = /^(.+)\/([^/\\]+)\/(\d+).(\d+)$/ +const MESSAGE_TYPE_REGEX = /^(.+)\/([^/\\]+)\/(\d+).(\d+)\/([^/\\]+)$/ + +export interface ParsedDidCommProtocolUri { + /** + * Version of the protocol + * + * @example 1.0 + */ + protocolVersion: string + + /** + * Major version of the protocol + * + * @example 1 + */ + protocolMajorVersion: number + + /** + * Minor version of the protocol + * + * @example 0 + */ + protocolMinorVersion: number + + /** + * Name of the protocol + * + * @example connections + */ + protocolName: string + + /** + * Document uri of the message. + * + * @example https://didcomm.org + */ + documentUri: string + + /** + * Uri identifier of the protocol. Includes the + * documentUri, protocolName and protocolVersion. + * Useful when working with feature discovery + * + * @example https://didcomm.org/connections/1.0 + */ + protocolUri: string +} + +export interface ParsedMessageType extends ParsedDidCommProtocolUri { + /** + * Message name + * + * @example request + */ + messageName: string + + /** + * Uri identifier of the message. Includes all parts + * or the message type. + * + * @example https://didcomm.org/connections/1.0/request + */ + messageTypeUri: string +} + +// TODO: rename to `parseDidCommMessageType` and `DidCommParsedProtocolUri` +// in the future +export function parseMessageType(messageType: string): ParsedMessageType { + const match = MESSAGE_TYPE_REGEX.exec(messageType) + + if (!match) { + throw new Error(`Invalid message type: ${messageType}`) + } + + const [, documentUri, protocolName, protocolVersionMajor, protocolVersionMinor, messageName] = match + + return { + documentUri, + protocolName, + protocolVersion: `${protocolVersionMajor}.${protocolVersionMinor}`, + protocolMajorVersion: parseInt(protocolVersionMajor), + protocolMinorVersion: parseInt(protocolVersionMinor), + messageName, + protocolUri: `${documentUri}/${protocolName}/${protocolVersionMajor}.${protocolVersionMinor}`, + messageTypeUri: messageType, + } +} + +export function parseDidCommProtocolUri(didCommProtocolUri: string): ParsedDidCommProtocolUri { + const match = PROTOCOL_URI_REGEX.exec(didCommProtocolUri) + + if (!match) { + throw new Error(`Invalid protocol uri: ${didCommProtocolUri}`) + } + + const [, documentUri, protocolName, protocolVersionMajor, protocolVersionMinor] = match + + return { + documentUri, + protocolName, + protocolVersion: `${protocolVersionMajor}.${protocolVersionMinor}`, + protocolMajorVersion: parseInt(protocolVersionMajor), + protocolMinorVersion: parseInt(protocolVersionMinor), + protocolUri: `${documentUri}/${protocolName}/${protocolVersionMajor}.${protocolVersionMinor}`, + } +} + +/** + * Check whether the incoming didcomm protocol uri is a protocol uri that can be handled by comparing it to the expected didcomm protocol uri. + * In this case the expected protocol uri is e.g. the handshake protocol supported (https://didcomm.org/connections/1.0), and the incoming protocol uri + * is the uri that is parsed from the incoming out of band invitation handshake_protocols. + * + * The method will make sure the following fields are equal: + * - documentUri + * - protocolName + * - majorVersion + * + * If allowLegacyDidSovPrefixMismatch is true (default) it will allow for the case where the incoming protocol uri still has the legacy + * did:sov:BzCbsNYhMrjHiqZDTUASHg;spec did prefix, but the expected message type does not. This only works for incoming messages with a prefix + * of did:sov:BzCbsNYhMrjHiqZDTUASHg;spec and the expected message type having a prefix value of https:/didcomm.org + * + * @example + * const incomingProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.0') + * const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.4') + * + * // Returns true because the incoming protocol uri is equal to the expected protocol uri, except for + * // the minor version, which is lower + * const isIncomingProtocolUriSupported = supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri) + * + * @example + * const incomingProtocolUri = parseDidCommProtocolUri('did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0') + * const expectedProtocolUri = parseDidCommProtocolUri('https://didcomm.org/connections/1.0') + * + * // Returns true because the incoming protocol uri is equal to the expected protocol uri, except for + * // the legacy did sov prefix. + * const isIncomingProtocolUriSupported = supportsIncomingDidCommProtocolUri(incomingProtocolUri, expectedProtocolUri) + */ +export function supportsIncomingDidCommProtocolUri( + incomingProtocolUri: ParsedDidCommProtocolUri, + expectedProtocolUri: ParsedDidCommProtocolUri, + { allowLegacyDidSovPrefixMismatch = true }: { allowLegacyDidSovPrefixMismatch?: boolean } = {} +) { + const incomingDocumentUri = allowLegacyDidSovPrefixMismatch + ? replaceLegacyDidSovPrefix(incomingProtocolUri.documentUri) + : incomingProtocolUri.documentUri + + const documentUriMatches = expectedProtocolUri.documentUri === incomingDocumentUri + const protocolNameMatches = expectedProtocolUri.protocolName === incomingProtocolUri.protocolName + const majorVersionMatches = expectedProtocolUri.protocolMajorVersion === incomingProtocolUri.protocolMajorVersion + + // Everything besides the minor version must match + return documentUriMatches && protocolNameMatches && majorVersionMatches +} + +/** + * Check whether the incoming message type is a message type that can be handled by comparing it to the expected message type. + * In this case the expected message type is e.g. the type declared on an agent message class, and the incoming message type is the type + * that is parsed from the incoming JSON. + * + * The method will make sure the following fields are equal: + * - documentUri + * - protocolName + * - majorVersion + * - messageName + * + * If allowLegacyDidSovPrefixMismatch is true (default) it will allow for the case where the incoming message type still has the legacy + * did:sov:BzCbsNYhMrjHiqZDTUASHg;spec did prefix, but the expected message type does not. This only works for incoming messages with a prefix + * of did:sov:BzCbsNYhMrjHiqZDTUASHg;spec and the expected message type having a prefix value of https:/didcomm.org + * + * @example + * const incomingMessageType = parseMessageType('https://didcomm.org/connections/1.0/request') + * const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + * + * // Returns true because the incoming message type is equal to the expected message type, except for + * // the minor version, which is lower + * const isIncomingMessageTypeSupported = supportsIncomingMessageType(incomingMessageType, expectedMessageType) + * + * @example + * const incomingMessageType = parseMessageType('did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/request') + * const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.0/request') + * + * // Returns true because the incoming message type is equal to the expected message type, except for + * // the legacy did sov prefix. + * const isIncomingMessageTypeSupported = supportsIncomingMessageType(incomingMessageType, expectedMessageType) + */ +export function supportsIncomingMessageType( + incomingMessageType: ParsedMessageType, + expectedMessageType: ParsedMessageType, + { allowLegacyDidSovPrefixMismatch = true }: { allowLegacyDidSovPrefixMismatch?: boolean } = {} +) { + const incomingDocumentUri = allowLegacyDidSovPrefixMismatch + ? replaceLegacyDidSovPrefix(incomingMessageType.documentUri) + : incomingMessageType.documentUri + + const documentUriMatches = expectedMessageType.documentUri === incomingDocumentUri + const protocolNameMatches = expectedMessageType.protocolName === incomingMessageType.protocolName + const majorVersionMatches = expectedMessageType.protocolMajorVersion === incomingMessageType.protocolMajorVersion + const messageNameMatches = expectedMessageType.messageName === incomingMessageType.messageName + + // Everything besides the minor version must match + return documentUriMatches && protocolNameMatches && majorVersionMatches && messageNameMatches +} + +export function canHandleMessageType( + messageClass: { type: ParsedMessageType }, + messageType: ParsedMessageType +): boolean { + return supportsIncomingMessageType(messageClass.type, messageType) +} + +/** + * class-validator decorator to check if the string message type value matches with the + * expected message type. This uses {@link supportsIncomingMessageType}. + */ +export function IsValidMessageType( + messageType: ParsedMessageType, + validationOptions?: ValidationOptions +): PropertyDecorator { + return ValidateBy( + { + name: 'isValidMessageType', + constraints: [messageType], + validator: { + validate: (value, args: ValidationArguments): boolean => { + const [expectedMessageType] = args.constraints as [ParsedMessageType] + + // Type must be string + if (typeof value !== 'string') { + return false + } + + const incomingMessageType = parseMessageType(value) + return supportsIncomingMessageType(incomingMessageType, expectedMessageType) + }, + defaultMessage: buildMessage( + (eachPrefix) => + eachPrefix + '$property does not match the expected message type (only minor version may be lower)', + validationOptions + ), + }, + }, + validationOptions + ) +} + +export function replaceLegacyDidSovPrefixOnMessage(message: PlaintextMessage | Record) { + message['@type'] = replaceLegacyDidSovPrefix(message['@type'] as string) +} + +export function replaceNewDidCommPrefixWithLegacyDidSovOnMessage(message: Record) { + message['@type'] = replaceNewDidCommPrefixWithLegacyDidSov(message['@type'] as string) +} + +export function replaceLegacyDidSovPrefix(messageType: string) { + const didSovPrefix = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec' + const didCommPrefix = 'https://didcomm.org' + + if (messageType.startsWith(didSovPrefix)) { + return messageType.replace(didSovPrefix, didCommPrefix) + } + + return messageType +} + +export function replaceNewDidCommPrefixWithLegacyDidSov(messageType: string) { + const didSovPrefix = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec' + const didCommPrefix = 'https://didcomm.org' + + if (messageType.startsWith(didCommPrefix)) { + return messageType.replace(didCommPrefix, didSovPrefix) + } + + return messageType +} diff --git a/src/lib/utils/mixins.ts b/packages/core/src/utils/mixins.ts similarity index 75% rename from src/lib/utils/mixins.ts rename to packages/core/src/utils/mixins.ts index fdf6f3fb5c..b55e1844ea 100644 --- a/src/lib/utils/mixins.ts +++ b/packages/core/src/utils/mixins.ts @@ -3,16 +3,19 @@ // @see https://www.typescriptlang.org/docs/handbook/mixins.html // eslint-disable-next-line @typescript-eslint/ban-types -export type Constructor = new (...args: any[]) => T; +export type Constructor = new (...args: any[]) => T + +export type NonConstructable = Omit +export type Constructable = T & (new (...args: any) => T) // Turns A | B | C into A & B & C -export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never // Merges constructor types. T[number] allows the type to be merged for each item in the array */ -export type MergeConstructorTypes = UnionToIntersection>>; +export type MergeConstructorTypes = UnionToIntersection>> // Take class as parameter, return class -type Mixin = (Base: Constructor) => Constructor; +type Mixin = (Base: Constructor) => Constructor /** * Apply a list of mixins functions to a base class. Applies extensions in order @@ -28,5 +31,5 @@ export function Compose( extensions: T ): Constructor> & B { // It errors without casting to any, but function + typings works - return extensions.reduce((extended, extend) => extend(extended), Base) as any; + return extensions.reduce((extended, extend) => extend(extended), Base) as any } diff --git a/packages/core/src/utils/objectEquality.ts b/packages/core/src/utils/objectEquality.ts new file mode 100644 index 0000000000..33db64084a --- /dev/null +++ b/packages/core/src/utils/objectEquality.ts @@ -0,0 +1,21 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function areObjectsEqual(a: A, b: B): boolean { + if (typeof a == 'object' && a != null && typeof b == 'object' && b != null) { + const definedA = Object.fromEntries(Object.entries(a).filter(([, value]) => value !== undefined)) + const definedB = Object.fromEntries(Object.entries(b).filter(([, value]) => value !== undefined)) + if (Object.keys(definedA).length !== Object.keys(definedB).length) return false + for (const key in definedA) { + if (!(key in definedB) || !areObjectsEqual(definedA[key], definedB[key])) { + return false + } + } + for (const key in definedB) { + if (!(key in definedA) || !areObjectsEqual(definedB[key], definedA[key])) { + return false + } + } + return true + } else { + return a === b + } +} diff --git a/packages/core/src/utils/parseInvitation.ts b/packages/core/src/utils/parseInvitation.ts new file mode 100644 index 0000000000..cbe8574983 --- /dev/null +++ b/packages/core/src/utils/parseInvitation.ts @@ -0,0 +1,168 @@ +import type { AgentDependencies } from '../agent/AgentDependencies' + +import { AbortController } from 'abort-controller' +import { parseUrl } from 'query-string' + +import { AgentMessage } from '../agent/AgentMessage' +import { CredoError } from '../error' +import { ConnectionInvitationMessage } from '../modules/connections' +import { OutOfBandDidCommService } from '../modules/oob/domain/OutOfBandDidCommService' +import { convertToNewInvitation } from '../modules/oob/helpers' +import { InvitationType, OutOfBandInvitation } from '../modules/oob/messages' + +import { JsonEncoder } from './JsonEncoder' +import { JsonTransformer } from './JsonTransformer' +import { MessageValidator } from './MessageValidator' +import { parseMessageType, supportsIncomingMessageType } from './messageType' + +const fetchShortUrl = async (invitationUrl: string, dependencies: AgentDependencies) => { + const abortController = new AbortController() + const id = setTimeout(() => abortController.abort(), 15000) + let response + try { + response = await dependencies.fetch(invitationUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }) + } catch (error) { + throw new CredoError(`Get request failed on provided url: ${error.message}`, { cause: error }) + } + clearTimeout(id) + return response +} + +/** + * Parses a JSON containing an invitation message and returns an OutOfBandInvitation instance + * + * @param invitationJson JSON object containing message + * @returns OutOfBandInvitation + */ +export const parseInvitationJson = (invitationJson: Record): OutOfBandInvitation => { + const messageType = invitationJson['@type'] as string + + if (!messageType) { + throw new CredoError('Invitation is not a valid DIDComm message') + } + + const parsedMessageType = parseMessageType(messageType) + if (supportsIncomingMessageType(parsedMessageType, OutOfBandInvitation.type)) { + const invitation = JsonTransformer.fromJSON(invitationJson, OutOfBandInvitation) + MessageValidator.validateSync(invitation) + invitation.invitationType = InvitationType.OutOfBand + return invitation + } else if (supportsIncomingMessageType(parsedMessageType, ConnectionInvitationMessage.type)) { + const invitation = JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage) + MessageValidator.validateSync(invitation) + const outOfBandInvitation = convertToNewInvitation(invitation) + outOfBandInvitation.invitationType = InvitationType.Connection + return outOfBandInvitation + } else if (invitationJson['~service']) { + // This is probably a legacy connectionless invitation + return transformLegacyConnectionlessInvitationToOutOfBandInvitation(invitationJson) + } else { + throw new CredoError(`Invitation with '@type' ${parsedMessageType.messageTypeUri} not supported.`) + } +} + +/** + * Parses URL containing encoded invitation and returns invitation message. + * + * @param invitationUrl URL containing encoded invitation + * + * @returns OutOfBandInvitation + */ +export const parseInvitationUrl = (invitationUrl: string): OutOfBandInvitation => { + const parsedUrl = parseUrl(invitationUrl).query + + const encodedInvitation = parsedUrl['oob'] ?? parsedUrl['c_i'] ?? parsedUrl['d_m'] + + if (typeof encodedInvitation === 'string') { + const invitationJson = JsonEncoder.fromBase64(encodedInvitation) as Record + return parseInvitationJson(invitationJson) + } + throw new CredoError( + 'InvitationUrl is invalid. It needs to contain one, and only one, of the following parameters: `oob`, `c_i` or `d_m`.' + ) +} + +// This currently does not follow the RFC because of issues with fetch, currently uses a janky work around +export const oobInvitationFromShortUrl = async (response: Response): Promise => { + if (response) { + if (response.headers.get('Content-Type')?.startsWith('application/json') && response.ok) { + const invitationJson = (await response.json()) as Record + return parseInvitationJson(invitationJson) + } else if (response['url']) { + // The following if else is for here for trinsic shorten urls + // Because the redirect targets a deep link the automatic redirect does not occur + let responseUrl + const location = response.headers.get('Location') + if ((response.status === 302 || response.status === 301) && location) responseUrl = location + else responseUrl = response['url'] + + return parseInvitationUrl(responseUrl) + } + } + throw new CredoError('HTTP request time out or did not receive valid response') +} + +export function transformLegacyConnectionlessInvitationToOutOfBandInvitation(messageJson: Record) { + const agentMessage = JsonTransformer.fromJSON(messageJson, AgentMessage) + + // ~service is required for legacy connectionless invitations + if (!agentMessage.service) { + throw new CredoError('Invalid legacy connectionless invitation url. Missing ~service decorator.') + } + + // This destructuring removes the ~service property from the message, and + // we can can use messageWithoutService to create the out of band invitation + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { '~service': service, ...messageWithoutService } = messageJson + + // transform into out of band invitation + const invitation = new OutOfBandInvitation({ + services: [OutOfBandDidCommService.fromResolvedDidCommService(agentMessage.service.resolvedDidCommService)], + }) + + invitation.invitationType = InvitationType.Connectionless + invitation.addRequest(JsonTransformer.fromJSON(messageWithoutService, AgentMessage)) + + return invitation +} + +/** + * Parses URL containing encoded invitation and returns invitation message. Compatible with + * parsing short Urls + * + * @param invitationUrl URL containing encoded invitation + * + * @param dependencies Agent dependencies containing fetch + * + * @returns OutOfBandInvitation + */ +export const parseInvitationShortUrl = async ( + invitationUrl: string, + dependencies: AgentDependencies +): Promise => { + const parsedUrl = parseUrl(invitationUrl).query + if (parsedUrl['oob'] || parsedUrl['c_i']) { + return parseInvitationUrl(invitationUrl) + } + // Legacy connectionless invitation + else if (parsedUrl['d_m']) { + const messageJson = JsonEncoder.fromBase64(parsedUrl['d_m'] as string) + return transformLegacyConnectionlessInvitationToOutOfBandInvitation(messageJson) + } else { + try { + const outOfBandInvitation = await oobInvitationFromShortUrl(await fetchShortUrl(invitationUrl, dependencies)) + outOfBandInvitation.invitationType = InvitationType.OutOfBand + return outOfBandInvitation + } catch (error) { + throw new CredoError( + 'InvitationUrl is invalid. It needs to contain one, and only one, of the following parameters: `oob`, `c_i` or `d_m`, or be valid shortened URL' + ) + } + } +} diff --git a/packages/core/src/utils/path.ts b/packages/core/src/utils/path.ts new file mode 100644 index 0000000000..6adbfe5616 --- /dev/null +++ b/packages/core/src/utils/path.ts @@ -0,0 +1,37 @@ +/** + * Extract directory from path (should also work with windows paths) + * + * @param path the path to extract the directory from + * @returns the directory path + */ +export function getDirFromFilePath(path: string) { + return path.substring(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))) +} + +/** + * Combine multiple uri parts into a single uri taking into account slashes. + * + * @param parts the parts to combine + * @returns the combined url + */ +export function joinUriParts(base: string, parts: string[]) { + if (parts.length === 0) return base + + // take base without trailing / + let combined = base.trim() + combined = base.endsWith('/') ? base.slice(0, base.length - 1) : base + + for (const part of parts) { + // Remove leading and trailing / + let strippedPart = part.trim() + strippedPart = strippedPart.startsWith('/') ? strippedPart.slice(1) : strippedPart + strippedPart = strippedPart.endsWith('/') ? strippedPart.slice(0, strippedPart.length - 1) : strippedPart + + // Don't want to add if empty + if (strippedPart === '') continue + + combined += `/${strippedPart}` + } + + return combined +} diff --git a/packages/core/src/utils/promises.ts b/packages/core/src/utils/promises.ts new file mode 100644 index 0000000000..0e843d73b5 --- /dev/null +++ b/packages/core/src/utils/promises.ts @@ -0,0 +1,44 @@ +// This file polyfills the allSettled method introduced in ESNext + +export type AllSettledFulfilled = { + status: 'fulfilled' + value: T +} + +export type AllSettledRejected = { + status: 'rejected' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reason: any +} + +export function allSettled(promises: Promise[]) { + return Promise.all( + promises.map((p) => + p + .then( + (value) => + ({ + status: 'fulfilled', + value, + } as AllSettledFulfilled) + ) + .catch( + (reason) => + ({ + status: 'rejected', + reason, + } as AllSettledRejected) + ) + ) + ) +} + +export function onlyFulfilled(entries: Array | AllSettledRejected>) { + // We filter for only the rejected values, so we can safely cast the type + return entries.filter((e) => e.status === 'fulfilled') as AllSettledFulfilled[] +} + +export function onlyRejected(entries: Array | AllSettledRejected>) { + // We filter for only the rejected values, so we can safely cast the type + return entries.filter((e) => e.status === 'rejected') as AllSettledRejected[] +} diff --git a/packages/core/src/utils/sleep.ts b/packages/core/src/utils/sleep.ts new file mode 100644 index 0000000000..5b675e3b71 --- /dev/null +++ b/packages/core/src/utils/sleep.ts @@ -0,0 +1,3 @@ +export function sleep(ms: number) { + return new Promise((res) => setTimeout(res, ms)) +} diff --git a/packages/core/src/utils/thread.ts b/packages/core/src/utils/thread.ts new file mode 100644 index 0000000000..a8dd1a668a --- /dev/null +++ b/packages/core/src/utils/thread.ts @@ -0,0 +1,5 @@ +import type { PlaintextMessage } from '../types' + +export function getThreadIdFromPlainTextMessage(message: PlaintextMessage) { + return message['~thread']?.thid ?? message['@id'] +} diff --git a/packages/core/src/utils/timestamp.ts b/packages/core/src/utils/timestamp.ts new file mode 100644 index 0000000000..4798fcc24f --- /dev/null +++ b/packages/core/src/utils/timestamp.ts @@ -0,0 +1,19 @@ +// Question: Spec isn't clear about the endianness. Assumes big-endian here +// since ACA-Py uses big-endian. +export default function timestamp(): Uint8Array { + let time = Date.now() + const bytes = [] + for (let i = 0; i < 8; i++) { + const byte = time & 0xff + bytes.push(byte) + time = (time - byte) / 256 // Javascript right shift (>>>) only works on 32 bit integers + } + return Uint8Array.from(bytes).reverse() +} + +/** + * Returns the current time in seconds + */ +export function nowInSeconds() { + return Math.floor(new Date().getTime() / 1000) +} diff --git a/packages/core/src/utils/transformers.ts b/packages/core/src/utils/transformers.ts new file mode 100644 index 0000000000..a19310b617 --- /dev/null +++ b/packages/core/src/utils/transformers.ts @@ -0,0 +1,98 @@ +import type { ValidationOptions } from 'class-validator' + +import { Transform, TransformationType } from 'class-transformer' +import { isString, ValidateBy, buildMessage } from 'class-validator' +import { DateTime } from 'luxon' + +import { Metadata } from '../storage/Metadata' + +/* + * Decorator that transforms to and from a metadata instance. + */ +export function MetadataTransformer() { + return Transform(({ value, type }) => { + if (type === TransformationType.CLASS_TO_PLAIN) { + return { ...value.data } + } + + if (type === TransformationType.PLAIN_TO_CLASS) { + return new Metadata(value) + } + + if (type === TransformationType.CLASS_TO_CLASS) { + return new Metadata({ ...value.data }) + } + }) +} + +/** + * Decorator that transforms to and from a date instance. + */ +export function DateTransformer() { + return Transform(({ value, type }) => { + if (value === undefined) return undefined + if (type === TransformationType.CLASS_TO_PLAIN) { + return value.toISOString() + } + + if (type === TransformationType.PLAIN_TO_CLASS) { + return new Date(value) + } + + if (type === TransformationType.CLASS_TO_CLASS) { + return new Date(value.getTime()) + } + }) +} + +/* + * Function that parses date from multiple formats + * including SQL formats. + */ + +export function DateParser(value: string): Date { + const parsedDate = new Date(value) + if (parsedDate instanceof Date && !isNaN(parsedDate.getTime())) { + return parsedDate + } + const luxonDate = DateTime.fromSQL(value) + if (luxonDate.isValid) { + return new Date(luxonDate.toString()) + } + return new Date() +} + +/** + * Checks if a given value is a Map + */ +export function IsMap(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'isMap', + validator: { + validate: (value: unknown): boolean => value instanceof Map, + defaultMessage: buildMessage((eachPrefix) => eachPrefix + '$property must be a Map', validationOptions), + }, + }, + validationOptions + ) +} + +/** + * Checks if a given value is a string or string array. + */ +export function IsStringOrStringArray(validationOptions?: Omit): PropertyDecorator { + return ValidateBy( + { + name: 'isStringOrStringArray', + validator: { + validate: (value): boolean => isString(value) || (Array.isArray(value) && value.every((v) => isString(v))), + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be a string or string array', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/utils/type.ts b/packages/core/src/utils/type.ts new file mode 100644 index 0000000000..064ca0ce75 --- /dev/null +++ b/packages/core/src/utils/type.ts @@ -0,0 +1,9 @@ +import type { JsonObject } from '../types' + +export type SingleOrArray = T | T[] + +export type Optional = Pick, K> & Omit + +export const isJsonObject = (value: unknown): value is JsonObject => { + return value !== undefined && typeof value === 'object' && value !== null && !Array.isArray(value) +} diff --git a/packages/core/src/utils/uri.ts b/packages/core/src/utils/uri.ts new file mode 100644 index 0000000000..b25a4433fb --- /dev/null +++ b/packages/core/src/utils/uri.ts @@ -0,0 +1,4 @@ +export function getProtocolScheme(url: string) { + const [protocolScheme] = url.split(':') + return protocolScheme +} diff --git a/packages/core/src/utils/uuid.ts b/packages/core/src/utils/uuid.ts new file mode 100644 index 0000000000..a6dd97a7f2 --- /dev/null +++ b/packages/core/src/utils/uuid.ts @@ -0,0 +1,9 @@ +import { v4, validate } from 'uuid' + +export function uuid() { + return v4() +} + +export function isValidUuid(id: string) { + return validate(id) +} diff --git a/packages/core/src/utils/validators.ts b/packages/core/src/utils/validators.ts new file mode 100644 index 0000000000..4d7207e5cc --- /dev/null +++ b/packages/core/src/utils/validators.ts @@ -0,0 +1,102 @@ +import type { Constructor } from './mixins' +import type { SingleOrArray } from './type' +import type { ValidationOptions } from 'class-validator' + +import { isString, ValidateBy, isInstance, buildMessage } from 'class-validator' + +import { asArray } from './array' + +export interface IsInstanceOrArrayOfInstancesValidationOptions extends ValidationOptions { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + classType: SingleOrArray any> + + /** + * Whether to allow empty arrays to pass validation + * @default false + */ + allowEmptyArray?: boolean +} + +/** + * Checks if the value is a string or the specified instance + */ +export function IsStringOrInstance(targetType: Constructor, validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'IsStringOrInstance', + constraints: [targetType], + validator: { + validate: (value, args): boolean => isString(value) || isInstance(value, args?.constraints[0]), + defaultMessage: buildMessage((eachPrefix, args) => { + if (args?.constraints[0]) { + return eachPrefix + `$property must be of type string or instance of ${args.constraints[0].name as string}` + } else { + return eachPrefix + `IsStringOrInstance decorator expects an object as value, but got falsy value.` + } + }, validationOptions), + }, + }, + validationOptions + ) +} + +export function IsInstanceOrArrayOfInstances( + validationOptions: IsInstanceOrArrayOfInstancesValidationOptions +): PropertyDecorator { + const classTypes = asArray(validationOptions.classType) + const allowEmptyArray = validationOptions.allowEmptyArray ?? false + + return ValidateBy( + { + name: 'isInstanceOrArrayOfInstances', + validator: { + validate: (values) => { + if (!values) return false + if (Array.isArray(values) && values.length === 0) return allowEmptyArray + + return ( + asArray(values) + // all values MUST be instance of one of the class types + .every((value) => classTypes.some((classType) => isInstance(value, classType))) + ) + }, + defaultMessage: buildMessage( + (eachPrefix) => + eachPrefix + + `$property value must be an instance of, or an array of instances containing ${classTypes + .map((c) => c.name) + .join(', ')}`, + validationOptions + ), + }, + }, + validationOptions + ) +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isStringArray(value: any): value is string[] { + return Array.isArray(value) && value.every((v) => typeof v === 'string') +} + +export const UriValidator = /\w+:(\/?\/?)[^\s]+/ + +export function isUri(value: string) { + return UriValidator.test(value) +} + +export function IsUri(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'isUri', + validator: { + validate: (value): boolean => isUri(value), + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + `$property must be an URI (that matches regex: ${UriValidator.source})`, + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/utils/version.ts b/packages/core/src/utils/version.ts new file mode 100644 index 0000000000..241ccbd838 --- /dev/null +++ b/packages/core/src/utils/version.ts @@ -0,0 +1,23 @@ +export function parseVersionString(version: VersionString): Version { + const [major, minor, patch] = version.split('.') + + return [Number(major), Number(minor), Number(patch ?? '0')] +} + +export function isFirstVersionHigherThanSecond(first: Version, second: Version) { + return ( + first[0] > second[0] || + (first[0] === second[0] && first[1] > second[1]) || + (first[0] === second[0] && first[1] === second[1] && first[2] > second[2]) + ) +} + +export function isFirstVersionEqualToSecond(first: Version, second: Version) { + return first[0] === second[0] && first[1] === second[1] && first[2] === second[2] +} + +export type VersionString = `${number}.${number}` | `${number}.${number}.${number}` +export type MajorVersion = number +export type MinorVersion = number +export type PatchVersion = number +export type Version = [MajorVersion, MinorVersion, PatchVersion] diff --git a/packages/core/src/wallet/Wallet.ts b/packages/core/src/wallet/Wallet.ts new file mode 100644 index 0000000000..a90e1e9f64 --- /dev/null +++ b/packages/core/src/wallet/Wallet.ts @@ -0,0 +1,89 @@ +import type { Key, KeyType } from '../crypto' +import type { Disposable } from '../plugins' +import type { + EncryptedMessage, + PlaintextMessage, + WalletConfig, + WalletConfigRekey, + WalletExportImportConfig, +} from '../types' +import type { Buffer } from '../utils/buffer' + +// Split up into WalletManager and Wallet instance +// WalletManager is responsible for: +// - create, open, delete, close, export, import +// Wallet is responsible for: +// - createKey, sign, verify, pack, unpack, generateNonce, generateWalletKey + +// - Split storage initialization from wallet initialization, as storage and wallet are not required to be the same +// - wallet handles key management, signing, and encryption +// - storage handles record storage and retrieval + +export interface Wallet extends Disposable { + isInitialized: boolean + isProvisioned: boolean + + create(walletConfig: WalletConfig): Promise + createAndOpen(walletConfig: WalletConfig): Promise + open(walletConfig: WalletConfig): Promise + rotateKey(walletConfig: WalletConfigRekey): Promise + close(): Promise + delete(): Promise + + /** + * Export the wallet to a file at the given path and encrypt it with the given key. + * + * @throws {WalletExportPathExistsError} When the export path already exists + */ + export(exportConfig: WalletExportImportConfig): Promise + import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig): Promise + + /** + * Create a key with an optional private key and keyType. + * + * @param options.privateKey Buffer Private key (formerly called 'seed') + * @param options.keyType KeyType the type of key that should be created + * + * @returns a `Key` instance + * + * @throws {WalletError} When an unsupported keytype is requested + * @throws {WalletError} When the key could not be created + * @throws {WalletKeyExistsError} When the key already exists in the wallet + */ + createKey(options: WalletCreateKeyOptions): Promise + sign(options: WalletSignOptions): Promise + verify(options: WalletVerifyOptions): Promise + + pack(payload: Record, recipientKeys: string[], senderVerkey?: string): Promise + unpack(encryptedMessage: EncryptedMessage): Promise + generateNonce(): Promise + generateWalletKey(): Promise + + /** + * Get the key types supported by the wallet implementation. + */ + supportedKeyTypes: KeyType[] +} + +export interface WalletCreateKeyOptions { + keyType: KeyType + seed?: Buffer + privateKey?: Buffer +} + +export interface WalletSignOptions { + data: Buffer | Buffer[] + key: Key +} + +export interface WalletVerifyOptions { + data: Buffer | Buffer[] + key: Key + signature: Buffer +} + +export interface UnpackedMessageContext { + plaintextMessage: PlaintextMessage + senderKey?: string + recipientKey?: string +} diff --git a/packages/core/src/wallet/WalletApi.ts b/packages/core/src/wallet/WalletApi.ts new file mode 100644 index 0000000000..ac987b1c75 --- /dev/null +++ b/packages/core/src/wallet/WalletApi.ts @@ -0,0 +1,136 @@ +import type { Wallet, WalletCreateKeyOptions } from './Wallet' +import type { WalletConfig, WalletConfigRekey, WalletExportImportConfig } from '../types' + +import { AgentContext } from '../agent' +import { InjectionSymbols } from '../constants' +import { Logger } from '../logger' +import { inject, injectable } from '../plugins' +import { StorageUpdateService } from '../storage' +import { CURRENT_FRAMEWORK_STORAGE_VERSION } from '../storage/migration/updates' + +import { WalletError } from './error/WalletError' +import { WalletNotFoundError } from './error/WalletNotFoundError' + +@injectable() +export class WalletApi { + private agentContext: AgentContext + private wallet: Wallet + private storageUpdateService: StorageUpdateService + private logger: Logger + private _walletConfig?: WalletConfig + + public constructor( + storageUpdateService: StorageUpdateService, + agentContext: AgentContext, + @inject(InjectionSymbols.Logger) logger: Logger + ) { + this.storageUpdateService = storageUpdateService + this.logger = logger + this.wallet = agentContext.wallet + this.agentContext = agentContext + } + + public get isInitialized() { + return this.wallet.isInitialized + } + + public get isProvisioned() { + return this.wallet.isProvisioned + } + + public get walletConfig() { + return this._walletConfig + } + + public async initialize(walletConfig: WalletConfig): Promise { + this.logger.info(`Initializing wallet '${walletConfig.id}'`, { + ...walletConfig, + key: walletConfig?.key ? '[*****]' : undefined, + storage: { + ...walletConfig?.storage, + credentials: walletConfig?.storage?.credentials ? '[*****]' : undefined, + }, + }) + + if (this.isInitialized) { + throw new WalletError( + 'Wallet instance already initialized. Close the currently opened wallet before re-initializing the wallet' + ) + } + + // Open wallet, creating if it doesn't exist yet + try { + await this.open(walletConfig) + } catch (error) { + // If the wallet does not exist yet, create it and try to open again + if (error instanceof WalletNotFoundError) { + // Keep the wallet open after creating it, this saves an extra round trip of closing/opening + // the wallet, which can save quite some time. + await this.createAndOpen(walletConfig) + } else { + throw error + } + } + } + + public async createAndOpen(walletConfig: WalletConfig): Promise { + // Always keep the wallet open, as we still need to store the storage version in the wallet. + await this.wallet.createAndOpen(walletConfig) + + this._walletConfig = walletConfig + + // Store the storage version in the wallet + await this.storageUpdateService.setCurrentStorageVersion(this.agentContext, CURRENT_FRAMEWORK_STORAGE_VERSION) + } + + public async create(walletConfig: WalletConfig): Promise { + await this.createAndOpen(walletConfig) + await this.close() + } + + public async open(walletConfig: WalletConfig): Promise { + await this.wallet.open(walletConfig) + this._walletConfig = walletConfig + } + + public async close(): Promise { + await this.wallet.close() + } + + public async rotateKey(walletConfig: WalletConfigRekey): Promise { + await this.wallet.rotateKey(walletConfig) + } + + public async generateNonce(): Promise { + return await this.wallet.generateNonce() + } + + public async delete(): Promise { + await this.wallet.delete() + } + + public async export(exportConfig: WalletExportImportConfig): Promise { + await this.wallet.export(exportConfig) + } + + public async import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig): Promise { + await this.wallet.import(walletConfig, importConfig) + } + + /** + * Create a key for and store it in the wallet. You can optionally provide a `privateKey` + * or `seed` for deterministic key generation. + * + * @param privateKey Buffer Private key (formerly called 'seed') + * @param seed Buffer (formerly called 'seed') + * @param keyType KeyType the type of key that should be created + * + * @returns a `Key` instance + * + * @throws {WalletError} When an unsupported `KeyType` is provided + * @throws {WalletError} When the key could not be created + */ + public async createKey(options: WalletCreateKeyOptions) { + return this.wallet.createKey(options) + } +} diff --git a/packages/core/src/wallet/WalletModule.ts b/packages/core/src/wallet/WalletModule.ts new file mode 100644 index 0000000000..6b603b6a17 --- /dev/null +++ b/packages/core/src/wallet/WalletModule.ts @@ -0,0 +1,16 @@ +import type { DependencyManager, Module } from '../plugins' + +import { WalletApi } from './WalletApi' + +// TODO: this should be moved into the modules directory +export class WalletModule implements Module { + public readonly api = WalletApi + + /** + * Registers the dependencies of the wallet module on the injection dependencyManager. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public register(dependencyManager: DependencyManager) { + // no-op, only API needs to be registered + } +} diff --git a/packages/core/src/wallet/__tests__/WalletModule.test.ts b/packages/core/src/wallet/__tests__/WalletModule.test.ts new file mode 100644 index 0000000000..a52a3a215f --- /dev/null +++ b/packages/core/src/wallet/__tests__/WalletModule.test.ts @@ -0,0 +1,13 @@ +import { DependencyManager } from '../../plugins/DependencyManager' +import { WalletModule } from '../WalletModule' + +jest.mock('../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +describe('WalletModule', () => { + test('registers dependencies on the dependency manager', () => { + new WalletModule().register(dependencyManager) + }) +}) diff --git a/packages/core/src/wallet/error/WalletDuplicateError.ts b/packages/core/src/wallet/error/WalletDuplicateError.ts new file mode 100644 index 0000000000..615b2563bb --- /dev/null +++ b/packages/core/src/wallet/error/WalletDuplicateError.ts @@ -0,0 +1,7 @@ +import { WalletError } from './WalletError' + +export class WalletDuplicateError extends WalletError { + public constructor(message: string, { walletType, cause }: { walletType: string; cause?: Error }) { + super(`${walletType}: ${message}`, { cause }) + } +} diff --git a/packages/core/src/wallet/error/WalletError.ts b/packages/core/src/wallet/error/WalletError.ts new file mode 100644 index 0000000000..414f2014aa --- /dev/null +++ b/packages/core/src/wallet/error/WalletError.ts @@ -0,0 +1,7 @@ +import { CredoError } from '../../error/CredoError' + +export class WalletError extends CredoError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/core/src/wallet/error/WalletExportPathExistsError.ts b/packages/core/src/wallet/error/WalletExportPathExistsError.ts new file mode 100644 index 0000000000..cf46e028e7 --- /dev/null +++ b/packages/core/src/wallet/error/WalletExportPathExistsError.ts @@ -0,0 +1,7 @@ +import { WalletError } from './WalletError' + +export class WalletExportPathExistsError extends WalletError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/core/src/wallet/error/WalletExportUnsupportedError.ts b/packages/core/src/wallet/error/WalletExportUnsupportedError.ts new file mode 100644 index 0000000000..db7a313e86 --- /dev/null +++ b/packages/core/src/wallet/error/WalletExportUnsupportedError.ts @@ -0,0 +1,7 @@ +import { WalletError } from './WalletError' + +export class WalletExportUnsupportedError extends WalletError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/core/src/wallet/error/WalletImportPathExistsError.ts b/packages/core/src/wallet/error/WalletImportPathExistsError.ts new file mode 100644 index 0000000000..32d9b46d67 --- /dev/null +++ b/packages/core/src/wallet/error/WalletImportPathExistsError.ts @@ -0,0 +1,7 @@ +import { WalletError } from './WalletError' + +export class WalletImportPathExistsError extends WalletError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/core/src/wallet/error/WalletInvalidKeyError.ts b/packages/core/src/wallet/error/WalletInvalidKeyError.ts new file mode 100644 index 0000000000..b7a29de2d9 --- /dev/null +++ b/packages/core/src/wallet/error/WalletInvalidKeyError.ts @@ -0,0 +1,7 @@ +import { WalletError } from './WalletError' + +export class WalletInvalidKeyError extends WalletError { + public constructor(message: string, { walletType, cause }: { walletType: string; cause?: Error }) { + super(`${walletType}: ${message}`, { cause }) + } +} diff --git a/packages/core/src/wallet/error/WalletKeyExistsError.ts b/packages/core/src/wallet/error/WalletKeyExistsError.ts new file mode 100644 index 0000000000..3e0a19e7b4 --- /dev/null +++ b/packages/core/src/wallet/error/WalletKeyExistsError.ts @@ -0,0 +1,7 @@ +import { WalletError } from './WalletError' + +export class WalletKeyExistsError extends WalletError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/core/src/wallet/error/WalletNotFoundError.ts b/packages/core/src/wallet/error/WalletNotFoundError.ts new file mode 100644 index 0000000000..a2e8d32d45 --- /dev/null +++ b/packages/core/src/wallet/error/WalletNotFoundError.ts @@ -0,0 +1,7 @@ +import { WalletError } from './WalletError' + +export class WalletNotFoundError extends WalletError { + public constructor(message: string, { walletType, cause }: { walletType: string; cause?: Error }) { + super(`${walletType}: ${message}`, { cause }) + } +} diff --git a/packages/core/src/wallet/error/index.ts b/packages/core/src/wallet/error/index.ts new file mode 100644 index 0000000000..343fd83913 --- /dev/null +++ b/packages/core/src/wallet/error/index.ts @@ -0,0 +1,8 @@ +export { WalletDuplicateError } from './WalletDuplicateError' +export { WalletNotFoundError } from './WalletNotFoundError' +export { WalletInvalidKeyError } from './WalletInvalidKeyError' +export { WalletError } from './WalletError' +export { WalletKeyExistsError } from './WalletKeyExistsError' +export { WalletImportPathExistsError } from './WalletImportPathExistsError' +export { WalletExportPathExistsError } from './WalletExportPathExistsError' +export { WalletExportUnsupportedError } from './WalletExportUnsupportedError' diff --git a/packages/core/src/wallet/index.ts b/packages/core/src/wallet/index.ts new file mode 100644 index 0000000000..e60dcfdb68 --- /dev/null +++ b/packages/core/src/wallet/index.ts @@ -0,0 +1,3 @@ +export * from './Wallet' +export * from './WalletApi' +export * from './WalletModule' diff --git a/packages/core/tests/TestMessage.ts b/packages/core/tests/TestMessage.ts new file mode 100644 index 0000000000..040e4303f7 --- /dev/null +++ b/packages/core/tests/TestMessage.ts @@ -0,0 +1,11 @@ +import { AgentMessage } from '../src/agent/AgentMessage' + +export class TestMessage extends AgentMessage { + public constructor() { + super() + + this.id = this.generateId() + } + + public type = 'https://didcomm.org/connections/1.0/invitation' +} diff --git a/packages/core/tests/__fixtures__/didKeyz6Mkqbe1.json b/packages/core/tests/__fixtures__/didKeyz6Mkqbe1.json new file mode 100644 index 0000000000..ed4d179434 --- /dev/null +++ b/packages/core/tests/__fixtures__/didKeyz6Mkqbe1.json @@ -0,0 +1,38 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "id": "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG", + "verificationMethod": [ + { + "id": "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG", + "type": "Ed25519VerificationKey2018", + "controller": "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG", + "publicKeyBase58": "C9Ny7yk9PRKsfW9EJsTJVY12Xn1yke1Jfm24JSy8MLUt" + }, + { + "id": "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6LSmTBUAjnhVwsrkbDQgJgViTH5cjozFjFMaguyvpUq2kcz", + "type": "X25519KeyAgreementKey2019", + "controller": "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG", + "publicKeyBase58": "An1JeRyqQVA7fCqe9fAYPs4bmbGsZ85ChiCJSMqJKNrE" + } + ], + "authentication": [ + "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG" + ], + "assertionMethod": [ + "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG" + ], + "capabilityInvocation": [ + "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG" + ], + "capabilityDelegation": [ + "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG" + ], + "keyAgreement": [ + "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6LSmTBUAjnhVwsrkbDQgJgViTH5cjozFjFMaguyvpUq2kcz" + ], + "service": [] +} diff --git a/packages/core/tests/agents.test.ts b/packages/core/tests/agents.test.ts new file mode 100644 index 0000000000..6a8a7b23b4 --- /dev/null +++ b/packages/core/tests/agents.test.ts @@ -0,0 +1,73 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { ConnectionRecord } from '../src/modules/connections' + +import { Agent } from '../src/agent/Agent' +import { HandshakeProtocol } from '../src/modules/connections' + +import { waitForBasicMessage, getInMemoryAgentOptions } from './helpers' +import { setupSubjectTransports } from './transport' + +const aliceAgentOptions = getInMemoryAgentOptions('Agents Alice', { + endpoints: ['rxjs:alice'], +}) +const bobAgentOptions = getInMemoryAgentOptions('Agents Bob', { + endpoints: ['rxjs:bob'], +}) + +describe('agents', () => { + let aliceAgent: Agent + let bobAgent: Agent + let aliceConnection: ConnectionRecord + let bobConnection: ConnectionRecord + + afterAll(async () => { + await bobAgent.shutdown() + await bobAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('make a connection between agents', async () => { + aliceAgent = new Agent(aliceAgentOptions) + bobAgent = new Agent(bobAgentOptions) + + setupSubjectTransports([aliceAgent, bobAgent]) + + await aliceAgent.initialize() + await bobAgent.initialize() + + const aliceBobOutOfBandRecord = await aliceAgent.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + const { connectionRecord: bobConnectionAtBobAlice } = await bobAgent.oob.receiveInvitation( + aliceBobOutOfBandRecord.outOfBandInvitation + ) + bobConnection = await bobAgent.connections.returnWhenIsConnected(bobConnectionAtBobAlice!.id) + + const [aliceConnectionAtAliceBob] = await aliceAgent.connections.findAllByOutOfBandId(aliceBobOutOfBandRecord.id) + aliceConnection = await aliceAgent.connections.returnWhenIsConnected(aliceConnectionAtAliceBob!.id) + + expect(aliceConnection).toBeConnectedWith(bobConnection) + expect(bobConnection).toBeConnectedWith(aliceConnection) + }) + + test('send a message to connection', async () => { + const message = 'hello, world' + await aliceAgent.basicMessages.sendMessage(aliceConnection.id, message) + + const basicMessage = await waitForBasicMessage(bobAgent, { + content: message, + }) + + expect(basicMessage.content).toBe(message) + }) + + test('can shutdown and re-initialize the same agent', async () => { + expect(aliceAgent.isInitialized).toBe(true) + await aliceAgent.shutdown() + expect(aliceAgent.isInitialized).toBe(false) + await aliceAgent.initialize() + expect(aliceAgent.isInitialized).toBe(true) + }) +}) diff --git a/packages/core/tests/connections.test.ts b/packages/core/tests/connections.test.ts new file mode 100644 index 0000000000..d158acc272 --- /dev/null +++ b/packages/core/tests/connections.test.ts @@ -0,0 +1,308 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { AgentMessageProcessedEvent, KeylistUpdate } from '../src' + +import { filter, firstValueFrom, map, timeout } from 'rxjs' + +import { + MediatorModule, + Key, + AgentEventTypes, + KeylistUpdateMessage, + DidExchangeState, + HandshakeProtocol, + KeylistUpdateAction, +} from '../src' +import { Agent } from '../src/agent/Agent' +import { didKeyToVerkey } from '../src/modules/dids/helpers' +import { OutOfBandState } from '../src/modules/oob/domain/OutOfBandState' + +import { getInMemoryAgentOptions, waitForTrustPingResponseReceivedEvent } from './helpers' +import { setupSubjectTransports } from './transport' + +describe('connections', () => { + let faberAgent: Agent + let aliceAgent: Agent + let acmeAgent: Agent + let mediatorAgent: Agent + + beforeEach(async () => { + const faberAgentOptions = getInMemoryAgentOptions('Faber Agent Connections', { + endpoints: ['rxjs:faber'], + }) + const aliceAgentOptions = getInMemoryAgentOptions('Alice Agent Connections', { + endpoints: ['rxjs:alice'], + }) + const acmeAgentOptions = getInMemoryAgentOptions('Acme Agent Connections', { + endpoints: ['rxjs:acme'], + }) + const mediatorAgentOptions = getInMemoryAgentOptions( + 'Mediator Agent Connections', + { + endpoints: ['rxjs:mediator'], + }, + { + mediator: new MediatorModule({ + autoAcceptMediationRequests: true, + }), + } + ) + + faberAgent = new Agent(faberAgentOptions) + aliceAgent = new Agent(aliceAgentOptions) + acmeAgent = new Agent(acmeAgentOptions) + mediatorAgent = new Agent(mediatorAgentOptions) + + setupSubjectTransports([faberAgent, aliceAgent, acmeAgent, mediatorAgent]) + + await faberAgent.initialize() + await aliceAgent.initialize() + await acmeAgent.initialize() + await mediatorAgent.initialize() + }) + + afterEach(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + await acmeAgent.shutdown() + await acmeAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + }) + + it('one agent should be able to send and receive a ping', async () => { + const faberOutOfBandRecord = await faberAgent.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + multiUseInvitation: true, + }) + + const invitation = faberOutOfBandRecord.outOfBandInvitation + const invitationUrl = invitation.toUrl({ domain: 'https://example.com' }) + + // Receive invitation with alice agent + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + const ping = await aliceAgent.connections.sendPing(aliceFaberConnection.id, {}) + + await waitForTrustPingResponseReceivedEvent(aliceAgent, { threadId: ping.threadId }) + }) + + it('one should be able to make multiple connections using a multi use invite', async () => { + const faberOutOfBandRecord = await faberAgent.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + multiUseInvitation: true, + }) + + const invitation = faberOutOfBandRecord.outOfBandInvitation + const invitationUrl = invitation.toUrl({ domain: 'https://example.com' }) + + // Receive invitation first time with alice agent + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + // Receive invitation second time with acme agent + let { connectionRecord: acmeFaberConnection } = await acmeAgent.oob.receiveInvitationFromUrl(invitationUrl, { + reuseConnection: false, + }) + acmeFaberConnection = await acmeAgent.connections.returnWhenIsConnected(acmeFaberConnection!.id) + expect(acmeFaberConnection.state).toBe(DidExchangeState.Completed) + + let faberAliceConnection = await faberAgent.connections.getByThreadId(aliceFaberConnection.threadId!) + let faberAcmeConnection = await faberAgent.connections.getByThreadId(acmeFaberConnection.threadId!) + + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection.id) + faberAcmeConnection = await faberAgent.connections.returnWhenIsConnected(faberAcmeConnection.id) + + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + expect(faberAcmeConnection).toBeConnectedWith(acmeFaberConnection) + + expect(faberAliceConnection.id).not.toBe(faberAcmeConnection.id) + + return expect(faberOutOfBandRecord.state).toBe(OutOfBandState.AwaitResponse) + }) + + it('tag connections with multiple types and query them', async () => { + const faberOutOfBandRecord = await faberAgent.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + multiUseInvitation: true, + }) + + const invitation = faberOutOfBandRecord.outOfBandInvitation + const invitationUrl = invitation.toUrl({ domain: 'https://example.com' }) + + // Receive invitation first time with alice agent + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + // Mark connection with three different types + aliceFaberConnection = await aliceAgent.connections.addConnectionType(aliceFaberConnection.id, 'alice-faber-1') + aliceFaberConnection = await aliceAgent.connections.addConnectionType(aliceFaberConnection.id, 'alice-faber-2') + aliceFaberConnection = await aliceAgent.connections.addConnectionType(aliceFaberConnection.id, 'alice-faber-3') + + // Now search for them + let connectionsFound = await aliceAgent.connections.findAllByConnectionTypes(['alice-faber-4']) + expect(connectionsFound).toEqual([]) + connectionsFound = await aliceAgent.connections.findAllByConnectionTypes(['alice-faber-1']) + expect(connectionsFound.map((item) => item.id)).toMatchObject([aliceFaberConnection.id]) + connectionsFound = await aliceAgent.connections.findAllByConnectionTypes(['alice-faber-2']) + expect(connectionsFound.map((item) => item.id)).toMatchObject([aliceFaberConnection.id]) + connectionsFound = await aliceAgent.connections.findAllByConnectionTypes(['alice-faber-3']) + expect(connectionsFound.map((item) => item.id)).toMatchObject([aliceFaberConnection.id]) + connectionsFound = await aliceAgent.connections.findAllByConnectionTypes(['alice-faber-1', 'alice-faber-3']) + expect(connectionsFound.map((item) => item.id)).toMatchObject([aliceFaberConnection.id]) + connectionsFound = await aliceAgent.connections.findAllByConnectionTypes([ + 'alice-faber-1', + 'alice-faber-2', + 'alice-faber-3', + ]) + expect(connectionsFound.map((item) => item.id)).toMatchObject([aliceFaberConnection.id]) + connectionsFound = await aliceAgent.connections.findAllByConnectionTypes(['alice-faber-1', 'alice-faber-4']) + expect(connectionsFound).toEqual([]) + }) + + xit('should be able to make multiple connections using a multi use invite', async () => { + const faberOutOfBandRecord = await faberAgent.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + multiUseInvitation: true, + }) + + const invitation = faberOutOfBandRecord.outOfBandInvitation + const invitationUrl = invitation.toUrl({ domain: 'https://example.com' }) + + // Create first connection + let { connectionRecord: aliceFaberConnection1 } = await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) + aliceFaberConnection1 = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection1!.id) + expect(aliceFaberConnection1.state).toBe(DidExchangeState.Completed) + + // Create second connection + let { connectionRecord: aliceFaberConnection2 } = await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl, { + reuseConnection: false, + }) + aliceFaberConnection2 = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection2!.id) + expect(aliceFaberConnection2.state).toBe(DidExchangeState.Completed) + + let faberAliceConnection1 = await faberAgent.connections.getByThreadId(aliceFaberConnection1.threadId!) + let faberAliceConnection2 = await faberAgent.connections.getByThreadId(aliceFaberConnection2.threadId!) + + faberAliceConnection1 = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection1.id) + faberAliceConnection2 = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection2.id) + + expect(faberAliceConnection1).toBeConnectedWith(aliceFaberConnection1) + expect(faberAliceConnection2).toBeConnectedWith(aliceFaberConnection2) + + expect(faberAliceConnection1.id).not.toBe(faberAliceConnection2.id) + + return expect(faberOutOfBandRecord.state).toBe(OutOfBandState.AwaitResponse) + }) + + it('agent using mediator should be able to make multiple connections using a multi use invite', async () => { + // Make Faber use a mediator + const { outOfBandInvitation: mediatorOutOfBandInvitation } = await mediatorAgent.oob.createInvitation({}) + let { connectionRecord } = await faberAgent.oob.receiveInvitation(mediatorOutOfBandInvitation) + connectionRecord = await faberAgent.connections.returnWhenIsConnected(connectionRecord!.id) + await faberAgent.mediationRecipient.provision(connectionRecord!) + await faberAgent.mediationRecipient.initialize() + + // Create observable for event + const keyAddMessageObservable = mediatorAgent.events + .observable(AgentEventTypes.AgentMessageProcessed) + .pipe( + filter((event) => event.payload.message.type === KeylistUpdateMessage.type.messageTypeUri), + map((event) => event.payload.message as KeylistUpdateMessage), + timeout(5000) + ) + + const keylistAddEvents: KeylistUpdate[] = [] + keyAddMessageObservable.subscribe((value) => { + value.updates.forEach((update) => + keylistAddEvents.push({ action: update.action, recipientKey: didKeyToVerkey(update.recipientKey) }) + ) + }) + + // Now create invitations that will be mediated + const faberOutOfBandRecord = await faberAgent.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + multiUseInvitation: true, + }) + + const invitation = faberOutOfBandRecord.outOfBandInvitation + const invitationUrl = invitation.toUrl({ domain: 'https://example.com' }) + + // Receive invitation first time with alice agent + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + // Receive invitation second time with acme agent + let { connectionRecord: acmeFaberConnection } = await acmeAgent.oob.receiveInvitationFromUrl(invitationUrl, { + reuseConnection: false, + }) + acmeFaberConnection = await acmeAgent.connections.returnWhenIsConnected(acmeFaberConnection!.id) + expect(acmeFaberConnection.state).toBe(DidExchangeState.Completed) + + let faberAliceConnection = await faberAgent.connections.getByThreadId(aliceFaberConnection.threadId!) + let faberAcmeConnection = await faberAgent.connections.getByThreadId(acmeFaberConnection.threadId!) + + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection.id) + faberAcmeConnection = await faberAgent.connections.returnWhenIsConnected(faberAcmeConnection.id) + + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + expect(faberAcmeConnection).toBeConnectedWith(acmeFaberConnection) + + expect(faberAliceConnection.id).not.toBe(faberAcmeConnection.id) + + expect(faberOutOfBandRecord.state).toBe(OutOfBandState.AwaitResponse) + + // Mediator should have received all new keys (the one of the invitation + the ones generated on each connection) + expect(keylistAddEvents.length).toEqual(3) + + expect(keylistAddEvents).toEqual( + expect.arrayContaining([ + { + action: KeylistUpdateAction.add, + recipientKey: Key.fromFingerprint(faberOutOfBandRecord.getTags().recipientKeyFingerprints[0]).publicKeyBase58, + }, + { + action: KeylistUpdateAction.add, + recipientKey: (await faberAgent.dids.resolveDidDocument(faberAliceConnection.did!)).recipientKeys[0] + .publicKeyBase58, + }, + { + action: KeylistUpdateAction.add, + recipientKey: (await faberAgent.dids.resolveDidDocument(faberAcmeConnection.did!)).recipientKeys[0] + .publicKeyBase58, + }, + ]) + ) + + for (const connection of [faberAcmeConnection, faberAliceConnection]) { + const keyRemoveMessagePromise = firstValueFrom( + mediatorAgent.events.observable(AgentEventTypes.AgentMessageProcessed).pipe( + filter((event) => event.payload.message.type === KeylistUpdateMessage.type.messageTypeUri), + map((event) => event.payload.message as KeylistUpdateMessage), + timeout(5000) + ) + ) + + await faberAgent.connections.deleteById(connection.id) + + const keyRemoveMessage = await keyRemoveMessagePromise + expect(keyRemoveMessage.updates.length).toEqual(1) + + expect( + keyRemoveMessage.updates.map((update) => ({ + action: update.action, + recipientKey: didKeyToVerkey(update.recipientKey), + }))[0] + ).toEqual({ + action: KeylistUpdateAction.remove, + recipientKey: (await faberAgent.dids.resolveDidDocument(connection.did!)).recipientKeys[0].publicKeyBase58, + }) + } + }) +}) diff --git a/packages/core/tests/events.ts b/packages/core/tests/events.ts new file mode 100644 index 0000000000..e48f689f1e --- /dev/null +++ b/packages/core/tests/events.ts @@ -0,0 +1,21 @@ +import type { Agent, BaseEvent } from '../src' + +import { ReplaySubject } from 'rxjs' + +export type EventReplaySubject = ReplaySubject + +export function setupEventReplaySubjects(agents: Agent[], eventTypes: string[]): ReplaySubject[] { + const replaySubjects: EventReplaySubject[] = [] + + for (const agent of agents) { + const replaySubject = new ReplaySubject() + + for (const eventType of eventTypes) { + agent.events.observable(eventType).subscribe(replaySubject) + } + + replaySubjects.push(replaySubject) + } + + return replaySubjects +} diff --git a/packages/core/tests/generic-records.test.ts b/packages/core/tests/generic-records.test.ts new file mode 100644 index 0000000000..bdf605d517 --- /dev/null +++ b/packages/core/tests/generic-records.test.ts @@ -0,0 +1,122 @@ +import type { GenericRecord } from '../src/modules/generic-records/repository/GenericRecord' + +import { Agent } from '../src/agent/Agent' +import { RecordNotFoundError } from '../src/error' + +import { getInMemoryAgentOptions } from './helpers' + +const aliceAgentOptions = getInMemoryAgentOptions('Generic Records Alice', { + endpoints: ['rxjs:alice'], +}) + +describe('genericRecords', () => { + let aliceAgent: Agent + + const fooString = { foo: 'Some data saved' } + const fooNumber = { foo: 42 } + + const barString: Record = fooString + const barNumber: Record = fooNumber + + afterAll(async () => { + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('store generic-record record', async () => { + aliceAgent = new Agent(aliceAgentOptions) + await aliceAgent.initialize() + + // Save genericRecord message (Minimal) + + const savedRecord1: GenericRecord = await aliceAgent.genericRecords.save({ content: barString }) + + // Save genericRecord message with tag + const tags1 = { myTag: 'foobar1' } + const tags2 = { myTag: 'foobar2' } + + const savedRecord2: GenericRecord = await aliceAgent.genericRecords.save({ content: barNumber, tags: tags1 }) + + expect(savedRecord1).toBeDefined() + expect(savedRecord2).toBeDefined() + + const savedRecord3: GenericRecord = await aliceAgent.genericRecords.save({ content: barString, tags: tags2 }) + expect(savedRecord3).toBeDefined() + + const record = await aliceAgent.genericRecords.save({ content: barString, tags: tags2, id: 'foo' }) + expect(record.id).toBe('foo') + }) + + test('get generic-record records', async () => { + //Create genericRecord message + const savedRecords = await aliceAgent.genericRecords.getAll() + expect(savedRecords.length).toBe(4) + }) + + test('get generic-record specific record', async () => { + //Create genericRecord message + const savedRecords1 = await aliceAgent.genericRecords.findAllByQuery({ myTag: 'foobar1' }) + expect(savedRecords1?.length === 1).toBe(true) + expect(savedRecords1[0].content).toEqual({ foo: 42 }) + + const savedRecords2 = await aliceAgent.genericRecords.findAllByQuery({ myTag: 'foobar2' }) + expect(savedRecords2.length === 2).toBe(true) + expect(savedRecords2[0].content).toEqual({ foo: 'Some data saved' }) + }) + + test('find generic record using id', async () => { + const myId = '100' + const savedRecord1: GenericRecord = await aliceAgent.genericRecords.save({ content: barString, id: myId }) + expect(savedRecord1).toBeDefined() + + const retrievedRecord = await aliceAgent.genericRecords.findById(savedRecord1.id) + expect(retrievedRecord?.content).toEqual({ foo: 'Some data saved' }) + }) + + test('delete generic record', async () => { + const myId = '101' + const savedRecord1: GenericRecord = await aliceAgent.genericRecords.save({ content: barString, id: myId }) + expect(savedRecord1).toBeDefined() + + await aliceAgent.genericRecords.delete(savedRecord1) + + const retrievedRecord = await aliceAgent.genericRecords.findById(savedRecord1.id) + expect(retrievedRecord).toBeNull() + }) + + test('delete generic record by id', async () => { + const myId = 'test-id' + const savedRecord = await aliceAgent.genericRecords.save({ content: barString, id: myId }) + expect(savedRecord).toBeDefined() + + await aliceAgent.genericRecords.deleteById(savedRecord.id) + + const retrievedRecord = await aliceAgent.genericRecords.findById(savedRecord.id) + expect(retrievedRecord).toBeNull() + }) + test('throws an error if record not found by id ', async () => { + const deleteRecordById = async () => { + await aliceAgent.genericRecords.deleteById('test') + } + expect(deleteRecordById).rejects.toThrow(RecordNotFoundError) + }) + + test('update generic record', async () => { + const myId = '102' + const savedRecord1: GenericRecord = await aliceAgent.genericRecords.save({ content: barString, id: myId }) + expect(savedRecord1).toBeDefined() + + let retrievedRecord = await aliceAgent.genericRecords.findById(savedRecord1.id) + expect(retrievedRecord).toBeDefined() + + const amendedFooString = { foo: 'Some even more cool data saved' } + const barString2: Record = amendedFooString + + savedRecord1.content = barString2 + + await aliceAgent.genericRecords.update(savedRecord1) + + retrievedRecord = await aliceAgent.genericRecords.findById(savedRecord1.id) + expect(retrievedRecord?.content).toEqual({ foo: 'Some even more cool data saved' }) + }) +}) diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts new file mode 100644 index 0000000000..79c3c5dd3f --- /dev/null +++ b/packages/core/tests/helpers.ts @@ -0,0 +1,763 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { AskarWalletSqliteStorageConfig } from '../../askar/src/wallet' +import type { + AgentDependencies, + BaseEvent, + BasicMessage, + BasicMessageStateChangedEvent, + ConnectionRecordProps, + CredentialStateChangedEvent, + InitConfig, + InjectionToken, + ProofStateChangedEvent, + Wallet, + Agent, + CredentialState, + ConnectionStateChangedEvent, + Buffer, + AgentMessageProcessedEvent, + RevocationNotificationReceivedEvent, + KeyDidCreateOptions, + ConnectionDidRotatedEvent, +} from '../src' +import type { AgentModulesInput, EmptyModuleMap } from '../src/agent/AgentModules' +import type { TrustPingReceivedEvent, TrustPingResponseReceivedEvent } from '../src/modules/connections/TrustPingEvents' +import type { ProofState } from '../src/modules/proofs/models/ProofState' +import type { WalletConfig } from '../src/types' +import type { Observable } from 'rxjs' + +import { readFileSync } from 'fs' +import path from 'path' +import { lastValueFrom, firstValueFrom, ReplaySubject } from 'rxjs' +import { catchError, filter, map, take, timeout } from 'rxjs/operators' + +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' +import { agentDependencies } from '../../node/src' +import { + AgentEventTypes, + OutOfBandDidCommService, + ConnectionsModule, + ConnectionEventTypes, + TypedArrayEncoder, + AgentConfig, + AgentContext, + BasicMessageEventTypes, + ConnectionRecord, + CredentialEventTypes, + DependencyManager, + DidExchangeRole, + DidExchangeState, + HandshakeProtocol, + InjectionSymbols, + ProofEventTypes, + TrustPingEventTypes, + DidsApi, +} from '../src' +import { Key, KeyType } from '../src/crypto' +import { DidKey } from '../src/modules/dids/methods/key' +import { OutOfBandRole } from '../src/modules/oob/domain/OutOfBandRole' +import { OutOfBandState } from '../src/modules/oob/domain/OutOfBandState' +import { OutOfBandInvitation } from '../src/modules/oob/messages' +import { OutOfBandRecord } from '../src/modules/oob/repository' +import { KeyDerivationMethod } from '../src/types' +import { sleep } from '../src/utils/sleep' +import { uuid } from '../src/utils/uuid' + +import testLogger, { TestLogger } from './logger' + +export const genesisPath = process.env.GENESIS_TXN_PATH + ? path.resolve(process.env.GENESIS_TXN_PATH) + : path.join(__dirname, '../../../network/genesis/local-genesis.txn') + +export const genesisTransactions = readFileSync(genesisPath).toString('utf-8') + +export const publicDidSeed = process.env.TEST_AGENT_PUBLIC_DID_SEED ?? '000000000000000000000000Trustee9' +export const taaVersion = (process.env.TEST_AGENT_TAA_VERSION ?? '1') as `${number}.${number}` | `${number}` +export const taaAcceptanceMechanism = process.env.TEST_AGENT_TAA_ACCEPTANCE_MECHANISM ?? 'accept' +export { agentDependencies } + +export function getAskarWalletConfig( + name: string, + { + inMemory = true, + random = uuid().slice(0, 4), + maxConnections, + }: { inMemory?: boolean; random?: string; maxConnections?: number } = {} +) { + return { + id: `Wallet: ${name} - ${random}`, + key: 'DZ9hPqFWTPxemcGea72C1X1nusqk5wFNLq6QPjwXGqAa', // generated using indy.generateWalletKey + keyDerivationMethod: KeyDerivationMethod.Raw, + // Use in memory by default + storage: { + type: 'sqlite', + config: { + inMemory, + maxConnections, + }, + } satisfies AskarWalletSqliteStorageConfig, + } satisfies WalletConfig +} + +export function getAgentOptions( + name: string, + extraConfig: Partial = {}, + inputModules?: AgentModules, + inMemoryWallet = true +): { config: InitConfig; modules: AgentModules; dependencies: AgentDependencies; inMemory?: boolean } { + const random = uuid().slice(0, 4) + const config: InitConfig = { + label: `Agent: ${name} - ${random}`, + walletConfig: getAskarWalletConfig(name, { inMemory: inMemoryWallet, random }), + // TODO: determine the log level based on an environment variable. This will make it + // possible to run e.g. failed github actions in debug mode for extra logs + logger: TestLogger.fromLogger(testLogger, name), + ...extraConfig, + } + + const m = (inputModules ?? {}) as AgentModulesInput + const modules = { + ...m, + // Make sure connections module is always defined so we can set autoAcceptConnections + connections: + m.connections ?? + new ConnectionsModule({ + autoAcceptConnections: true, + }), + } + + return { config, modules: modules as AgentModules, dependencies: agentDependencies } as const +} + +export function getInMemoryAgentOptions( + name: string, + extraConfig: Partial = {}, + inputModules?: AgentModules +): { config: InitConfig; modules: AgentModules; dependencies: AgentDependencies } { + const random = uuid().slice(0, 4) + const config: InitConfig = { + label: `Agent: ${name} - ${random}`, + walletConfig: { + id: `Wallet: ${name} - ${random}`, + key: `Wallet: ${name}`, + }, + // TODO: determine the log level based on an environment variable. This will make it + // possible to run e.g. failed github actions in debug mode for extra logs + logger: TestLogger.fromLogger(testLogger, name), + ...extraConfig, + } + + const m = (inputModules ?? {}) as AgentModulesInput + const modules = { + ...m, + inMemory: new InMemoryWalletModule(), + // Make sure connections module is always defined so we can set autoAcceptConnections + connections: + m.connections ?? + new ConnectionsModule({ + autoAcceptConnections: true, + }), + } + + return { config, modules: modules as unknown as AgentModules, dependencies: agentDependencies } as const +} + +export async function importExistingIndyDidFromPrivateKey(agent: Agent, privateKey: Buffer) { + const key = await agent.wallet.createKey({ + keyType: KeyType.Ed25519, + privateKey, + }) + + // did is first 16 bytes of public key encoded as base58 + const unqualifiedIndyDid = TypedArrayEncoder.toBase58(key.publicKey.slice(0, 16)) + + // import the did in the wallet so it can be used + await agent.dids.import({ did: `did:indy:pool:localtest:${unqualifiedIndyDid}` }) + + return unqualifiedIndyDid +} + +export function getAgentConfig( + name: string, + extraConfig: Partial = {} +): AgentConfig & { walletConfig: WalletConfig } { + const { config, dependencies } = getAgentOptions(name, extraConfig) + return new AgentConfig(config, dependencies) as AgentConfig & { walletConfig: WalletConfig } +} + +export function getAgentContext({ + dependencyManager = new DependencyManager(), + wallet, + agentConfig, + contextCorrelationId = 'mock', + registerInstances = [], +}: { + dependencyManager?: DependencyManager + wallet?: Wallet + agentConfig?: AgentConfig + contextCorrelationId?: string + // Must be an array of arrays as objects can't have injection tokens + // as keys (it must be number, string or symbol) + registerInstances?: Array<[InjectionToken, unknown]> +} = {}) { + if (wallet) dependencyManager.registerInstance(InjectionSymbols.Wallet, wallet) + if (agentConfig) dependencyManager.registerInstance(AgentConfig, agentConfig) + + // Register custom instances on the dependency manager + for (const [token, instance] of registerInstances.values()) { + dependencyManager.registerInstance(token, instance) + } + + return new AgentContext({ dependencyManager, contextCorrelationId }) +} + +export async function waitForProofExchangeRecord( + agent: Agent, + options: { + threadId?: string + parentThreadId?: string + state?: ProofState + previousState?: ProofState | null + timeoutMs?: number + } +) { + const observable = agent.events.observable(ProofEventTypes.ProofStateChanged) + + return waitForProofExchangeRecordSubject(observable, options) +} + +const isProofStateChangedEvent = (e: BaseEvent): e is ProofStateChangedEvent => + e.type === ProofEventTypes.ProofStateChanged +const isCredentialStateChangedEvent = (e: BaseEvent): e is CredentialStateChangedEvent => + e.type === CredentialEventTypes.CredentialStateChanged +const isConnectionStateChangedEvent = (e: BaseEvent): e is ConnectionStateChangedEvent => + e.type === ConnectionEventTypes.ConnectionStateChanged +const isConnectionDidRotatedEvent = (e: BaseEvent): e is ConnectionDidRotatedEvent => + e.type === ConnectionEventTypes.ConnectionDidRotated +const isTrustPingReceivedEvent = (e: BaseEvent): e is TrustPingReceivedEvent => + e.type === TrustPingEventTypes.TrustPingReceivedEvent +const isTrustPingResponseReceivedEvent = (e: BaseEvent): e is TrustPingResponseReceivedEvent => + e.type === TrustPingEventTypes.TrustPingResponseReceivedEvent +const isAgentMessageProcessedEvent = (e: BaseEvent): e is AgentMessageProcessedEvent => + e.type === AgentEventTypes.AgentMessageProcessed + +export function waitForProofExchangeRecordSubject( + subject: ReplaySubject | Observable, + { + threadId, + parentThreadId, + state, + previousState, + timeoutMs = 10000, + count = 1, + }: { + threadId?: string + parentThreadId?: string + state?: ProofState + previousState?: ProofState | null + timeoutMs?: number + count?: number + } +) { + const observable: Observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return lastValueFrom( + observable.pipe( + filter(isProofStateChangedEvent), + filter((e) => previousState === undefined || e.payload.previousState === previousState), + filter((e) => threadId === undefined || e.payload.proofRecord.threadId === threadId), + filter((e) => parentThreadId === undefined || e.payload.proofRecord.parentThreadId === parentThreadId), + filter((e) => state === undefined || e.payload.proofRecord.state === state), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `ProofStateChangedEvent event not emitted within specified timeout: ${timeoutMs} + previousState: ${previousState}, + threadId: ${threadId}, + parentThreadId: ${parentThreadId}, + state: ${state} + }` + ) + }), + take(count), + map((e) => e.payload.proofRecord) + ) + ) +} + +export async function waitForTrustPingReceivedEvent( + agent: Agent, + options: { + threadId?: string + timeoutMs?: number + } +) { + const observable = agent.events.observable(TrustPingEventTypes.TrustPingReceivedEvent) + + return waitForTrustPingReceivedEventSubject(observable, options) +} + +export function waitForTrustPingReceivedEventSubject( + subject: ReplaySubject | Observable, + { + threadId, + timeoutMs = 10000, + }: { + threadId?: string + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( + observable.pipe( + filter(isTrustPingReceivedEvent), + filter((e) => threadId === undefined || e.payload.message.threadId === threadId), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `TrustPingReceivedEvent event not emitted within specified timeout: ${timeoutMs} + threadId: ${threadId}, +}` + ) + }), + map((e) => e.payload.message) + ) + ) +} + +export async function waitForTrustPingResponseReceivedEvent( + agent: Agent, + options: { + threadId?: string + timeoutMs?: number + } +) { + const observable = agent.events.observable( + TrustPingEventTypes.TrustPingResponseReceivedEvent + ) + + return waitForTrustPingResponseReceivedEventSubject(observable, options) +} + +export function waitForTrustPingResponseReceivedEventSubject( + subject: ReplaySubject | Observable, + { + threadId, + timeoutMs = 10000, + }: { + threadId?: string + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( + observable.pipe( + filter(isTrustPingResponseReceivedEvent), + filter((e) => threadId === undefined || e.payload.message.threadId === threadId), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `TrustPingResponseReceivedEvent event not emitted within specified timeout: ${timeoutMs} + threadId: ${threadId}, +}` + ) + }), + map((e) => e.payload.message) + ) + ) +} + +export async function waitForAgentMessageProcessedEvent( + agent: Agent, + options: { + threadId?: string + messageType?: string + timeoutMs?: number + } +) { + const observable = agent.events.observable(AgentEventTypes.AgentMessageProcessed) + + return waitForAgentMessageProcessedEventSubject(observable, options) +} + +export function waitForAgentMessageProcessedEventSubject( + subject: ReplaySubject | Observable, + { + threadId, + timeoutMs = 10000, + messageType, + }: { + threadId?: string + messageType?: string + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( + observable.pipe( + filter(isAgentMessageProcessedEvent), + filter((e) => threadId === undefined || e.payload.message.threadId === threadId), + filter((e) => messageType === undefined || e.payload.message.type === messageType), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `AgentMessageProcessedEvent event not emitted within specified timeout: ${timeoutMs} + threadId: ${threadId}, messageType: ${messageType} +}` + ) + }), + map((e) => e.payload.message) + ) + ) +} + +export function waitForCredentialRecordSubject( + subject: ReplaySubject | Observable, + { + threadId, + state, + previousState, + timeoutMs = 15000, // sign and store credential in W3c credential protocols take several seconds + }: { + threadId?: string + state?: CredentialState + previousState?: CredentialState | null + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + + return firstValueFrom( + observable.pipe( + filter(isCredentialStateChangedEvent), + filter((e) => previousState === undefined || e.payload.previousState === previousState), + filter((e) => threadId === undefined || e.payload.credentialRecord.threadId === threadId), + filter((e) => state === undefined || e.payload.credentialRecord.state === state), + timeout(timeoutMs), + catchError(() => { + throw new Error(`CredentialStateChanged event not emitted within specified timeout: { + previousState: ${previousState}, + threadId: ${threadId}, + state: ${state} +}`) + }), + map((e) => e.payload.credentialRecord) + ) + ) +} + +export async function waitForCredentialRecord( + agent: Agent, + options: { + threadId?: string + state?: CredentialState + previousState?: CredentialState | null + timeoutMs?: number + } +) { + const observable = agent.events.observable(CredentialEventTypes.CredentialStateChanged) + return waitForCredentialRecordSubject(observable, options) +} + +export function waitForDidRotateSubject( + subject: ReplaySubject | Observable, + { + threadId, + state, + timeoutMs = 15000, // sign and store credential in W3c credential protocols take several seconds + }: { + threadId?: string + state?: DidExchangeState + previousState?: DidExchangeState | null + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + + return firstValueFrom( + observable.pipe( + filter(isConnectionDidRotatedEvent), + filter((e) => threadId === undefined || e.payload.connectionRecord.threadId === threadId), + filter((e) => state === undefined || e.payload.connectionRecord.state === state), + timeout(timeoutMs), + catchError(() => { + throw new Error(`ConnectionDidRotated event not emitted within specified timeout: { + threadId: ${threadId}, + state: ${state} +}`) + }), + map((e) => e.payload) + ) + ) +} + +export function waitForConnectionRecordSubject( + subject: ReplaySubject | Observable, + { + threadId, + state, + previousState, + timeoutMs = 15000, // sign and store credential in W3c credential protocols take several seconds + }: { + threadId?: string + state?: DidExchangeState + previousState?: DidExchangeState | null + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + + return firstValueFrom( + observable.pipe( + filter(isConnectionStateChangedEvent), + filter((e) => previousState === undefined || e.payload.previousState === previousState), + filter((e) => threadId === undefined || e.payload.connectionRecord.threadId === threadId), + filter((e) => state === undefined || e.payload.connectionRecord.state === state), + timeout(timeoutMs), + catchError(() => { + throw new Error(`ConnectionStateChanged event not emitted within specified timeout: { + previousState: ${previousState}, + threadId: ${threadId}, + state: ${state} +}`) + }), + map((e) => e.payload.connectionRecord) + ) + ) +} + +export async function waitForConnectionRecord( + agent: Agent, + options: { + threadId?: string + state?: DidExchangeState + previousState?: DidExchangeState | null + timeoutMs?: number + } +) { + const observable = agent.events.observable(ConnectionEventTypes.ConnectionStateChanged) + return waitForConnectionRecordSubject(observable, options) +} + +export async function waitForDidRotate( + agent: Agent, + options: { + threadId?: string + state?: DidExchangeState + timeoutMs?: number + } +) { + const observable = agent.events.observable(ConnectionEventTypes.ConnectionDidRotated) + return waitForDidRotateSubject(observable, options) +} + +export async function waitForBasicMessage( + agent: Agent, + { content, connectionId }: { content?: string; connectionId?: string } +): Promise { + return new Promise((resolve) => { + const listener = (event: BasicMessageStateChangedEvent) => { + const contentMatches = content === undefined || event.payload.message.content === content + const connectionIdMatches = + connectionId === undefined || event.payload.basicMessageRecord.connectionId === connectionId + + if (contentMatches && connectionIdMatches) { + agent.events.off(BasicMessageEventTypes.BasicMessageStateChanged, listener) + + resolve(event.payload.message) + } + } + + agent.events.on(BasicMessageEventTypes.BasicMessageStateChanged, listener) + }) +} + +export async function waitForRevocationNotification( + agent: Agent, + options: { + threadId?: string + timeoutMs?: number + } +) { + const observable = agent.events.observable( + CredentialEventTypes.RevocationNotificationReceived + ) + + return waitForRevocationNotificationSubject(observable, options) +} + +export function waitForRevocationNotificationSubject( + subject: ReplaySubject | Observable, + { + threadId, + timeoutMs = 10000, + }: { + threadId?: string + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( + observable.pipe( + filter((e) => threadId === undefined || e.payload.credentialRecord.threadId === threadId), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `RevocationNotificationReceivedEvent event not emitted within specified timeout: { + threadId: ${threadId}, + }` + ) + }), + map((e) => e.payload.credentialRecord) + ) + ) +} + +export function getMockConnection({ + state = DidExchangeState.InvitationReceived, + role = DidExchangeRole.Requester, + id = 'test', + did = 'test-did', + threadId = 'threadId', + tags = {}, + theirLabel, + theirDid = 'their-did', +}: Partial = {}) { + return new ConnectionRecord({ + did, + threadId, + theirDid, + id, + role, + state, + tags, + theirLabel, + }) +} + +export function getMockOutOfBand({ + label, + serviceEndpoint, + recipientKeys = [ + new DidKey(Key.fromPublicKeyBase58('ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7', KeyType.Ed25519)).did, + ], + mediatorId, + role, + state, + reusable, + reuseConnectionId, + imageUrl, +}: { + label?: string + serviceEndpoint?: string + mediatorId?: string + recipientKeys?: string[] + role?: OutOfBandRole + state?: OutOfBandState + reusable?: boolean + reuseConnectionId?: string + imageUrl?: string +} = {}) { + const options = { + label: label ?? 'label', + imageUrl: imageUrl ?? undefined, + accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], + handshakeProtocols: [HandshakeProtocol.DidExchange], + services: [ + new OutOfBandDidCommService({ + id: `#inline-0`, + serviceEndpoint: serviceEndpoint ?? 'http://example.com', + recipientKeys, + routingKeys: [], + }), + ], + } + const outOfBandInvitation = new OutOfBandInvitation(options) + const outOfBandRecord = new OutOfBandRecord({ + mediatorId, + role: role || OutOfBandRole.Receiver, + state: state || OutOfBandState.Initial, + outOfBandInvitation: outOfBandInvitation, + reusable, + reuseConnectionId, + tags: { + recipientKeyFingerprints: recipientKeys.map((didKey) => DidKey.fromDid(didKey).key.fingerprint), + }, + }) + return outOfBandRecord +} + +export async function makeConnection(agentA: Agent, agentB: Agent) { + const agentAOutOfBand = await agentA.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + let { connectionRecord: agentBConnection } = await agentB.oob.receiveInvitation(agentAOutOfBand.outOfBandInvitation) + + agentBConnection = await agentB.connections.returnWhenIsConnected(agentBConnection!.id) + let [agentAConnection] = await agentA.connections.findAllByOutOfBandId(agentAOutOfBand.id) + agentAConnection = await agentA.connections.returnWhenIsConnected(agentAConnection!.id) + + return [agentAConnection, agentBConnection] +} + +/** + * Returns mock of function with correct type annotations according to original function `fn`. + * It can be used also for class methods. + * + * @param fn function you want to mock + * @returns mock function with type annotations + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function mockFunction any>(fn: T): jest.MockedFunction { + return fn as jest.MockedFunction +} + +/** + * Set a property using a getter value on a mocked oject. + */ +// eslint-disable-next-line @typescript-eslint/ban-types +export function mockProperty(object: T, property: K, value: T[K]) { + Object.defineProperty(object, property, { get: () => value }) +} + +export async function retryUntilResult Promise>( + method: M, + { + intervalMs = 500, + delay = 1000, + maxAttempts = 5, + }: { + intervalMs?: number + delay?: number + maxAttempts?: number + } = {} +): Promise { + await sleep(delay) + + for (let i = 0; i < maxAttempts; i++) { + const result = await method() + if (result) return result + await sleep(intervalMs) + } + + throw new Error(`Unable to get result from method in ${maxAttempts} attempts`) +} + +export type CreateDidKidVerificationMethodReturn = Awaited> +export async function createDidKidVerificationMethod(agentContext: AgentContext, secretKey?: string) { + const dids = agentContext.dependencyManager.resolve(DidsApi) + const didCreateResult = await dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: secretKey ? TypedArrayEncoder.fromString(secretKey) : undefined }, + }) + + const did = didCreateResult.didState.did as string + const didKey = DidKey.fromDid(did) + const kid = `${did}#${didKey.key.fingerprint}` + + const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + + return { did, kid, verificationMethod } +} diff --git a/packages/core/tests/index.ts b/packages/core/tests/index.ts new file mode 100644 index 0000000000..4c4cb7819f --- /dev/null +++ b/packages/core/tests/index.ts @@ -0,0 +1,8 @@ +export * from './jsonld' +export * from './transport' +export * from './events' +export * from './helpers' + +import testLogger, { TestLogger } from './logger' + +export { testLogger, TestLogger } diff --git a/packages/core/tests/jsonld.ts b/packages/core/tests/jsonld.ts new file mode 100644 index 0000000000..69ecb70f10 --- /dev/null +++ b/packages/core/tests/jsonld.ts @@ -0,0 +1,183 @@ +import type { EventReplaySubject } from './events' +import type { AutoAcceptCredential, AutoAcceptProof, ConnectionRecord } from '../src' + +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' +import { askarModule } from '../../askar/tests/helpers' +import { BbsModule } from '../../bbs-signatures/src/BbsModule' +import { + DifPresentationExchangeProofFormatService, + V2ProofProtocol, + CacheModule, + CredentialEventTypes, + InMemoryLruCache, + ProofEventTypes, + Agent, + ProofsModule, + CredentialsModule, + JsonLdCredentialFormatService, + V2CredentialProtocol, + W3cCredentialsModule, +} from '../src' +import { customDocumentLoader } from '../src/modules/vc/data-integrity/__tests__/documentLoader' + +import { setupEventReplaySubjects } from './events' +import { getAgentOptions, makeConnection } from './helpers' +import { setupSubjectTransports } from './transport' + +export type JsonLdTestsAgent = Agent> + +export const getJsonLdModules = ({ + autoAcceptCredentials, + autoAcceptProofs, + useBbs = false, +}: { autoAcceptCredentials?: AutoAcceptCredential; autoAcceptProofs?: AutoAcceptProof; useBbs?: boolean } = {}) => + ({ + credentials: new CredentialsModule({ + credentialProtocols: [new V2CredentialProtocol({ credentialFormats: [new JsonLdCredentialFormatService()] })], + autoAcceptCredentials, + }), + w3cCredentials: new W3cCredentialsModule({ + documentLoader: customDocumentLoader, + }), + proofs: new ProofsModule({ + autoAcceptProofs, + proofProtocols: [new V2ProofProtocol({ proofFormats: [new DifPresentationExchangeProofFormatService()] })], + }), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 100 }), + }), + // We don't support signing provider in in memory wallet yet, so if BBS is used we need to use Askar + ...(useBbs + ? { + askar: askarModule, + bbs: new BbsModule(), + } + : { + inMemory: new InMemoryWalletModule(), + }), + } as const) + +interface SetupJsonLdTestsReturn { + issuerAgent: JsonLdTestsAgent + issuerReplay: EventReplaySubject + + holderAgent: JsonLdTestsAgent + holderReplay: EventReplaySubject + + issuerHolderConnectionId: CreateConnections extends true ? string : undefined + holderIssuerConnectionId: CreateConnections extends true ? string : undefined + + verifierHolderConnectionId: CreateConnections extends true + ? VerifierName extends string + ? string + : undefined + : undefined + holderVerifierConnectionId: CreateConnections extends true + ? VerifierName extends string + ? string + : undefined + : undefined + + verifierAgent: VerifierName extends string ? JsonLdTestsAgent : undefined + verifierReplay: VerifierName extends string ? EventReplaySubject : undefined + + credentialDefinitionId: string +} + +export async function setupJsonLdTests< + VerifierName extends string | undefined = undefined, + CreateConnections extends boolean = true +>({ + issuerName, + holderName, + verifierName, + autoAcceptCredentials, + autoAcceptProofs, + createConnections, + useBbs = false, +}: { + issuerName: string + holderName: string + verifierName?: VerifierName + autoAcceptCredentials?: AutoAcceptCredential + autoAcceptProofs?: AutoAcceptProof + createConnections?: CreateConnections + useBbs?: boolean +}): Promise> { + const modules = getJsonLdModules({ + autoAcceptCredentials, + autoAcceptProofs, + useBbs, + }) + + const issuerAgent = new Agent( + getAgentOptions( + issuerName, + { + endpoints: ['rxjs:issuer'], + }, + modules + ) + ) + + const holderAgent = new Agent( + getAgentOptions( + holderName, + { + endpoints: ['rxjs:holder'], + }, + modules + ) + ) + + const verifierAgent = verifierName + ? new Agent( + getAgentOptions( + verifierName, + { + endpoints: ['rxjs:verifier'], + }, + modules + ) + ) + : undefined + + setupSubjectTransports(verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent]) + const [issuerReplay, holderReplay, verifierReplay] = setupEventReplaySubjects( + verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent], + [CredentialEventTypes.CredentialStateChanged, ProofEventTypes.ProofStateChanged] + ) + + await issuerAgent.initialize() + await holderAgent.initialize() + if (verifierAgent) await verifierAgent.initialize() + + let issuerHolderConnection: ConnectionRecord | undefined + let holderIssuerConnection: ConnectionRecord | undefined + let verifierHolderConnection: ConnectionRecord | undefined + let holderVerifierConnection: ConnectionRecord | undefined + + if (createConnections ?? true) { + ;[issuerHolderConnection, holderIssuerConnection] = await makeConnection(issuerAgent, holderAgent) + + if (verifierAgent) { + ;[holderVerifierConnection, verifierHolderConnection] = await makeConnection(holderAgent, verifierAgent) + } + } + + return { + issuerAgent, + issuerReplay, + + holderAgent, + holderReplay, + + verifierAgent: verifierName ? verifierAgent : undefined, + verifierReplay: verifierName ? verifierReplay : undefined, + + issuerHolderConnectionId: issuerHolderConnection?.id, + holderIssuerConnectionId: holderIssuerConnection?.id, + holderVerifierConnectionId: holderVerifierConnection?.id, + verifierHolderConnectionId: verifierHolderConnection?.id, + } as unknown as SetupJsonLdTestsReturn +} diff --git a/packages/core/tests/logger.ts b/packages/core/tests/logger.ts new file mode 100644 index 0000000000..a04f57ad57 --- /dev/null +++ b/packages/core/tests/logger.ts @@ -0,0 +1,105 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { ILogObj } from 'tslog' + +import { appendFileSync } from 'fs' +import { Logger } from 'tslog' + +import { LogLevel } from '../src/logger' +import { BaseLogger } from '../src/logger/BaseLogger' +import { replaceError } from '../src/logger/replaceError' + +function logToTransport(logObject: ILogObj) { + appendFileSync('logs.txt', JSON.stringify(logObject) + '\n') +} + +export class TestLogger extends BaseLogger { + public readonly logger: Logger + + // Map our log levels to tslog levels + private tsLogLevelStringMap = { + [LogLevel.test]: 'silly', + [LogLevel.trace]: 'trace', + [LogLevel.debug]: 'debug', + [LogLevel.info]: 'info', + [LogLevel.warn]: 'warn', + [LogLevel.error]: 'error', + [LogLevel.fatal]: 'fatal', + } as const + + // Map our log levels to tslog levels + private tsLogLevelNumberMap = { + [LogLevel.test]: 0, + [LogLevel.trace]: 1, + [LogLevel.debug]: 2, + [LogLevel.info]: 3, + [LogLevel.warn]: 4, + [LogLevel.error]: 5, + [LogLevel.fatal]: 6, + } as const + + public static fromLogger(logger: TestLogger, name?: string) { + return new TestLogger(logger.logLevel, name, logger.logger) + } + + public constructor(logLevel: LogLevel, name?: string, logger?: Logger) { + super(logLevel) + + if (logger) { + this.logger = logger.getSubLogger({ + name, + minLevel: this.logLevel == LogLevel.off ? undefined : this.tsLogLevelNumberMap[this.logLevel], + }) + } else { + this.logger = new Logger({ + name, + minLevel: this.logLevel == LogLevel.off ? undefined : this.tsLogLevelNumberMap[this.logLevel], + attachedTransports: [logToTransport], + }) + } + } + + private log(level: Exclude, message: string, data?: Record): void { + const tsLogLevel = this.tsLogLevelStringMap[level] + + if (this.logLevel === LogLevel.off) return + + if (data) { + this.logger[tsLogLevel](message, JSON.parse(JSON.stringify(data, replaceError, 2))) + } else { + this.logger[tsLogLevel](message) + } + } + + public test(message: string, data?: Record): void { + this.log(LogLevel.test, message, data) + } + + public trace(message: string, data?: Record): void { + this.log(LogLevel.trace, message, data) + } + + public debug(message: string, data?: Record): void { + this.log(LogLevel.debug, message, data) + } + + public info(message: string, data?: Record): void { + this.log(LogLevel.info, message, data) + } + + public warn(message: string, data?: Record): void { + this.log(LogLevel.warn, message, data) + } + + public error(message: string, data?: Record): void { + this.log(LogLevel.error, message, data) + } + + public fatal(message: string, data?: Record): void { + this.log(LogLevel.fatal, message, data) + } +} + +const testLogger = new TestLogger(LogLevel.off) + +export default testLogger diff --git a/packages/core/tests/middleware.test.ts b/packages/core/tests/middleware.test.ts new file mode 100644 index 0000000000..cf76ca8031 --- /dev/null +++ b/packages/core/tests/middleware.test.ts @@ -0,0 +1,121 @@ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' +import type { ConnectionRecord, InboundMessageContext } from '../src' + +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { + TrustPingResponseMessage, + BasicMessage, + getOutboundMessageContext, + MessageSender, + AgentMessage, + JsonTransformer, + Agent, +} from '../src' + +import { + getInMemoryAgentOptions, + makeConnection, + waitForAgentMessageProcessedEvent, + waitForBasicMessage, +} from './helpers' + +const faberConfig = getInMemoryAgentOptions('Faber Message Handler Middleware', { + endpoints: ['rxjs:faber'], +}) + +const aliceConfig = getInMemoryAgentOptions('Alice Message Handler Middleware', { + endpoints: ['rxjs:alice'], +}) + +describe('Message Handler Middleware E2E', () => { + let faberAgent: Agent + let aliceAgent: Agent + let faberConnection: ConnectionRecord + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let aliceConnection: ConnectionRecord + + beforeEach(async () => { + const faberMessages = new Subject() + const aliceMessages = new Subject() + const subjectMap = { + 'rxjs:faber': faberMessages, + 'rxjs:alice': aliceMessages, + } + + faberAgent = new Agent(faberConfig) + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + + aliceAgent = new Agent(aliceConfig) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + ;[aliceConnection, faberConnection] = await makeConnection(aliceAgent, faberAgent) + }) + + afterEach(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Correctly calls the fallback message handler if no message handler is defined', async () => { + // Fallback message handler + aliceAgent.dependencyManager.setFallbackMessageHandler((messageContext) => { + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message: new BasicMessage({ + content: "Hey there, I'm not sure I understand the message you sent to me", + }), + }) + }) + + const message = JsonTransformer.fromJSON( + { + '@type': 'https://credo.js.org/custom-messaging/1.0/say-hello', + '@id': 'b630b69a-2b82-4764-87ba-56aa2febfb97', + }, + AgentMessage + ) + + // Send a custom message + const messageSender = faberAgent.dependencyManager.resolve(MessageSender) + const outboundMessageContext = await getOutboundMessageContext(faberAgent.context, { + connectionRecord: faberConnection, + message, + }) + await messageSender.sendMessage(outboundMessageContext) + + // Expect the basic message sent by the fallback message handler + await waitForBasicMessage(faberAgent, { + content: "Hey there, I'm not sure I understand the message you sent to me", + }) + }) + + test('Correctly calls the registered message handler middleware', async () => { + aliceAgent.dependencyManager.registerMessageHandlerMiddleware( + async (inboundMessageContext: InboundMessageContext, next) => { + await next() + + if (inboundMessageContext.responseMessage) { + inboundMessageContext.responseMessage.message.setTiming({ + outTime: new Date('2021-01-01'), + }) + } + } + ) + + await faberAgent.connections.sendPing(faberConnection.id, {}) + const receiveMessage = await waitForAgentMessageProcessedEvent(faberAgent, { + messageType: TrustPingResponseMessage.type.messageTypeUri, + }) + + // Should have sent the message with the timing added in the middleware + expect(receiveMessage.timing?.outTime).toEqual(new Date('2021-01-01')) + }) +}) diff --git a/packages/core/tests/migration.test.ts b/packages/core/tests/migration.test.ts new file mode 100644 index 0000000000..bc39a9382a --- /dev/null +++ b/packages/core/tests/migration.test.ts @@ -0,0 +1,61 @@ +import type { VersionString } from '../src/utils/version' + +import { askarModule } from '../../askar/tests/helpers' +import { Agent } from '../src/agent/Agent' +import { UpdateAssistant } from '../src/storage/migration/UpdateAssistant' + +import { getAgentOptions } from './helpers' + +const agentOptions = getAgentOptions('Migration', {}, { askar: askarModule }) + +describe('migration', () => { + test('manually initiating the update assistant to perform an update', async () => { + const agent = new Agent(agentOptions) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { mediationRoleUpdateStrategy: 'allMediator' }, + }) + await updateAssistant.initialize() + + if (!(await updateAssistant.isUpToDate())) { + await updateAssistant.update() + } + + await agent.initialize() + + await agent.shutdown() + await agent.wallet.delete() + }) + + test('manually initiating the update, but storing the current framework version outside of the agent storage', async () => { + // The storage version will normally be stored in e.g. persistent storage on a mobile device + let currentStorageVersion: VersionString = '0.1' + + const agent = new Agent(agentOptions) + + if (currentStorageVersion !== UpdateAssistant.frameworkStorageVersion) { + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { mediationRoleUpdateStrategy: 'recipientIfEndpoint' }, + }) + await updateAssistant.initialize() + await updateAssistant.update() + + // Store the version so we can leverage it during the next agent startup and don't have + // to initialize the update assistant again until a new version is released + currentStorageVersion = UpdateAssistant.frameworkStorageVersion + } + + await agent.initialize() + + await agent.shutdown() + await agent.wallet.delete() + }) + + test('Automatic update on agent startup', async () => { + const agent = new Agent({ ...agentOptions, config: { ...agentOptions.config, autoUpdateStorageOnStartup: true } }) + + await agent.initialize() + await agent.shutdown() + await agent.wallet.delete() + }) +}) diff --git a/packages/core/tests/mocks/MockWallet.ts b/packages/core/tests/mocks/MockWallet.ts new file mode 100644 index 0000000000..ff3e3b98fd --- /dev/null +++ b/packages/core/tests/mocks/MockWallet.ts @@ -0,0 +1,75 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { Wallet } from '../../src' +import type { Key } from '../../src/crypto' +import type { EncryptedMessage, WalletConfig, WalletExportImportConfig, WalletConfigRekey } from '../../src/types' +import type { Buffer } from '../../src/utils/buffer' +import type { + UnpackedMessageContext, + WalletCreateKeyOptions, + WalletSignOptions, + WalletVerifyOptions, +} from '../../src/wallet' + +export class MockWallet implements Wallet { + public isInitialized = true + public isProvisioned = true + + public supportedKeyTypes = [] + + public create(walletConfig: WalletConfig): Promise { + throw new Error('Method not implemented.') + } + public createAndOpen(walletConfig: WalletConfig): Promise { + throw new Error('Method not implemented.') + } + public open(walletConfig: WalletConfig): Promise { + throw new Error('Method not implemented.') + } + public rotateKey(walletConfig: WalletConfigRekey): Promise { + throw new Error('Method not implemented.') + } + public close(): Promise { + throw new Error('Method not implemented.') + } + public delete(): Promise { + throw new Error('Method not implemented.') + } + public export(exportConfig: WalletExportImportConfig): Promise { + throw new Error('Method not implemented.') + } + public import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig): Promise { + throw new Error('Method not implemented.') + } + public pack( + payload: Record, + recipientKeys: string[], + senderVerkey?: string + ): Promise { + throw new Error('Method not implemented.') + } + public unpack(encryptedMessage: EncryptedMessage): Promise { + throw new Error('Method not implemented.') + } + public sign(options: WalletSignOptions): Promise { + throw new Error('Method not implemented.') + } + public verify(options: WalletVerifyOptions): Promise { + throw new Error('Method not implemented.') + } + + public createKey(options: WalletCreateKeyOptions): Promise { + throw new Error('Method not implemented.') + } + + public generateNonce(): Promise { + throw new Error('Method not implemented.') + } + + public generateWalletKey(): Promise { + throw new Error('Method not implemented.') + } + + public dispose() { + // Nothing to do here + } +} diff --git a/packages/core/tests/mocks/index.ts b/packages/core/tests/mocks/index.ts new file mode 100644 index 0000000000..3dbf2226a2 --- /dev/null +++ b/packages/core/tests/mocks/index.ts @@ -0,0 +1 @@ +export * from './MockWallet' diff --git a/packages/core/tests/multi-protocol-version.test.ts b/packages/core/tests/multi-protocol-version.test.ts new file mode 100644 index 0000000000..1f9fa3c915 --- /dev/null +++ b/packages/core/tests/multi-protocol-version.test.ts @@ -0,0 +1,128 @@ +import type { AgentMessageProcessedEvent } from '../src/agent/Events' + +import { filter, firstValueFrom, timeout } from 'rxjs' + +import { parseMessageType, MessageSender, AgentMessage, IsValidMessageType } from '../src' +import { Agent } from '../src/agent/Agent' +import { AgentEventTypes } from '../src/agent/Events' +import { OutboundMessageContext } from '../src/agent/models' + +import { getInMemoryAgentOptions } from './helpers' +import { setupSubjectTransports } from './transport' + +const aliceAgentOptions = getInMemoryAgentOptions('Multi Protocol Versions - Alice', { + endpoints: ['rxjs:alice'], +}) +const bobAgentOptions = getInMemoryAgentOptions('Multi Protocol Versions - Bob', { + endpoints: ['rxjs:bob'], +}) + +describe('multi version protocols', () => { + let aliceAgent: Agent + let bobAgent: Agent + + afterAll(async () => { + await bobAgent.shutdown() + await bobAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('should successfully handle a message with a lower minor version than the currently supported version', async () => { + bobAgent = new Agent(bobAgentOptions) + aliceAgent = new Agent(aliceAgentOptions) + setupSubjectTransports([aliceAgent, bobAgent]) + + // Register the test handler with the v1.3 version of the message + const mockHandle = jest.fn() + aliceAgent.dependencyManager.registerMessageHandlers([{ supportedMessages: [TestMessageV13], handle: mockHandle }]) + + await aliceAgent.initialize() + await bobAgent.initialize() + + const { outOfBandInvitation, id } = await aliceAgent.oob.createInvitation() + let { connectionRecord: bobConnection } = await bobAgent.oob.receiveInvitation(outOfBandInvitation, { + autoAcceptConnection: true, + autoAcceptInvitation: true, + }) + + if (!bobConnection) { + throw new Error('No connection for bob') + } + + bobConnection = await bobAgent.connections.returnWhenIsConnected(bobConnection.id) + + let [aliceConnection] = await aliceAgent.connections.findAllByOutOfBandId(id) + aliceConnection = await aliceAgent.connections.returnWhenIsConnected(aliceConnection.id) + + expect(aliceConnection).toBeConnectedWith(bobConnection) + expect(bobConnection).toBeConnectedWith(aliceConnection) + + const bobMessageSender = bobAgent.dependencyManager.resolve(MessageSender) + + // Start event listener for message processed + const agentMessageV11ProcessedPromise = firstValueFrom( + aliceAgent.events.observable(AgentEventTypes.AgentMessageProcessed).pipe( + filter((event) => event.payload.message.type === TestMessageV11.type.messageTypeUri), + timeout(8000) + ) + ) + + await bobMessageSender.sendMessage( + new OutboundMessageContext(new TestMessageV11(), { agentContext: bobAgent.context, connection: bobConnection }) + ) + + // Wait for the agent message processed event to be called + await agentMessageV11ProcessedPromise + + expect(mockHandle).toHaveBeenCalledTimes(1) + + // Start event listener for message processed + const agentMessageV15ProcessedPromise = firstValueFrom( + aliceAgent.events.observable(AgentEventTypes.AgentMessageProcessed).pipe( + filter((event) => event.payload.message.type === TestMessageV15.type.messageTypeUri), + timeout(8000) + ) + ) + + await bobMessageSender.sendMessage( + new OutboundMessageContext(new TestMessageV15(), { agentContext: bobAgent.context, connection: bobConnection }) + ) + await agentMessageV15ProcessedPromise + + expect(mockHandle).toHaveBeenCalledTimes(2) + }) +}) + +class TestMessageV11 extends AgentMessage { + public constructor() { + super() + this.id = this.generateId() + } + + @IsValidMessageType(TestMessageV11.type) + public readonly type = TestMessageV11.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/custom-protocol/1.1/test-message') +} + +class TestMessageV13 extends AgentMessage { + public constructor() { + super() + this.id = this.generateId() + } + + @IsValidMessageType(TestMessageV13.type) + public readonly type = TestMessageV13.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/custom-protocol/1.3/test-message') +} + +class TestMessageV15 extends AgentMessage { + public constructor() { + super() + this.id = this.generateId() + } + + @IsValidMessageType(TestMessageV15.type) + public readonly type = TestMessageV15.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/custom-protocol/1.5/test-message') +} diff --git a/packages/core/tests/oob-mediation-provision.test.ts b/packages/core/tests/oob-mediation-provision.test.ts new file mode 100644 index 0000000000..3bf27ba9eb --- /dev/null +++ b/packages/core/tests/oob-mediation-provision.test.ts @@ -0,0 +1,119 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { OutOfBandInvitation } from '../src/modules/oob/messages' + +import { Agent } from '../src/agent/Agent' +import { DidExchangeState, HandshakeProtocol } from '../src/modules/connections' +import { + MediationState, + MediatorModule, + MediatorPickupStrategy, + MediationRecipientModule, +} from '../src/modules/routing' + +import { getInMemoryAgentOptions, waitForBasicMessage } from './helpers' +import { setupSubjectTransports } from './transport' + +const faberAgentOptions = getInMemoryAgentOptions('OOB mediation provision - Faber Agent', { + endpoints: ['rxjs:faber'], +}) +const aliceAgentOptions = getInMemoryAgentOptions( + 'OOB mediation provision - Alice Recipient Agent', + { + endpoints: ['rxjs:alice'], + }, + { + mediationRecipient: new MediationRecipientModule({ + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + } +) +const mediatorAgentOptions = getInMemoryAgentOptions( + 'OOB mediation provision - Mediator Agent', + { + endpoints: ['rxjs:mediator'], + }, + { mediator: new MediatorModule({ autoAcceptMediationRequests: true }) } +) + +describe('out of band with mediation set up with provision method', () => { + const makeConnectionConfig = { + goal: 'To make a connection', + goalCode: 'p2p-messaging', + label: 'Faber College', + handshake: true, + multiUseInvitation: false, + } + + let faberAgent: Agent + let aliceAgent: Agent + let mediatorAgent: Agent + + let mediatorOutOfBandInvitation: OutOfBandInvitation + + beforeAll(async () => { + mediatorAgent = new Agent(mediatorAgentOptions) + aliceAgent = new Agent(aliceAgentOptions) + faberAgent = new Agent(faberAgentOptions) + + setupSubjectTransports([mediatorAgent, aliceAgent, faberAgent]) + + await mediatorAgent.initialize() + await aliceAgent.initialize() + await faberAgent.initialize() + + const mediationOutOfBandRecord = await mediatorAgent.oob.createInvitation(makeConnectionConfig) + mediatorOutOfBandInvitation = mediationOutOfBandRecord.outOfBandInvitation + + let { connectionRecord } = await aliceAgent.oob.receiveInvitation(mediatorOutOfBandInvitation) + connectionRecord = await aliceAgent.connections.returnWhenIsConnected(connectionRecord!.id) + await aliceAgent.mediationRecipient.provision(connectionRecord!) + await aliceAgent.mediationRecipient.initialize() + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + }) + + test(`make a connection with ${HandshakeProtocol.DidExchange} on OOB invitation encoded in URL`, async () => { + // Check if mediation between Alice and Mediator has been set + const defaultMediator = await aliceAgent.mediationRecipient.findDefaultMediator() + expect(defaultMediator).not.toBeNull() + expect(defaultMediator?.state).toBe(MediationState.Granted) + + // Make a connection between Alice and Faber + const outOfBandRecord = await faberAgent.oob.createInvitation(makeConnectionConfig) + const { outOfBandInvitation } = outOfBandRecord + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitationFromUrl(urlMessage) + + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + + await aliceAgent.basicMessages.sendMessage(aliceFaberConnection.id, 'hello') + const basicMessage = await waitForBasicMessage(faberAgent, {}) + + expect(basicMessage.content).toBe('hello') + + // Test if we can call provision for the same out-of-band record, respectively connection + const reusedOutOfBandRecord = await aliceAgent.oob.findByReceivedInvitationId(mediatorOutOfBandInvitation.id) + const [reusedAliceMediatorConnection] = reusedOutOfBandRecord + ? await aliceAgent.connections.findAllByOutOfBandId(reusedOutOfBandRecord.id) + : [] + await aliceAgent.mediationRecipient.provision(reusedAliceMediatorConnection!) + const mediators = await aliceAgent.mediationRecipient.getMediators() + expect(mediators).toHaveLength(1) + }) +}) diff --git a/packages/core/tests/oob-mediation.test.ts b/packages/core/tests/oob-mediation.test.ts new file mode 100644 index 0000000000..3372ccc8cf --- /dev/null +++ b/packages/core/tests/oob-mediation.test.ts @@ -0,0 +1,206 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' +import type { AgentMessageProcessedEvent } from '../src/agent/Events' +import type { OutOfBandDidCommService } from '../src/modules/oob' + +import { filter, firstValueFrom, map, Subject, timeout } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { Agent } from '../src/agent/Agent' +import { AgentEventTypes } from '../src/agent/Events' +import { DidExchangeState, HandshakeProtocol } from '../src/modules/connections' +import { ConnectionType } from '../src/modules/connections/models/ConnectionType' +import { didKeyToVerkey } from '../src/modules/dids/helpers' +import { + KeylistUpdateMessage, + KeylistUpdateAction, + MediationState, + MediatorPickupStrategy, + MediationRecipientModule, + MediatorModule, +} from '../src/modules/routing' + +import { getInMemoryAgentOptions, waitForBasicMessage } from './helpers' + +const faberAgentOptions = getInMemoryAgentOptions('OOB mediation - Faber Agent', { + endpoints: ['rxjs:faber'], +}) +const aliceAgentOptions = getInMemoryAgentOptions( + 'OOB mediation - Alice Recipient Agent', + { + endpoints: ['rxjs:alice'], + }, + { + mediationRecipient: new MediationRecipientModule({ + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + } +) +const mediatorAgentOptions = getInMemoryAgentOptions( + 'OOB mediation - Mediator Agent', + { + endpoints: ['rxjs:mediator'], + }, + { mediator: new MediatorModule({ autoAcceptMediationRequests: true }) } +) + +describe('out of band with mediation', () => { + const makeConnectionConfig = { + goal: 'To make a connection', + goalCode: 'p2p-messaging', + label: 'Faber College', + handshake: true, + multiUseInvitation: false, + } + + let faberAgent: Agent + let aliceAgent: Agent + let mediatorAgent: Agent + + beforeAll(async () => { + const faberMessages = new Subject() + const aliceMessages = new Subject() + const mediatorMessages = new Subject() + const subjectMap = { + 'rxjs:faber': faberMessages, + 'rxjs:alice': aliceMessages, + 'rxjs:mediator': mediatorMessages, + } + + faberAgent = new Agent(faberAgentOptions) + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + + aliceAgent = new Agent(aliceAgentOptions) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + mediatorAgent = new Agent(mediatorAgentOptions) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await mediatorAgent.initialize() + + // ========== Make a connection between Alice and Mediator agents ========== + const mediationOutOfBandRecord = await mediatorAgent.oob.createInvitation(makeConnectionConfig) + const { outOfBandInvitation: mediatorOutOfBandInvitation } = mediationOutOfBandRecord + const mediatorUrlMessage = mediatorOutOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + let { connectionRecord: aliceMediatorConnection } = await aliceAgent.oob.receiveInvitationFromUrl( + mediatorUrlMessage + ) + + aliceMediatorConnection = await aliceAgent.connections.returnWhenIsConnected(aliceMediatorConnection!.id) + expect(aliceMediatorConnection.state).toBe(DidExchangeState.Completed) + + // Tag the connection with an initial type + aliceMediatorConnection = await aliceAgent.connections.addConnectionType(aliceMediatorConnection.id, 'initial-type') + + let [mediatorAliceConnection] = await mediatorAgent.connections.findAllByOutOfBandId(mediationOutOfBandRecord.id) + mediatorAliceConnection = await mediatorAgent.connections.returnWhenIsConnected(mediatorAliceConnection!.id) + expect(mediatorAliceConnection.state).toBe(DidExchangeState.Completed) + + // ========== Set mediation between Alice and Mediator agents ========== + let connectionTypes = await aliceAgent.connections.getConnectionTypes(aliceMediatorConnection.id) + expect(connectionTypes).toMatchObject(['initial-type']) + + const mediationRecord = await aliceAgent.mediationRecipient.requestAndAwaitGrant(aliceMediatorConnection) + connectionTypes = await aliceAgent.connections.getConnectionTypes(mediationRecord.connectionId) + expect(connectionTypes.sort()).toMatchObject(['initial-type', ConnectionType.Mediator].sort()) + await aliceAgent.connections.removeConnectionType(mediationRecord.connectionId, 'initial-type') + connectionTypes = await aliceAgent.connections.getConnectionTypes(mediationRecord.connectionId) + expect(connectionTypes).toMatchObject([ConnectionType.Mediator]) + expect(mediationRecord.state).toBe(MediationState.Granted) + + await aliceAgent.mediationRecipient.setDefaultMediator(mediationRecord) + await aliceAgent.mediationRecipient.initiateMessagePickup(mediationRecord) + const defaultMediator = await aliceAgent.mediationRecipient.findDefaultMediator() + expect(defaultMediator?.id).toBe(mediationRecord.id) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + }) + + test(`make a connection with ${HandshakeProtocol.DidExchange} on OOB invitation encoded in URL`, async () => { + // ========== Make a connection between Alice and Faber ========== + const outOfBandRecord = await faberAgent.oob.createInvitation({ multiUseInvitation: false }) + + const { outOfBandInvitation } = outOfBandRecord + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitationFromUrl(urlMessage) + + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + + await aliceAgent.basicMessages.sendMessage(aliceFaberConnection.id, 'hello') + const basicMessage = await waitForBasicMessage(faberAgent, {}) + + expect(basicMessage.content).toBe('hello') + }) + + test(`create and delete OOB invitation when using mediation`, async () => { + // Alice creates an invitation: the key is notified to her mediator + + const keyAddMessagePromise = firstValueFrom( + mediatorAgent.events.observable(AgentEventTypes.AgentMessageProcessed).pipe( + filter((event) => event.payload.message.type === KeylistUpdateMessage.type.messageTypeUri), + map((event) => event.payload.message as KeylistUpdateMessage), + timeout(5000) + ) + ) + + const outOfBandRecord = await aliceAgent.oob.createInvitation({}) + const { outOfBandInvitation } = outOfBandRecord + + const keyAddMessage = await keyAddMessagePromise + + expect(keyAddMessage.updates.length).toEqual(1) + expect( + keyAddMessage.updates.map((update) => ({ + action: update.action, + recipientKey: didKeyToVerkey(update.recipientKey), + }))[0] + ).toEqual({ + action: KeylistUpdateAction.add, + recipientKey: didKeyToVerkey((outOfBandInvitation.getServices()[0] as OutOfBandDidCommService).recipientKeys[0]), + }) + + const keyRemoveMessagePromise = firstValueFrom( + mediatorAgent.events.observable(AgentEventTypes.AgentMessageProcessed).pipe( + filter((event) => event.payload.message.type === KeylistUpdateMessage.type.messageTypeUri), + map((event) => event.payload.message as KeylistUpdateMessage), + timeout(5000) + ) + ) + + await aliceAgent.oob.deleteById(outOfBandRecord.id) + + const keyRemoveMessage = await keyRemoveMessagePromise + expect(keyRemoveMessage.updates.length).toEqual(1) + expect( + keyRemoveMessage.updates.map((update) => ({ + action: update.action, + recipientKey: didKeyToVerkey(update.recipientKey), + }))[0] + ).toEqual({ + action: KeylistUpdateAction.remove, + recipientKey: didKeyToVerkey((outOfBandInvitation.getServices()[0] as OutOfBandDidCommService).recipientKeys[0]), + }) + }) +}) diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts new file mode 100644 index 0000000000..c5fb26d71e --- /dev/null +++ b/packages/core/tests/oob.test.ts @@ -0,0 +1,1048 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' +import type { AnonCredsCredentialFormatService } from '../../anoncreds/src' +import type { CreateCredentialOfferOptions, V2CredentialProtocol } from '../src/modules/credentials' +import type { AgentMessage, AgentMessageReceivedEvent } from '@credo-ts/core' + +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { getAnonCredsIndyModules } from '../../anoncreds/tests/legacyAnonCredsSetup' +import { + anoncredsDefinitionFourAttributesNoRevocation, + storePreCreatedAnonCredsDefinition, +} from '../../anoncreds/tests/preCreatedAnonCredsDefinition' +import { Agent } from '../src/agent/Agent' +import { Key } from '../src/crypto' +import { DidExchangeState, HandshakeProtocol } from '../src/modules/connections' +import { OutOfBandDidCommService } from '../src/modules/oob/domain/OutOfBandDidCommService' +import { OutOfBandEventTypes } from '../src/modules/oob/domain/OutOfBandEvents' +import { OutOfBandRole } from '../src/modules/oob/domain/OutOfBandRole' +import { OutOfBandState } from '../src/modules/oob/domain/OutOfBandState' +import { OutOfBandInvitation } from '../src/modules/oob/messages' +import { JsonEncoder, JsonTransformer } from '../src/utils' + +import { TestMessage } from './TestMessage' +import { getInMemoryAgentOptions, waitForCredentialRecord } from './helpers' + +import { AgentEventTypes, CredoError, AutoAcceptCredential, CredentialState } from '@credo-ts/core' + +const faberAgentOptions = getInMemoryAgentOptions( + 'Faber Agent OOB', + { + endpoints: ['rxjs:faber'], + }, + getAnonCredsIndyModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }) +) +const aliceAgentOptions = getInMemoryAgentOptions( + 'Alice Agent OOB', + { + endpoints: ['rxjs:alice'], + }, + getAnonCredsIndyModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }) +) + +describe('out of band', () => { + const makeConnectionConfig = { + goal: 'To make a connection', + goalCode: 'p2p-messaging', + label: 'Faber College', + alias: `Faber's connection with Alice`, + imageUrl: 'http://faber.image.url', + } + + const issueCredentialConfig = { + goal: 'To issue a credential', + goalCode: 'issue-vc', + label: 'Faber College', + handshake: false, + } + + const receiveInvitationConfig = { + autoAcceptConnection: false, + } + + let faberAgent: Agent> + let aliceAgent: Agent> + let credentialTemplate: CreateCredentialOfferOptions<[V2CredentialProtocol<[AnonCredsCredentialFormatService]>]> + + beforeAll(async () => { + const faberMessages = new Subject() + const aliceMessages = new Subject() + const subjectMap = { + 'rxjs:faber': faberMessages, + 'rxjs:alice': aliceMessages, + } + + faberAgent = new Agent(faberAgentOptions) + + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + + aliceAgent = new Agent(aliceAgentOptions) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + await aliceAgent.modules.anoncreds.createLinkSecret() + + const { credentialDefinitionId } = await storePreCreatedAnonCredsDefinition( + faberAgent, + anoncredsDefinitionFourAttributesNoRevocation + ) + credentialTemplate = { + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + attributes: [ + { + name: 'name', + value: 'name', + }, + { + name: 'age', + value: 'age', + }, + { + name: 'profile_picture', + value: 'profile_picture', + }, + { + name: 'x-ray', + value: 'x-ray', + }, + ], + credentialDefinitionId, + }, + }, + autoAcceptCredential: AutoAcceptCredential.Never, + } + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + afterEach(async () => { + const credentials = await aliceAgent.credentials.getAll() + for (const credential of credentials) { + await aliceAgent.credentials.deleteById(credential.id) + } + + const connections = await faberAgent.connections.getAll() + for (const connection of connections) { + await faberAgent.connections.deleteById(connection.id) + } + + jest.resetAllMocks() + }) + + describe('createInvitation', () => { + test('throw error when there is no handshake or message', async () => { + await expect(faberAgent.oob.createInvitation({ label: 'test-connection', handshake: false })).rejects.toEqual( + new CredoError('One or both of handshake_protocols and requests~attach MUST be included in the message.') + ) + }) + + test('throw error when multiUseInvitation is true and messages are provided', async () => { + await expect( + faberAgent.oob.createInvitation({ + label: 'test-connection', + messages: [{} as AgentMessage], + multiUseInvitation: true, + }) + ).rejects.toEqual(new CredoError("Attribute 'multiUseInvitation' can not be 'true' when 'messages' is defined.")) + }) + + test('handles empty messages array as no messages being passed', async () => { + await expect( + faberAgent.oob.createInvitation({ + messages: [], + handshake: false, + }) + ).rejects.toEqual( + new CredoError('One or both of handshake_protocols and requests~attach MUST be included in the message.') + ) + }) + + test('create OOB record', async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation(makeConnectionConfig) + // expect contains services + + expect(outOfBandRecord.autoAcceptConnection).toBe(true) + expect(outOfBandRecord.role).toBe(OutOfBandRole.Sender) + expect(outOfBandRecord.state).toBe(OutOfBandState.AwaitResponse) + expect(outOfBandRecord.alias).toBe(makeConnectionConfig.alias) + expect(outOfBandRecord.reusable).toBe(false) + expect(outOfBandRecord.outOfBandInvitation.goal).toBe(makeConnectionConfig.goal) + expect(outOfBandRecord.outOfBandInvitation.goalCode).toBe(makeConnectionConfig.goalCode) + expect(outOfBandRecord.outOfBandInvitation.label).toBe(makeConnectionConfig.label) + expect(outOfBandRecord.outOfBandInvitation.imageUrl).toBe(makeConnectionConfig.imageUrl) + }) + + test('create OOB message only with handshake', async () => { + const { outOfBandInvitation } = await faberAgent.oob.createInvitation(makeConnectionConfig) + + // expect supported handshake protocols + expect(outOfBandInvitation.handshakeProtocols).toContain('https://didcomm.org/didexchange/1.1') + expect(outOfBandInvitation.getRequests()).toBeUndefined() + + // expect contains services + const [service] = outOfBandInvitation.getInlineServices() + expect(service).toMatchObject( + new OutOfBandDidCommService({ + id: expect.any(String), + serviceEndpoint: 'rxjs:faber', + recipientKeys: [expect.stringContaining('did:key:')], + routingKeys: [], + }) + ) + }) + + test('create OOB message only with requests', async () => { + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ + label: 'test-connection', + handshake: false, + messages: [message], + }) + + // expect supported handshake protocols + expect(outOfBandInvitation.handshakeProtocols).toBeUndefined() + expect(outOfBandInvitation.getRequests()).toHaveLength(1) + + // expect contains services + const [service] = outOfBandInvitation.getServices() + expect(service).toMatchObject( + new OutOfBandDidCommService({ + id: expect.any(String), + serviceEndpoint: 'rxjs:faber', + recipientKeys: [expect.stringContaining('did:key:')], + routingKeys: [], + }) + ) + }) + + test('create OOB message with both handshake and requests', async () => { + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ + label: 'test-connection', + handshakeProtocols: [HandshakeProtocol.Connections], + messages: [message], + }) + + // expect supported handshake protocols + expect(outOfBandInvitation.handshakeProtocols).toContain('https://didcomm.org/connections/1.0') + expect(outOfBandInvitation.getRequests()).toHaveLength(1) + + // expect contains services + const [service] = outOfBandInvitation.getInlineServices() + expect(service).toMatchObject( + new OutOfBandDidCommService({ + id: expect.any(String), + serviceEndpoint: 'rxjs:faber', + recipientKeys: [expect.stringMatching('did:key:')], + routingKeys: [], + }) + ) + }) + + test('emits OutOfBandStateChanged event', async () => { + const eventListener = jest.fn() + + faberAgent.events.on(OutOfBandEventTypes.OutOfBandStateChanged, eventListener) + const outOfBandRecord = await faberAgent.oob.createInvitation({ + label: 'test-connection', + handshake: true, + }) + + faberAgent.events.off(OutOfBandEventTypes.OutOfBandStateChanged, eventListener) + + expect(eventListener).toHaveBeenCalledWith({ + type: OutOfBandEventTypes.OutOfBandStateChanged, + metadata: { + contextCorrelationId: 'default', + }, + payload: { + outOfBandRecord, + previousState: null, + }, + }) + }) + }) + + describe('receiveInvitation', () => { + test('receive OOB connection invitation', async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation(makeConnectionConfig) + const { outOfBandInvitation } = outOfBandRecord + + const { outOfBandRecord: receivedOutOfBandRecord, connectionRecord } = await aliceAgent.oob.receiveInvitation( + outOfBandInvitation, + { + autoAcceptInvitation: false, + autoAcceptConnection: false, + } + ) + + expect(connectionRecord).not.toBeDefined() + expect(receivedOutOfBandRecord.role).toBe(OutOfBandRole.Receiver) + expect(receivedOutOfBandRecord.state).toBe(OutOfBandState.Initial) + expect(receivedOutOfBandRecord.outOfBandInvitation).toEqual(outOfBandInvitation) + }) + + test(`make a connection with ${HandshakeProtocol.DidExchange} on OOB invitation encoded in URL`, async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation(makeConnectionConfig) + const { outOfBandInvitation } = outOfBandRecord + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + // eslint-disable-next-line prefer-const + let { outOfBandRecord: receivedOutOfBandRecord, connectionRecord: aliceFaberConnection } = + await aliceAgent.oob.receiveInvitationFromUrl(urlMessage) + expect(receivedOutOfBandRecord.state).toBe(OutOfBandState.PrepareResponse) + + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord!.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection?.state).toBe(DidExchangeState.Completed) + + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection!) + expect(aliceFaberConnection.imageUrl).toBe(makeConnectionConfig.imageUrl) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + expect(faberAliceConnection.alias).toBe(makeConnectionConfig.alias) + }) + + test(`make a connection with ${HandshakeProtocol.Connections} based on OOB invitation encoded in URL`, async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation({ + ...makeConnectionConfig, + handshakeProtocols: [HandshakeProtocol.Connections], + }) + const { outOfBandInvitation } = outOfBandRecord + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitationFromUrl(urlMessage) + + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord!.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + expect(faberAliceConnection.alias).toBe(makeConnectionConfig.alias) + }) + + test('make a connection based on old connection invitation encoded in URL', async () => { + const { outOfBandRecord, invitation } = await faberAgent.oob.createLegacyInvitation(makeConnectionConfig) + const urlMessage = invitation.toUrl({ domain: 'http://example.com' }) + + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitationFromUrl(urlMessage) + + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + }) + + test('make a connection based on old connection invitation with multiple endpoints uses first endpoint for invitation', async () => { + const { invitation } = await faberAgent.oob.createLegacyInvitation({ + ...makeConnectionConfig, + routing: { + endpoints: ['https://endpoint-1.com', 'https://endpoint-2.com'], + routingKeys: [Key.fromFingerprint('z6MkiP5ghmdLFh1GyGRQQQLVJhJtjQjTpxUY3AnY3h5gu3BE')], + recipientKey: Key.fromFingerprint('z6MkuXrzmDjBoy7r9LA1Czjv9eQXMGr9gt6JBH8zPUMKkCQH'), + }, + }) + + expect(invitation.serviceEndpoint).toBe('https://endpoint-1.com') + }) + + test('process credential offer requests based on OOB message', async () => { + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ + ...issueCredentialConfig, + messages: [message], + }) + + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + }) + await aliceAgent.oob.receiveInvitationFromUrl(urlMessage, receiveInvitationConfig) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + }) + + test('process credential offer requests with legacy did:sov prefix on message type based on OOB message', async () => { + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + + // we need to override the message type to use the legacy did:sov prefix + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + message.type = message.type.replace('https://didcomm.org', 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec') + const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ + ...issueCredentialConfig, + messages: [message], + }) + + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + }) + await aliceAgent.oob.receiveInvitationFromUrl(urlMessage, receiveInvitationConfig) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + }) + + test('do not process requests when a connection is not ready', async () => { + const eventListener = jest.fn() + aliceAgent.events.on(AgentEventTypes.AgentMessageReceived, eventListener) + + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ + ...makeConnectionConfig, + messages: [message], + }) + + // First, we crate a connection but we won't accept it, therefore it won't be ready + await aliceAgent.oob.receiveInvitation(outOfBandInvitation, { autoAcceptConnection: false }) + + // Event should not be emitted because an agent must wait until the connection is ready + expect(eventListener).toHaveBeenCalledTimes(0) + + aliceAgent.events.off(AgentEventTypes.AgentMessageReceived, eventListener) + }) + + test('make a connection based on OOB invitation and process requests after the acceptation', async () => { + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + const outOfBandRecord = await faberAgent.oob.createInvitation({ + ...makeConnectionConfig, + messages: [message], + }) + const { outOfBandInvitation } = outOfBandRecord + + // First, we crate a connection but we won't accept it, therefore it won't be ready + const { outOfBandRecord: aliceFaberOutOfBandRecord } = await aliceAgent.oob.receiveInvitation( + outOfBandInvitation, + { + autoAcceptInvitation: false, + autoAcceptConnection: false, + } + ) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + // We need to create the connection beforehand so it can take a while to complete + timeoutMs: 20000, + }) + + // Accept connection invitation + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.acceptInvitation( + aliceFaberOutOfBandRecord.id, + { + label: 'alice', + autoAcceptConnection: true, + } + ) + + // Wait until connection is ready + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord!.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + }) + + test('do not create a new connection when no messages and handshake reuse succeeds', async () => { + const aliceReuseListener = jest.fn() + const faberReuseListener = jest.fn() + + // Create first connection + const outOfBandRecord = await faberAgent.oob.createInvitation(makeConnectionConfig) + let { connectionRecord: firstAliceFaberConnection } = await aliceAgent.oob.receiveInvitation( + outOfBandRecord.outOfBandInvitation + ) + firstAliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(firstAliceFaberConnection!.id) + + const [firstFaberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + + // Create second connection + const outOfBandRecord2 = await faberAgent.oob.createInvitation(makeConnectionConfig) + + // Take over the recipientKeys from the first invitation so they match when encoded + const [firstInvitationService] = outOfBandRecord.outOfBandInvitation.getInlineServices() + const [secondInvitationService] = outOfBandRecord2.outOfBandInvitation.getInlineServices() + secondInvitationService.recipientKeys = firstInvitationService.recipientKeys + + aliceAgent.events.on(OutOfBandEventTypes.HandshakeReused, aliceReuseListener) + faberAgent.events.on(OutOfBandEventTypes.HandshakeReused, faberReuseListener) + + const { + connectionRecord: secondAliceFaberConnection, + outOfBandRecord: { id: secondOobRecordId }, + } = await aliceAgent.oob.receiveInvitation(outOfBandRecord2.outOfBandInvitation, { reuseConnection: true }) + + aliceAgent.events.off(OutOfBandEventTypes.HandshakeReused, aliceReuseListener) + faberAgent.events.off(OutOfBandEventTypes.HandshakeReused, faberReuseListener) + await aliceAgent.connections.returnWhenIsConnected(secondAliceFaberConnection!.id) + + // There shouldn't be any connection records for this oob id, as we reused an existing one + expect((await faberAgent.connections.findAllByOutOfBandId(secondOobRecordId)).length).toBe(0) + + expect(firstAliceFaberConnection.id).toEqual(secondAliceFaberConnection?.id) + + expect(faberReuseListener).toHaveBeenCalledTimes(1) + expect(aliceReuseListener).toHaveBeenCalledTimes(1) + const [[faberEvent]] = faberReuseListener.mock.calls + const [[aliceEvent]] = aliceReuseListener.mock.calls + + const reuseThreadId = faberEvent.payload.reuseThreadId + + expect(faberEvent).toMatchObject({ + type: OutOfBandEventTypes.HandshakeReused, + payload: { + connectionRecord: { + id: firstFaberAliceConnection.id, + }, + outOfBandRecord: { + id: outOfBandRecord2.id, + }, + reuseThreadId, + }, + }) + + expect(aliceEvent).toMatchObject({ + type: OutOfBandEventTypes.HandshakeReused, + payload: { + connectionRecord: { + id: firstAliceFaberConnection.id, + }, + outOfBandRecord: { + id: secondOobRecordId, + }, + reuseThreadId, + }, + }) + }) + + test('create a new connection when connection exists and reuse is false', async () => { + const reuseListener = jest.fn() + + // Create first connection + const outOfBandRecord = await faberAgent.oob.createInvitation(makeConnectionConfig) + let { connectionRecord: firstAliceFaberConnection } = await aliceAgent.oob.receiveInvitation( + outOfBandRecord.outOfBandInvitation + ) + firstAliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(firstAliceFaberConnection!.id) + + // Create second connection + const outOfBandRecord2 = await faberAgent.oob.createInvitation(makeConnectionConfig) + + aliceAgent.events.on(OutOfBandEventTypes.HandshakeReused, reuseListener) + faberAgent.events.on(OutOfBandEventTypes.HandshakeReused, reuseListener) + + const { connectionRecord: secondAliceFaberConnection } = await aliceAgent.oob.receiveInvitation( + outOfBandRecord2.outOfBandInvitation, + { reuseConnection: false } + ) + + aliceAgent.events.off(OutOfBandEventTypes.HandshakeReused, reuseListener) + faberAgent.events.off(OutOfBandEventTypes.HandshakeReused, reuseListener) + await aliceAgent.connections.returnWhenIsConnected(secondAliceFaberConnection!.id) + + // If we're not reusing the connection, the reuse listener shouldn't be called + expect(reuseListener).not.toHaveBeenCalled() + expect(firstAliceFaberConnection.id).not.toEqual(secondAliceFaberConnection?.id) + + const faberConnections = await faberAgent.connections.getAll() + let [firstFaberAliceConnection, secondFaberAliceConnection] = faberConnections + firstFaberAliceConnection = await faberAgent.connections.returnWhenIsConnected(firstFaberAliceConnection.id) + secondFaberAliceConnection = await faberAgent.connections.returnWhenIsConnected(secondFaberAliceConnection.id) + + // expect the two connections contain the two out of band ids + expect(faberConnections.map((c) => c.outOfBandId)).toEqual( + expect.arrayContaining([outOfBandRecord.id, outOfBandRecord2.id]) + ) + + expect(faberConnections).toHaveLength(2) + expect(firstFaberAliceConnection.state).toBe(DidExchangeState.Completed) + expect(secondFaberAliceConnection.state).toBe(DidExchangeState.Completed) + }) + + test('throws an error when the invitation has already been received', async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation(makeConnectionConfig) + const { outOfBandInvitation } = outOfBandRecord + + const { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitation(outOfBandInvitation) + + // Wait until connection is ready + await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + + const [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + + // Try to receive the invitation again + await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation)).rejects.toThrow( + new CredoError( + `An out of band record with invitation ${outOfBandInvitation.id} has already been received. Invitations should have a unique id.` + ) + ) + }) + + test('emits OutOfBandStateChanged event', async () => { + const eventListener = jest.fn() + const { outOfBandInvitation, id } = await faberAgent.oob.createInvitation(makeConnectionConfig) + + aliceAgent.events.on(OutOfBandEventTypes.OutOfBandStateChanged, eventListener) + + const { outOfBandRecord, connectionRecord } = await aliceAgent.oob.receiveInvitation(outOfBandInvitation, { + autoAcceptConnection: true, + autoAcceptInvitation: true, + }) + + // Wait for the connection to complete so we don't get wallet closed errors + await aliceAgent.connections.returnWhenIsConnected(connectionRecord!.id) + aliceAgent.events.off(OutOfBandEventTypes.OutOfBandStateChanged, eventListener) + + const [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(id) + await faberAgent.connections.returnWhenIsConnected(faberAliceConnection.id) + + // Receiving the invitation + expect(eventListener).toHaveBeenNthCalledWith(1, { + type: OutOfBandEventTypes.OutOfBandStateChanged, + metadata: { + contextCorrelationId: 'default', + }, + payload: { + outOfBandRecord: expect.objectContaining({ state: OutOfBandState.Initial }), + previousState: null, + }, + }) + + // Accepting the invitation + expect(eventListener).toHaveBeenNthCalledWith(2, { + type: OutOfBandEventTypes.OutOfBandStateChanged, + metadata: { + contextCorrelationId: 'default', + }, + payload: { + outOfBandRecord, + previousState: OutOfBandState.Initial, + }, + }) + }) + + test.skip('do not create a new connection when connection exists and multiuse is false', async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation({ + ...makeConnectionConfig, + multiUseInvitation: false, + }) + const { outOfBandInvitation } = outOfBandRecord + + let { connectionRecord: firstAliceFaberConnection } = await aliceAgent.oob.receiveInvitation(outOfBandInvitation) + firstAliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(firstAliceFaberConnection!.id) + + await aliceAgent.oob.receiveInvitation(outOfBandInvitation) + + // TODO Somehow check agents throws an error or sends problem report + + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord!.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + + const faberConnections = await faberAgent.connections.getAll() + expect(faberConnections).toHaveLength(1) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + expect(firstAliceFaberConnection.state).toBe(DidExchangeState.Completed) + }) + + test('throw an error when handshake protocols are not supported', async () => { + const outOfBandInvitation = new OutOfBandInvitation({ label: 'test-connection', services: [] }) + const unsupportedProtocol = 'https://didcomm.org/unsupported-connections-protocol/1.0' + outOfBandInvitation.handshakeProtocols = [unsupportedProtocol as HandshakeProtocol] + + await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation, receiveInvitationConfig)).rejects.toEqual( + new CredoError( + `Handshake protocols [${unsupportedProtocol}] are not supported. Supported protocols are [https://didcomm.org/didexchange/1.x,https://didcomm.org/connections/1.x]` + ) + ) + }) + + test('throw an error when the OOB message does not contain either handshake or requests', async () => { + const outOfBandInvitation = new OutOfBandInvitation({ label: 'test-connection', services: [] }) + + await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation, receiveInvitationConfig)).rejects.toEqual( + new CredoError('One or both of handshake_protocols and requests~attach MUST be included in the message.') + ) + }) + + test('throw an error when the OOB message contains unsupported message request', async () => { + const testMessage = new TestMessage() + testMessage.type = 'https://didcomm.org/test-protocol/1.0/test-message' + const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ + ...issueCredentialConfig, + messages: [testMessage], + }) + + await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation, receiveInvitationConfig)).rejects.toEqual( + new CredoError('There is no message in requests~attach supported by agent.') + ) + }) + + test(`make two connections with ${HandshakeProtocol.DidExchange} by reusing the did from the first connection as the 'invitationDid' in oob invitation for the second connection`, async () => { + const outOfBandRecord1 = await faberAgent.oob.createInvitation({}) + + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitation( + outOfBandRecord1.outOfBandInvitation + ) + + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord1!.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection?.state).toBe(DidExchangeState.Completed) + + // Use the invitation did from the first connection to create the second connection + const outOfBandRecord2 = await faberAgent.oob.createInvitation({ + invitationDid: outOfBandRecord1.outOfBandInvitation.invitationDids[0], + }) + + let { connectionRecord: aliceFaberConnection2 } = await aliceAgent.oob.receiveInvitation( + outOfBandRecord2.outOfBandInvitation + ) + aliceFaberConnection2 = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection2!.id) + expect(aliceFaberConnection2.state).toBe(DidExchangeState.Completed) + + let [faberAliceConnection2] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord2!.id) + faberAliceConnection2 = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection2!.id) + expect(faberAliceConnection2?.state).toBe(DidExchangeState.Completed) + }) + }) + + describe('messages and connection exchange', () => { + test('oob exchange with handshake where response is received to invitation', async () => { + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + const outOfBandRecord = await faberAgent.oob.createInvitation({ + handshake: true, + messages: [message], + }) + const { outOfBandInvitation } = outOfBandRecord + + await aliceAgent.oob.receiveInvitation(outOfBandInvitation) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + await faberCredentialRecordPromise + }) + + test('oob exchange with reuse where response is received to invitation', async () => { + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + + const routing = await faberAgent.mediationRecipient.getRouting({}) + const connectionOutOfBandRecord = await faberAgent.oob.createInvitation({ + routing, + }) + + // Create connection + const { connectionRecord } = await aliceAgent.oob.receiveInvitation(connectionOutOfBandRecord.outOfBandInvitation) + if (!connectionRecord) throw new Error('Connection record is undefined') + await aliceAgent.connections.returnWhenIsConnected(connectionRecord.id) + + // Create offer and reuse + const outOfBandRecord = await faberAgent.oob.createInvitation({ + routing, + messages: [message], + }) + // Create connection + const { connectionRecord: offerConnectionRecord } = await aliceAgent.oob.receiveInvitation( + outOfBandRecord.outOfBandInvitation, + { + reuseConnection: true, + } + ) + if (!offerConnectionRecord) throw new Error('Connection record is undefined') + + // Should be the same, as connection is reused. + expect(offerConnectionRecord.id).toEqual(connectionRecord.id) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + await faberCredentialRecordPromise + }) + }) + + describe('connection-less exchange', () => { + test('oob exchange without handshake where response is received to invitation', async () => { + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + const outOfBandRecord = await faberAgent.oob.createInvitation({ + handshake: false, + messages: [message], + }) + const { outOfBandInvitation } = outOfBandRecord + + await aliceAgent.oob.receiveInvitation(outOfBandInvitation) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + await faberCredentialRecordPromise + }) + + test('oob exchange without handshake where response is received and custom routing is used on recipient', async () => { + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + const outOfBandRecord = await faberAgent.oob.createInvitation({ + handshake: false, + messages: [message], + }) + const { outOfBandInvitation } = outOfBandRecord + + const routing = await aliceAgent.mediationRecipient.getRouting({}) + + await aliceAgent.oob.receiveInvitation(outOfBandInvitation, { + routing, + }) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + const faberCredentialRecord = await faberCredentialRecordPromise + + const faberCredentialRequest = await faberAgent.credentials.findRequestMessage(faberCredentialRecord.id) + + expect(JsonTransformer.toJSON(faberCredentialRequest?.service)).toEqual({ + recipientKeys: [routing.recipientKey.publicKeyBase58], + serviceEndpoint: routing.endpoints[0], + routingKeys: routing.routingKeys.map((r) => r.publicKeyBase58), + }) + }) + + test('legacy connectionless exchange where response is received to invitation', async () => { + const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + const { invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + domain: 'http://example.com', + message, + recordId: credentialRecord.id, + }) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + await faberCredentialRecordPromise + }) + + test('legacy connectionless exchange where response is received to invitation and custom routing is used on recipient', async () => { + const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + const { invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + domain: 'http://example.com', + message, + recordId: credentialRecord.id, + }) + + const routing = await aliceAgent.mediationRecipient.getRouting({}) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl, { routing }) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + const faberCredentialRecord = await faberCredentialRecordPromise + + const faberCredentialRequest = await faberAgent.credentials.findRequestMessage(faberCredentialRecord.id) + + expect(JsonTransformer.toJSON(faberCredentialRequest?.service)).toEqual({ + recipientKeys: [routing.recipientKey.publicKeyBase58], + serviceEndpoint: routing.endpoints[0], + routingKeys: routing.routingKeys.map((r) => r.publicKeyBase58), + }) + }) + + test('legacy connectionless exchange without receiving message through oob receiveInvitation, where response is received to invitation', async () => { + const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + const { message: messageWithService } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + domain: 'http://example.com', + message, + recordId: credentialRecord.id, + }) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + await aliceAgent.receiveMessage(messageWithService.toJSON()) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + await faberCredentialRecordPromise + }) + + test('add ~service decorator to the message and returns invitation url in createLegacyConnectionlessInvitation', async () => { + const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + + const { message: offerMessage, invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: credentialRecord.id, + domain: 'https://test.com', + message, + }) + + expect(offerMessage.service).toMatchObject({ + serviceEndpoint: expect.any(String), + recipientKeys: [expect.any(String)], + routingKeys: [], + }) + + expect(invitationUrl).toEqual(expect.stringContaining('https://test.com?d_m=')) + + const messageBase64 = invitationUrl.split('https://test.com?d_m=')[1] + + expect(JsonEncoder.fromBase64(messageBase64)).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + }) + }) + }) +}) diff --git a/packages/core/tests/proofs-sub-protocol.e2e.test.ts b/packages/core/tests/proofs-sub-protocol.e2e.test.ts new file mode 100644 index 0000000000..244eacd496 --- /dev/null +++ b/packages/core/tests/proofs-sub-protocol.e2e.test.ts @@ -0,0 +1,417 @@ +import type { EventReplaySubject } from './events' +import type { AnonCredsTestsAgent } from '../../anoncreds/tests/legacyAnonCredsSetup' + +import { issueLegacyAnonCredsCredential, setupAnonCredsTests } from '../../anoncreds/tests/legacyAnonCredsSetup' +import { ProofState } from '../src/modules/proofs/models/ProofState' +import { uuid } from '../src/utils/uuid' + +import { waitForProofExchangeRecord } from './helpers' +import testLogger from './logger' + +describe('Present Proof Subprotocol', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let credentialDefinitionId: string + let faberConnectionId: string + let aliceConnectionId: string + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber agent', + holderName: 'Alice agent', + attributeNames: ['name', 'age'], + })) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + attributes: [ + { + name: 'name', + value: 'Alice', + }, + { + name: 'age', + value: '50', + }, + ], + credentialDefinitionId, + }, + }) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with v1 proof proposal to Faber with parentThreadId', async () => { + const parentThreadId = uuid() + + // Alice sends a presentation proposal to Faber + testLogger.test('Alice sends a presentation proposal to Faber') + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + parentThreadId, + state: ProofState.ProposalReceived, + }) + + const aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v1', + parentThreadId, + proofFormats: { + indy: { + name: 'abc', + version: '1.0', + attributes: [ + { + name: 'name', + credentialDefinitionId, + value: 'Alice', + }, + ], + predicates: [ + { + credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 40, + }, + ], + }, + }, + }) + + expect(aliceProofExchangeRecord.parentThreadId).toBe(parentThreadId) + const proofsByParentThread = await aliceAgent.proofs.getByParentThreadAndConnectionId(parentThreadId) + expect(proofsByParentThread.length).toEqual(1) + expect(proofsByParentThread[0].parentThreadId).toBe(parentThreadId) + + const threadId = aliceProofExchangeRecord.threadId + + testLogger.test('Faber waits for a presentation proposal from Alice') + let faberProofExchangeRecord = await faberProofExchangeRecordPromise + + // Faber accepts the presentation proposal from Alice + testLogger.test('Faber accepts the presentation proposal from Alice') + await faberAgent.proofs.acceptProposal({ proofRecordId: faberProofExchangeRecord.id }) + + testLogger.test('Alice waits till it receives presentation ack') + await waitForProofExchangeRecord(aliceAgent, { + threadId, + parentThreadId, + state: ProofState.RequestReceived, + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: requestedCredentials.proofFormats, + }) + + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await waitForProofExchangeRecord(faberAgent, { + threadId, + parentThreadId, + state: ProofState.PresentationReceived, + }) + + // Faber accepts the presentation provided by Alice + testLogger.test('Faber accepts the presentation provided by Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + await waitForProofExchangeRecord(aliceAgent, { + threadId, + parentThreadId, + state: ProofState.Done, + }) + }) + + test('Faber starts with v1 proof requests to Alice with parentThreadId', async () => { + const parentThreadId = uuid() + testLogger.test('Faber sends presentation request to Alice') + + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + parentThreadId, + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + const faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + connectionId: faberConnectionId, + parentThreadId, + protocolVersion: 'v1', + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + expect(faberProofExchangeRecord.parentThreadId).toBe(parentThreadId) + const proofsByParentThread = await faberAgent.proofs.getByParentThreadAndConnectionId(parentThreadId) + expect(proofsByParentThread.length).toEqual(1) + expect(proofsByParentThread[0].parentThreadId).toBe(parentThreadId) + + const threadId = faberProofExchangeRecord.threadId + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + const aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: requestedCredentials.proofFormats, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + await waitForProofExchangeRecord(faberAgent, { + threadId, + parentThreadId, + state: ProofState.PresentationReceived, + }) + + // Faber accepts the presentation + testLogger.test('Faber accept the presentation from Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she receives a presentation acknowledgement + testLogger.test('Alice waits for acceptance by Faber') + await waitForProofExchangeRecord(aliceAgent, { + threadId, + parentThreadId, + state: ProofState.Done, + }) + }) + + test('Alice starts with v2 proof proposal to Faber with parentThreadId', async () => { + const parentThreadId = uuid() + + // Alice sends a presentation proposal to Faber + testLogger.test('Alice sends a presentation proposal to Faber') + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + parentThreadId, + state: ProofState.ProposalReceived, + }) + + const aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + parentThreadId, + proofFormats: { + indy: { + name: 'abc', + version: '1.0', + attributes: [ + { + name: 'name', + credentialDefinitionId, + value: 'Alice', + }, + ], + predicates: [ + { + credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 40, + }, + ], + }, + }, + }) + + expect(aliceProofExchangeRecord.parentThreadId).toBe(parentThreadId) + const proofsByParentThread = await aliceAgent.proofs.getByParentThreadAndConnectionId(parentThreadId) + expect(proofsByParentThread.length).toEqual(1) + expect(proofsByParentThread[0].parentThreadId).toBe(parentThreadId) + + const threadId = aliceProofExchangeRecord.threadId + + testLogger.test('Faber waits for a presentation proposal from Alice') + let faberProofExchangeRecord = await faberProofExchangeRecordPromise + + // Faber accepts the presentation proposal from Alice + testLogger.test('Faber accepts the presentation proposal from Alice') + await faberAgent.proofs.acceptProposal({ proofRecordId: faberProofExchangeRecord.id }) + + testLogger.test('Alice waits till it receives presentation ack') + await waitForProofExchangeRecord(aliceAgent, { + threadId, + parentThreadId, + state: ProofState.RequestReceived, + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: requestedCredentials.proofFormats, + }) + + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await waitForProofExchangeRecord(faberAgent, { + threadId, + parentThreadId, + state: ProofState.PresentationReceived, + }) + + // Faber accepts the presentation provided by Alice + testLogger.test('Faber accepts the presentation provided by Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + await waitForProofExchangeRecord(aliceAgent, { + threadId, + parentThreadId, + state: ProofState.Done, + }) + }) + + test('Faber starts with v2 proof requests to Alice with parentThreadId', async () => { + const parentThreadId = uuid() + testLogger.test('Faber sends presentation request to Alice') + + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + parentThreadId, + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + const faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + connectionId: faberConnectionId, + parentThreadId, + protocolVersion: 'v2', + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + expect(faberProofExchangeRecord.parentThreadId).toBe(parentThreadId) + const proofsByParentThread = await faberAgent.proofs.getByParentThreadAndConnectionId(parentThreadId) + expect(proofsByParentThread.length).toEqual(1) + expect(proofsByParentThread[0].parentThreadId).toBe(parentThreadId) + + const threadId = faberProofExchangeRecord.threadId + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + const aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: requestedCredentials.proofFormats, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + await waitForProofExchangeRecord(faberAgent, { + threadId, + parentThreadId, + state: ProofState.PresentationReceived, + }) + + // Faber accepts the presentation + testLogger.test('Faber accept the presentation from Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she receives a presentation acknowledgement + testLogger.test('Alice waits for acceptance by Faber') + await waitForProofExchangeRecord(aliceAgent, { + threadId, + parentThreadId, + state: ProofState.Done, + }) + }) +}) diff --git a/packages/core/tests/setup.ts b/packages/core/tests/setup.ts new file mode 100644 index 0000000000..a2ba1429d2 --- /dev/null +++ b/packages/core/tests/setup.ts @@ -0,0 +1,25 @@ +import 'reflect-metadata' + +import type { ConnectionRecord } from '../src/modules/connections/repository/ConnectionRecord' + +jest.setTimeout(120000) +expect.extend({ toBeConnectedWith }) + +// Custom matchers which can be used to extend Jest matchers via extend, e. g. `expect.extend({ toBeConnectedWith })`. +function toBeConnectedWith(actual: ConnectionRecord, expected: ConnectionRecord) { + actual.assertReady() + expected.assertReady() + + const pass = actual.theirDid === expected.did + if (pass) { + return { + message: () => `expected connection ${actual.theirDid} not to be connected to with ${expected.did}`, + pass: true, + } + } else { + return { + message: () => `expected connection ${actual.theirDid} to be connected to with ${expected.did}`, + pass: false, + } + } +} diff --git a/packages/core/tests/transport.ts b/packages/core/tests/transport.ts new file mode 100644 index 0000000000..2577fdd428 --- /dev/null +++ b/packages/core/tests/transport.ts @@ -0,0 +1,18 @@ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' +import type { Agent } from '../src' + +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' + +export function setupSubjectTransports(agents: Agent[]) { + const subjectMap: Record> = {} + + for (const agent of agents) { + const messages = new Subject() + subjectMap[agent.config.endpoints[0]] = messages + agent.registerInboundTransport(new SubjectInboundTransport(messages)) + agent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + } +} diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json new file mode 100644 index 0000000000..0a015be666 --- /dev/null +++ b/packages/core/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + + "compilerOptions": { + "outDir": "./build" + }, + + "include": ["src/**/*", "types"] +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000000..93d9dd32b5 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "allowJs": false, + "typeRoots": ["../../node_modules/@types", "src/types"], + "types": ["jest"] + } +} diff --git a/packages/core/types/jest.d.ts b/packages/core/types/jest.d.ts new file mode 100644 index 0000000000..4f68767c96 --- /dev/null +++ b/packages/core/types/jest.d.ts @@ -0,0 +1,9 @@ +import type { ConnectionRecord } from '../src/modules/connections/repository/ConnectionRecord' + +declare global { + namespace jest { + interface Matchers { + toBeConnectedWith(connection: ConnectionRecord): R + } + } +} diff --git a/packages/drpc/CHANGELOG.md b/packages/drpc/CHANGELOG.md new file mode 100644 index 0000000000..e3b8f5afd2 --- /dev/null +++ b/packages/drpc/CHANGELOG.md @@ -0,0 +1,60 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release +- Updated dependencies [66e696d] + - @credo-ts/core@0.5.6 + +## 0.5.5 + +### Patch Changes + +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) +- Updated dependencies [3239ef3] +- Updated dependencies [d548fa4] +- Updated dependencies [482a630] + - @credo-ts/core@0.5.5 + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +**Note:** Version bump only for package @credo-ts/drpc + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +### Features + +- apply new version of SD JWT package ([#1787](https://github.com/openwallet-foundation/credo-ts/issues/1787)) ([b41e158](https://github.com/openwallet-foundation/credo-ts/commit/b41e158098773d2f59b5b5cfb82cc6be06a57acd)) + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +**Note:** Version bump only for package @credo-ts/drpc + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +### Bug Fixes + +- stopped recvRequest from receiving outbound messages ([#1786](https://github.com/openwallet-foundation/credo-ts/issues/1786)) ([2005566](https://github.com/openwallet-foundation/credo-ts/commit/20055668765e1070cbf4db13a598e3e0d7881599)) + +### Features + +- support DRPC protocol ([#1753](https://github.com/openwallet-foundation/credo-ts/issues/1753)) ([4f58925](https://github.com/openwallet-foundation/credo-ts/commit/4f58925dc3adb6bae1ab2a24e00b461e9c4881b9)) diff --git a/packages/drpc/README.md b/packages/drpc/README.md new file mode 100644 index 0000000000..ef7cc2c9c0 --- /dev/null +++ b/packages/drpc/README.md @@ -0,0 +1,73 @@ +

+
+ Credo Logo +

+

Credo DRPC Module

+

+ License + typescript + @credo-ts/question-answer version + +

+
+ +DRPC module for [Credo](https://github.com/openwallet-foundation/credo-ts.git). Implements [Aries RFC 0804](https://github.com/hyperledger/aries-rfcs/blob/ea87d2e37640ef944568e3fa01df1f36fe7f0ff3/features/0804-didcomm-rpc/README.md). + +### Quick start + +In order for this module to work, we have to inject it into the agent to access agent functionality. See the example for more information. + +### Example of usage + +```ts +import { DrpcModule } from '@credo-ts/drpc' + +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + drpc: new DrpcModule(), + /* other custom modules */ + }, +}) + +await agent.initialize() + +// Send a request to the specified connection +const responseListener = await senderAgent.modules.drpc.sendRequest(connectionId, { + jsonrpc: '2.0', + method: 'hello', + id: 1, +}) + +// Listen for any incoming requests +const { request, sendResponse } = await receiverAgent.modules.drpc.recvRequest() + +// Process the received request and create a response +const result = + request.method === 'hello' + ? { jsonrpc: '2.0', result: 'Hello world!', id: request.id } + : { jsonrpc: '2.0', error: { code: DrpcErrorCode.METHOD_NOT_FOUND, message: 'Method not found' } } + +// Send the response back +await sendResponse(result) +``` diff --git a/packages/drpc/jest.config.ts b/packages/drpc/jest.config.ts new file mode 100644 index 0000000000..93c0197296 --- /dev/null +++ b/packages/drpc/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/drpc/package.json b/packages/drpc/package.json new file mode 100644 index 0000000000..11203e4d0f --- /dev/null +++ b/packages/drpc/package.json @@ -0,0 +1,39 @@ +{ + "name": "@credo-ts/drpc", + "main": "src/index", + "types": "src/index", + "version": "0.5.6", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/drpc", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/drpc" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build", + "test": "jest" + }, + "dependencies": { + "@credo-ts/core": "workspace:*", + "class-transformer": "^0.5.1", + "class-validator": "0.14.1" + }, + "devDependencies": { + "@credo-ts/node": "workspace:*", + "reflect-metadata": "^0.1.13", + "rimraf": "^4.4.0", + "typescript": "~5.5.2" + } +} diff --git a/packages/drpc/src/DrpcApi.ts b/packages/drpc/src/DrpcApi.ts new file mode 100644 index 0000000000..08dac02bd7 --- /dev/null +++ b/packages/drpc/src/DrpcApi.ts @@ -0,0 +1,185 @@ +import type { DrpcRequest, DrpcResponse, DrpcRequestMessage, DrpcResponseMessage } from './messages' +import type { DrpcRecord } from './repository/DrpcRecord' +import type { ConnectionRecord } from '@credo-ts/core' + +import { + AgentContext, + MessageHandlerRegistry, + MessageSender, + OutboundMessageContext, + injectable, + ConnectionService, +} from '@credo-ts/core' + +import { DrpcRequestHandler, DrpcResponseHandler } from './handlers' +import { DrpcRole } from './models' +import { DrpcService } from './services' + +@injectable() +export class DrpcApi { + private drpcMessageService: DrpcService + private messageSender: MessageSender + private connectionService: ConnectionService + private agentContext: AgentContext + + public constructor( + messageHandlerRegistry: MessageHandlerRegistry, + drpcMessageService: DrpcService, + messageSender: MessageSender, + connectionService: ConnectionService, + agentContext: AgentContext + ) { + this.drpcMessageService = drpcMessageService + this.messageSender = messageSender + this.connectionService = connectionService + this.agentContext = agentContext + this.registerMessageHandlers(messageHandlerRegistry) + } + + /** + * sends the request object to the connection and returns a function that will resolve to the response + * @param connectionId the connection to send the request to + * @param request the request object + * @returns curried function that waits for the response with an optional timeout in seconds + */ + public async sendRequest( + connectionId: string, + request: DrpcRequest + ): Promise<() => Promise> { + const connection = await this.connectionService.getById(this.agentContext, connectionId) + const { requestMessage: drpcMessage, record: drpcMessageRecord } = + await this.drpcMessageService.createRequestMessage(this.agentContext, request, connection.id) + const messageId = drpcMessage.id + await this.sendMessage(connection, drpcMessage, drpcMessageRecord) + return async (timeout?: number) => { + return await this.recvResponse(messageId, timeout) + } + } + + /** + * Listen for a response that has a thread id matching the provided messageId + * @param messageId the id to match the response to + * @param timeoutMs the time in milliseconds to wait for a response + * @returns the response object + */ + private async recvResponse(messageId: string, timeoutMs?: number): Promise { + return new Promise((resolve) => { + const listener = ({ + drpcMessageRecord, + removeListener, + }: { + drpcMessageRecord: DrpcRecord + removeListener: () => void + }) => { + const response = drpcMessageRecord.response + if (drpcMessageRecord.threadId === messageId) { + removeListener() + resolve(response) + } + } + + const cancelListener = this.drpcMessageService.createResponseListener(listener) + if (timeoutMs) { + const handle = setTimeout(() => { + clearTimeout(handle) + cancelListener() + resolve(undefined) + }, timeoutMs) + } + }) + } + + /** + * Listen for a request and returns the request object and a function to send the response + * @param timeoutMs the time in seconds to wait for a request + * @returns the request object and a function to send the response + */ + public async recvRequest(timeoutMs?: number): Promise< + | { + request: DrpcRequest + sendResponse: (response: DrpcResponse) => Promise + } + | undefined + > { + return new Promise((resolve) => { + const listener = ({ + drpcMessageRecord, + removeListener, + }: { + drpcMessageRecord: DrpcRecord + removeListener: () => void + }) => { + const request = drpcMessageRecord.request + if (request && drpcMessageRecord.role === DrpcRole.Server) { + removeListener() + resolve({ + sendResponse: async (response: DrpcResponse) => { + await this.sendResponse({ + connectionId: drpcMessageRecord.connectionId, + threadId: drpcMessageRecord.threadId, + response, + }) + }, + request, + }) + } + } + + const cancelListener = this.drpcMessageService.createRequestListener(listener) + + if (timeoutMs) { + const handle = setTimeout(() => { + clearTimeout(handle) + cancelListener() + resolve(undefined) + }, timeoutMs) + } + }) + } + + /** + * Sends a drpc response to a connection + * @param connectionId the connection id to use + * @param threadId the thread id to respond to + * @param response the drpc response object to send + */ + private async sendResponse(options: { + connectionId: string + threadId: string + response: DrpcResponse + }): Promise { + const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + const drpcMessageRecord = await this.drpcMessageService.findByThreadAndConnectionId( + this.agentContext, + options.connectionId, + options.threadId + ) + if (!drpcMessageRecord) { + throw new Error(`No request found for threadId ${options.threadId}`) + } + const { responseMessage, record } = await this.drpcMessageService.createResponseMessage( + this.agentContext, + options.response, + drpcMessageRecord + ) + await this.sendMessage(connection, responseMessage, record) + } + + private async sendMessage( + connection: ConnectionRecord, + message: DrpcRequestMessage | DrpcResponseMessage, + messageRecord: DrpcRecord + ): Promise { + const outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection, + associatedRecord: messageRecord, + }) + await this.messageSender.sendMessage(outboundMessageContext) + } + + private registerMessageHandlers(messageHandlerRegistry: MessageHandlerRegistry) { + messageHandlerRegistry.registerMessageHandler(new DrpcRequestHandler(this.drpcMessageService)) + messageHandlerRegistry.registerMessageHandler(new DrpcResponseHandler(this.drpcMessageService)) + } +} diff --git a/packages/drpc/src/DrpcModule.ts b/packages/drpc/src/DrpcModule.ts new file mode 100644 index 0000000000..2bf6657bc4 --- /dev/null +++ b/packages/drpc/src/DrpcModule.ts @@ -0,0 +1,38 @@ +import type { FeatureRegistry, DependencyManager, Module } from '@credo-ts/core' + +import { Protocol, AgentConfig } from '@credo-ts/core' + +import { DrpcApi } from './DrpcApi' +import { DrpcRole } from './models/DrpcRole' +import { DrpcRepository } from './repository' +import { DrpcService } from './services' + +export class DrpcModule implements Module { + public readonly api = DrpcApi + + /** + * Registers the dependencies of the drpc message module on the dependency manager. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The '@credo-ts/drpc' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." + ) + + // Services + dependencyManager.registerSingleton(DrpcService) + + // Repositories + dependencyManager.registerSingleton(DrpcRepository) + + // Features + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/drpc/1.0', + roles: [DrpcRole.Client, DrpcRole.Server], + }) + ) + } +} diff --git a/packages/drpc/src/DrpcRequestEvents.ts b/packages/drpc/src/DrpcRequestEvents.ts new file mode 100644 index 0000000000..74ea74b84f --- /dev/null +++ b/packages/drpc/src/DrpcRequestEvents.ts @@ -0,0 +1,12 @@ +import type { DrpcRecord } from './repository' +import type { BaseEvent } from '@credo-ts/core' + +export enum DrpcRequestEventTypes { + DrpcRequestStateChanged = 'DrpcRequestStateChanged', +} +export interface DrpcRequestStateChangedEvent extends BaseEvent { + type: typeof DrpcRequestEventTypes.DrpcRequestStateChanged + payload: { + drpcMessageRecord: DrpcRecord + } +} diff --git a/packages/drpc/src/DrpcResponseEvents.ts b/packages/drpc/src/DrpcResponseEvents.ts new file mode 100644 index 0000000000..e3e15d2158 --- /dev/null +++ b/packages/drpc/src/DrpcResponseEvents.ts @@ -0,0 +1,12 @@ +import type { DrpcRecord } from './repository' +import type { BaseEvent } from '@credo-ts/core' + +export enum DrpcResponseEventTypes { + DrpcResponseStateChanged = 'DrpcResponseStateChanged', +} +export interface DrpcResponseStateChangedEvent extends BaseEvent { + type: typeof DrpcResponseEventTypes.DrpcResponseStateChanged + payload: { + drpcMessageRecord: DrpcRecord + } +} diff --git a/packages/drpc/src/__tests__/DrpcMessageService.test.ts b/packages/drpc/src/__tests__/DrpcMessageService.test.ts new file mode 100644 index 0000000000..a1f904ab19 --- /dev/null +++ b/packages/drpc/src/__tests__/DrpcMessageService.test.ts @@ -0,0 +1,95 @@ +import type { DrpcRequestObject } from '../messages' + +import { DidExchangeState } from '@credo-ts/core' + +import { EventEmitter } from '../../../core/src/agent/EventEmitter' +import { InboundMessageContext } from '../../../core/src/agent/models/InboundMessageContext' +import { getAgentContext, getMockConnection } from '../../../core/tests/helpers' +import { DrpcRequestMessage } from '../messages' +import { DrpcRole } from '../models/DrpcRole' +import { DrpcRecord } from '../repository/DrpcRecord' +import { DrpcRepository } from '../repository/DrpcRepository' +import { DrpcService } from '../services' + +jest.mock('../repository/DrpcRepository') +const DrpcRepositoryMock = DrpcRepository as jest.Mock +const drpcMessageRepository = new DrpcRepositoryMock() + +jest.mock('../../../core/src/agent/EventEmitter') +const EventEmitterMock = EventEmitter as jest.Mock +const eventEmitter = new EventEmitterMock() + +const agentContext = getAgentContext() + +describe('DrpcService', () => { + let drpcMessageService: DrpcService + const mockConnectionRecord = getMockConnection({ + id: 'd3849ac3-c981-455b-a1aa-a10bea6cead8', + did: 'did:sov:C2SsBf5QUQpqSAQfhu3sd2', + state: DidExchangeState.Completed, + }) + + beforeEach(() => { + drpcMessageService = new DrpcService(drpcMessageRepository, eventEmitter) + }) + + describe('createMessage', () => { + it(`creates message and record, and emits message and basic message record`, async () => { + const messageRequest: DrpcRequestObject = { + jsonrpc: '2.0', + method: 'hello', + id: 1, + } + const { requestMessage } = await drpcMessageService.createRequestMessage( + agentContext, + messageRequest, + mockConnectionRecord.id + ) + + expect(requestMessage).toBeInstanceOf(DrpcRequestMessage) + expect((requestMessage.request as DrpcRequestObject).method).toBe('hello') + + expect(drpcMessageRepository.save).toHaveBeenCalledWith(agentContext, expect.any(DrpcRecord)) + expect(eventEmitter.emit).toHaveBeenCalledWith(agentContext, { + type: 'DrpcRequestStateChanged', + payload: { + drpcMessageRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + request: { + id: 1, + jsonrpc: '2.0', + method: 'hello', + }, + role: DrpcRole.Client, + }), + }, + }) + }) + }) + + describe('recieve request', () => { + it(`stores record and emits message and basic message record`, async () => { + const drpcMessage = new DrpcRequestMessage({ request: { jsonrpc: '2.0', method: 'hello', id: 1 } }) + + const messageContext = new InboundMessageContext(drpcMessage, { agentContext, connection: mockConnectionRecord }) + + await drpcMessageService.receiveRequest(messageContext) + + expect(drpcMessageRepository.save).toHaveBeenCalledWith(agentContext, expect.any(DrpcRecord)) + expect(eventEmitter.emit).toHaveBeenCalledWith(agentContext, { + type: 'DrpcRequestStateChanged', + payload: { + drpcMessageRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + request: { + id: 1, + jsonrpc: '2.0', + method: 'hello', + }, + role: DrpcRole.Server, + }), + }, + }) + }) + }) +}) diff --git a/packages/drpc/src/__tests__/DrpcMessagesModule.test.ts b/packages/drpc/src/__tests__/DrpcMessagesModule.test.ts new file mode 100644 index 0000000000..7b4ce8ae8b --- /dev/null +++ b/packages/drpc/src/__tests__/DrpcMessagesModule.test.ts @@ -0,0 +1,29 @@ +import type { DependencyManager } from '../../../core/src/plugins/DependencyManager' + +import { FeatureRegistry } from '../../../core/src/agent/FeatureRegistry' +import { DrpcModule } from '../DrpcModule' +import { DrpcRepository } from '../repository' +import { DrpcService } from '../services' + +jest.mock('../../../core/src/plugins/DependencyManager') + +jest.mock('../../../core/src/agent/FeatureRegistry') +const FeatureRegistryMock = FeatureRegistry as jest.Mock + +const featureRegistry = new FeatureRegistryMock() + +const dependencyManager = { + registerInstance: jest.fn(), + registerSingleton: jest.fn(), + resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), +} as unknown as DependencyManager + +describe('DrpcModule', () => { + test('registers dependencies on the dependency manager', () => { + new DrpcModule().register(dependencyManager, featureRegistry) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DrpcService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DrpcRepository) + }) +}) diff --git a/packages/drpc/src/handlers/DrpcRequestHandler.ts b/packages/drpc/src/handlers/DrpcRequestHandler.ts new file mode 100644 index 0000000000..2e91072055 --- /dev/null +++ b/packages/drpc/src/handlers/DrpcRequestHandler.ts @@ -0,0 +1,17 @@ +import type { DrpcService } from '../services/DrpcService' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { DrpcRequestMessage } from '../messages' + +export class DrpcRequestHandler implements MessageHandler { + private drpcMessageService: DrpcService + public supportedMessages = [DrpcRequestMessage] + + public constructor(drpcMessageService: DrpcService) { + this.drpcMessageService = drpcMessageService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.drpcMessageService.receiveRequest(messageContext) + } +} diff --git a/packages/drpc/src/handlers/DrpcResponseHandler.ts b/packages/drpc/src/handlers/DrpcResponseHandler.ts new file mode 100644 index 0000000000..45b92e4de8 --- /dev/null +++ b/packages/drpc/src/handlers/DrpcResponseHandler.ts @@ -0,0 +1,17 @@ +import type { DrpcService } from '../services/DrpcService' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { DrpcResponseMessage } from '../messages' + +export class DrpcResponseHandler implements MessageHandler { + private drpcMessageService: DrpcService + public supportedMessages = [DrpcResponseMessage] + + public constructor(drpcMessageService: DrpcService) { + this.drpcMessageService = drpcMessageService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.drpcMessageService.receiveResponse(messageContext) + } +} diff --git a/packages/drpc/src/handlers/index.ts b/packages/drpc/src/handlers/index.ts new file mode 100644 index 0000000000..a4380cc356 --- /dev/null +++ b/packages/drpc/src/handlers/index.ts @@ -0,0 +1,2 @@ +export * from './DrpcResponseHandler' +export * from './DrpcRequestHandler' diff --git a/packages/drpc/src/index.ts b/packages/drpc/src/index.ts new file mode 100644 index 0000000000..cc043c4156 --- /dev/null +++ b/packages/drpc/src/index.ts @@ -0,0 +1,8 @@ +export * from './messages' +export * from './services' +export * from './repository' +export * from './DrpcRequestEvents' +export * from './DrpcResponseEvents' +export * from './DrpcApi' +export * from './models/DrpcRole' +export * from './DrpcModule' diff --git a/packages/drpc/src/messages/DrpcRequestMessage.ts b/packages/drpc/src/messages/DrpcRequestMessage.ts new file mode 100644 index 0000000000..6e8ae1ea6a --- /dev/null +++ b/packages/drpc/src/messages/DrpcRequestMessage.ts @@ -0,0 +1,32 @@ +import { IsValidMessageType, parseMessageType, AgentMessage } from '@credo-ts/core' +import { Expose } from 'class-transformer' + +import { IsValidDrpcRequest } from '../models' + +export interface DrpcRequestObject { + jsonrpc: string + method: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params?: any[] | object + id: string | number | null +} + +export type DrpcRequest = DrpcRequestObject | DrpcRequestObject[] + +export class DrpcRequestMessage extends AgentMessage { + public constructor(options: { request: DrpcRequest }) { + super() + if (options) { + this.id = this.generateId() + this.request = options.request + } + } + + @IsValidMessageType(DrpcRequestMessage.type) + public readonly type = DrpcRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/drpc/1.0/request') + + @Expose({ name: 'request' }) + @IsValidDrpcRequest() + public request!: DrpcRequest +} diff --git a/packages/drpc/src/messages/DrpcResponseMessage.ts b/packages/drpc/src/messages/DrpcResponseMessage.ts new file mode 100644 index 0000000000..a148760bfd --- /dev/null +++ b/packages/drpc/src/messages/DrpcResponseMessage.ts @@ -0,0 +1,42 @@ +import type { DrpcErrorCode } from '../models' + +import { IsValidMessageType, parseMessageType, AgentMessage } from '@credo-ts/core' +import { Expose } from 'class-transformer' + +import { IsValidDrpcResponse } from '../models' + +export type DrpcResponse = DrpcResponseObject | (DrpcResponseObject | Record)[] | Record + +export interface DrpcResponseError { + code: DrpcErrorCode + message: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: any +} + +export interface DrpcResponseObject { + jsonrpc: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result?: any + error?: DrpcResponseError + id: string | number | null +} + +export class DrpcResponseMessage extends AgentMessage { + public constructor(options: { response: DrpcResponse; threadId: string }) { + super() + if (options) { + this.id = this.generateId() + this.response = options.response + this.setThread({ threadId: options.threadId }) + } + } + + @IsValidMessageType(DrpcResponseMessage.type) + public readonly type = DrpcResponseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/drpc/1.0/response') + + @Expose({ name: 'response' }) + @IsValidDrpcResponse() + public response!: DrpcResponse +} diff --git a/packages/drpc/src/messages/index.ts b/packages/drpc/src/messages/index.ts new file mode 100644 index 0000000000..cab3129a90 --- /dev/null +++ b/packages/drpc/src/messages/index.ts @@ -0,0 +1,2 @@ +export * from './DrpcResponseMessage' +export * from './DrpcRequestMessage' diff --git a/packages/drpc/src/models/DrpcErrorCodes.ts b/packages/drpc/src/models/DrpcErrorCodes.ts new file mode 100644 index 0000000000..1e3220bf78 --- /dev/null +++ b/packages/drpc/src/models/DrpcErrorCodes.ts @@ -0,0 +1,8 @@ +export enum DrpcErrorCode { + METHOD_NOT_FOUND = -32601, + PARSE_ERROR = -32700, + INVALID_REQUEST = -32600, + INVALID_PARAMS = -32602, + INTERNAL_ERROR = -32603, + SERVER_ERROR = -32000, +} diff --git a/packages/drpc/src/models/DrpcRole.ts b/packages/drpc/src/models/DrpcRole.ts new file mode 100644 index 0000000000..e5cdccccf7 --- /dev/null +++ b/packages/drpc/src/models/DrpcRole.ts @@ -0,0 +1,4 @@ +export enum DrpcRole { + Client = 'client', + Server = 'server', +} diff --git a/packages/drpc/src/models/DrpcState.ts b/packages/drpc/src/models/DrpcState.ts new file mode 100644 index 0000000000..ed59f1a8c4 --- /dev/null +++ b/packages/drpc/src/models/DrpcState.ts @@ -0,0 +1,5 @@ +export enum DrpcState { + RequestSent = 'request-sent', + RequestReceived = 'request-received', + Completed = 'completed', +} diff --git a/packages/drpc/src/models/ValidRequest.ts b/packages/drpc/src/models/ValidRequest.ts new file mode 100644 index 0000000000..7c2be91bb9 --- /dev/null +++ b/packages/drpc/src/models/ValidRequest.ts @@ -0,0 +1,45 @@ +import type { ValidationOptions } from 'class-validator' + +import { ValidateBy, ValidationError, buildMessage } from 'class-validator' + +export function IsValidDrpcRequest(validationOptions?: ValidationOptions): PropertyDecorator { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (target: any, propertyKey: string | symbol) => { + ValidateBy( + { + name: 'isValidDrpcRequest', + validator: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + validate: (value: any): boolean => { + // Check if value is a DrpcRequestObject or an array of DrpcRequestObject + let isValid = false + if (!Array.isArray(value)) { + isValid = isValidDrpcRequest(value) + } else { + isValid = value.every(isValidDrpcRequest) + } + + if (!isValid) { + throw new ValidationError() + } + + return isValid + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property is not a valid DrpcRequest', + validationOptions + ), + }, + }, + validationOptions + )(target, propertyKey) + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isValidDrpcRequest(value: any): boolean { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + return false + } + return 'jsonrpc' in value && 'method' in value && 'id' in value +} diff --git a/packages/drpc/src/models/ValidResponse.ts b/packages/drpc/src/models/ValidResponse.ts new file mode 100644 index 0000000000..83dadc9905 --- /dev/null +++ b/packages/drpc/src/models/ValidResponse.ts @@ -0,0 +1,69 @@ +import type { ValidationOptions } from 'class-validator' + +import { ValidateBy, ValidationError, buildMessage } from 'class-validator' + +export function IsValidDrpcResponse(validationOptions?: ValidationOptions): PropertyDecorator { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (target: any, propertyKey: string | symbol) => { + ValidateBy( + { + name: 'isValidDrpcResponse', + validator: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + validate: (value: any): boolean => { + // Check if value is a valid DrpcResponseObject, an array of DrpcResponseObject (possibly mixed with empty objects), or an empty object + let isValid = false + if (Array.isArray(value)) { + if (value.length > 0) { + isValid = value.every(isValidDrpcResponse) + } + } else { + isValid = isValidDrpcResponse(value) + } + if (!isValid) { + throw new ValidationError() + } + return isValid + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property is not a valid DrpcResponse', + validationOptions + ), + }, + }, + validationOptions + )(target, propertyKey) + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isValidDrpcResponse(value: any): boolean { + // Check if value is an object + if (typeof value !== 'object' || value === null) { + return false + } + + // Check if it's an empty object + if (Object.keys(value).length === 0) { + return true + } + + // Check if it's a valid DrpcResponseObject + if ('jsonrpc' in value && 'id' in value) { + // Check if 'result' and 'error' are valid + if ('result' in value && typeof value.result === 'undefined') { + return false + } + if ('error' in value && !isValidDrpcResponseError(value.error)) { + return false + } + return true + } + + return false +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isValidDrpcResponseError(error: any): boolean { + return typeof error === 'object' && error !== null && 'code' in error && 'message' in error +} diff --git a/packages/drpc/src/models/index.ts b/packages/drpc/src/models/index.ts new file mode 100644 index 0000000000..9fc94c2d08 --- /dev/null +++ b/packages/drpc/src/models/index.ts @@ -0,0 +1,5 @@ +export * from './DrpcRole' +export * from './DrpcState' +export * from './ValidRequest' +export * from './ValidResponse' +export * from './DrpcErrorCodes' diff --git a/packages/drpc/src/repository/DrpcRecord.ts b/packages/drpc/src/repository/DrpcRecord.ts new file mode 100644 index 0000000000..a31e2015b6 --- /dev/null +++ b/packages/drpc/src/repository/DrpcRecord.ts @@ -0,0 +1,77 @@ +import type { DrpcRequest, DrpcResponse } from '../messages' +import type { DrpcRole, DrpcState } from '../models' +import type { RecordTags, TagsBase } from '@credo-ts/core' + +import { BaseRecord, CredoError, utils } from '@credo-ts/core' + +export type CustomDrpcMessageTags = TagsBase +export type DefaultDrpcMessageTags = { + connectionId: string + threadId: string +} + +export type DrpcMessageTags = RecordTags + +export interface DrpcStorageProps { + id?: string + connectionId: string + role: DrpcRole + tags?: CustomDrpcMessageTags + request?: DrpcRequest + response?: DrpcResponse + state: DrpcState + threadId: string +} + +export class DrpcRecord extends BaseRecord { + public request?: DrpcRequest + public response?: DrpcResponse + public connectionId!: string + public role!: DrpcRole + public state!: DrpcState + public threadId!: string + + public static readonly type = 'DrpcRecord' + public readonly type = DrpcRecord.type + + public constructor(props: DrpcStorageProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.request = props.request + this.response = props.response + this.connectionId = props.connectionId + this._tags = props.tags ?? {} + this.role = props.role + this.state = props.state + this.threadId = props.threadId + } + } + + public getTags() { + return { + ...this._tags, + connectionId: this.connectionId, + threadId: this.threadId, + } + } + + public assertRole(expectedRole: DrpcRole) { + if (this.role !== expectedRole) { + throw new CredoError(`Invalid DRPC record role ${this.role}, expected is ${expectedRole}.`) + } + } + + public assertState(expectedStates: DrpcState | DrpcState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new CredoError( + `DRPC response record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.` + ) + } + } +} diff --git a/packages/drpc/src/repository/DrpcRepository.ts b/packages/drpc/src/repository/DrpcRepository.ts new file mode 100644 index 0000000000..7af4a79f78 --- /dev/null +++ b/packages/drpc/src/repository/DrpcRepository.ts @@ -0,0 +1,13 @@ +import { EventEmitter, InjectionSymbols, inject, injectable, Repository, StorageService } from '@credo-ts/core' + +import { DrpcRecord } from './DrpcRecord' + +@injectable() +export class DrpcRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(DrpcRecord, storageService, eventEmitter) + } +} diff --git a/packages/drpc/src/repository/index.ts b/packages/drpc/src/repository/index.ts new file mode 100644 index 0000000000..cbf2daeeb0 --- /dev/null +++ b/packages/drpc/src/repository/index.ts @@ -0,0 +1,2 @@ +export * from './DrpcRecord' +export * from './DrpcRepository' diff --git a/packages/drpc/src/services/DrpcService.ts b/packages/drpc/src/services/DrpcService.ts new file mode 100644 index 0000000000..9f68b58701 --- /dev/null +++ b/packages/drpc/src/services/DrpcService.ts @@ -0,0 +1,188 @@ +import type { DrpcRequestStateChangedEvent } from '../DrpcRequestEvents' +import type { DrpcResponseStateChangedEvent } from '../DrpcResponseEvents' +import type { DrpcRequest, DrpcResponse } from '../messages' +import type { AgentContext, InboundMessageContext, Query, QueryOptions } from '@credo-ts/core' + +import { EventEmitter, injectable } from '@credo-ts/core' + +import { DrpcRequestEventTypes } from '../DrpcRequestEvents' +import { DrpcResponseEventTypes } from '../DrpcResponseEvents' +import { DrpcRequestMessage, DrpcResponseMessage } from '../messages' +import { DrpcRole, DrpcState, isValidDrpcRequest, isValidDrpcResponse } from '../models' +import { DrpcRecord, DrpcRepository } from '../repository' + +@injectable() +export class DrpcService { + private drpcMessageRepository: DrpcRepository + private eventEmitter: EventEmitter + + public constructor(drpcMessageRepository: DrpcRepository, eventEmitter: EventEmitter) { + this.drpcMessageRepository = drpcMessageRepository + this.eventEmitter = eventEmitter + } + + public async createRequestMessage(agentContext: AgentContext, request: DrpcRequest, connectionId: string) { + const drpcMessage = new DrpcRequestMessage({ request }) + + const drpcMessageRecord = new DrpcRecord({ + request, + connectionId, + state: DrpcState.RequestSent, + threadId: drpcMessage.threadId, + role: DrpcRole.Client, + }) + + await this.drpcMessageRepository.save(agentContext, drpcMessageRecord) + this.emitStateChangedEvent(agentContext, drpcMessageRecord) + + return { requestMessage: drpcMessage, record: drpcMessageRecord } + } + + public async createResponseMessage(agentContext: AgentContext, response: DrpcResponse, drpcRecord: DrpcRecord) { + const drpcMessage = new DrpcResponseMessage({ response, threadId: drpcRecord.threadId }) + + drpcRecord.assertState(DrpcState.RequestReceived) + + drpcRecord.response = response + drpcRecord.request = undefined + + await this.updateState(agentContext, drpcRecord, DrpcState.Completed) + + return { responseMessage: drpcMessage, record: drpcRecord } + } + + public createRequestListener( + callback: (params: { drpcMessageRecord: DrpcRecord; removeListener: () => void }) => void | Promise + ) { + const listener = async (event: DrpcRequestStateChangedEvent) => { + const { drpcMessageRecord } = event.payload + await callback({ + drpcMessageRecord, + removeListener: () => this.eventEmitter.off(DrpcRequestEventTypes.DrpcRequestStateChanged, listener), + }) + } + this.eventEmitter.on(DrpcRequestEventTypes.DrpcRequestStateChanged, listener) + + return () => { + this.eventEmitter.off(DrpcRequestEventTypes.DrpcRequestStateChanged, listener) + } + } + + public createResponseListener( + callback: (params: { drpcMessageRecord: DrpcRecord; removeListener: () => void }) => void | Promise + ) { + const listener = async (event: DrpcResponseStateChangedEvent) => { + const { drpcMessageRecord } = event.payload + await callback({ + drpcMessageRecord, + removeListener: () => this.eventEmitter.off(DrpcResponseEventTypes.DrpcResponseStateChanged, listener), + }) + } + this.eventEmitter.on(DrpcResponseEventTypes.DrpcResponseStateChanged, listener) + return () => { + this.eventEmitter.off(DrpcResponseEventTypes.DrpcResponseStateChanged, listener) + } + } + + public async receiveResponse(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + const drpcMessageRecord = await this.findByThreadAndConnectionId( + messageContext.agentContext, + connection.id, + messageContext.message.threadId + ) + + if (!drpcMessageRecord) { + throw new Error('DRPC message record not found') + } + + drpcMessageRecord.assertRole(DrpcRole.Client) + drpcMessageRecord.assertState(DrpcState.RequestSent) + drpcMessageRecord.response = messageContext.message.response + drpcMessageRecord.request = undefined + + await this.updateState(messageContext.agentContext, drpcMessageRecord, DrpcState.Completed) + return drpcMessageRecord + } + + public async receiveRequest(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + const record = await this.findByThreadAndConnectionId( + messageContext.agentContext, + connection.id, + messageContext.message.threadId + ) + + if (record) { + throw new Error('DRPC message record already exists') + } + const drpcMessageRecord = new DrpcRecord({ + request: messageContext.message.request, + connectionId: connection.id, + role: DrpcRole.Server, + state: DrpcState.RequestReceived, + threadId: messageContext.message.id, + }) + + await this.drpcMessageRepository.save(messageContext.agentContext, drpcMessageRecord) + this.emitStateChangedEvent(messageContext.agentContext, drpcMessageRecord) + return drpcMessageRecord + } + + private emitStateChangedEvent(agentContext: AgentContext, drpcMessageRecord: DrpcRecord) { + if ( + drpcMessageRecord.request && + (isValidDrpcRequest(drpcMessageRecord.request) || + (Array.isArray(drpcMessageRecord.request) && + drpcMessageRecord.request.length > 0 && + isValidDrpcRequest(drpcMessageRecord.request[0]))) + ) { + this.eventEmitter.emit(agentContext, { + type: DrpcRequestEventTypes.DrpcRequestStateChanged, + payload: { drpcMessageRecord: drpcMessageRecord.clone() }, + }) + } else if ( + drpcMessageRecord.response && + (isValidDrpcResponse(drpcMessageRecord.response) || + (Array.isArray(drpcMessageRecord.response) && + drpcMessageRecord.response.length > 0 && + isValidDrpcResponse(drpcMessageRecord.response[0]))) + ) { + this.eventEmitter.emit(agentContext, { + type: DrpcResponseEventTypes.DrpcResponseStateChanged, + payload: { drpcMessageRecord: drpcMessageRecord.clone() }, + }) + } + } + + private async updateState(agentContext: AgentContext, drpcRecord: DrpcRecord, newState: DrpcState) { + drpcRecord.state = newState + await this.drpcMessageRepository.update(agentContext, drpcRecord) + + this.emitStateChangedEvent(agentContext, drpcRecord) + } + + public findByThreadAndConnectionId( + agentContext: AgentContext, + connectionId: string, + threadId: string + ): Promise { + return this.drpcMessageRepository.findSingleByQuery(agentContext, { + connectionId, + threadId, + }) + } + + public async findAllByQuery(agentContext: AgentContext, query: Query, queryOptions?: QueryOptions) { + return this.drpcMessageRepository.findByQuery(agentContext, query, queryOptions) + } + + public async getById(agentContext: AgentContext, drpcMessageRecordId: string) { + return this.drpcMessageRepository.getById(agentContext, drpcMessageRecordId) + } + + public async deleteById(agentContext: AgentContext, drpcMessageRecordId: string) { + const drpcMessageRecord = await this.getById(agentContext, drpcMessageRecordId) + return this.drpcMessageRepository.delete(agentContext, drpcMessageRecord) + } +} diff --git a/packages/drpc/src/services/index.ts b/packages/drpc/src/services/index.ts new file mode 100644 index 0000000000..86d88f20c4 --- /dev/null +++ b/packages/drpc/src/services/index.ts @@ -0,0 +1 @@ +export * from './DrpcService' diff --git a/packages/drpc/tests/drpc-messages.e2e.test.ts b/packages/drpc/tests/drpc-messages.e2e.test.ts new file mode 100644 index 0000000000..002d8d98d8 --- /dev/null +++ b/packages/drpc/tests/drpc-messages.e2e.test.ts @@ -0,0 +1,308 @@ +import type { ConnectionRecord } from '../../core/src/modules/connections' +import type { DrpcRequest, DrpcRequestObject, DrpcResponseObject } from '../src/messages' + +import { Agent } from '../../core/src/agent/Agent' +import { setupSubjectTransports } from '../../core/tests' +import { getInMemoryAgentOptions, makeConnection } from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' +import { DrpcModule } from '../src/DrpcModule' +import { DrpcErrorCode } from '../src/models' + +const modules = { + drpc: new DrpcModule(), +} + +const faberConfig = getInMemoryAgentOptions( + 'Faber Drpc Messages', + { + endpoints: ['rxjs:faber'], + }, + modules +) + +const aliceConfig = getInMemoryAgentOptions( + 'Alice Drpc Messages', + { + endpoints: ['rxjs:alice'], + }, + modules +) + +const handleMessageOrError = async ( + handlers: Map Promise>>, + message: DrpcRequestObject +) => { + const handler = handlers.get(message.method) + if (handler) { + return handler(message) + } + return { + jsonrpc: '2.0', + id: message.id, + error: { code: DrpcErrorCode.METHOD_NOT_FOUND, message: 'Method not found' }, + } +} + +const sendAndRecieve = async ( + sender: Agent, + receiver: Agent, + connectionRecord: ConnectionRecord, + message: DrpcRequestObject, + messageHandlers: Map Promise>> +) => { + const responseListener = await sender.modules.drpc.sendRequest(connectionRecord.id, message) + const { request, sendResponse } = await receiver.modules.drpc.recvRequest() + const result = await handleMessageOrError(messageHandlers, request as DrpcRequestObject) + await sendResponse(result as DrpcResponseObject) + + const helloRecord = await responseListener() + return helloRecord as DrpcResponseObject +} + +const sendAndRecieveBatch = async ( + sender: Agent, + receiver: Agent, + connectionRecord: ConnectionRecord, + message: DrpcRequestObject[], + messageHandlers: Map Promise>> +) => { + const responseListener = await sender.modules.drpc.sendRequest(connectionRecord.id, message) + const { request: batchRequest, sendResponse: sendBatchResponse } = await receiver.modules.drpc.recvRequest() + const batchRequests = batchRequest as DrpcRequestObject[] + const batchResults: (DrpcResponseObject | Record)[] = [] + for (const request of batchRequests) { + batchResults.push(await handleMessageOrError(messageHandlers, request)) + } + await sendBatchResponse(batchResults) + const batchRecord = await responseListener() + return batchRecord as DrpcResponseObject[] +} + +describe('Drpc Messages E2E', () => { + let faberAgent: Agent + let aliceAgent: Agent + let aliceConnection: ConnectionRecord + let messageHandlers: Map Promise>> + + beforeEach(async () => { + faberAgent = new Agent(faberConfig) + aliceAgent = new Agent(aliceConfig) + + setupSubjectTransports([faberAgent, aliceAgent]) + + await faberAgent.initialize() + await aliceAgent.initialize() + ;[aliceConnection] = await makeConnection(aliceAgent, faberAgent) + + messageHandlers = new Map() + messageHandlers.set('hello', async (message: DrpcRequestObject) => { + return { jsonrpc: '2.0', result: 'Hello', id: message.id } + }) + messageHandlers.set('add', async (message) => { + const operands = message.params as number[] + const result = operands.reduce((a, b) => a + b, 0) + return { jsonrpc: '2.0', result, id: message.id } + }) + messageHandlers.set('parseFoo', async (message) => { + const params = message.params as { foo: string } + return { jsonrpc: '2.0', result: params.foo, id: message.id } + }) + }) + + afterEach(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice and Faber exchange messages', async () => { + testLogger.test('Alice sends message to Faber') + const helloRecord = await sendAndRecieve( + aliceAgent, + faberAgent, + aliceConnection, + { + jsonrpc: '2.0', + method: 'hello', + id: 1, + }, + messageHandlers + ) + expect((helloRecord as DrpcResponseObject).result).toBe('Hello') + + testLogger.test('Alice sends message with positional parameters to Faber') + + const addRecord = await sendAndRecieve( + aliceAgent, + faberAgent, + aliceConnection, + { + jsonrpc: '2.0', + method: 'add', + params: [2, 3, 7], + id: 2, + }, + messageHandlers + ) + + expect((addRecord as DrpcResponseObject).result).toBe(12) + + testLogger.test('Alice sends message with keyed parameters to Faber') + + const parseFooRecord = await sendAndRecieve( + aliceAgent, + faberAgent, + aliceConnection, + { + jsonrpc: '2.0', + method: 'parseFoo', + params: { foo: 'bar' }, + id: 3, + }, + messageHandlers + ) + expect((parseFooRecord as DrpcResponseObject).result).toBe('bar') + + testLogger.test('Alice sends message with invalid method to Faber') + + const errorRecord = await sendAndRecieve( + aliceAgent, + faberAgent, + aliceConnection, + { + jsonrpc: '2.0', + method: 'error', + id: 4, + }, + messageHandlers + ) + expect((errorRecord as DrpcResponseObject).error).toBeDefined() + expect((errorRecord as DrpcResponseObject).error?.code).toBe(DrpcErrorCode.METHOD_NOT_FOUND) + }) + + test('Alice sends Faber Drpc batch message', async () => { + testLogger.test('Alice sends batch message to Faber') + + const batchRecord = await sendAndRecieveBatch( + aliceAgent, + faberAgent, + aliceConnection, + [ + { jsonrpc: '2.0', method: 'hello', id: 1 }, + { jsonrpc: '2.0', method: 'add', params: [2, 3, 7], id: 2 }, + { jsonrpc: '2.0', method: 'parseFoo', params: { foo: 'bar' }, id: 3 }, + { jsonrpc: '2.0', method: 'error', id: 4 }, + ], + messageHandlers + ) + expect(batchRecord as DrpcResponseObject[]).toHaveLength(4) + expect((batchRecord as DrpcResponseObject[]).find((item) => item.id === 1)?.result).toBe('Hello') + expect((batchRecord as DrpcResponseObject[]).find((item) => item.id === 2)?.result).toBe(12) + expect((batchRecord as DrpcResponseObject[]).find((item) => item.id === 3)?.result).toBe('bar') + expect((batchRecord as DrpcResponseObject[]).find((item) => item.id === 4)?.error).toBeDefined() + expect((batchRecord as DrpcResponseObject[]).find((item) => item.id === 4)?.error?.code).toBe( + DrpcErrorCode.METHOD_NOT_FOUND + ) + }) + + test('Alice sends Faber Drpc notification', async () => { + testLogger.test('Alice sends notification to Faber') + let notified = false + messageHandlers.set('notify', async () => { + notified = true + return {} + }) + const notifyRecord = await sendAndRecieve( + aliceAgent, + faberAgent, + aliceConnection, + { + jsonrpc: '2.0', + method: 'notify', + id: null, + }, + messageHandlers + ) + expect(notifyRecord).toMatchObject({}) + expect(notified).toBe(true) + + testLogger.test('Alice sends batch notification to Faber') + notified = false + + const notifyBatchRecord = await sendAndRecieveBatch( + aliceAgent, + faberAgent, + aliceConnection, + [ + { jsonrpc: '2.0', method: 'hello', id: 1 }, + { jsonrpc: '2.0', method: 'notify', id: null }, + ], + messageHandlers + ) + expect( + (notifyBatchRecord as (DrpcResponseObject | Record)[]).find( + (item) => (item as DrpcResponseObject)?.id === 1 + ) + ).toMatchObject({ jsonrpc: '2.0', result: 'Hello', id: 1 }) + expect( + (notifyBatchRecord as (DrpcResponseObject | Record)[]).find( + (item) => !(item as DrpcResponseObject)?.id + ) + ).toMatchObject({}) + expect(notified).toBe(true) + }) + + test('Alice sends Faber invalid Drpc message | Faber responds with invalid Drpc message', async () => { + messageHandlers.set('hello', async () => { + return [] as unknown as DrpcResponseObject + }) + let error = false + try { + await aliceAgent.modules.drpc.sendRequest(aliceConnection.id, 'test' as unknown as DrpcRequest) + } catch { + error = true + } + expect(error).toBe(true) + await aliceAgent.modules.drpc.sendRequest(aliceConnection.id, { + jsonrpc: '2.0', + method: 'hello', + id: 1, + }) + const { request, sendResponse } = await faberAgent.modules.drpc.recvRequest() + const result = await handleMessageOrError(messageHandlers, request as DrpcRequestObject) + let responseError = false + try { + await sendResponse(result as DrpcResponseObject) + } catch { + responseError = true + } + expect(responseError).toBe(true) + }) + + test('Request times out', async () => { + // recvRequest timeout + setTimeout(async () => { + await aliceAgent.modules.drpc.sendRequest(aliceConnection.id, { jsonrpc: '2.0', method: 'hello', id: 1 }) + }, 500) + const req = await faberAgent.modules.drpc.recvRequest(100) + expect(req).toBe(undefined) + + // response listener timeout + const responseListener = await aliceAgent.modules.drpc.sendRequest(aliceConnection.id, { + jsonrpc: '2.0', + method: 'hello', + id: 1, + }) + const { request, sendResponse } = await faberAgent.modules.drpc.recvRequest() + setTimeout(async () => { + const result = await handleMessageOrError(messageHandlers, request) + sendResponse(result as DrpcResponseObject) + }, 500) + + const helloRecord = await responseListener(100) + expect(helloRecord).toBe(undefined) + + await new Promise((r) => setTimeout(r, 1500)) + }) +}) diff --git a/packages/drpc/tests/setup.ts b/packages/drpc/tests/setup.ts new file mode 100644 index 0000000000..78143033f2 --- /dev/null +++ b/packages/drpc/tests/setup.ts @@ -0,0 +1,3 @@ +import 'reflect-metadata' + +jest.setTimeout(120000) diff --git a/packages/drpc/tsconfig.build.json b/packages/drpc/tsconfig.build.json new file mode 100644 index 0000000000..2b75d0adab --- /dev/null +++ b/packages/drpc/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build" + }, + "include": ["src/**/*"] +} diff --git a/packages/drpc/tsconfig.json b/packages/drpc/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/drpc/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/packages/indy-sdk-to-askar-migration/CHANGELOG.md b/packages/indy-sdk-to-askar-migration/CHANGELOG.md new file mode 100644 index 0000000000..fac41855b7 --- /dev/null +++ b/packages/indy-sdk-to-askar-migration/CHANGELOG.md @@ -0,0 +1,82 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release +- Updated dependencies [66e696d] + - @credo-ts/anoncreds@0.5.6 + - @credo-ts/askar@0.5.6 + - @credo-ts/core@0.5.6 + - @credo-ts/node@0.5.6 + +## 0.5.5 + +### Patch Changes + +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) +- Updated dependencies [3239ef3] +- Updated dependencies [d548fa4] +- Updated dependencies [482a630] + - @credo-ts/core@0.5.5 + - @credo-ts/anoncreds@0.5.5 + - @credo-ts/askar@0.5.5 + - @credo-ts/node@0.5.5 + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +**Note:** Version bump only for package @credo-ts/indy-sdk-to-askar-migration + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +### Bug Fixes + +- node-ffi-napi compatibility ([#1821](https://github.com/openwallet-foundation/credo-ts/issues/1821)) ([81d351b](https://github.com/openwallet-foundation/credo-ts/commit/81d351bc9d4d508ebfac9e7f2b2f10276ab1404a)) + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +**Note:** Version bump only for package @credo-ts/indy-sdk-to-askar-migration + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +**Note:** Version bump only for package @credo-ts/indy-sdk-to-askar-migration + +## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) + +**Note:** Version bump only for package @credo-ts/indy-sdk-to-askar-migration + +## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) + +**Note:** Version bump only for package @credo-ts/indy-sdk-to-askar-migration + +# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) + +### Bug Fixes + +- **askar:** default key derivation method ([#1420](https://github.com/hyperledger/aries-framework-javascript/issues/1420)) ([7b59629](https://github.com/hyperledger/aries-framework-javascript/commit/7b5962917488cfd0c5adc170d3c3fc64aa82ef2c)) +- migration of link secret ([#1444](https://github.com/hyperledger/aries-framework-javascript/issues/1444)) ([9a43afe](https://github.com/hyperledger/aries-framework-javascript/commit/9a43afec7ea72a6fa8c6133f0fad05d8a3d2a595)) +- remove `deleteOnFinish` and added documentation ([#1418](https://github.com/hyperledger/aries-framework-javascript/issues/1418)) ([c8b16a6](https://github.com/hyperledger/aries-framework-javascript/commit/c8b16a6fec8bb693e67e65709ded05d19fd1919f)) +- small issues with migration and WAL files ([#1443](https://github.com/hyperledger/aries-framework-javascript/issues/1443)) ([83cf387](https://github.com/hyperledger/aries-framework-javascript/commit/83cf387fa52bb51d8adb2d5fedc5111994d4dde1)) + +### Features + +- **anoncreds:** store method name in records ([#1387](https://github.com/hyperledger/aries-framework-javascript/issues/1387)) ([47636b4](https://github.com/hyperledger/aries-framework-javascript/commit/47636b4a08ffbfa9a3f2a5a3c5aebda44f7d16c8)) +- indy sdk aries askar migration script ([#1289](https://github.com/hyperledger/aries-framework-javascript/issues/1289)) ([4a6b99c](https://github.com/hyperledger/aries-framework-javascript/commit/4a6b99c617de06edbaf1cb07c8adfa8de9b3ec15)) diff --git a/packages/indy-sdk-to-askar-migration/README.md b/packages/indy-sdk-to-askar-migration/README.md new file mode 100644 index 0000000000..5fc388662d --- /dev/null +++ b/packages/indy-sdk-to-askar-migration/README.md @@ -0,0 +1,31 @@ +

+
+ Credo Logo +

+

Credo Indy SDK To Askar Migration Module

+

+ License + typescript + @credo-ts/indy-sdk-to-askar-migration version + +

+
+ +Credo Indy SDK to Askar migration provides migration from the legacy and deprecated Indy SDK to Aries Askar. See the [Indy SDK to Askar Migration Guide](https://credo.js.org/guides/updating/update-indy-sdk-to-askar) for instructions. diff --git a/packages/indy-sdk-to-askar-migration/jest.config.ts b/packages/indy-sdk-to-askar-migration/jest.config.ts new file mode 100644 index 0000000000..93c0197296 --- /dev/null +++ b/packages/indy-sdk-to-askar-migration/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/indy-sdk-to-askar-migration/package.json b/packages/indy-sdk-to-askar-migration/package.json new file mode 100644 index 0000000000..f4f61e6599 --- /dev/null +++ b/packages/indy-sdk-to-askar-migration/package.json @@ -0,0 +1,43 @@ +{ + "name": "@credo-ts/indy-sdk-to-askar-migration", + "main": "src/index", + "types": "src/index", + "version": "0.5.6", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/indy-sdk-to-askar-migration", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/indy-sdk-to-askar-migration" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build", + "test": "jest" + }, + "dependencies": { + "@credo-ts/anoncreds": "workspace:*", + "@credo-ts/askar": "workspace:*", + "@credo-ts/core": "workspace:*", + "@credo-ts/node": "workspace:*" + }, + "devDependencies": { + "@hyperledger/aries-askar-nodejs": "^0.2.1", + "@hyperledger/aries-askar-shared": "^0.2.1", + "rimraf": "^4.4.0", + "typescript": "~5.5.2" + }, + "peerDependencies": { + "@hyperledger/aries-askar-shared": "^0.2.1" + } +} diff --git a/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts b/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts new file mode 100644 index 0000000000..3d9f51aced --- /dev/null +++ b/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts @@ -0,0 +1,422 @@ +import type { AnonCredsCredentialValue } from '@credo-ts/anoncreds' +import type { Agent, FileSystem, WalletConfig } from '@credo-ts/core' +import type { EntryObject } from '@hyperledger/aries-askar-shared' + +import { AnonCredsCredentialRecord, AnonCredsLinkSecretRecord } from '@credo-ts/anoncreds' +import { AskarWallet } from '@credo-ts/askar' +import { InjectionSymbols, KeyDerivationMethod, JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' +import { Migration, Key, KeyAlgs, Store } from '@hyperledger/aries-askar-shared' + +import { IndySdkToAskarMigrationError } from './errors/IndySdkToAskarMigrationError' +import { keyDerivationMethodToStoreKeyMethod, transformFromRecordTagValues } from './utils' + +/** + * + * Migration class to move a wallet form the indy-sdk structure to the new + * askar wallet structure. + * + * Right now, this is ONLY supported within React Native environments AND only sqlite. + * + * The reason it only works within React Native is that we ONLY update the + * keys, masterSecret and credentials for now. If you have an agent in Node.JS + * where it only contains these records, it may be used but we cannot + * guarantee a successful migration. + * + */ +export class IndySdkToAskarMigrationUpdater { + private store?: Store + private walletConfig: WalletConfig + private defaultLinkSecretId: string + private agent: Agent + private dbPath: string + private fs: FileSystem + + private constructor(walletConfig: WalletConfig, agent: Agent, dbPath: string, defaultLinkSecretId?: string) { + this.walletConfig = walletConfig + this.dbPath = dbPath + this.agent = agent + this.fs = this.agent.dependencyManager.resolve(InjectionSymbols.FileSystem) + this.defaultLinkSecretId = defaultLinkSecretId ?? walletConfig.id + } + + public static async initialize({ + dbPath, + agent, + defaultLinkSecretId, + }: { + dbPath: string + agent: Agent + defaultLinkSecretId?: string + }) { + const { + config: { walletConfig }, + } = agent + if (typeof process?.versions?.node !== 'undefined') { + agent.config.logger.warn( + 'Node.JS is not fully supported. Using this will likely leave the wallet in a half-migrated state' + ) + } + + if (!walletConfig) { + throw new IndySdkToAskarMigrationError('Wallet config is required for updating the wallet') + } + + if (walletConfig.storage && walletConfig.storage.type !== 'sqlite') { + throw new IndySdkToAskarMigrationError('Only sqlite wallets are supported, right now') + } + + if (agent.isInitialized) { + throw new IndySdkToAskarMigrationError('Wallet migration can not be done on an initialized agent') + } + + if (!(agent.dependencyManager.resolve(InjectionSymbols.Wallet) instanceof AskarWallet)) { + throw new IndySdkToAskarMigrationError("Wallet on the agent must be of instance 'AskarWallet'") + } + + return new IndySdkToAskarMigrationUpdater(walletConfig, agent, dbPath, defaultLinkSecretId) + } + + /** + * This function migrates the old database to the new structure. + * + * This doubles checks some fields as later it might be possible to run this function + */ + private async migrate() { + const specUri = this.backupFile + const kdfLevel = this.walletConfig.keyDerivationMethod ?? KeyDerivationMethod.Argon2IMod + const walletName = this.walletConfig.id + const walletKey = this.walletConfig.key + const storageType = this.walletConfig.storage?.type ?? 'sqlite' + + if (storageType !== 'sqlite') { + throw new IndySdkToAskarMigrationError("Storage type defined and not of type 'sqlite'") + } + + if (!walletKey) { + throw new IndySdkToAskarMigrationError('Wallet key is not defined in the wallet configuration') + } + + this.agent.config.logger.info('Migration indy-sdk database structure to askar') + await Migration.migrate({ specUri, walletKey, kdfLevel, walletName }) + } + + /* + * Checks whether the destination locations are already used. This might + * happen if you want to migrate a wallet when you already have a new wallet + * with the same id. + */ + private async assertDestinationsAreFree() { + const areAllDestinationsTaken = + (await this.fs.exists(this.backupFile)) || (await this.fs.exists(this.newWalletPath)) + + if (areAllDestinationsTaken) { + throw new IndySdkToAskarMigrationError( + `Files already exist at paths that will be used for backing up. Please remove them manually. Backup path: '${this.backupFile}' and new wallet path: ${this.newWalletPath} ` + ) + } + } + + /** + * Location of the new wallet + */ + public get newWalletPath() { + return `${this.fs.dataPath}/wallet/${this.walletConfig.id}/sqlite.db` + } + + /** + * Temporary backup location of the pre-migrated script + */ + private get backupFile() { + return `${this.fs.tempPath}/${this.walletConfig.id}.db` + } + + private async copyDatabaseWithOptionalWal(src: string, dest: string) { + // Copy the supplied database to the backup destination + await this.fs.copyFile(src, dest) + + // If a wal-file is included, also copy it (https://www.sqlite.org/wal.html) + if (await this.fs.exists(`${src}-wal`)) { + await this.fs.copyFile(`${src}-wal`, `${dest}-wal`) + } + } + + /** + * Backup the database file. This function makes sure that the the indy-sdk + * database file is backed up within our temporary directory path. If some + * error occurs, `this.revertDatabase()` will be called to revert the backup. + */ + private async backupDatabase() { + const src = this.dbPath + const dest = this.backupFile + this.agent.config.logger.trace(`Creating backup from '${src}' to '${dest}'`) + + // Create the directories for the backup + await this.fs.createDirectory(dest) + + // Copy the supplied database to the backup destination, with optional wal-file + await this.copyDatabaseWithOptionalWal(src, dest) + + if (!(await this.fs.exists(dest))) { + throw new IndySdkToAskarMigrationError('Could not locate the new backup file') + } + } + + // Delete the backup as `this.fs.copyFile` only copies and no deletion + // Since we use `tempPath` which is cleared when certain events happen, + // e.g. cron-job and system restart (depending on the os) we could omit + // this call `await this.fs.delete(this.backupFile)`. + private async cleanBackup() { + this.agent.config.logger.trace(`Deleting the backup file at '${this.backupFile}'`) + await this.fs.delete(this.backupFile) + + // Also delete wal-file if it exists + if (await this.fs.exists(`${this.backupFile}-wal`)) { + await this.fs.delete(`${this.backupFile}-wal`) + } + } + + /** + * Move the migrated and updated database file to the new location according + * to the `FileSystem.dataPath`. + */ + private async moveToNewLocation() { + const src = this.backupFile + // New path for the database + const dest = this.newWalletPath + + // create the wallet directory + await this.fs.createDirectory(dest) + + this.agent.config.logger.trace(`Moving upgraded database from ${src} to ${dest}`) + + // Copy the file from the database path to the new location, with optional wal-file + await this.copyDatabaseWithOptionalWal(src, dest) + } + + /** + * Function that updates the values from an indy-sdk structure to the new askar structure. + * + * > NOTE: It is very important that this script is ran before the 0.3.x to + * 0.4.x migration script. This can easily be done by calling this when you + * upgrade, before you initialize the agent with `autoUpdateStorageOnStartup: + * true`. + * + * - Assert that the paths that will be used are free + * - Create a backup of the database + * - Migrate the database to askar structure + * - Update the Keys + * - Update the Master Secret (Link Secret) + * - Update the credentials + * If any of those failed: + * - Revert the database + * - Clear the backup from the temporary directory + */ + public async update() { + await this.assertDestinationsAreFree() + + await this.backupDatabase() + try { + // Migrate the database + await this.migrate() + + const keyMethod = keyDerivationMethodToStoreKeyMethod( + this.walletConfig.keyDerivationMethod ?? KeyDerivationMethod.Argon2IMod + ) + this.store = await Store.open({ uri: `sqlite://${this.backupFile}`, passKey: this.walletConfig.key, keyMethod }) + + // Update the values to reflect the new structure + await this.updateKeys() + await this.updateCredentialDefinitions() + await this.updateMasterSecret() + await this.updateCredentials() + + // Move the migrated and updated file to the expected location for credo + await this.moveToNewLocation() + } catch (err) { + this.agent.config.logger.error(`Migration failed. Restoring state. ${err.message}`) + + throw new IndySdkToAskarMigrationError(`Migration failed. State has been restored. ${err.message}`, { + cause: err.cause, + }) + } finally { + await this.cleanBackup() + } + } + + private async updateKeys() { + if (!this.store) { + throw new IndySdkToAskarMigrationError('Update keys can not be called outside of the `update()` function') + } + + const category = 'Indy::Key' + + this.agent.config.logger.info(`Migrating category: ${category}`) + + let updateCount = 0 + const session = this.store.transaction() + for (;;) { + const txn = await session.open() + const keys = await txn.fetchAll({ category, limit: 50 }) + if (!keys || keys.length === 0) { + await txn.close() + break + } + + for (const row of keys) { + this.agent.config.logger.debug(`Migrating ${row.name} to the new askar format`) + const signKey: string = JSON.parse(row.value as string).signkey + const keySk = TypedArrayEncoder.fromBase58(signKey) + const key = Key.fromSecretBytes({ + algorithm: KeyAlgs.Ed25519, + secretKey: new Uint8Array(keySk.slice(0, 32)), + }) + await txn.insertKey({ name: row.name, key }) + + await txn.remove({ category, name: row.name }) + key.handle.free() + updateCount++ + } + await txn.commit() + } + + this.agent.config.logger.info(`Migrated ${updateCount} records of type ${category}`) + } + + private async updateCredentialDefinitions() { + if (!this.store) { + throw new IndySdkToAskarMigrationError('Update keys can not be called outside of the `update()` function') + } + + const category = 'Indy::CredentialDefinition' + + this.agent.config.logger.info(`Migrating category: ${category}`) + + const session = this.store.transaction() + for (;;) { + const txn = await session.open() + const keys = await txn.fetchAll({ category, limit: 50 }) + if (!keys || keys.length === 0) { + await txn.close() + break + } else { + // This will be entered if there are credential definitions in the wallet + await txn.close() + throw new IndySdkToAskarMigrationError('Migration of Credential Definitions is not yet supported') + } + } + } + + private async updateMasterSecret() { + if (!this.store) { + throw new IndySdkToAskarMigrationError( + 'Update master secret can not be called outside of the `update()` function' + ) + } + + const category = 'Indy::MasterSecret' + + this.agent.config.logger.info(`Migrating category: ${category}`) + + let updateCount = 0 + const session = this.store.transaction() + + for (;;) { + const txn = await session.open() + const masterSecrets = await txn.fetchAll({ category, limit: 50 }) + if (!masterSecrets || masterSecrets.length === 0) { + await txn.close() + break + } + + if (!masterSecrets.some((ms: EntryObject) => ms.name === this.defaultLinkSecretId)) { + throw new IndySdkToAskarMigrationError('defaultLinkSecretId can not be established.') + } + + this.agent.config.logger.info(`Default link secret id for migration is ${this.defaultLinkSecretId}`) + + for (const row of masterSecrets) { + this.agent.config.logger.debug(`Migrating ${row.name} to the new askar format`) + + const isDefault = masterSecrets.length === 0 || row.name === this.walletConfig.id + + const { + value: { ms }, + } = JSON.parse(row.value as string) as { value: { ms: string } } + + const record = new AnonCredsLinkSecretRecord({ linkSecretId: row.name, value: ms }) + record.setTag('isDefault', isDefault) + const value = JsonTransformer.serialize(record) + + const tags = transformFromRecordTagValues(record.getTags()) + + await txn.insert({ category: record.type, name: record.id, value, tags }) + + await txn.remove({ category, name: row.name }) + updateCount++ + } + await txn.commit() + } + + this.agent.config.logger.info(`Migrated ${updateCount} records of type ${category}`) + } + + private async updateCredentials() { + if (!this.store) { + throw new IndySdkToAskarMigrationError('Update credentials can not be called outside of the `update()` function') + } + + const category = 'Indy::Credential' + + this.agent.config.logger.info(`Migrating category: ${category}`) + + let updateCount = 0 + const session = this.store.transaction() + for (;;) { + const txn = await session.open() + const credentials = await txn.fetchAll({ category, limit: 50 }) + if (!credentials || credentials.length === 0) { + await txn.close() + break + } + + for (const row of credentials) { + this.agent.config.logger.debug(`Migrating ${row.name} to the new askar format`) + const data = JSON.parse(row.value as string) as { + schema_id: string + cred_def_id: string + rev_reg_id?: string + values: Record + signature: Record + signature_correctness_proof: Record + rev_reg?: Record + witness?: Record + } + const [issuerId] = data.cred_def_id.split(':') + const [schemaIssuerId, , schemaName, schemaVersion] = data.schema_id.split(':') + + const record = new AnonCredsCredentialRecord({ + credential: data, + issuerId, + schemaName, + schemaIssuerId, + schemaVersion, + credentialId: row.name, + linkSecretId: this.defaultLinkSecretId, + // Hardcode methodName to indy as all IndySDK credentials are indy credentials + methodName: 'indy', + }) + + const tags = transformFromRecordTagValues(record.getTags()) + const value = JsonTransformer.serialize(record) + + await txn.insert({ category: record.type, name: record.id, value, tags }) + + await txn.remove({ category, name: row.name }) + updateCount++ + } + await txn.commit() + } + + this.agent.config.logger.info(`Migrated ${updateCount} records of type ${category}`) + } +} diff --git a/packages/indy-sdk-to-askar-migration/src/errors/IndySdkToAskarMigrationError.ts b/packages/indy-sdk-to-askar-migration/src/errors/IndySdkToAskarMigrationError.ts new file mode 100644 index 0000000000..287fdb507c --- /dev/null +++ b/packages/indy-sdk-to-askar-migration/src/errors/IndySdkToAskarMigrationError.ts @@ -0,0 +1,6 @@ +import { CredoError } from '@credo-ts/core' + +/** + * @internal + */ +export class IndySdkToAskarMigrationError extends CredoError {} diff --git a/packages/indy-sdk-to-askar-migration/src/index.ts b/packages/indy-sdk-to-askar-migration/src/index.ts new file mode 100644 index 0000000000..daac1c7b49 --- /dev/null +++ b/packages/indy-sdk-to-askar-migration/src/index.ts @@ -0,0 +1 @@ +export { IndySdkToAskarMigrationUpdater } from './IndySdkToAskarMigrationUpdater' diff --git a/packages/indy-sdk-to-askar-migration/src/utils.ts b/packages/indy-sdk-to-askar-migration/src/utils.ts new file mode 100644 index 0000000000..b1dff7343d --- /dev/null +++ b/packages/indy-sdk-to-askar-migration/src/utils.ts @@ -0,0 +1,53 @@ +import type { TagsBase } from '@credo-ts/core' + +import { KeyDerivationMethod } from '@credo-ts/core' +import { KdfMethod, StoreKeyMethod } from '@hyperledger/aries-askar-shared' + +/** + * Adopted from `AskarStorageService` implementation and should be kept in sync. + */ +export const transformFromRecordTagValues = (tags: TagsBase): { [key: string]: string | undefined } => { + const transformedTags: { [key: string]: string | undefined } = {} + + for (const [key, value] of Object.entries(tags)) { + // If the value is of type null we use the value undefined + // Askar doesn't support null as a value + if (value === null) { + transformedTags[key] = undefined + } + // If the value is a boolean use the Askar + // '1' or '0' syntax + else if (typeof value === 'boolean') { + transformedTags[key] = value ? '1' : '0' + } + // If the value is 1 or 0, we need to add something to the value, otherwise + // the next time we deserialize the tag values it will be converted to boolean + else if (value === '1' || value === '0') { + transformedTags[key] = `n__${value}` + } + // If the value is an array we create a tag for each array + // item ("tagName:arrayItem" = "1") + else if (Array.isArray(value)) { + value.forEach((item) => { + const tagName = `${key}:${item}` + transformedTags[tagName] = '1' + }) + } + // Otherwise just use the value + else { + transformedTags[key] = value + } + } + + return transformedTags +} + +export const keyDerivationMethodToStoreKeyMethod = (keyDerivationMethod: KeyDerivationMethod) => { + const correspondenceTable = { + [KeyDerivationMethod.Raw]: KdfMethod.Raw, + [KeyDerivationMethod.Argon2IInt]: KdfMethod.Argon2IInt, + [KeyDerivationMethod.Argon2IMod]: KdfMethod.Argon2IMod, + } + + return new StoreKeyMethod(correspondenceTable[keyDerivationMethod]) +} diff --git a/packages/indy-sdk-to-askar-migration/tests/indy-sdk-040-wallet.db b/packages/indy-sdk-to-askar-migration/tests/indy-sdk-040-wallet.db new file mode 100644 index 0000000000..1fc1e28b40 Binary files /dev/null and b/packages/indy-sdk-to-askar-migration/tests/indy-sdk-040-wallet.db differ diff --git a/packages/indy-sdk-to-askar-migration/tests/migrate.test.ts b/packages/indy-sdk-to-askar-migration/tests/migrate.test.ts new file mode 100644 index 0000000000..c1e69a2961 --- /dev/null +++ b/packages/indy-sdk-to-askar-migration/tests/migrate.test.ts @@ -0,0 +1,97 @@ +import type { InitConfig } from '@credo-ts/core' + +import { KeyDerivationMethod, Agent } from '@credo-ts/core' +import { agentDependencies } from '@credo-ts/node' +import { copyFileSync, existsSync, mkdirSync, unlinkSync } from 'fs' +import { homedir } from 'os' +import path from 'path' + +import { askarModule } from '../../askar/tests/helpers' +import { IndySdkToAskarMigrationUpdater } from '../src' +import { IndySdkToAskarMigrationError } from '../src/errors/IndySdkToAskarMigrationError' + +describe('Indy SDK To Askar Migration', () => { + test('indy-sdk sqlite to aries-askar sqlite successful migration', async () => { + const indySdkAndAskarConfig: InitConfig = { + label: `indy | indy-sdk sqlite to aries-askar sqlite successful migration`, + walletConfig: { + id: `indy-sdk sqlite to aries-askar sqlite successful migration`, + key: 'GfwU1DC7gEZNs3w41tjBiZYj7BNToDoFEqKY6wZXqs1A', + keyDerivationMethod: KeyDerivationMethod.Raw, + }, + autoUpdateStorageOnStartup: true, + } + + const indySdkAgentDbPath = `${homedir()}/.indy_client/wallet/${indySdkAndAskarConfig.walletConfig?.id}/sqlite.db` + const indySdkWalletTestPath = path.join(__dirname, 'indy-sdk-040-wallet.db') + const askarAgent = new Agent({ + config: indySdkAndAskarConfig, + modules: { askar: askarModule }, + dependencies: agentDependencies, + }) + const updater = await IndySdkToAskarMigrationUpdater.initialize({ dbPath: indySdkAgentDbPath, agent: askarAgent }) + + // Remove new wallet path (if exists) + if (existsSync(updater.newWalletPath)) unlinkSync(updater.newWalletPath) + + // Create old wallet path and copy test wallet + mkdirSync(path.dirname(indySdkAgentDbPath), { recursive: true }) + copyFileSync(indySdkWalletTestPath, indySdkAgentDbPath) + + await updater.update() + await askarAgent.initialize() + + await expect(askarAgent.genericRecords.getAll()).resolves.toMatchObject([ + { + content: { + foo: 'bar', + }, + }, + ]) + + await askarAgent.shutdown() + }) + + /* + * - Initialize an agent + * - Save a generic record + * - try to migrate with invalid state (wrong key) + * - Migration will be attempted, fails, and restores + * - Check if the record can still be accessed + */ + test('indy-sdk sqlite to aries-askar sqlite fails and restores', async () => { + const indySdkAndAskarConfig: InitConfig = { + label: `indy | indy-sdk sqlite to aries-askar sqlite fails and restores`, + walletConfig: { + id: `indy-sdk sqlite to aries-askar sqlite fails and restores`, + // NOTE: wrong key passed + key: 'wrong-key', + keyDerivationMethod: KeyDerivationMethod.Raw, + }, + } + + const indySdkAgentDbPath = `${homedir()}/.indy_client/wallet/${indySdkAndAskarConfig.walletConfig?.id}/sqlite.db` + const indySdkWalletTestPath = path.join(__dirname, 'indy-sdk-040-wallet.db') + + const askarAgent = new Agent({ + config: indySdkAndAskarConfig, + modules: { askar: askarModule }, + dependencies: agentDependencies, + }) + + const updater = await IndySdkToAskarMigrationUpdater.initialize({ + dbPath: indySdkAgentDbPath, + agent: askarAgent, + }) + + // Remove new wallet path (if exists) + if (existsSync(updater.newWalletPath)) unlinkSync(updater.newWalletPath) + + // Create old wallet path and copy test wallet + mkdirSync(path.dirname(indySdkAgentDbPath), { recursive: true }) + copyFileSync(indySdkWalletTestPath, indySdkAgentDbPath) + + await expect(updater.update()).rejects.toThrow(IndySdkToAskarMigrationError) + expect(existsSync(indySdkWalletTestPath)).toBe(true) + }) +}) diff --git a/packages/indy-sdk-to-askar-migration/tests/setup.ts b/packages/indy-sdk-to-askar-migration/tests/setup.ts new file mode 100644 index 0000000000..226f7031fa --- /dev/null +++ b/packages/indy-sdk-to-askar-migration/tests/setup.ts @@ -0,0 +1 @@ +jest.setTimeout(20000) diff --git a/packages/indy-sdk-to-askar-migration/tsconfig.build.json b/packages/indy-sdk-to-askar-migration/tsconfig.build.json new file mode 100644 index 0000000000..2b75d0adab --- /dev/null +++ b/packages/indy-sdk-to-askar-migration/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build" + }, + "include": ["src/**/*"] +} diff --git a/packages/indy-sdk-to-askar-migration/tsconfig.json b/packages/indy-sdk-to-askar-migration/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/indy-sdk-to-askar-migration/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/packages/indy-vdr/CHANGELOG.md b/packages/indy-vdr/CHANGELOG.md new file mode 100644 index 0000000000..91e13b863d --- /dev/null +++ b/packages/indy-vdr/CHANGELOG.md @@ -0,0 +1,120 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release +- Updated dependencies [66e696d] + - @credo-ts/anoncreds@0.5.6 + - @credo-ts/core@0.5.6 + +## 0.5.5 + +### Patch Changes + +- d548fa4: feat: support new 'DIDCommMessaging' didcomm v2 service type (in addition to older 'DIDComm' service type) +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) +- Updated dependencies [3239ef3] +- Updated dependencies [d548fa4] +- Updated dependencies [482a630] + - @credo-ts/core@0.5.5 + - @credo-ts/anoncreds@0.5.5 + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +### Bug Fixes + +- store recipient keys by default ([#1847](https://github.com/openwallet-foundation/credo-ts/issues/1847)) ([e9238cf](https://github.com/openwallet-foundation/credo-ts/commit/e9238cfde4d76c5b927f6f76b3529d4c80808a3a)) + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +### Bug Fixes + +- node-ffi-napi compatibility ([#1821](https://github.com/openwallet-foundation/credo-ts/issues/1821)) ([81d351b](https://github.com/openwallet-foundation/credo-ts/commit/81d351bc9d4d508ebfac9e7f2b2f10276ab1404a)) + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +**Note:** Version bump only for package @credo-ts/indy-vdr + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +### Bug Fixes + +- **indy-vdr:** for creating latest delta ([#1737](https://github.com/openwallet-foundation/credo-ts/issues/1737)) ([68f0d70](https://github.com/openwallet-foundation/credo-ts/commit/68f0d70b9fd2b7acc8b6120b23b65144c93af391)) + +- feat(indy-vdr)!: include config in getAllPoolTransactions (#1770) ([29c589d](https://github.com/openwallet-foundation/credo-ts/commit/29c589dd2f5b6da0a6bed129b5f733851785ccba)), closes [#1770](https://github.com/openwallet-foundation/credo-ts/issues/1770) + +### Features + +- **anoncreds:** issue revocable credentials ([#1427](https://github.com/openwallet-foundation/credo-ts/issues/1427)) ([c59ad59](https://github.com/openwallet-foundation/credo-ts/commit/c59ad59fbe63b6d3760d19030e0f95fb2ea8488a)) +- bump indy-vdr version ([#1637](https://github.com/openwallet-foundation/credo-ts/issues/1637)) ([a641a96](https://github.com/openwallet-foundation/credo-ts/commit/a641a9699b7816825a88f2c883c9e65aaa4c0f87)) +- **indy-vdr:** ability to refresh the pool manually ([#1623](https://github.com/openwallet-foundation/credo-ts/issues/1623)) ([0865ea5](https://github.com/openwallet-foundation/credo-ts/commit/0865ea52fb99103fba0cc71cb118f0eb3fb909e4)) +- **indy-vdr:** register revocation registry definitions and status list ([#1693](https://github.com/openwallet-foundation/credo-ts/issues/1693)) ([ee34fe7](https://github.com/openwallet-foundation/credo-ts/commit/ee34fe71780a0787db96e28575eeedce3b4704bd)) +- **openid4vc:** persistance and events ([#1793](https://github.com/openwallet-foundation/credo-ts/issues/1793)) ([f4c386a](https://github.com/openwallet-foundation/credo-ts/commit/f4c386a6ccf8adb829cad30b81d524e6ffddb029)) + +### BREAKING CHANGES + +- `IndyVdrApi.getAllPoolTransactions()` now returns an array of objects containing transactions and config of each pool + +``` +{ + config: IndyVdrPoolConfig; + transactions: Transactions; +} +``` + +## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) + +**Note:** Version bump only for package @credo-ts/indy-vdr + +## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) + +### Bug Fixes + +- **indy-vdr:** role property not included in nym request ([#1488](https://github.com/hyperledger/aries-framework-javascript/issues/1488)) ([002be4f](https://github.com/hyperledger/aries-framework-javascript/commit/002be4f578729aed1c8ae337f3d2eeecce9e3725)) + +# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) + +### Bug Fixes + +- **anoncreds:** make revocation status list inline with the spec ([#1421](https://github.com/hyperledger/aries-framework-javascript/issues/1421)) ([644e860](https://github.com/hyperledger/aries-framework-javascript/commit/644e860a05f40166e26c497a2e8619c9a38df11d)) +- did cache key not being set correctly ([#1394](https://github.com/hyperledger/aries-framework-javascript/issues/1394)) ([1125e81](https://github.com/hyperledger/aries-framework-javascript/commit/1125e81962ffa752bf40fa8f7f4226e186f22013)) +- expose indy pool configs and action menu messages ([#1333](https://github.com/hyperledger/aries-framework-javascript/issues/1333)) ([518e5e4](https://github.com/hyperledger/aries-framework-javascript/commit/518e5e4dfb59f9c0457bfd233409e9f4b3c429ee)) +- **indy-vdr:** do not force indy-vdr version ([#1434](https://github.com/hyperledger/aries-framework-javascript/issues/1434)) ([8a933c0](https://github.com/hyperledger/aries-framework-javascript/commit/8a933c057e0c88870779bf8eb98b4684de4745de)) +- **indy-vdr:** export relevant packages from root ([#1291](https://github.com/hyperledger/aries-framework-javascript/issues/1291)) ([b570e0f](https://github.com/hyperledger/aries-framework-javascript/commit/b570e0f923fc46adef3ce20ee76a683a867b85f4)) +- issuance with unqualified identifiers ([#1431](https://github.com/hyperledger/aries-framework-javascript/issues/1431)) ([de90caf](https://github.com/hyperledger/aries-framework-javascript/commit/de90cafb8d12b7a940f881184cd745c4b5043cbc)) +- reference to indyLedgers in IndyXXXNotConfiguredError ([#1397](https://github.com/hyperledger/aries-framework-javascript/issues/1397)) ([d6e2ea2](https://github.com/hyperledger/aries-framework-javascript/commit/d6e2ea2194a4860265fe299ef8ee4cb4799ab1a6)) +- remove named capture groups ([#1378](https://github.com/hyperledger/aries-framework-javascript/issues/1378)) ([a4204ef](https://github.com/hyperledger/aries-framework-javascript/commit/a4204ef2db769de53d12f0d881d2c4422545c390)) +- seed and private key validation and return type in registrars ([#1324](https://github.com/hyperledger/aries-framework-javascript/issues/1324)) ([c0e5339](https://github.com/hyperledger/aries-framework-javascript/commit/c0e5339edfa32df92f23fb9c920796b4b59adf52)) + +### Features + +- 0.4.0 migration script ([#1392](https://github.com/hyperledger/aries-framework-javascript/issues/1392)) ([bc5455f](https://github.com/hyperledger/aries-framework-javascript/commit/bc5455f7b42612a2b85e504bc6ddd36283a42bfa)) +- add fetch indy schema method ([#1290](https://github.com/hyperledger/aries-framework-javascript/issues/1290)) ([1d782f5](https://github.com/hyperledger/aries-framework-javascript/commit/1d782f54bbb4abfeb6b6db6cd4f7164501b6c3d9)) +- **anoncreds:** store method name in records ([#1387](https://github.com/hyperledger/aries-framework-javascript/issues/1387)) ([47636b4](https://github.com/hyperledger/aries-framework-javascript/commit/47636b4a08ffbfa9a3f2a5a3c5aebda44f7d16c8)) +- **indy-vdr:** add indy-vdr package and indy vdr pool ([#1160](https://github.com/hyperledger/aries-framework-javascript/issues/1160)) ([e8d6ac3](https://github.com/hyperledger/aries-framework-javascript/commit/e8d6ac31a8e18847d99d7998bd7658439e48875b)) +- **indy-vdr:** add IndyVdrAnonCredsRegistry ([#1270](https://github.com/hyperledger/aries-framework-javascript/issues/1270)) ([d056316](https://github.com/hyperledger/aries-framework-javascript/commit/d056316712b5ee5c42a159816b5dda0b05ad84a8)) +- **indy-vdr:** did:sov resolver ([#1247](https://github.com/hyperledger/aries-framework-javascript/issues/1247)) ([b5eb08e](https://github.com/hyperledger/aries-framework-javascript/commit/b5eb08e99d7ea61adefb8c6c0c5c99c6c1ba1597)) +- **indy-vdr:** module registration ([#1285](https://github.com/hyperledger/aries-framework-javascript/issues/1285)) ([51030d4](https://github.com/hyperledger/aries-framework-javascript/commit/51030d43a7e3cca3da29c5add38e35f731576927)) +- **indy-vdr:** resolver and registrar for did:indy ([#1253](https://github.com/hyperledger/aries-framework-javascript/issues/1253)) ([efab8dd](https://github.com/hyperledger/aries-framework-javascript/commit/efab8ddfc34e47a3f0ffe35b55fa5018a7e96544)) +- **indy-vdr:** schema + credential definition endorsement ([#1451](https://github.com/hyperledger/aries-framework-javascript/issues/1451)) ([25b981b](https://github.com/hyperledger/aries-framework-javascript/commit/25b981b6e23d02409e90dabdccdccc8904d4e357)) +- **indy-vdr:** use [@hyperledger](https://github.com/hyperledger) packages ([#1252](https://github.com/hyperledger/aries-framework-javascript/issues/1252)) ([acdb20a](https://github.com/hyperledger/aries-framework-javascript/commit/acdb20a79d038fb4163d281ee8de0ccb649fdc32)) +- IndyVdrAnonCredsRegistry revocation methods ([#1328](https://github.com/hyperledger/aries-framework-javascript/issues/1328)) ([fb7ee50](https://github.com/hyperledger/aries-framework-javascript/commit/fb7ee5048c33d5335cd9f07cad3dffc60dee7376)) diff --git a/packages/indy-vdr/README.md b/packages/indy-vdr/README.md new file mode 100644 index 0000000000..0a2178c1b7 --- /dev/null +++ b/packages/indy-vdr/README.md @@ -0,0 +1,31 @@ +

+
+ Credo Logo +

+

Credo - Indy Verifiable Data Registry (Indy VDR) Module

+

+ License + typescript + @credo-ts/anoncreds version + +

+
+ +Credo Indy VDR provides integration of the Hyperledger Indy blockchain into Credo. See the [Indy VDR Setup](https://credo.js.org/guides/getting-started/set-up/indy-vdr) for installation instructions. diff --git a/packages/indy-vdr/jest.config.ts b/packages/indy-vdr/jest.config.ts new file mode 100644 index 0000000000..93c0197296 --- /dev/null +++ b/packages/indy-vdr/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json new file mode 100644 index 0000000000..2e170613c4 --- /dev/null +++ b/packages/indy-vdr/package.json @@ -0,0 +1,45 @@ +{ + "name": "@credo-ts/indy-vdr", + "main": "build/index", + "types": "build/index", + "version": "0.5.6-revocation", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/indy-vdr", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/indy-vdr" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build", + "test": "jest" + }, + "dependencies": { + "@credo-ts/anoncreds": "0.5.6-revocation", + "@credo-ts/core": "0.5.6-revocation" + }, + "devDependencies": { + "@hyperledger/indy-vdr-nodejs": "^0.2.2", + "@hyperledger/indy-vdr-shared": "^0.2.2", + "@stablelib/ed25519": "^1.0.2", + "@types/ref-array-di": "^1.2.6", + "@types/ref-struct-di": "^1.1.10", + "rimraf": "^4.4.0", + "rxjs": "^7.8.0", + "typescript": "~5.5.2" + }, + "peerDependencies": { + "@hyperledger/indy-vdr-shared": "^0.2.2" + } +} diff --git a/packages/indy-vdr/src/IndyVdrApi.ts b/packages/indy-vdr/src/IndyVdrApi.ts new file mode 100644 index 0000000000..d8b67c18d0 --- /dev/null +++ b/packages/indy-vdr/src/IndyVdrApi.ts @@ -0,0 +1,81 @@ +import type { Key } from '@credo-ts/core' +import type { IndyVdrRequest } from '@hyperledger/indy-vdr-shared' + +import { parseIndyDid } from '@credo-ts/anoncreds' +import { AgentContext, injectable } from '@credo-ts/core' +import { CustomRequest } from '@hyperledger/indy-vdr-shared' + +import { verificationKeyForIndyDid } from './dids/didIndyUtil' +import { IndyVdrError } from './error' +import { IndyVdrPoolService } from './pool' +import { multiSignRequest, signRequest } from './utils/sign' + +@injectable() +export class IndyVdrApi { + private agentContext: AgentContext + private indyVdrPoolService: IndyVdrPoolService + + public constructor(agentContext: AgentContext, indyVdrPoolService: IndyVdrPoolService) { + this.agentContext = agentContext + this.indyVdrPoolService = indyVdrPoolService + } + + private async multiSignRequest( + request: Request, + signingKey: Key, + identifier: string + ) { + return multiSignRequest(this.agentContext, request, signingKey, identifier) + } + + private async signRequest(request: Request, submitterDid: string) { + const { pool } = await this.indyVdrPoolService.getPoolForDid(this.agentContext, submitterDid) + return signRequest(this.agentContext, pool, request, submitterDid) + } + + /** + * This method refreshes the pool connection and ensures the pool is up to date with the ledger. + */ + public refreshPoolConnections() { + return this.indyVdrPoolService.refreshPoolConnections() + } + + /** + * This method gets the updated transactions of the pool. + * @returns The transactions of the pool ledger + */ + public getAllPoolTransactions() { + return this.indyVdrPoolService.getAllPoolTransactions() + } + + /** + * This method endorses a transaction. The transaction can be either a string or a JSON object. + * If the transaction has a signature, it means the transaction was created by another author and will be endorsed. + * This requires the `endorser` on the transaction to be equal to the unqualified variant of the `endorserDid`. + * + * If the transaction is not signed, we have a special case where the endorser will author the transaction. + * This is required when a new did is created, as the author and the endorser did must already exist on the ledger. + * In this case, the author did (`identifier`) must be equal to the unqualified identifier of the `endorserDid`. + * @param transaction the transaction body to be endorsed + * @param endorserDid the did of the endorser + * @returns An endorsed transaction + */ + public async endorseTransaction(transaction: string | Record, endorserDid: string) { + const endorserSigningKey = await verificationKeyForIndyDid(this.agentContext, endorserDid) + const { namespaceIdentifier } = parseIndyDid(endorserDid) + + const request = new CustomRequest({ customRequest: transaction }) + let endorsedTransaction: CustomRequest + + // the request is not parsed correctly due to too large numbers. The reqId overflows. + const txBody = typeof transaction === 'string' ? JSON.parse(transaction) : transaction + if (txBody.signature) { + if (txBody.endorser !== namespaceIdentifier) throw new IndyVdrError('Submitter does not match Endorser') + endorsedTransaction = await this.multiSignRequest(request, endorserSigningKey, namespaceIdentifier) + } else { + if (txBody.identifier !== namespaceIdentifier) throw new IndyVdrError('Submitter does not match identifier') + endorsedTransaction = await this.signRequest(request, endorserDid) + } + return endorsedTransaction.body + } +} diff --git a/packages/indy-vdr/src/IndyVdrModule.ts b/packages/indy-vdr/src/IndyVdrModule.ts new file mode 100644 index 0000000000..63d0b262e9 --- /dev/null +++ b/packages/indy-vdr/src/IndyVdrModule.ts @@ -0,0 +1,36 @@ +import type { IndyVdrModuleConfigOptions } from './IndyVdrModuleConfig' +import type { AgentContext, DependencyManager, Module } from '@credo-ts/core' + +import { IndyVdrApi } from './IndyVdrApi' +import { IndyVdrModuleConfig } from './IndyVdrModuleConfig' +import { IndyVdrPoolService } from './pool/IndyVdrPoolService' + +/** + * @public + * */ +export class IndyVdrModule implements Module { + public readonly config: IndyVdrModuleConfig + public readonly api = IndyVdrApi + + public constructor(config: IndyVdrModuleConfigOptions) { + this.config = new IndyVdrModuleConfig(config) + } + + public register(dependencyManager: DependencyManager) { + // Config + dependencyManager.registerInstance(IndyVdrModuleConfig, this.config) + + // Services + dependencyManager.registerSingleton(IndyVdrPoolService) + } + + public async initialize(agentContext: AgentContext): Promise { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + for (const pool of indyVdrPoolService.pools) { + if (pool.config.connectOnStartup) { + await pool.connect() + } + } + } +} diff --git a/packages/indy-vdr/src/IndyVdrModuleConfig.ts b/packages/indy-vdr/src/IndyVdrModuleConfig.ts new file mode 100644 index 0000000000..086846ddf7 --- /dev/null +++ b/packages/indy-vdr/src/IndyVdrModuleConfig.ts @@ -0,0 +1,78 @@ +import type { IndyVdrPoolConfig } from './pool' +import type { IndyVdr } from '@hyperledger/indy-vdr-shared' + +export interface IndyVdrModuleConfigOptions { + /** + * + * ## Node.JS + * + * ```ts + * import { indyVdr } from '@hyperledger/indy-vdr-nodejs'; + * + * const agent = new Agent({ + * config: {}, + * dependencies: agentDependencies, + * modules: { + * indyVdr: new IndyVdrModule({ + * indyVdr, + * }) + * } + * }) + * ``` + * + * ## React Native + * + * ```ts + * import { indyVdr } from '@hyperledger/indy-vdr-react-native'; + * + * const agent = new Agent({ + * config: {}, + * dependencies: agentDependencies, + * modules: { + * indyVdr: new IndyVdrModule({ + * indyVdr, + * }) + * } + * }) + * ``` + */ + indyVdr: IndyVdr + + /** + * Array of indy networks to connect to. + * + * @default [] + * + * @example + * ``` + * { + * isProduction: false, + * genesisTransactions: 'xxx', + * indyNamespace: 'localhost:test', + * transactionAuthorAgreement: { + * version: '1', + * acceptanceMechanism: 'accept' + * } + * } + * ``` + */ + networks: [IndyVdrPoolConfig, ...IndyVdrPoolConfig[]] +} + +export class IndyVdrModuleConfig { + private options: IndyVdrModuleConfigOptions + + public constructor(options: IndyVdrModuleConfigOptions) { + this.options = options + } + + /** See {@link IndyVdrModuleConfigOptions.networks} */ + public get networks() { + return this.options.networks + } + + /** See {@link IndyVdrModuleConfigOptions.indyVdr} */ + public get indyVdr() { + return this.options.indyVdr + } +} diff --git a/packages/indy-vdr/src/__tests__/IndyVdrModule.test.ts b/packages/indy-vdr/src/__tests__/IndyVdrModule.test.ts new file mode 100644 index 0000000000..3ca6553695 --- /dev/null +++ b/packages/indy-vdr/src/__tests__/IndyVdrModule.test.ts @@ -0,0 +1,41 @@ +import type { DependencyManager } from '@credo-ts/core' + +import { indyVdr } from '@hyperledger/indy-vdr-nodejs' + +import { IndyVdrModule } from '../IndyVdrModule' +import { IndyVdrModuleConfig } from '../IndyVdrModuleConfig' +import { IndyVdrPoolService } from '../pool' + +const dependencyManager = { + registerInstance: jest.fn(), + registerSingleton: jest.fn(), + registerContextScoped: jest.fn(), + resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), +} as unknown as DependencyManager + +describe('IndyVdrModule', () => { + test('registers dependencies on the dependency manager', () => { + const indyVdrModule = new IndyVdrModule({ + indyVdr, + networks: [ + { + isProduction: false, + genesisTransactions: 'xxx', + indyNamespace: 'localhost:test', + transactionAuthorAgreement: { + version: '1', + acceptanceMechanism: 'accept', + }, + }, + ], + }) + + indyVdrModule.register(dependencyManager) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(IndyVdrPoolService) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(IndyVdrModuleConfig, indyVdrModule.config) + }) +}) diff --git a/packages/indy-vdr/src/__tests__/IndyVdrModuleConfig.test.ts b/packages/indy-vdr/src/__tests__/IndyVdrModuleConfig.test.ts new file mode 100644 index 0000000000..b1abb77784 --- /dev/null +++ b/packages/indy-vdr/src/__tests__/IndyVdrModuleConfig.test.ts @@ -0,0 +1,18 @@ +import type { IndyVdrPoolConfig } from '../pool' + +import { indyVdr } from '@hyperledger/indy-vdr-nodejs' + +import { IndyVdrModuleConfig } from '../IndyVdrModuleConfig' + +describe('IndyVdrModuleConfig', () => { + test('sets values', () => { + const networkConfig = {} as IndyVdrPoolConfig + + const config = new IndyVdrModuleConfig({ + indyVdr, + networks: [networkConfig], + }) + + expect(config.networks).toEqual([networkConfig]) + }) +}) diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts new file mode 100644 index 0000000000..99ea1131fe --- /dev/null +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -0,0 +1,1199 @@ +import type { RevocationRegistryDelta } from './utils/transform' +import type { + AnonCredsRegistry, + GetCredentialDefinitionReturn, + GetSchemaReturn, + RegisterSchemaReturn, + RegisterCredentialDefinitionReturn, + GetRevocationStatusListReturn, + GetRevocationRegistryDefinitionReturn, + AnonCredsRevocationRegistryDefinition, + RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationStatusListReturn, + AnonCredsSchema, + AnonCredsCredentialDefinition, + RegisterSchemaReturnStateFailed, + RegisterSchemaReturnStateFinished, + RegisterSchemaReturnStateAction, + RegisterSchemaReturnStateWait, + RegisterCredentialDefinitionReturnStateAction, + RegisterCredentialDefinitionReturnStateWait, + RegisterCredentialDefinitionReturnStateFinished, + RegisterCredentialDefinitionReturnStateFailed, + RegisterRevocationRegistryDefinitionReturnStateFinished, + RegisterRevocationRegistryDefinitionReturnStateFailed, + RegisterRevocationRegistryDefinitionReturnStateWait, + RegisterRevocationRegistryDefinitionReturnStateAction, + RegisterRevocationStatusListReturnStateFinished, + RegisterRevocationStatusListReturnStateFailed, + RegisterRevocationStatusListReturnStateWait, + RegisterRevocationStatusListReturnStateAction, + RegisterRevocationStatusListOptions, +} from '@credo-ts/anoncreds' +import type { AgentContext } from '@credo-ts/core' +import type { GetCredentialDefinitionResponse, SchemaResponse } from '@hyperledger/indy-vdr-shared' + +import { + getUnqualifiedCredentialDefinitionId, + getUnqualifiedRevocationRegistryDefinitionId, + getUnqualifiedSchemaId, + parseIndyCredentialDefinitionId, + parseIndyDid, + parseIndyRevocationRegistryId, + parseIndySchemaId, + dateToTimestamp, +} from '@credo-ts/anoncreds' +import { CredoError } from '@credo-ts/core' +import { + RevocationRegistryEntryRequest, + RevocationRegistryDefinitionRequest, + GetSchemaRequest, + SchemaRequest, + GetCredentialDefinitionRequest, + CredentialDefinitionRequest, + GetTransactionRequest, + GetRevocationRegistryDeltaRequest, + GetRevocationRegistryDefinitionRequest, + CustomRequest, +} from '@hyperledger/indy-vdr-shared' + +import { verificationKeyForIndyDid } from '../dids/didIndyUtil' +import { IndyVdrPoolService } from '../pool' +import { multiSignRequest } from '../utils/sign' + +import { + indyVdrAnonCredsRegistryIdentifierRegex, + getDidIndySchemaId, + getDidIndyCredentialDefinitionId, + getDidIndyRevocationRegistryDefinitionId, + getDidIndyRevocationRegistryEntryId, +} from './utils/identifiers' +import { indyVdrCreateLatestRevocationDelta, anonCredsRevocationStatusListFromIndyVdr } from './utils/transform' + +export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { + public readonly methodName = 'indy' + + public readonly supportedIdentifier = indyVdrAnonCredsRegistryIdentifierRegex + + public async getSchema(agentContext: AgentContext, schemaId: string): Promise { + try { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + // parse schema id (supports did:indy and legacy) + const { did, namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(schemaId) + const { pool } = await indyVdrPoolService.getPoolForDid(agentContext, did) + agentContext.config.logger.debug(`Getting schema '${schemaId}' from ledger '${pool.indyNamespace}'`) + + // even though we support did:indy and legacy identifiers we always need to fetch using the legacy identifier + const legacySchemaId = getUnqualifiedSchemaId(namespaceIdentifier, schemaName, schemaVersion) + const request = new GetSchemaRequest({ schemaId: legacySchemaId }) + + agentContext.config.logger.trace( + `Submitting get schema request for schema '${schemaId}' to ledger '${pool.indyNamespace}'` + ) + const response = await pool.submitRequest(request) + + agentContext.config.logger.trace(`Got un-parsed schema '${schemaId}' from ledger '${pool.indyNamespace}'`, { + response, + }) + + if (!('attr_names' in response.result.data)) { + agentContext.config.logger.error(`Error retrieving schema '${schemaId}'`) + + return { + schemaId, + resolutionMetadata: { + error: 'notFound', + message: `unable to find schema with id ${schemaId}`, + }, + schemaMetadata: {}, + } + } + + return { + schema: { + attrNames: response.result.data.attr_names, + name: response.result.data.name, + version: response.result.data.version, + issuerId: did, + }, + schemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: pool.indyNamespace, + // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. + // For this reason we return it in the metadata. + indyLedgerSeqNo: response.result.seqNo, + }, + } + } catch (error) { + agentContext.config.logger.error(`Error retrieving schema '${schemaId}'`, { + error, + schemaId, + }) + + return { + schemaId, + resolutionMetadata: { + error: 'notFound', + }, + schemaMetadata: {}, + } + } + } + + public async registerSchema( + agentContext: AgentContext, + options: IndyVdrRegisterSchema + ): Promise { + const schema = options.schema + const { issuerId, name, version, attrNames } = schema + try { + // This will throw an error if trying to register a schema with a legacy indy identifier. We only support did:indy identifiers + // for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. + + const { namespaceIdentifier, namespace } = parseIndyDid(issuerId) + const { endorserDid, endorserMode } = options.options + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const pool = indyVdrPoolService.getPoolForNamespace(namespace) + + let writeRequest: CustomRequest + const didIndySchemaId = getDidIndySchemaId(namespace, namespaceIdentifier, schema.name, schema.version) + + const endorsedTransaction = options.options.endorsedTransaction + if (endorsedTransaction) { + agentContext.config.logger.debug( + `Preparing endorsed tx '${endorsedTransaction}' for submission on ledger '${namespace}' with did '${issuerId}'`, + schema + ) + writeRequest = new CustomRequest({ customRequest: endorsedTransaction }) + } else { + agentContext.config.logger.debug(`Create schema tx on ledger '${namespace}' with did '${issuerId}'`, schema) + const legacySchemaId = getUnqualifiedSchemaId(namespaceIdentifier, name, version) + + const schemaRequest = new SchemaRequest({ + submitterDid: namespaceIdentifier, + schema: { id: legacySchemaId, name, ver: '1.0', version, attrNames }, + }) + + const submitterKey = await verificationKeyForIndyDid(agentContext, issuerId) + writeRequest = await pool.prepareWriteRequest( + agentContext, + schemaRequest, + submitterKey, + endorserDid !== issuerId ? endorserDid : undefined + ) + + if (endorserMode === 'external') { + return { + jobId: didIndySchemaId, + schemaState: { + state: 'action', + action: 'endorseIndyTransaction', + schemaId: didIndySchemaId, + schema: schema, + schemaRequest: writeRequest.body, + }, + registrationMetadata: {}, + schemaMetadata: {}, + } + } + + if (endorserMode === 'internal' && endorserDid !== issuerId) { + const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) + } + } + const response = await pool.submitRequest(writeRequest) + + agentContext.config.logger.debug(`Registered schema '${didIndySchemaId}' on ledger '${pool.indyNamespace}'`, { + response, + writeRequest, + }) + + return { + schemaState: { + state: 'finished', + schema: schema, + schemaId: didIndySchemaId, + }, + registrationMetadata: {}, + schemaMetadata: { + // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. + // For this reason we return it in the metadata. + // Cast to SchemaResponse to pass type check + indyLedgerSeqNo: (response as SchemaResponse)?.result?.txnMetadata?.seqNo, + }, + } + } catch (error) { + agentContext.config.logger.error(`Error registering schema for did '${issuerId}'`, { + error, + did: issuerId, + schema: schema, + }) + + return { + schemaMetadata: {}, + registrationMetadata: {}, + schemaState: { + state: 'failed', + schema: schema, + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async getCredentialDefinition( + agentContext: AgentContext, + credentialDefinitionId: string + ): Promise { + try { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + // we support did:indy and legacy identifiers + const { did, namespaceIdentifier, schemaSeqNo, tag } = parseIndyCredentialDefinitionId(credentialDefinitionId) + const { pool } = await indyVdrPoolService.getPoolForDid(agentContext, did) + + agentContext.config.logger.debug( + `Getting credential definition '${credentialDefinitionId}' from ledger '${pool.indyNamespace}'` + ) + + const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId(namespaceIdentifier, schemaSeqNo, tag) + const request = new GetCredentialDefinitionRequest({ + credentialDefinitionId: legacyCredentialDefinitionId, + }) + + agentContext.config.logger.trace( + `Submitting get credential definition request for credential definition '${credentialDefinitionId}' to ledger '${pool.indyNamespace}'` + ) + const response: GetCredentialDefinitionResponse = await pool.submitRequest(request) + + // We need to fetch the schema to determine the schemaId (we only have the seqNo) + const schema = await this.fetchIndySchemaWithSeqNo(agentContext, response.result.ref, namespaceIdentifier) + + if (!schema || !response.result.data) { + agentContext.config.logger.error(`Error retrieving credential definition '${credentialDefinitionId}'`) + + return { + credentialDefinitionId, + credentialDefinitionMetadata: {}, + resolutionMetadata: { + error: 'notFound', + message: `unable to resolve credential definition with id ${credentialDefinitionId}`, + }, + } + } + + // Format the schema id based on the type of the credential definition id + const schemaId = credentialDefinitionId.startsWith('did:indy') + ? getDidIndySchemaId(pool.indyNamespace, schema.schema.issuerId, schema.schema.name, schema.schema.version) + : schema.schema.schemaId + + return { + credentialDefinitionId, + credentialDefinition: { + issuerId: did, + schemaId, + tag: response.result.tag, + type: 'CL', + value: response.result.data, + }, + credentialDefinitionMetadata: { + didIndyNamespace: pool.indyNamespace, + }, + resolutionMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error(`Error retrieving credential definition '${credentialDefinitionId}'`, { + error, + credentialDefinitionId, + }) + + return { + credentialDefinitionId, + credentialDefinitionMetadata: {}, + resolutionMetadata: { + error: 'notFound', + message: `unable to resolve credential definition: ${error.message}`, + }, + } + } + } + + public async registerCredentialDefinition( + agentContext: AgentContext, + options: IndyVdrRegisterCredentialDefinition + ): Promise { + const credentialDefinition = options.credentialDefinition + const { schemaId, issuerId, tag, value } = credentialDefinition + + try { + // This will throw an error if trying to register a credential definition with a legacy indy identifier. We only support did:indy + // identifiers for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. + const { namespaceIdentifier, namespace } = parseIndyDid(issuerId) + const { endorserDid, endorserMode } = options.options + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + const pool = indyVdrPoolService.getPoolForNamespace(namespace) + + agentContext.config.logger.debug( + `Registering credential definition on ledger '${namespace}' with did '${issuerId}'`, + options.credentialDefinition + ) + + let writeRequest: CustomRequest + let didIndyCredentialDefinitionId: string + let schemaSeqNo: number + + const endorsedTransaction = options.options.endorsedTransaction + if (endorsedTransaction) { + agentContext.config.logger.debug( + `Preparing endorsed tx '${endorsedTransaction}' for submission on ledger '${namespace}' with did '${issuerId}'`, + credentialDefinition + ) + writeRequest = new CustomRequest({ customRequest: endorsedTransaction }) + const operation = JSON.parse(endorsedTransaction)?.operation + // extract the seqNo from the endorsed transaction, which is contained in the ref field of the operation + schemaSeqNo = Number(operation?.ref) + didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId( + namespace, + namespaceIdentifier, + schemaSeqNo, + tag + ) + } else { + // TODO: this will bypass caching if done on a higher level. + const { schemaMetadata, resolutionMetadata } = await this.getSchema(agentContext, schemaId) + + if (!schemaMetadata?.indyLedgerSeqNo || typeof schemaMetadata.indyLedgerSeqNo !== 'number') { + return { + registrationMetadata: {}, + credentialDefinitionMetadata: { + didIndyNamespace: pool.indyNamespace, + }, + credentialDefinitionState: { + credentialDefinition: options.credentialDefinition, + state: 'failed', + reason: `error resolving schema with id ${schemaId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, + }, + } + } + schemaSeqNo = schemaMetadata.indyLedgerSeqNo + + // FIXME: we need to check if schemaId has same namespace as issuerId and it should not be a legacy identifier + // FIXME: in the other methods we also need to add checks. E.g. when creating a revocation + // status list, you can only create a revocation status list for a credential definition registry that is created + // under the same namespace and by the same issuer id (you can create a cred def for a schema created by another issuer + // but you can't create a revocation registry based on a cred def created by another issuer. We need to add these checks + // to all register methods in this file) + const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId(issuerId, schemaSeqNo, tag) + didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId( + namespace, + namespaceIdentifier, + schemaSeqNo, + tag + ) + + const credentialDefinitionRequest = new CredentialDefinitionRequest({ + submitterDid: namespaceIdentifier, + credentialDefinition: { + ver: '1.0', + id: legacyCredentialDefinitionId, + schemaId: schemaSeqNo.toString(), + type: 'CL', + tag, + value, + }, + }) + + const submitterKey = await verificationKeyForIndyDid(agentContext, issuerId) + writeRequest = await pool.prepareWriteRequest( + agentContext, + credentialDefinitionRequest, + submitterKey, + endorserDid !== issuerId ? endorserDid : undefined + ) + + if (endorserMode === 'external') { + return { + jobId: didIndyCredentialDefinitionId, + credentialDefinitionState: { + state: 'action', + action: 'endorseIndyTransaction', + credentialDefinition: credentialDefinition, + credentialDefinitionId: didIndyCredentialDefinitionId, + credentialDefinitionRequest: writeRequest.body, + }, + registrationMetadata: {}, + credentialDefinitionMetadata: {}, + } + } + + if (endorserMode === 'internal' && endorserDid !== issuerId) { + const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) + } + } + + const response = await pool.submitRequest(writeRequest) + agentContext.config.logger.debug( + `Registered credential definition '${didIndyCredentialDefinitionId}' on ledger '${pool.indyNamespace}'`, + { + response, + credentialDefinition: options.credentialDefinition, + } + ) + + return { + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + credentialDefinition: credentialDefinition, + credentialDefinitionId: didIndyCredentialDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error(`Error registering credential definition for schema '${schemaId}'`, { + error, + did: issuerId, + credentialDefinition: options.credentialDefinition, + }) + + return { + credentialDefinitionMetadata: {}, + registrationMetadata: {}, + credentialDefinitionState: { + credentialDefinition: options.credentialDefinition, + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async getRevocationRegistryDefinition( + agentContext: AgentContext, + revocationRegistryDefinitionId: string + ): Promise { + try { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const { did, namespaceIdentifier, credentialDefinitionTag, revocationRegistryTag, schemaSeqNo } = + parseIndyRevocationRegistryId(revocationRegistryDefinitionId) + const { pool } = await indyVdrPoolService.getPoolForDid(agentContext, did) + + agentContext.config.logger.debug( + `Using ledger '${pool.indyNamespace}' to retrieve revocation registry definition '${revocationRegistryDefinitionId}'` + ) + + const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + const request = new GetRevocationRegistryDefinitionRequest({ + revocationRegistryId: legacyRevocationRegistryId, + }) + + agentContext.config.logger.trace( + `Submitting get revocation registry definition request for revocation registry definition '${revocationRegistryDefinitionId}' to ledger` + ) + const response = await pool.submitRequest(request) + + if (!response.result.data) { + agentContext.config.logger.error( + `Error retrieving revocation registry definition '${revocationRegistryDefinitionId}' from ledger`, + { + revocationRegistryDefinitionId, + } + ) + + return { + resolutionMetadata: { + error: 'notFound', + message: 'unable to resolve revocation registry definition', + }, + revocationRegistryDefinitionId, + revocationRegistryDefinitionMetadata: {}, + } + } + + agentContext.config.logger.trace( + `Got revocation registry definition '${revocationRegistryDefinitionId}' from ledger '${pool.indyNamespace}'`, + { + response, + } + ) + + const credentialDefinitionId = revocationRegistryDefinitionId.startsWith('did:indy:') + ? getDidIndyCredentialDefinitionId( + pool.indyNamespace, + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag + ) + : getUnqualifiedCredentialDefinitionId(namespaceIdentifier, schemaSeqNo, credentialDefinitionTag) + + const revocationRegistryDefinition = { + issuerId: did, + revocDefType: response.result.data.revocDefType, + value: { + maxCredNum: response.result.data.value.maxCredNum, + tailsHash: response.result.data.value.tailsHash, + tailsLocation: response.result.data.value.tailsLocation, + publicKeys: { + accumKey: { + z: response.result.data.value.publicKeys.accumKey.z, + }, + }, + }, + tag: response.result.data.tag, + credDefId: credentialDefinitionId, + } satisfies AnonCredsRevocationRegistryDefinition + + return { + revocationRegistryDefinitionId, + revocationRegistryDefinition, + revocationRegistryDefinitionMetadata: { + issuanceType: response.result.data.value.issuanceType, + didIndyNamespace: pool.indyNamespace, + }, + resolutionMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error( + `Error retrieving revocation registry definition '${revocationRegistryDefinitionId}' from ledger`, + { + error, + revocationRegistryDefinitionId, + } + ) + + return { + resolutionMetadata: { + error: 'notFound', + message: `unable to resolve revocation registry definition: ${error.message}`, + }, + revocationRegistryDefinitionId, + revocationRegistryDefinitionMetadata: {}, + } + } + } + + public async registerRevocationRegistryDefinition( + agentContext: AgentContext, + { options, revocationRegistryDefinition }: IndyVdrRegisterRevocationRegistryDefinition + ): Promise { + try { + // This will throw an error if trying to register a credential definition with a legacy indy identifier. We only support did:indy + // identifiers for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. + const { namespaceIdentifier, namespace } = parseIndyDid(revocationRegistryDefinition.issuerId) + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + const pool = indyVdrPoolService.getPoolForNamespace(namespace) + + agentContext.config.logger.debug( + `Registering revocation registry definition on ledger '${namespace}' with did '${revocationRegistryDefinition.issuerId}'`, + revocationRegistryDefinition + ) + + let writeRequest: CustomRequest + let didIndyRevocationRegistryDefinitionId: string + + const { schemaSeqNo, tag: credentialDefinitionTag } = parseIndyCredentialDefinitionId( + revocationRegistryDefinition.credDefId + ) + + const { endorsedTransaction, endorserDid, endorserMode } = options + if (endorsedTransaction) { + agentContext.config.logger.debug( + `Preparing endorsed tx '${endorsedTransaction}' for submission on ledger '${namespace}' with did '${revocationRegistryDefinition.issuerId}'`, + revocationRegistryDefinition + ) + writeRequest = new CustomRequest({ customRequest: endorsedTransaction }) + didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryDefinitionId( + namespace, + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryDefinition.tag + ) + } else { + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryDefinition.tag + ) + + didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryDefinitionId( + namespace, + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryDefinition.tag + ) + + const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag + ) + + const revocationRegistryDefinitionRequest = new RevocationRegistryDefinitionRequest({ + submitterDid: namespaceIdentifier, + revocationRegistryDefinitionV1: { + id: legacyRevocationRegistryDefinitionId, + ver: '1.0', + credDefId: legacyCredentialDefinitionId, + tag: revocationRegistryDefinition.tag, + revocDefType: revocationRegistryDefinition.revocDefType, + value: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + ...revocationRegistryDefinition.value, + }, + }, + }) + + const submitterKey = await verificationKeyForIndyDid(agentContext, revocationRegistryDefinition.issuerId) + writeRequest = await pool.prepareWriteRequest( + agentContext, + revocationRegistryDefinitionRequest, + submitterKey, + endorserDid !== revocationRegistryDefinition.issuerId ? endorserDid : undefined + ) + + if (endorserMode === 'external') { + return { + jobId: didIndyRevocationRegistryDefinitionId, + revocationRegistryDefinitionState: { + state: 'action', + action: 'endorseIndyTransaction', + revocationRegistryDefinition, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + revocationRegistryDefinitionRequest: writeRequest.body, + }, + registrationMetadata: {}, + revocationRegistryDefinitionMetadata: {}, + } + } + + if (endorserMode === 'internal' && endorserDid !== revocationRegistryDefinition.issuerId) { + const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) + } + } + + const response = await pool.submitRequest(writeRequest) + agentContext.config.logger.debug( + `Registered revocation registry definition '${didIndyRevocationRegistryDefinitionId}' on ledger '${pool.indyNamespace}'`, + { + response, + revocationRegistryDefinition, + } + ) + + return { + revocationRegistryDefinitionMetadata: {}, + revocationRegistryDefinitionState: { + revocationRegistryDefinition, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error( + `Error registering revocation registry definition for credential definition '${revocationRegistryDefinition.credDefId}'`, + { + error, + did: revocationRegistryDefinition.issuerId, + revocationRegistryDefinition, + } + ) + + return { + revocationRegistryDefinitionMetadata: {}, + registrationMetadata: {}, + revocationRegistryDefinitionState: { + revocationRegistryDefinition, + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async getRevocationStatusList( + agentContext: AgentContext, + revocationRegistryDefinitionId: string, + timestamp: number + ): Promise { + try { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const { did } = parseIndyRevocationRegistryId(revocationRegistryDefinitionId) + const { pool } = await indyVdrPoolService.getPoolForDid(agentContext, did) + + const revocationDelta = await this.fetchIndyRevocationDelta( + agentContext, + revocationRegistryDefinitionId, + timestamp + ) + + if (!revocationDelta) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Error retrieving revocation registry delta '${revocationRegistryDefinitionId}' from ledger, potentially revocation interval ends before revocation registry creation`, + }, + revocationStatusListMetadata: {}, + } + } + + const { revocationRegistryDefinition, resolutionMetadata, revocationRegistryDefinitionMetadata } = + await this.getRevocationRegistryDefinition(agentContext, revocationRegistryDefinitionId) + + if ( + !revocationRegistryDefinition || + !revocationRegistryDefinitionMetadata.issuanceType || + typeof revocationRegistryDefinitionMetadata.issuanceType !== 'string' + ) { + return { + resolutionMetadata: { + error: `error resolving revocation registry definition with id ${revocationRegistryDefinitionId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, + }, + revocationStatusListMetadata: { + didIndyNamespace: pool.indyNamespace, + }, + } + } + + const isIssuanceByDefault = revocationRegistryDefinitionMetadata.issuanceType === 'ISSUANCE_BY_DEFAULT' + + return { + resolutionMetadata: {}, + revocationStatusList: anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId, + revocationRegistryDefinition, + revocationDelta, + isIssuanceByDefault + ), + revocationStatusListMetadata: { + didIndyNamespace: pool.indyNamespace, + }, + } + } catch (error) { + agentContext.config.logger.error( + `Error retrieving revocation registry delta '${revocationRegistryDefinitionId}' from ledger, potentially revocation interval ends before revocation registry creation?"`, + { + error, + revocationRegistryId: revocationRegistryDefinitionId, + } + ) + + return { + resolutionMetadata: { + error: 'notFound', + message: `Error retrieving revocation registry delta '${revocationRegistryDefinitionId}' from ledger, potentially revocation interval ends before revocation registry creation: ${error.message}`, + }, + revocationStatusListMetadata: {}, + } + } + } + + public async registerRevocationStatusList( + agentContext: AgentContext, + { options, revocationStatusList }: IndyVdrRegisterRevocationStatusList + ): Promise { + try { + // This will throw an error if trying to register a revocation status list with a legacy indy identifier. We only support did:indy + // identifiers for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. + const { endorsedTransaction, endorserDid, endorserMode } = options + const { namespaceIdentifier, namespace } = parseIndyDid(revocationStatusList.issuerId) + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + const pool = indyVdrPoolService.getPoolForNamespace(namespace) + + agentContext.config.logger.debug( + `Registering revocation status list on ledger '${namespace}' with did '${revocationStatusList.issuerId}'`, + revocationStatusList + ) + + let writeRequest: CustomRequest + + // Parse the revocation registry id + const { + schemaSeqNo, + credentialDefinitionTag, + namespaceIdentifier: revocationRegistryNamespaceIdentifier, + revocationRegistryTag, + namespace: revocationRegistryNamespace, + } = parseIndyRevocationRegistryId(revocationStatusList.revRegDefId) + + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + + const didIndyRevocationRegistryEntryId = getDidIndyRevocationRegistryEntryId( + namespace, + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + + if (revocationRegistryNamespace && revocationRegistryNamespace !== namespace) { + throw new CredoError( + `Issued id '${revocationStatusList.issuerId}' does not have the same namespace (${namespace}) as the revocation registry definition '${revocationRegistryNamespace}'` + ) + } + + if (revocationRegistryNamespaceIdentifier !== namespaceIdentifier) { + throw new CredoError( + `Cannot register revocation registry definition using a different DID. Revocation registry definition contains '${revocationRegistryNamespaceIdentifier}', but DID used was '${namespaceIdentifier}'` + ) + } + + if (endorsedTransaction) { + agentContext.config.logger.debug( + `Preparing endorsed tx '${endorsedTransaction}' for submission on ledger '${namespace}' with did '${revocationStatusList.issuerId}'`, + revocationStatusList + ) + + writeRequest = new CustomRequest({ customRequest: endorsedTransaction }) + } else { + const previousDelta = await this.fetchIndyRevocationDelta( + agentContext, + legacyRevocationRegistryDefinitionId, + // Fetch revocation delta for current timestamp + dateToTimestamp(new Date()) + ) + + const revocationRegistryDefinitionEntryValue = indyVdrCreateLatestRevocationDelta( + revocationStatusList.currentAccumulator, + revocationStatusList.revocationList, + previousDelta ?? undefined + ) + + const revocationRegistryDefinitionRequest = new RevocationRegistryEntryRequest({ + submitterDid: namespaceIdentifier, + revocationRegistryEntry: { + ver: '1.0', + value: revocationRegistryDefinitionEntryValue, + }, + revocationRegistryDefinitionType: 'CL_ACCUM', + revocationRegistryDefinitionId: legacyRevocationRegistryDefinitionId, + }) + + const submitterKey = await verificationKeyForIndyDid(agentContext, revocationStatusList.issuerId) + writeRequest = await pool.prepareWriteRequest( + agentContext, + revocationRegistryDefinitionRequest, + submitterKey, + endorserDid !== revocationStatusList.issuerId ? endorserDid : undefined + ) + + if (endorserMode === 'external') { + return { + jobId: didIndyRevocationRegistryEntryId, + revocationStatusListState: { + state: 'action', + action: 'endorseIndyTransaction', + revocationStatusList, + revocationStatusListRequest: writeRequest.body, + }, + registrationMetadata: {}, + revocationStatusListMetadata: {}, + } + } + + if (endorserMode === 'internal' && endorserDid !== revocationStatusList.issuerId) { + const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) + } + } + + const response = await pool.submitRequest( + writeRequest as RevocationRegistryEntryRequest + ) + agentContext.config.logger.debug( + `Registered revocation status list '${didIndyRevocationRegistryEntryId}' on ledger '${pool.indyNamespace}'`, + { + response, + revocationStatusList, + } + ) + + return { + revocationStatusListMetadata: {}, + revocationStatusListState: { + revocationStatusList: { + ...revocationStatusList, + timestamp: response.result.txnMetadata.txnTime, + }, + state: 'finished', + }, + registrationMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error( + `Error registering revocation status list for revocation registry definition '${revocationStatusList.revRegDefId}}'`, + { + error, + did: revocationStatusList.issuerId, + } + ) + + return { + registrationMetadata: {}, + revocationStatusListMetadata: {}, + revocationStatusListState: { + revocationStatusList, + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + private async fetchIndySchemaWithSeqNo(agentContext: AgentContext, seqNo: number, did: string) { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const { pool } = await indyVdrPoolService.getPoolForDid(agentContext, did) + + agentContext.config.logger.debug(`Getting transaction with seqNo '${seqNo}' from ledger '${pool.indyNamespace}'`) + // ledgerType 1 is domain ledger + const request = new GetTransactionRequest({ ledgerType: 1, seqNo }) + + agentContext.config.logger.trace(`Submitting get transaction request to ledger '${pool.indyNamespace}'`) + const response = await pool.submitRequest(request) + + if (response.result.data?.txn.type !== '101') { + agentContext.config.logger.error(`Could not get schema from ledger for seq no ${seqNo}'`) + return null + } + + const schema = response.result.data?.txn.data as SchemaType + const schemaDid = response.result.data?.txn.metadata['from'] as string + const schemaId = getUnqualifiedSchemaId(schemaDid, schema.data.name, schema.data.version) + + return { + schema: { + schemaId, + attr_name: schema.data.attr_names, + name: schema.data.name, + version: schema.data.version, + issuerId: schemaDid, + seqNo, + }, + indyNamespace: pool.indyNamespace, + } + } + + private async fetchIndyRevocationDelta( + agentContext: AgentContext, + revocationRegistryDefinitionId: string, + toTs: number + ): Promise { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const { did, namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag } = + parseIndyRevocationRegistryId(revocationRegistryDefinitionId) + const { pool } = await indyVdrPoolService.getPoolForDid(agentContext, did) + + agentContext.config.logger.debug( + `Using ledger '${pool.indyNamespace}' to retrieve revocation registry deltas with revocation registry definition id '${revocationRegistryDefinitionId}' until ${toTs}` + ) + + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + + const deltaRequest = new GetRevocationRegistryDeltaRequest({ + toTs, + submitterDid: namespaceIdentifier, + revocationRegistryId: legacyRevocationRegistryDefinitionId, + }) + + agentContext.config.logger.trace(`Submitting get transaction request to ledger '${pool.indyNamespace}'`) + const response = await pool.submitRequest(deltaRequest) + const { + result: { data, type, txnTime }, + } = response + + // Indicating there are no deltas + if (type !== '117' || data === null || !txnTime) { + agentContext.config.logger.warn( + `Could not get any deltas from ledger for revocation registry definition '${revocationRegistryDefinitionId}' from ledger '${pool.indyNamespace}'` + ) + return null + } + + return { + revoked: data.value.revoked, + issued: data.value.issued, + accum: data.value.accum_to.value.accum, + txnTime, + } + } +} + +interface SchemaType { + data: { + attr_names: string[] + version: string + name: string + } +} + +type InternalEndorsement = { endorserMode: 'internal'; endorserDid: string; endorsedTransaction?: never } +type ExternalEndorsementCreate = { endorserMode: 'external'; endorserDid: string; endorsedTransaction?: never } +type ExternalEndorsementSubmit = { endorserMode: 'external'; endorserDid?: never; endorsedTransaction: string } + +export interface IndyVdrRegisterSchemaInternalOptions { + schema: AnonCredsSchema + options: InternalEndorsement +} + +export interface IndyVdrRegisterSchemaExternalCreateOptions { + schema: AnonCredsSchema + options: ExternalEndorsementCreate +} + +export interface IndyVdrRegisterSchemaExternalSubmitOptions { + schema: AnonCredsSchema + options: ExternalEndorsementSubmit +} + +export interface IndyVdrRegisterSchemaReturnStateAction extends RegisterSchemaReturnStateAction { + action: 'endorseIndyTransaction' + schemaRequest: string +} + +export interface IndyVdrRegisterSchemaReturn extends RegisterSchemaReturn { + schemaState: + | RegisterSchemaReturnStateWait + | IndyVdrRegisterSchemaReturnStateAction + | RegisterSchemaReturnStateFinished + | RegisterSchemaReturnStateFailed +} + +export type IndyVdrRegisterSchema = + | IndyVdrRegisterSchemaInternalOptions + | IndyVdrRegisterSchemaExternalCreateOptions + | IndyVdrRegisterSchemaExternalSubmitOptions + +export type IndyVdrRegisterSchemaOptions = IndyVdrRegisterSchema['options'] + +export interface IndyVdrRegisterCredentialDefinitionInternalOptions { + credentialDefinition: AnonCredsCredentialDefinition + options: InternalEndorsement +} + +export interface IndyVdrRegisterCredentialDefinitionExternalCreateOptions { + credentialDefinition: AnonCredsCredentialDefinition + options: ExternalEndorsementCreate +} + +export interface IndyVdrRegisterCredentialDefinitionExternalSubmitOptions { + credentialDefinition: AnonCredsCredentialDefinition + options: ExternalEndorsementSubmit +} + +export interface IndyVdrRegisterCredentialDefinitionReturnStateAction + extends RegisterCredentialDefinitionReturnStateAction { + action: 'endorseIndyTransaction' + credentialDefinitionRequest: string +} + +export interface IndyVdrRegisterCredentialDefinitionReturn extends RegisterCredentialDefinitionReturn { + credentialDefinitionState: + | RegisterCredentialDefinitionReturnStateWait + | IndyVdrRegisterCredentialDefinitionReturnStateAction + | RegisterCredentialDefinitionReturnStateFinished + | RegisterCredentialDefinitionReturnStateFailed +} + +export type IndyVdrRegisterCredentialDefinition = + | IndyVdrRegisterCredentialDefinitionInternalOptions + | IndyVdrRegisterCredentialDefinitionExternalCreateOptions + | IndyVdrRegisterCredentialDefinitionExternalSubmitOptions + +export type IndyVdrRegisterCredentialDefinitionOptions = IndyVdrRegisterCredentialDefinition['options'] + +export interface IndyVdrRegisterRevocationRegistryDefinitionInternalOptions { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + options: InternalEndorsement +} + +export interface IndyVdrRegisterRevocationRegistryDefinitionExternalCreateOptions { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + options: ExternalEndorsementCreate +} + +export interface IndyVdrRegisterRevocationRegistryDefinitionExternalSubmitOptions { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + options: ExternalEndorsementSubmit +} + +export interface IndyVdrRegisterRevocationRegistryDefinitionReturnStateAction + extends RegisterRevocationRegistryDefinitionReturnStateAction { + action: 'endorseIndyTransaction' + revocationRegistryDefinitionRequest: string +} + +export interface IndyVdrRegisterRevocationRegistryDefinitionReturn extends RegisterRevocationRegistryDefinitionReturn { + revocationRegistryDefinitionState: + | IndyVdrRegisterRevocationRegistryDefinitionReturnStateAction + | RegisterRevocationRegistryDefinitionReturnStateWait + | RegisterRevocationRegistryDefinitionReturnStateFinished + | RegisterRevocationRegistryDefinitionReturnStateFailed +} + +export type IndyVdrRegisterRevocationRegistryDefinition = + | IndyVdrRegisterRevocationRegistryDefinitionInternalOptions + | IndyVdrRegisterRevocationRegistryDefinitionExternalCreateOptions + | IndyVdrRegisterRevocationRegistryDefinitionExternalSubmitOptions + +export type IndyVdrRegisterRevocationRegistryDefinitionOptions = IndyVdrRegisterRevocationRegistryDefinition['options'] + +export interface IndyVdrRegisterRevocationStatusListInternalOptions extends RegisterRevocationStatusListOptions { + options: InternalEndorsement +} + +export interface IndyVdrRegisterRevocationStatusListExternalCreateOptions extends RegisterRevocationStatusListOptions { + options: ExternalEndorsementCreate +} + +export interface IndyVdrRegisterRevocationStatusListExternalSubmitOptions extends RegisterRevocationStatusListOptions { + options: ExternalEndorsementSubmit +} + +export interface IndyVdrRegisterRevocationStatusListReturnStateAction + extends RegisterRevocationStatusListReturnStateAction { + action: 'endorseIndyTransaction' + revocationStatusListRequest: string +} + +export interface IndyVdrRegisterRevocationStatusListReturn extends RegisterRevocationStatusListReturn { + revocationStatusListState: + | IndyVdrRegisterRevocationStatusListReturnStateAction + | RegisterRevocationStatusListReturnStateWait + | RegisterRevocationStatusListReturnStateFinished + | RegisterRevocationStatusListReturnStateFailed +} + +export type IndyVdrRegisterRevocationStatusList = + | IndyVdrRegisterRevocationStatusListInternalOptions + | IndyVdrRegisterRevocationStatusListExternalCreateOptions + | IndyVdrRegisterRevocationStatusListExternalSubmitOptions + +export type IndyVdrRegisterRevocationStatusListOptions = IndyVdrRegisterRevocationStatusList['options'] \ No newline at end of file diff --git a/packages/indy-vdr/src/anoncreds/index.ts b/packages/indy-vdr/src/anoncreds/index.ts new file mode 100644 index 0000000000..c1e469b307 --- /dev/null +++ b/packages/indy-vdr/src/anoncreds/index.ts @@ -0,0 +1 @@ +export * from './IndyVdrAnonCredsRegistry' diff --git a/packages/indy-vdr/src/anoncreds/utils/__tests__/identifiers.test.ts b/packages/indy-vdr/src/anoncreds/utils/__tests__/identifiers.test.ts new file mode 100644 index 0000000000..0028a3bfa2 --- /dev/null +++ b/packages/indy-vdr/src/anoncreds/utils/__tests__/identifiers.test.ts @@ -0,0 +1,79 @@ +import { + getDidIndyCredentialDefinitionId, + getDidIndyRevocationRegistryDefinitionId, + getDidIndySchemaId, + indyVdrAnonCredsRegistryIdentifierRegex, +} from '../identifiers' + +describe('identifiers', () => { + describe('indyVdrAnonCredsRegistryIdentifierRegex', () => { + test('matches against a legacy schema id, credential definition id and revocation registry id', () => { + const did = '7Tqg6BwSSWapxgUDm9KKgg' + const schemaId = 'BQ42WeE24jFHeyGg8x9XAz:2:Medical Bill:1.0' + const credentialDefinitionId = 'N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID' + const revocationRegistryId = + 'N7baRMcyvPwWc8v85CtZ6e:4:N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID:CL_ACCUM:1-1024' + + const anotherId = 'some:id' + + // unqualified issuerId not in regex on purpose. See note in implementation. + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(did)).toEqual(false) + + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(schemaId)).toEqual(true) + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(credentialDefinitionId)).toEqual(true) + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(revocationRegistryId)).toEqual(true) + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(anotherId)).toEqual(false) + }) + + test('matches against a did indy did, schema id, credential definition id and revocation registry id', () => { + const did = 'did:indy:local:7Tqg6BwSSWapxgUDm9KKgg' + const schemaId = 'did:indy:local:BQ42WeE24jFHeyGg8x9XAz/anoncreds/v0/SCHEMA/Medical Bill/1.0' + const credentialDefinitionId = + 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/CLAIM_DEF/100669/SCH Employee ID' + const revocationRegistryId = + 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/REV_REG_DEF/100669/SCH Employee ID/1-1024' + + const anotherId = 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/SOME_DEF' + + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(did)).toEqual(true) + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(schemaId)).toEqual(true) + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(credentialDefinitionId)).toEqual(true) + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(revocationRegistryId)).toEqual(true) + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(anotherId)).toEqual(false) + }) + }) + + test('getDidIndySchemaId returns a valid schema id given a did, name, and version', () => { + const namespace = 'sovrin:test' + const did = '12345' + const name = 'backbench' + const version = '420' + + expect(getDidIndySchemaId(namespace, did, name, version)).toEqual( + 'did:indy:sovrin:test:12345/anoncreds/v0/SCHEMA/backbench/420' + ) + }) + + test('getDidIndyCredentialDefinitionId returns a valid credential definition id given a did, seqNo, and tag', () => { + const namespace = 'sovrin:test' + const did = '12345' + const seqNo = 420 + const tag = 'someTag' + + expect(getDidIndyCredentialDefinitionId(namespace, did, seqNo, tag)).toEqual( + 'did:indy:sovrin:test:12345/anoncreds/v0/CLAIM_DEF/420/someTag' + ) + }) + + test('getDidIndyRevocationRegistryId returns a valid credential definition id given a did, seqNo, and tag', () => { + const namespace = 'sovrin:test' + const did = '12345' + const seqNo = 420 + const credentialDefinitionTag = 'someTag' + const tag = 'anotherTag' + + expect(getDidIndyRevocationRegistryDefinitionId(namespace, did, seqNo, credentialDefinitionTag, tag)).toEqual( + 'did:indy:sovrin:test:12345/anoncreds/v0/REV_REG_DEF/420/someTag/anotherTag' + ) + }) +}) diff --git a/packages/indy-vdr/src/anoncreds/utils/__tests__/transform.test.ts b/packages/indy-vdr/src/anoncreds/utils/__tests__/transform.test.ts new file mode 100644 index 0000000000..0b4bdb6c2c --- /dev/null +++ b/packages/indy-vdr/src/anoncreds/utils/__tests__/transform.test.ts @@ -0,0 +1,190 @@ +import type { RevocationRegistryDelta } from '../transform' +import type { AnonCredsRevocationRegistryDefinition } from '@credo-ts/anoncreds' + +import { indyVdrCreateLatestRevocationDelta, anonCredsRevocationStatusListFromIndyVdr } from '../transform' + +const createRevocationRegistryDefinition = (maxCreds: number): AnonCredsRevocationRegistryDefinition => ({ + value: { + tailsHash: 'hash', + maxCredNum: maxCreds, + publicKeys: { + accumKey: { + z: 'key', + }, + }, + tailsLocation: 'nowhere', + }, + revocDefType: 'CL_ACCUM', + tag: 'REV_TAG', + issuerId: 'does:not:matter', + credDefId: 'does:not:matter', +}) + +describe('transform', () => { + const accum = 'does not matter' + const revocationRegistryDefinitionId = 'does:not:matter' + + describe('indy vdr delta to anoncreds revocation status list', () => { + test('issued and revoked are filled', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [0, 1, 2, 3, 4], + revoked: [5, 6, 7, 8, 9], + txnTime: 1, + } + + const revocationRegistryDefinition = createRevocationRegistryDefinition(10) + + const statusList = anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId, + revocationRegistryDefinition, + delta, + true + ) + + expect(statusList.revocationList).toStrictEqual([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) + }) + + test('issued and revoked are empty (issuance by default)', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [], + revoked: [], + txnTime: 1, + } + + const revocationRegistryDefinition = createRevocationRegistryDefinition(10) + + const statusList = anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId, + revocationRegistryDefinition, + delta, + true + ) + + expect(statusList.revocationList).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + }) + + test('issued and revoked are empty (issuance on demand)', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [], + revoked: [], + txnTime: 1, + } + + const revocationRegistryDefinition = createRevocationRegistryDefinition(10) + + const statusList = anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId, + revocationRegistryDefinition, + delta, + false + ) + + expect(statusList.revocationList).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + }) + + test('issued index is too high', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [200], + revoked: [5, 6, 7, 8, 9], + txnTime: 1, + } + + const revocationRegistryDefinition = createRevocationRegistryDefinition(10) + + expect(() => + anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId, + revocationRegistryDefinition, + delta, + true + ) + ).toThrowError() + }) + }) + + describe('create latest indy vdr delta from status list and previous delta', () => { + test('delta and status list are equal', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [0, 1, 2, 3, 4], + revoked: [5, 6, 7, 8, 9], + txnTime: 1, + } + + const revocationStatusList = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] + + const { revoked, issued } = indyVdrCreateLatestRevocationDelta(accum, revocationStatusList, delta) + + expect(revoked).toStrictEqual([]) + expect(issued).toStrictEqual([]) + }) + + test('real world case', () => { + const revocationStatusList = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ] + + const delta: RevocationRegistryDelta = { + revoked: [], + issued: [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, + 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + ], + accum: + '1 22D10308FEB1A73E5D231FBB231385A75AEFF05AFFC26C76CF6080B6831AC8F8 1 24D6BF977A437AD8BD4501070123CD096F680246D22A00B498FB1A660FAAD062 1 207D22D26EAD5941316BBDE33E19F41FE727507888ED750A2C49863AA2ACDCD1 1 243C032573EADB924D9C28BD21106AA2FB7994C85C80A2DAE89F5C011BCFA70C 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + txnTime: 1706887938, + } + + const { revoked, issued } = indyVdrCreateLatestRevocationDelta(accum, revocationStatusList, delta) + + expect(issued).toStrictEqual([]) + expect(revoked).toStrictEqual([10]) + }) + + test('no previous delta', () => { + const revocationStatusList = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] + + const { revoked, issued } = indyVdrCreateLatestRevocationDelta(accum, revocationStatusList, undefined) + + expect(issued).toStrictEqual([0, 1, 2, 3, 4]) + expect(revoked).toStrictEqual([5, 6, 7, 8, 9]) + }) + + test('status list and previous delta are out of sync', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [0], + revoked: [5], + txnTime: 1, + } + + const revocationStatusList = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] + + const { revoked, issued } = indyVdrCreateLatestRevocationDelta(accum, revocationStatusList, delta) + + expect(issued).toStrictEqual([1, 2, 3, 4]) + expect(revoked).toStrictEqual([6, 7, 8, 9]) + }) + + test('previous delta index exceeds length of revocation status list', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [200], + revoked: [5], + txnTime: 1, + } + + const revocationStatusList = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] + + expect(() => indyVdrCreateLatestRevocationDelta(accum, revocationStatusList, delta)).toThrowError() + }) + }) +}) diff --git a/packages/indy-vdr/src/anoncreds/utils/identifiers.ts b/packages/indy-vdr/src/anoncreds/utils/identifiers.ts new file mode 100644 index 0000000000..7681a51327 --- /dev/null +++ b/packages/indy-vdr/src/anoncreds/utils/identifiers.ts @@ -0,0 +1,68 @@ +import { + unqualifiedSchemaIdRegex, + unqualifiedCredentialDefinitionIdRegex, + unqualifiedRevocationRegistryIdRegex, + didIndyCredentialDefinitionIdRegex, + didIndyRevocationRegistryIdRegex, + didIndySchemaIdRegex, + didIndyRegex, +} from '@credo-ts/anoncreds' + +// combines both legacy and did:indy anoncreds identifiers and also the issuer id +const indyVdrAnonCredsRegexes = [ + // NOTE: we only include the qualified issuer id here, as we don't support registering objects based on legacy issuer ids. + // you can still resolve using legacy issuer ids, but you need to use the full did:indy identifier when registering. + // As we find a matching anoncreds registry based on the issuerId only when creating an object, this will make sure + // it will throw an no registry found for identifier error. + // issuer id + didIndyRegex, + + // schema + didIndySchemaIdRegex, + unqualifiedSchemaIdRegex, + + // credential definition + didIndyCredentialDefinitionIdRegex, + unqualifiedCredentialDefinitionIdRegex, + + // revocation registry + unqualifiedRevocationRegistryIdRegex, + didIndyRevocationRegistryIdRegex, +] + +export const indyVdrAnonCredsRegistryIdentifierRegex = new RegExp( + indyVdrAnonCredsRegexes.map((r) => r.source).join('|') +) + +export function getDidIndySchemaId(namespace: string, unqualifiedDid: string, name: string, version: string) { + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/SCHEMA/${name}/${version}` +} + +export function getDidIndyCredentialDefinitionId( + namespace: string, + unqualifiedDid: string, + schemaSeqNo: string | number, + tag: string +) { + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/${tag}` +} + +export function getDidIndyRevocationRegistryDefinitionId( + namespace: string, + unqualifiedDid: string, + schemaSeqNo: string | number, + credentialDefinitionTag: string, + revocationRegistryTag: string +) { + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_DEF/${schemaSeqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` +} + +export function getDidIndyRevocationRegistryEntryId( + namespace: string, + unqualifiedDid: string, + schemaSeqNo: string | number, + credentialDefinitionTag: string, + revocationRegistryTag: string +) { + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_ENTRY/${schemaSeqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` +} diff --git a/packages/indy-vdr/src/anoncreds/utils/transform.ts b/packages/indy-vdr/src/anoncreds/utils/transform.ts new file mode 100644 index 0000000000..7673b897fe --- /dev/null +++ b/packages/indy-vdr/src/anoncreds/utils/transform.ts @@ -0,0 +1,121 @@ +import type { AnonCredsRevocationStatusList, AnonCredsRevocationRegistryDefinition } from '@credo-ts/anoncreds' + +import { CredoError } from '@credo-ts/core' + +export type RevocationRegistryDelta = { + accum: string + issued: number[] + revoked: number[] + txnTime: number +} + +enum RevocationState { + Active, + Revoked, +} + +export function anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId: string, + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition, + delta: RevocationRegistryDelta, + isIssuanceByDefault: boolean +): AnonCredsRevocationStatusList { + // Check whether the highest delta index is supported in the `maxCredNum` field of the + // revocation registry definition. This will likely also be checked on other levels as well + // by the ledger or the indy-vdr library itself + if (Math.max(...delta.issued, ...delta.revoked) >= revocationRegistryDefinition.value.maxCredNum) { + throw new CredoError( + `Highest delta index '${Math.max( + ...delta.issued, + ...delta.revoked + )}' is too large for the Revocation registry maxCredNum '${revocationRegistryDefinition.value.maxCredNum}' ` + ) + } + + // 0 means unrevoked, 1 means revoked + const defaultState = isIssuanceByDefault ? RevocationState.Active : RevocationState.Revoked + + // Fill with default value + const revocationList = new Array(revocationRegistryDefinition.value.maxCredNum).fill(defaultState) + + // Set all `issuer` indexes to 0 (not revoked) + for (const issued of delta.issued ?? []) { + revocationList[issued] = RevocationState.Active + } + + // Set all `revoked` indexes to 1 (revoked) + for (const revoked of delta.revoked ?? []) { + revocationList[revoked] = RevocationState.Revoked + } + + return { + issuerId: revocationRegistryDefinition.issuerId, + currentAccumulator: delta.accum, + revRegDefId: revocationRegistryDefinitionId, + revocationList, + timestamp: delta.txnTime, + } +} + +/** + * + * Transforms the previous deltas and the full revocation status list into the latest delta + * + * ## Example + * + * input: + * + * revocationStatusList: [0, 1, 1, 1, 0, 0, 0, 1, 1, 0] + * previousDelta: + * - issued: [1, 2, 5, 8, 9] + * - revoked: [0, 3, 4, 6, 7] + * + * output: + * - issued: [5, 9] + * - revoked: [3, 7] + * + */ +export function indyVdrCreateLatestRevocationDelta( + currentAccumulator: string, + revocationStatusList: Array, + previousDelta?: RevocationRegistryDelta +) { + if (previousDelta && Math.max(...previousDelta.issued, ...previousDelta.revoked) > revocationStatusList.length - 1) { + throw new CredoError( + `Indy Vdr delta contains an index '${Math.max( + ...previousDelta.revoked, + ...previousDelta.issued + )}' that exceeds the length of the revocation status list '${revocationStatusList.length}'` + ) + } + + const issued: Array = [] + const revoked: Array = [] + + if (previousDelta) { + revocationStatusList.forEach((revocationStatus, idx) => { + // Check whether the revocationStatusList entry is not included in the previous delta issued indices + if (revocationStatus === RevocationState.Active && !previousDelta.issued.includes(idx)) { + issued.push(idx) + } + + // Check whether the revocationStatusList entry is not included in the previous delta revoked indices + if (revocationStatus === RevocationState.Revoked && !previousDelta.revoked.includes(idx)) { + revoked.push(idx) + } + }) + } else { + // No delta is provided, initial state, so the entire revocation status list is converted to two list of indices + revocationStatusList.forEach((revocationStatus, idx) => { + if (revocationStatus === RevocationState.Active) issued.push(idx) + if (revocationStatus === RevocationState.Revoked) revoked.push(idx) + }) + } + + return { + issued, + revoked, + accum: currentAccumulator, + prevAccum: previousDelta?.accum, + } +} diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts new file mode 100644 index 0000000000..01752cc8cc --- /dev/null +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts @@ -0,0 +1,574 @@ +import type { IndyEndpointAttrib } from './didSovUtil' +import type { IndyVdrPool } from '../pool' +import type { + AgentContext, + Buffer, + DidCreateOptions, + DidCreateResult, + DidDeactivateResult, + DidDocument, + DidDocumentService, + DidOperationStateActionBase, + DidRegistrar, + DidUpdateResult, +} from '@credo-ts/core' +import type { IndyVdrRequest } from '@hyperledger/indy-vdr-shared' + +import { parseIndyDid } from '@credo-ts/anoncreds' +import { + DidCommV1Service, + NewDidCommV2Service, + DidDocumentRole, + DidRecord, + DidRepository, + Hasher, + IndyAgentService, + Key, + KeyType, + DidCommV2Service, + TypedArrayEncoder, +} from '@credo-ts/core' +import { AttribRequest, CustomRequest, NymRequest } from '@hyperledger/indy-vdr-shared' + +import { IndyVdrError } from '../error' +import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' + +import { + buildDidDocument, + createKeyAgreementKey, + didDocDiff, + indyDidDocumentFromDid, + isSelfCertifiedIndyDid, + verificationKeyForIndyDid, +} from './didIndyUtil' +import { endpointsAttribFromServices } from './didSovUtil' + +export class IndyVdrIndyDidRegistrar implements DidRegistrar { + public readonly supportedMethods = ['indy'] + + private didCreateActionResult({ + namespace, + didAction, + did, + }: { + namespace: string + didAction: EndorseDidTxAction + did: string + }): IndyVdrDidCreateResult { + return { + jobId: did, + didDocumentMetadata: {}, + didRegistrationMetadata: { + didIndyNamespace: namespace, + }, + didState: didAction, + } + } + + private didCreateFailedResult({ reason }: { reason: string }): IndyVdrDidCreateResult { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: reason, + }, + } + } + + private didCreateFinishedResult({ + seed, + privateKey, + did, + didDocument, + namespace, + }: { + seed: Buffer | undefined + privateKey: Buffer | undefined + did: string + didDocument: DidDocument + namespace: string + }): IndyVdrDidCreateResult { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: { + didIndyNamespace: namespace, + }, + didState: { + state: 'finished', + did, + didDocument, + secret: { + // FIXME: the uni-registrar creates the seed in the registrar method + // if it doesn't exist so the seed can always be returned. Currently + // we can only return it if the seed was passed in by the user. Once + // we have a secure method for generating seeds we should use the same + // approach + seed: seed, + privateKey: privateKey, + }, + }, + } + } + + public async parseInput(agentContext: AgentContext, options: IndyVdrDidCreateOptions): Promise { + let did = options.did + let namespaceIdentifier: string + let verificationKey: Key + const seed = options.secret?.seed + const privateKey = options.secret?.privateKey + + if (options.options.endorsedTransaction) { + const _did = did as string + const { namespace } = parseIndyDid(_did) + // endorser did from the transaction + const endorserNamespaceIdentifier = JSON.parse(options.options.endorsedTransaction.nymRequest).identifier + + return { + status: 'ok', + did: _did, + namespace: namespace, + namespaceIdentifier: parseIndyDid(_did).namespaceIdentifier, + endorserNamespaceIdentifier, + seed, + privateKey, + } + } + + const endorserDid = options.options.endorserDid + const { namespace: endorserNamespace, namespaceIdentifier: endorserNamespaceIdentifier } = parseIndyDid(endorserDid) + + const allowOne = [privateKey, seed, did].filter((e) => e !== undefined) + if (allowOne.length > 1) { + return { + status: 'error', + reason: `Only one of 'seed', 'privateKey' and 'did' must be provided`, + } + } + + if (did) { + if (!options.options.verkey) { + return { + status: 'error', + reason: 'If a did is defined, a matching verkey must be provided', + } + } + + const { namespace: didNamespace, namespaceIdentifier: didNamespaceIdentifier } = parseIndyDid(did) + namespaceIdentifier = didNamespaceIdentifier + verificationKey = Key.fromPublicKeyBase58(options.options.verkey, KeyType.Ed25519) + + if (!isSelfCertifiedIndyDid(did, options.options.verkey)) { + return { + status: 'error', + reason: `Initial verkey ${options.options.verkey} does not match did ${did}`, + } + } + + if (didNamespace !== endorserNamespace) { + return { + status: 'error', + reason: `The endorser did uses namespace: '${endorserNamespace}' and the did to register uses namespace: '${didNamespace}'. Namespaces must match.`, + } + } + } else { + // Create a new key and calculate did according to the rules for indy did method + verificationKey = await agentContext.wallet.createKey({ privateKey, seed, keyType: KeyType.Ed25519 }) + const buffer = Hasher.hash(verificationKey.publicKey, 'sha-256') + + namespaceIdentifier = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + did = `did:indy:${endorserNamespace}:${namespaceIdentifier}` + } + + return { + status: 'ok', + did, + verificationKey, + namespaceIdentifier, + namespace: endorserNamespace, + endorserNamespaceIdentifier, + seed, + privateKey, + } + } + + public async saveDidRecord(agentContext: AgentContext, did: string, didDocument: DidDocument): Promise { + // Save the did so we know we created it and can issue with it + const didRecord = new DidRecord({ + did, + role: DidDocumentRole.Created, + didDocument, + }) + + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + await didRepository.save(agentContext, didRecord) + } + + private createDidDocument( + did: string, + verificationKey: Key, + services: DidDocumentService[] | undefined, + useEndpointAttrib: boolean | undefined + ) { + // Create base did document + const didDocumentBuilder = indyDidDocumentFromDid(did, verificationKey.publicKeyBase58) + let diddocContent + + // Add services if object was passed + if (services) { + services.forEach((item) => { + const prependDidIfNotPresent = (id: string) => { + return id.startsWith('#') ? `${did}${id}` : id + } + + // Prepend the did to the service id if it is not already there + item.id = prependDidIfNotPresent(item.id) + + // TODO: should we also prepend the did to routingKeys? + if (item instanceof DidCommV1Service) { + item.recipientKeys = item.recipientKeys.map(prependDidIfNotPresent) + } + + didDocumentBuilder.addService(item) + }) + + const commTypes = [IndyAgentService.type, DidCommV1Service.type, NewDidCommV2Service.type, DidCommV2Service.type] + const serviceTypes = new Set(services.map((item) => item.type)) + + const keyAgreementId = `${did}#key-agreement-1` + + // If there is at least a communication service, add the key agreement key + if (commTypes.some((type) => serviceTypes.has(type))) { + didDocumentBuilder + .addContext('https://w3id.org/security/suites/x25519-2019/v1') + .addVerificationMethod({ + controller: did, + id: keyAgreementId, + publicKeyBase58: createKeyAgreementKey(verificationKey.publicKeyBase58), + type: 'X25519KeyAgreementKey2019', + }) + .addKeyAgreement(keyAgreementId) + } + + // FIXME: it doesn't seem this context exists? + // If there is a DIDComm V2 service, add context + if (serviceTypes.has(NewDidCommV2Service.type) || serviceTypes.has(DidCommV2Service.type)) { + didDocumentBuilder.addContext('https://didcomm.org/messaging/contexts/v2') + } + + if (!useEndpointAttrib) { + // create diddocContent parameter based on the diff between the base and the resulting DID Document + diddocContent = didDocDiff( + didDocumentBuilder.build().toJSON(), + indyDidDocumentFromDid(did, verificationKey.publicKeyBase58).build().toJSON() + ) + } + } + + // Build did document + const didDocument = didDocumentBuilder.build() + + return { + diddocContent, + didDocument, + } + } + + public async create(agentContext: AgentContext, options: IndyVdrDidCreateOptions): Promise { + try { + const res = await this.parseInput(agentContext, options) + if (res.status === 'error') return this.didCreateFailedResult({ reason: res.reason }) + + const { did, namespaceIdentifier, endorserNamespaceIdentifier, verificationKey, namespace, seed, privateKey } = + res + + const pool = agentContext.dependencyManager.resolve(IndyVdrPoolService).getPoolForNamespace(namespace) + + let nymRequest: NymRequest | CustomRequest + let didDocument: DidDocument | undefined + let attribRequest: AttribRequest | CustomRequest | undefined + let alias: string | undefined + + if (options.options.endorsedTransaction) { + const { nymRequest: _nymRequest, attribRequest: _attribRequest } = options.options.endorsedTransaction + nymRequest = new CustomRequest({ customRequest: _nymRequest }) + attribRequest = _attribRequest ? new CustomRequest({ customRequest: _attribRequest }) : undefined + } else { + const { services, useEndpointAttrib } = options.options + alias = options.options.alias + if (!verificationKey) throw new Error('VerificationKey not defined') + + const { didDocument: _didDocument, diddocContent } = this.createDidDocument( + did, + verificationKey, + services, + useEndpointAttrib + ) + didDocument = _didDocument + + let didRegisterSigningKey: Key | undefined = undefined + if (options.options.endorserMode === 'internal') + didRegisterSigningKey = await verificationKeyForIndyDid(agentContext, options.options.endorserDid) + + nymRequest = await this.createRegisterDidWriteRequest({ + agentContext, + pool, + signingKey: didRegisterSigningKey, + submitterNamespaceIdentifier: endorserNamespaceIdentifier, + namespaceIdentifier, + verificationKey, + alias, + diddocContent, + role: options.options.role, + }) + + if (services && useEndpointAttrib) { + const endpoints = endpointsAttribFromServices(services) + attribRequest = await this.createSetDidEndpointsRequest({ + agentContext, + pool, + signingKey: verificationKey, + endorserDid: options.options.endorserMode === 'external' ? options.options.endorserDid : undefined, + unqualifiedDid: namespaceIdentifier, + endpoints, + }) + } + + if (options.options.endorserMode === 'external') { + const didAction: EndorseDidTxAction = { + state: 'action', + action: 'endorseIndyTransaction', + endorserDid: options.options.endorserDid, + nymRequest: nymRequest.body, + attribRequest: attribRequest?.body, + did: did, + secret: { seed, privateKey }, + } + + return this.didCreateActionResult({ namespace, didAction, did }) + } + } + await this.registerPublicDid(agentContext, pool, nymRequest) + if (attribRequest) await this.setEndpointsForDid(agentContext, pool, attribRequest) + didDocument = didDocument ?? (await buildDidDocument(agentContext, pool, did)) + await this.saveDidRecord(agentContext, did, didDocument) + return this.didCreateFinishedResult({ did, didDocument, namespace, seed, privateKey }) + } catch (error) { + return this.didCreateFailedResult({ reason: `unknownError: ${error.message}` }) + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:indy not implemented yet`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:indy not implemented yet`, + }, + } + } + + private async createRegisterDidWriteRequest(options: { + agentContext: AgentContext + pool: IndyVdrPool + submitterNamespaceIdentifier: string + namespaceIdentifier: string + verificationKey: Key + signingKey?: Key + alias: string | undefined + diddocContent?: Record + role?: NymRequestRole + }) { + const { + agentContext, + pool, + submitterNamespaceIdentifier, + namespaceIdentifier, + verificationKey, + alias, + signingKey, + role, + } = options + + // FIXME: Add diddocContent when supported by indy-vdr + if (options.diddocContent) { + throw new IndyVdrError('diddocContent is not yet supported') + } + + const request = new NymRequest({ + submitterDid: submitterNamespaceIdentifier, + dest: namespaceIdentifier, + verkey: verificationKey.publicKeyBase58, + alias, + role, + }) + + if (!signingKey) return request + const writeRequest = await pool.prepareWriteRequest(agentContext, request, signingKey, undefined) + return writeRequest + } + + private async registerPublicDid( + agentContext: AgentContext, + pool: IndyVdrPool, + writeRequest: Request + ) { + const body = writeRequest.body + try { + const response = await pool.submitRequest(writeRequest) + + agentContext.config.logger.debug(`Register public did on ledger '${pool.indyNamespace}'\nRequest: ${body}}`, { + response, + }) + + return + } catch (error) { + agentContext.config.logger.error( + `Error Registering public did on ledger '${pool.indyNamespace}'\nRequest: ${body}}` + ) + + throw error + } + } + + private async createSetDidEndpointsRequest(options: { + agentContext: AgentContext + pool: IndyVdrPool + signingKey: Key + endorserDid?: string + unqualifiedDid: string + endpoints: IndyEndpointAttrib + }): Promise { + const { agentContext, pool, endpoints, unqualifiedDid, signingKey, endorserDid } = options + const request = new AttribRequest({ + submitterDid: unqualifiedDid, + targetDid: unqualifiedDid, + raw: JSON.stringify({ endpoint: endpoints }), + }) + + const writeRequest = await pool.prepareWriteRequest(agentContext, request, signingKey, endorserDid) + return writeRequest + } + + private async setEndpointsForDid( + agentContext: AgentContext, + pool: IndyVdrPool, + writeRequest: Request + ): Promise { + const body = writeRequest.body + try { + const response = await pool.submitRequest(writeRequest) + + agentContext.config.logger.debug( + `Successfully set endpoints for did on ledger '${pool.indyNamespace}'.\nRequest: ${body}}`, + { + response, + } + ) + } catch (error) { + agentContext.config.logger.error( + `Error setting endpoints for did on ledger '${pool.indyNamespace}'.\nRequest: ${body}}` + ) + + throw new IndyVdrError(error) + } + } +} + +interface IndyVdrDidCreateOptionsBase extends DidCreateOptions { + didDocument?: never // Not yet supported + options: { + alias?: string + role?: NymRequestRole + services?: DidDocumentService[] + useEndpointAttrib?: boolean + verkey?: string + + // endorserDid is always required. We just have internal or external mode + endorserDid: string + // if endorserMode is 'internal', the endorserDid MUST be present in the wallet + // if endorserMode is 'external', the endorserDid doesn't have to be present in the wallet + endorserMode: 'internal' | 'external' + endorsedTransaction?: never + } + secret?: { + seed?: Buffer + privateKey?: Buffer + } +} + +interface IndyVdrDidCreateOptionsWithDid extends IndyVdrDidCreateOptionsBase { + method?: never + did: string +} + +interface IndyVdrDidCreateOptionsWithoutDid extends IndyVdrDidCreateOptionsBase { + method: 'indy' + did?: never +} + +// When transactions have been endorsed. Only supported for external mode +// this is a separate interface so we can remove all the properties we don't need anymore. +interface IndyVdrDidCreateOptionsForSubmission extends DidCreateOptions { + didDocument?: never + did: string // for submission MUST always have a did, so we know which did we're submitting the transaction for. We MUST check whether the did passed here, matches with the + method?: never + options: { + endorserMode: 'external' + + // provide the endorsed transactions. If these are provided + // we will submit the transactions to the ledger + endorsedTransaction: { + nymRequest: string + attribRequest?: string + } + } + secret?: { + seed?: Buffer + privateKey?: Buffer + } +} + +export type IndyVdrDidCreateOptions = + | IndyVdrDidCreateOptionsWithDid + | IndyVdrDidCreateOptionsWithoutDid + | IndyVdrDidCreateOptionsForSubmission + +type ParseInputOk = { + status: 'ok' + did: string + verificationKey?: Key + namespaceIdentifier: string + namespace: string + endorserNamespaceIdentifier: string + seed: Buffer | undefined + privateKey: Buffer | undefined +} + +type parseInputError = { status: 'error'; reason: string } + +type ParseInputResult = ParseInputOk | parseInputError + +export interface EndorseDidTxAction extends DidOperationStateActionBase { + action: 'endorseIndyTransaction' + endorserDid: string + nymRequest: string + attribRequest?: string + did: string +} + +export type IndyVdrDidCreateResult = DidCreateResult + +export type NymRequestRole = 'STEWARD' | 'TRUSTEE' | 'ENDORSER' | 'NETWORK_MONITOR' diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts new file mode 100644 index 0000000000..f2c8aa90a3 --- /dev/null +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts @@ -0,0 +1,40 @@ +import type { AgentContext, DidResolutionResult, DidResolver } from '@credo-ts/core' + +import { parseIndyDid } from '@credo-ts/anoncreds' + +import { IndyVdrPoolService } from '../pool' + +import { buildDidDocument } from './didIndyUtil' + +export class IndyVdrIndyDidResolver implements DidResolver { + public readonly supportedMethods = ['indy'] + + public readonly allowsCaching = true + public readonly allowsLocalDidRecord = true + + public async resolve(agentContext: AgentContext, did: string): Promise { + const didDocumentMetadata = {} + try { + const poolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + const pool = poolService.getPoolForNamespace(parseIndyDid(did).namespace) + + // Get DID Document from Get NYM response + const didDocument = await buildDidDocument(agentContext, pool, did) + + return { + didDocument, + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } +} diff --git a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts new file mode 100644 index 0000000000..5a6208b4a5 --- /dev/null +++ b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts @@ -0,0 +1,100 @@ +import type { GetNymResponseData, IndyEndpointAttrib } from './didSovUtil' +import type { IndyVdrPool } from '../pool' +import type { DidResolutionResult, ParsedDid, DidResolver, AgentContext } from '@credo-ts/core' + +import { GetAttribRequest, GetNymRequest } from '@hyperledger/indy-vdr-shared' + +import { IndyVdrError, IndyVdrNotFoundError } from '../error' +import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' + +import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovUtil' + +export class IndyVdrSovDidResolver implements DidResolver { + public readonly supportedMethods = ['sov'] + + public readonly allowsCaching = true + + public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { + const didDocumentMetadata = {} + + try { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + // FIXME: this actually fetches the did twice (if not cached), once for the pool and once for the nym + // we do not store the diddocContent in the pool cache currently so we need to fetch it again + // The logic is mostly to determine which pool to use for a did + const { pool } = await indyVdrPoolService.getPoolForDid(agentContext, parsed.id) + const nym = await this.getPublicDid(pool, parsed.id) + const endpoints = await this.getEndpointsForDid(agentContext, pool, parsed.id) + + const keyAgreementId = `${parsed.did}#key-agreement-1` + const builder = sovDidDocumentFromDid(parsed.did, nym.verkey) + + if (endpoints) { + addServicesFromEndpointsAttrib(builder, parsed.did, endpoints, keyAgreementId) + } + + return { + didDocument: builder.build(), + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } + + private async getPublicDid(pool: IndyVdrPool, unqualifiedDid: string) { + const request = new GetNymRequest({ dest: unqualifiedDid }) + const didResponse = await pool.submitRequest(request) + + if (!didResponse.result.data) { + throw new IndyVdrNotFoundError(`DID ${unqualifiedDid} not found`) + } + return JSON.parse(didResponse.result.data) as GetNymResponseData + } + + private async getEndpointsForDid(agentContext: AgentContext, pool: IndyVdrPool, did: string) { + try { + agentContext.config.logger.debug(`Get endpoints for did '${did}' from ledger '${pool.indyNamespace}'`) + + const request = new GetAttribRequest({ targetDid: did, raw: 'endpoint' }) + + agentContext.config.logger.debug( + `Submitting get endpoint ATTRIB request for did '${did}' to ledger '${pool.indyNamespace}'` + ) + const response = await pool.submitRequest(request) + + if (!response.result.data) { + return null + } + + const endpoints = JSON.parse(response.result.data as string)?.endpoint as IndyEndpointAttrib + agentContext.config.logger.debug( + `Got endpoints '${JSON.stringify(endpoints)}' for did '${did}' from ledger '${pool.indyNamespace}'`, + { + response, + endpoints, + } + ) + + return endpoints ?? null + } catch (error) { + agentContext.config.logger.error( + `Error retrieving endpoints for did '${did}' from ledger '${pool.indyNamespace}'`, + { + error, + } + ) + + throw new IndyVdrError(error) + } + } +} diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts new file mode 100644 index 0000000000..136ae0b3a8 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts @@ -0,0 +1,778 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import type { DidRecord, RecordSavedEvent } from '@credo-ts/core' + +import { + DidCommV1Service, + NewDidCommV2Service, + DidDocumentService, + DidDocument, + DidDocumentRole, + DidRepository, + DidsApi, + EventEmitter, + JsonTransformer, + Key, + KeyType, + RepositoryEventTypes, + TypedArrayEncoder, + VerificationMethod, + NewDidCommV2ServiceEndpoint, +} from '@credo-ts/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' +import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' +import { agentDependencies, getAgentConfig, getAgentContext, mockProperty } from '../../../../core/tests' +import { IndyVdrPool, IndyVdrPoolService } from '../../pool' +import { IndyVdrIndyDidRegistrar } from '../IndyVdrIndyDidRegistrar' + +jest.mock('../../pool/IndyVdrPool') +const IndyVdrPoolMock = IndyVdrPool as jest.Mock +const poolMock = new IndyVdrPoolMock() +mockProperty(poolMock, 'indyNamespace', 'ns1') + +const agentConfig = getAgentConfig('IndyVdrIndyDidRegistrar') +const wallet = new InMemoryWallet() + +jest + .spyOn(wallet, 'createKey') + .mockResolvedValue(Key.fromPublicKeyBase58('E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', KeyType.Ed25519)) +const storageService = new InMemoryStorageService() +const eventEmitter = new EventEmitter(agentDependencies, new Subject()) +const didRepository = new DidRepository(storageService, eventEmitter) + +const agentContext = getAgentContext({ + wallet, + registerInstances: [ + [DidRepository, didRepository], + [IndyVdrPoolService, { getPoolForNamespace: jest.fn().mockReturnValue(poolMock) }], + [ + DidsApi, + { + resolve: jest.fn().mockResolvedValue({ + didDocument: new DidDocument({ + id: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + authentication: [ + new VerificationMethod({ + id: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg#verkey', + type: 'Ed25519VerificationKey2018', + controller: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + }), + ], + }), + }), + }, + ], + ], + agentConfig, +}) + +const indyVdrIndyDidRegistrar = new IndyVdrIndyDidRegistrar() + +describe('IndyVdrIndyDidRegistrar', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + test('returns an error state if both did and privateKey are provided', async () => { + const result = await indyVdrIndyDidRegistrar.create(agentContext, { + did: 'did:indy:pool1:did-value', + options: { + alias: 'Hello', + endorserMode: 'internal', + endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + }, + secret: { + privateKey: TypedArrayEncoder.fromString('key'), + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `Only one of 'seed', 'privateKey' and 'did' must be provided`, + }, + }) + }) + + test('returns an error state if the endorser did is not a valid did:indy did', async () => { + const result = await indyVdrIndyDidRegistrar.create(agentContext, { + method: 'indy', + options: { + endorserMode: 'internal', + endorserDid: 'BzCbsNYhMrjHiqZDTUASHg', + alias: 'Hello', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'unknownError: BzCbsNYhMrjHiqZDTUASHg is not a valid did:indy did', + }, + }) + }) + + test('returns an error state if did is provided, but it is not a valid did:indy did', async () => { + const result = await indyVdrIndyDidRegistrar.create(agentContext, { + did: 'BzCbsNYhMrjHiqZDTUASHg', + options: { + endorserMode: 'internal', + endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + verkey: 'verkey', + alias: 'Hello', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'unknownError: BzCbsNYhMrjHiqZDTUASHg is not a valid did:indy did', + }, + }) + }) + + test('returns an error state if did is provided, but no verkey', async () => { + const result = await indyVdrIndyDidRegistrar.create(agentContext, { + did: 'BzCbsNYhMrjHiqZDTUASHg', + options: { + endorserMode: 'internal', + endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + alias: 'Hello', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'If a did is defined, a matching verkey must be provided', + }, + }) + }) + + test('returns an error state if did and verkey are provided, but the did is not self certifying', async () => { + const result = await indyVdrIndyDidRegistrar.create(agentContext, { + did: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + options: { + endorserMode: 'internal', + endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + verkey: 'verkey', + alias: 'Hello', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Initial verkey verkey does not match did did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + }, + }) + }) + + test('returns an error state if did is provided, but does not match with the namespace from the endorserDid', async () => { + const result = await indyVdrIndyDidRegistrar.create(agentContext, { + did: 'did:indy:pool2:B6xaJg1c2xU3D9ppCtt1CZ', + options: { + endorserMode: 'internal', + endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + alias: 'Hello', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: + "The endorser did uses namespace: 'pool1' and the did to register uses namespace: 'pool2'. Namespaces must match.", + }, + }) + }) + + test('creates a did:indy document without services', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + + // @ts-ignore - method is private + const createRegisterDidWriteRequest = jest.spyOn( + indyVdrIndyDidRegistrar, + 'createRegisterDidWriteRequest' + ) + // @ts-ignore type check fails because method is private + createRegisterDidWriteRequest.mockImplementationOnce(() => Promise.resolve()) + + // @ts-ignore - method is private + const registerPublicDidSpy = jest.spyOn(indyVdrIndyDidRegistrar, 'registerPublicDid') + // @ts-ignore type check fails because method is private + registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) + + const result = await indyVdrIndyDidRegistrar.create(agentContext, { + method: 'indy', + options: { + alias: 'Hello', + endorserMode: 'internal', + endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + role: 'STEWARD', + }, + secret: { + privateKey, + }, + }) + + expect(createRegisterDidWriteRequest).toHaveBeenCalledWith({ + agentContext, + pool: poolMock, + signingKey: expect.any(Key), + submitterNamespaceIdentifier: 'BzCbsNYhMrjHiqZDTUASHg', + namespaceIdentifier: 'B6xaJg1c2xU3D9ppCtt1CZ', + verificationKey: expect.any(Key), + alias: 'Hello', + diddocContent: undefined, + role: 'STEWARD', + }) + + expect(registerPublicDidSpy).toHaveBeenCalledWith(agentContext, poolMock, undefined) + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + didDocument: { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + verificationMethod: [ + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey', + type: 'Ed25519VerificationKey2018', + controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + }, + ], + authentication: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey'], + assertionMethod: undefined, + keyAgreement: undefined, + }, + secret: { + privateKey, + }, + }, + }) + }) + + test('creates a did:indy document by passing did', async () => { + // @ts-ignore - method is private + const createRegisterDidWriteRequest = jest.spyOn( + indyVdrIndyDidRegistrar, + 'createRegisterDidWriteRequest' + ) + // @ts-ignore type check fails because method is private + createRegisterDidWriteRequest.mockImplementationOnce(() => Promise.resolve()) + + // @ts-ignore - method is private + const registerPublicDidSpy = jest.spyOn(indyVdrIndyDidRegistrar, 'registerPublicDid') + // @ts-ignore type check fails because method is private + registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) + + const result = await indyVdrIndyDidRegistrar.create(agentContext, { + did: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + options: { + verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + alias: 'Hello', + endorserMode: 'internal', + endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + role: 'STEWARD', + }, + secret: {}, + }) + + expect(createRegisterDidWriteRequest).toHaveBeenCalledWith({ + agentContext, + pool: poolMock, + signingKey: expect.any(Key), + submitterNamespaceIdentifier: 'BzCbsNYhMrjHiqZDTUASHg', + namespaceIdentifier: 'B6xaJg1c2xU3D9ppCtt1CZ', + verificationKey: expect.any(Key), + alias: 'Hello', + diddocContent: undefined, + role: 'STEWARD', + }) + + expect(registerPublicDidSpy).toHaveBeenCalledWith( + agentContext, + poolMock, + // writeRequest + undefined + ) + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + didDocument: { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + verificationMethod: [ + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey', + type: 'Ed25519VerificationKey2018', + controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + }, + ], + authentication: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey'], + assertionMethod: undefined, + keyAgreement: undefined, + }, + secret: {}, + }, + }) + }) + + test('creates a did:indy document with services using diddocContent', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + + // @ts-ignore - method is private + const createRegisterDidWriteRequestSpy = jest.spyOn( + indyVdrIndyDidRegistrar, + 'createRegisterDidWriteRequest' + ) + // @ts-ignore type check fails because method is private + createRegisterDidWriteRequestSpy.mockImplementationOnce(() => Promise.resolve()) + + // @ts-ignore - method is private + const registerPublicDidSpy = jest.spyOn(indyVdrIndyDidRegistrar, 'registerPublicDid') + // @ts-ignore type check fails because method is private + registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) + + // @ts-ignore - method is private + const setEndpointsForDidSpy = jest.spyOn(indyVdrIndyDidRegistrar, 'setEndpointsForDid') + + const result = await indyVdrIndyDidRegistrar.create(agentContext, { + method: 'indy', + options: { + alias: 'Hello', + endorserMode: 'internal', + endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + role: 'STEWARD', + services: [ + new DidDocumentService({ + id: `#endpoint`, + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }), + new DidCommV1Service({ + id: `#did-communication`, + priority: 0, + recipientKeys: [`#key-agreement-1`], + routingKeys: ['key-1'], + serviceEndpoint: 'https://example.com/endpoint', + accept: ['didcomm/aip2;env=rfc19'], + }), + new NewDidCommV2Service({ + id: `#didcomm-messaging-1`, + serviceEndpoint: new NewDidCommV2ServiceEndpoint({ + accept: ['didcomm/v2'], + routingKeys: ['key-1'], + uri: 'https://example.com/endpoint', + }), + }), + ], + }, + secret: { + privateKey, + }, + }) + + expect(createRegisterDidWriteRequestSpy).toHaveBeenCalledWith({ + agentContext, + pool: poolMock, + signingKey: expect.any(Key), + submitterNamespaceIdentifier: 'BzCbsNYhMrjHiqZDTUASHg', + namespaceIdentifier: 'B6xaJg1c2xU3D9ppCtt1CZ', + verificationKey: expect.any(Key), + alias: 'Hello', + role: 'STEWARD', + diddocContent: { + '@context': [], + authentication: [], + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + keyAgreement: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1'], + service: [ + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#endpoint', + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }, + { + accept: ['didcomm/aip2;env=rfc19'], + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#did-communication', + priority: 0, + recipientKeys: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1'], + routingKeys: ['key-1'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#didcomm-messaging-1', + serviceEndpoint: { + uri: 'https://example.com/endpoint', + accept: ['didcomm/v2'], + routingKeys: ['key-1'], + }, + type: 'DIDCommMessaging', + }, + ], + verificationMethod: [ + { + controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1', + publicKeyBase58: 'Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', + type: 'X25519KeyAgreementKey2019', + }, + ], + }, + }) + + expect(registerPublicDidSpy).toHaveBeenCalledWith( + agentContext, + poolMock, + // writeRequest + undefined + ) + expect(setEndpointsForDidSpy).not.toHaveBeenCalled() + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + 'https://didcomm.org/messaging/contexts/v2', + ], + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + verificationMethod: [ + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey', + type: 'Ed25519VerificationKey2018', + controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + }, + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1', + type: 'X25519KeyAgreementKey2019', + controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + publicKeyBase58: 'Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', + }, + ], + service: [ + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#endpoint', + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }, + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#did-communication', + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + priority: 0, + recipientKeys: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1'], + routingKeys: ['key-1'], + accept: ['didcomm/aip2;env=rfc19'], + }, + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#didcomm-messaging-1', + type: 'DIDCommMessaging', + serviceEndpoint: { + uri: 'https://example.com/endpoint', + routingKeys: ['key-1'], + accept: ['didcomm/v2'], + }, + }, + ], + authentication: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey'], + assertionMethod: undefined, + keyAgreement: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1'], + }, + secret: { + privateKey, + }, + }, + }) + }) + + test('creates a did:indy document with services using attrib', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + + // @ts-ignore - method is private + const createRegisterDidWriteRequestSpy = jest.spyOn( + indyVdrIndyDidRegistrar, + 'createRegisterDidWriteRequest' + ) + // @ts-ignore type check fails because method is private + createRegisterDidWriteRequestSpy.mockImplementationOnce(() => Promise.resolve()) + + // @ts-ignore - method is private + const registerPublicDidSpy = jest.spyOn(indyVdrIndyDidRegistrar, 'registerPublicDid') + // @ts-ignore type check fails because method is private + registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) + + // @ts-ignore - method is private + const createSetDidEndpointsRequestSpy = jest.spyOn( + indyVdrIndyDidRegistrar, + 'createSetDidEndpointsRequest' + ) + // @ts-ignore type check fails because method is private + createSetDidEndpointsRequestSpy.mockImplementationOnce(() => Promise.resolve(undefined)) + + // @ts-ignore - method is private + const setEndpointsForDidSpy = jest.spyOn(indyVdrIndyDidRegistrar, 'setEndpointsForDid') + // @ts-ignore type check fails because method is private + setEndpointsForDidSpy.mockImplementationOnce(() => Promise.resolve(undefined)) + + const result = await indyVdrIndyDidRegistrar.create(agentContext, { + method: 'indy', + options: { + alias: 'Hello', + endorserMode: 'internal', + endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + role: 'STEWARD', + useEndpointAttrib: true, + services: [ + new DidDocumentService({ + id: `#endpoint`, + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }), + new DidCommV1Service({ + id: `#did-communication`, + priority: 0, + recipientKeys: [`#key-agreement-1`], + routingKeys: ['key-1'], + serviceEndpoint: 'https://example.com/endpoint', + accept: ['didcomm/aip2;env=rfc19'], + }), + new NewDidCommV2Service({ + id: `#didcomm-messaging-1`, + serviceEndpoint: new NewDidCommV2ServiceEndpoint({ + accept: ['didcomm/v2'], + routingKeys: ['key-1'], + uri: 'https://example.com/endpoint', + }), + }), + ], + }, + secret: { + privateKey, + }, + }) + expect(result.didState.state).toEqual('finished') + + expect(createRegisterDidWriteRequestSpy).toHaveBeenCalledWith({ + agentContext, + pool: poolMock, + signingKey: expect.any(Key), + submitterNamespaceIdentifier: 'BzCbsNYhMrjHiqZDTUASHg', + namespaceIdentifier: 'B6xaJg1c2xU3D9ppCtt1CZ', + verificationKey: expect.any(Key), + alias: 'Hello', + diddocContent: undefined, + role: 'STEWARD', + }) + + expect(registerPublicDidSpy).toHaveBeenCalledWith( + agentContext, + poolMock, + // writeRequest + undefined + ) + expect(createSetDidEndpointsRequestSpy).toHaveBeenCalledWith({ + agentContext, + pool: poolMock, + signingKey: expect.any(Key), + endorserDid: undefined, + // Unqualified created indy did + unqualifiedDid: 'B6xaJg1c2xU3D9ppCtt1CZ', + endpoints: { + endpoint: 'https://example.com/endpoint', + routingKeys: ['key-1'], + types: ['endpoint', 'did-communication', 'DIDCommMessaging'], + }, + }) + expect(setEndpointsForDidSpy).not.toHaveBeenCalled() + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + 'https://didcomm.org/messaging/contexts/v2', + ], + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + verificationMethod: [ + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey', + type: 'Ed25519VerificationKey2018', + controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + }, + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1', + type: 'X25519KeyAgreementKey2019', + controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + publicKeyBase58: 'Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', + }, + ], + service: [ + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#endpoint', + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }, + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#did-communication', + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + priority: 0, + recipientKeys: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1'], + routingKeys: ['key-1'], + accept: ['didcomm/aip2;env=rfc19'], + }, + { + id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#didcomm-messaging-1', + type: 'DIDCommMessaging', + serviceEndpoint: { uri: 'https://example.com/endpoint', routingKeys: ['key-1'], accept: ['didcomm/v2'] }, + }, + ], + authentication: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey'], + assertionMethod: undefined, + keyAgreement: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1'], + }, + secret: { + privateKey, + }, + }, + }) + }) + + test('stores the did document', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + + // @ts-ignore - method is private + const createRegisterDidWriteRequestSpy = jest.spyOn( + indyVdrIndyDidRegistrar, + 'createRegisterDidWriteRequest' + ) + // @ts-ignore type check fails because method is private + createRegisterDidWriteRequestSpy.mockImplementationOnce(() => Promise.resolve()) + + // @ts-ignore - method is private + const registerPublicDidSpy = jest.spyOn(indyVdrIndyDidRegistrar, 'registerPublicDid') + // @ts-ignore type check fails because method is private + registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) + + // @ts-ignore - method is private + const setEndpointsForDidSpy = jest.spyOn(indyVdrIndyDidRegistrar, 'setEndpointsForDid') + // @ts-ignore type check fails because method is private + setEndpointsForDidSpy.mockImplementationOnce(() => Promise.resolve(undefined)) + + const saveCalled = jest.fn() + eventEmitter.on>(RepositoryEventTypes.RecordSaved, saveCalled) + + await indyVdrIndyDidRegistrar.create(agentContext, { + method: 'indy', + options: { + alias: 'Hello', + endorserMode: 'internal', + endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + role: 'STEWARD', + services: [ + new DidDocumentService({ + id: `#endpoint`, + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }), + new DidCommV1Service({ + id: `#did-communication`, + priority: 0, + recipientKeys: [`#key-agreement-1`], + routingKeys: ['key-1'], + serviceEndpoint: 'https://example.com/endpoint', + accept: ['didcomm/aip2;env=rfc19'], + }), + new NewDidCommV2Service({ + id: `#didcomm-messaging-1`, + serviceEndpoint: new NewDidCommV2ServiceEndpoint({ + accept: ['didcomm/v2'], + routingKeys: ['key-1'], + uri: 'https://example.com/endpoint', + }), + }), + ], + }, + secret: { + privateKey, + }, + }) + + expect(saveCalled).toHaveBeenCalledTimes(1) + const [saveEvent] = saveCalled.mock.calls[0] + + expect(saveEvent.payload.record.getTags()).toMatchObject({ + recipientKeyFingerprints: ['z6LSrH6AdsQeZuKKmG6Ehx7abEQZsVg2psR2VU536gigUoAe'], + }) + expect(saveEvent.payload.record).toMatchObject({ + did: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + role: DidDocumentRole.Created, + didDocument: expect.any(DidDocument), + }) + }) + + test('returns an error state when calling update', async () => { + const result = await indyVdrIndyDidRegistrar.update() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:indy not implemented yet`, + }, + }) + }) + + test('returns an error state when calling deactivate', async () => { + const result = await indyVdrIndyDidRegistrar.deactivate() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:indy not implemented yet`, + }, + }) + }) +}) diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts new file mode 100644 index 0000000000..194ebfd9a6 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts @@ -0,0 +1,148 @@ +import { JsonTransformer } from '@credo-ts/core' + +import { getAgentConfig, getAgentContext, mockProperty } from '../../../../core/tests/helpers' +import { IndyVdrPool, IndyVdrPoolService } from '../../pool' +import { IndyVdrIndyDidResolver } from '../IndyVdrIndyDidResolver' + +import didIndyLjgpST2rjsoxYegQDRm7EL from './__fixtures__/didIndyLjgpST2rjsoxYegQDRm7EL.json' +import didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent from './__fixtures__/didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent.json' +import didIndyR1xKJw17sUoXhejEpugMYJFixture from './__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json' +import didIndyWJz9mHyW9BZksioQnRsrAoFixture from './__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json' + +jest.mock('../../pool/IndyVdrPool') +const IndyVdrPoolMock = IndyVdrPool as jest.Mock +const poolMock = new IndyVdrPoolMock() +mockProperty(poolMock, 'indyNamespace', 'ns1') + +const agentConfig = getAgentConfig('IndyVdrIndyDidResolver') + +const agentContext = getAgentContext({ + agentConfig, + registerInstances: [[IndyVdrPoolService, { getPoolForNamespace: jest.fn().mockReturnValue(poolMock) }]], +}) + +const resolver = new IndyVdrIndyDidResolver() + +describe('IndyVdrIndyDidResolver', () => { + describe('NYMs with diddocContent', () => { + it('should correctly resolve a did:indy document with arbitrary diddocContent', async () => { + const did = 'did:indy:ns2:LjgpST2rjsoxYegQDRm7EL' + + const nymResponse = { + result: { + data: JSON.stringify({ + did: 'LjgpST2rjsoxYegQDRm7EL', + verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + role: 'ENDORSER', + diddocContent: didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent, + }), + }, + } + + const poolMockSubmitRequest = jest.spyOn(poolMock, 'submitRequest') + poolMockSubmitRequest.mockResolvedValueOnce(nymResponse) + + const result = await resolver.resolve(agentContext, did) + + expect(poolMockSubmitRequest).toHaveBeenCalledTimes(1) + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didIndyLjgpST2rjsoxYegQDRm7EL, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + }) + + describe('NYMs without diddocContent', () => { + it('should correctly resolve a did:indy document without endpoint attrib', async () => { + const did = 'did:indy:ns1:R1xKJw17sUoXhejEpugMYJ' + + const nymResponse = { + result: { + data: JSON.stringify({ + did: 'R1xKJw17sUoXhejEpugMYJ', + verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + role: 'ENDORSER', + }), + }, + } + + const attribResponse = { + result: { + data: null, + }, + } + + jest.spyOn(poolMock, 'submitRequest').mockResolvedValueOnce(nymResponse) + jest.spyOn(poolMock, 'submitRequest').mockResolvedValueOnce(attribResponse) + + const result = await resolver.resolve(agentContext, did) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didIndyR1xKJw17sUoXhejEpugMYJFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should correctly resolve a did:indy document with endpoint attrib', async () => { + const did = 'did:indy:ns1:WJz9mHyW9BZksioQnRsrAo' + + const nymResponse = { + result: { + data: JSON.stringify({ + did: 'WJz9mHyW9BZksioQnRsrAo', + verkey: 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8', + role: 'ENDORSER', + }), + }, + } + + const attribResponse = { + result: { + data: JSON.stringify({ + endpoint: { + endpoint: 'https://agent.com', + types: ['endpoint', 'did-communication', 'DIDComm'], + routingKeys: ['routingKey1', 'routingKey2'], + }, + }), + }, + } + + jest.spyOn(poolMock, 'submitRequest').mockResolvedValueOnce(nymResponse) + jest.spyOn(poolMock, 'submitRequest').mockResolvedValueOnce(attribResponse) + + const result = await resolver.resolve(agentContext, did) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didIndyWJz9mHyW9BZksioQnRsrAoFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should return did resolution metadata with error if the indy ledger service throws an error', async () => { + const did = 'did:indy:ns1:R1xKJw17sUoXhejEpugMYJ' + + jest.spyOn(poolMock, 'submitRequest').mockRejectedValue(new Error('Error submitting read request')) + + const result = await resolver.resolve(agentContext, did) + + expect(result).toMatchObject({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did 'did:indy:ns1:R1xKJw17sUoXhejEpugMYJ': Error: Error submitting read request`, + }, + }) + }) + }) +}) diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts new file mode 100644 index 0000000000..2c9ad30494 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts @@ -0,0 +1,123 @@ +import { JsonTransformer } from '@credo-ts/core' + +import { parseDid } from '../../../../core/src/modules/dids/domain/parse' +import { getAgentConfig, getAgentContext, mockProperty } from '../../../../core/tests/helpers' +import { IndyVdrPool } from '../../pool/IndyVdrPool' +import { IndyVdrPoolService } from '../../pool/IndyVdrPoolService' +import { IndyVdrSovDidResolver } from '../IndyVdrSovDidResolver' + +import didSovR1xKJw17sUoXhejEpugMYJFixture from './__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json' +import didSovWJz9mHyW9BZksioQnRsrAoFixture from './__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json' + +jest.mock('../../pool/IndyVdrPool') +const IndyVdrPoolMock = IndyVdrPool as jest.Mock +const poolMock = new IndyVdrPoolMock() +mockProperty(poolMock, 'indyNamespace', 'local') + +const agentConfig = getAgentConfig('IndyVdrSovDidResolver') + +const agentContext = getAgentContext({ + agentConfig, + registerInstances: [[IndyVdrPoolService, { getPoolForDid: jest.fn().mockReturnValue({ pool: poolMock }) }]], +}) + +const resolver = new IndyVdrSovDidResolver() + +describe('DidResolver', () => { + describe('IndyVdrSovDidResolver', () => { + it('should correctly resolve a did:sov document', async () => { + const did = 'did:sov:R1xKJw17sUoXhejEpugMYJ' + + const nymResponse = { + result: { + data: JSON.stringify({ + did: 'R1xKJw17sUoXhejEpugMYJ', + verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + role: 'ENDORSER', + }), + }, + } + + const attribResponse = { + result: { + data: JSON.stringify({ + endpoint: { + endpoint: 'https://ssi.com', + profile: 'https://profile.com', + hub: 'https://hub.com', + }, + }), + }, + } + + jest.spyOn(poolMock, 'submitRequest').mockResolvedValueOnce(nymResponse) + jest.spyOn(poolMock, 'submitRequest').mockResolvedValueOnce(attribResponse) + + const result = await resolver.resolve(agentContext, did, parseDid(did)) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didSovR1xKJw17sUoXhejEpugMYJFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should resolve a did:sov document with routingKeys and types entries in the attrib', async () => { + const did = 'did:sov:WJz9mHyW9BZksioQnRsrAo' + + const nymResponse = { + result: { + data: JSON.stringify({ + did: 'WJz9mHyW9BZksioQnRsrAo', + verkey: 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8', + role: 'ENDORSER', + }), + }, + } + + const attribResponse = { + result: { + data: JSON.stringify({ + endpoint: { + endpoint: 'https://agent.com', + types: ['endpoint', 'did-communication', 'DIDComm'], + routingKeys: ['routingKey1', 'routingKey2'], + }, + }), + }, + } + + jest.spyOn(poolMock, 'submitRequest').mockResolvedValueOnce(nymResponse) + jest.spyOn(poolMock, 'submitRequest').mockResolvedValueOnce(attribResponse) + + const result = await resolver.resolve(agentContext, did, parseDid(did)) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didSovWJz9mHyW9BZksioQnRsrAoFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should return did resolution metadata with error if the indy ledger service throws an error', async () => { + const did = 'did:sov:R1xKJw17sUoXhejEpugMYJ' + + jest.spyOn(poolMock, 'submitRequest').mockRejectedValue(new Error('Error submitting read request')) + + const result = await resolver.resolve(agentContext, did, parseDid(did)) + + expect(result).toMatchObject({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did 'did:sov:R1xKJw17sUoXhejEpugMYJ': Error: Error submitting read request`, + }, + }) + }) + }) +}) diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123.json new file mode 100644 index 0000000000..c26715ddc1 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123.json @@ -0,0 +1,100 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:example:123", + "alsoKnownAs": ["did:example:456"], + "controller": ["did:example:456"], + "verificationMethod": [ + { + "id": "did:example:123#verkey", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC X..." + }, + { + "id": "did:example:123#key-2", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "-----BEGIN PUBLIC 9..." + }, + { + "id": "did:example:123#key-3", + "type": "Secp256k1VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyHex": "-----BEGIN PUBLIC A..." + } + ], + "service": [ + { + "id": "did:example:123#service-1", + "type": "Mediator", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" + }, + { + "id": "did:example:123#service-2", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "priority": 5 + }, + { + "id": "did:example:123#service-3", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["DADEajsDSaksLng9h"], + "routingKeys": ["DADEajsDSaksLng9h"], + "priority": 10 + } + ], + "authentication": [ + "did:example:123#verkey", + { + "id": "did:example:123#authentication-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "assertionMethod": [ + "did:example:123#verkey", + { + "id": "did:example:123#assertionMethod-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityDelegation": [ + "did:example:123#verkey", + { + "id": "did:example:123#capabilityDelegation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityInvocation": [ + "did:example:123#verkey", + { + "id": "did:example:123#capabilityInvocation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "keyAgreement": [ + "did:example:123#verkey", + { + "id": "did:example:123#keyAgreement-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + }, + { + "id": "did:example:123#keyAgreement-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ] +} diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123base.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123base.json new file mode 100644 index 0000000000..ce8b62392f --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123base.json @@ -0,0 +1,12 @@ +{ + "id": "did:example:123", + "verificationMethod": [ + { + "id": "did:example:123#verkey", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC X..." + } + ], + "authentication": ["did:example:123#verkey"] +} diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123extracontent.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123extracontent.json new file mode 100644 index 0000000000..81397021cd --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123extracontent.json @@ -0,0 +1,92 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "alsoKnownAs": ["did:example:456"], + "controller": ["did:example:456"], + "verificationMethod": [ + { + "id": "did:example:123#key-2", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "-----BEGIN PUBLIC 9..." + }, + { + "id": "did:example:123#key-3", + "type": "Secp256k1VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyHex": "-----BEGIN PUBLIC A..." + } + ], + "service": [ + { + "id": "did:example:123#service-1", + "type": "Mediator", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" + }, + { + "id": "did:example:123#service-2", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "priority": 5 + }, + { + "id": "did:example:123#service-3", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["DADEajsDSaksLng9h"], + "routingKeys": ["DADEajsDSaksLng9h"], + "priority": 10 + } + ], + "authentication": [ + { + "id": "did:example:123#authentication-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "assertionMethod": [ + "did:example:123#verkey", + { + "id": "did:example:123#assertionMethod-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityDelegation": [ + "did:example:123#verkey", + { + "id": "did:example:123#capabilityDelegation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityInvocation": [ + "did:example:123#verkey", + { + "id": "did:example:123#capabilityInvocation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "keyAgreement": [ + "did:example:123#verkey", + { + "id": "did:example:123#keyAgreement-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + }, + { + "id": "did:example:123#keyAgreement-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ] +} diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7EL.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7EL.json new file mode 100644 index 0000000000..49e43fa742 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7EL.json @@ -0,0 +1,110 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL", + "alsoKnownAs": ["did:indy:ns2:R1xKJw17sUoXhejEpugMYJ"], + "controller": ["did:indy:ns2:R1xKJw17sUoXhejEpugMYJ"], + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL", + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#verkey", + "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-2", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC X..." + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-3", + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyBase58": "-----BEGIN PUBLIC 9..." + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-4", + "type": "Secp256k1VerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyHex": "-----BEGIN PUBLIC A..." + } + ], + "service": [ + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#service-1", + "type": "Mediator", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#service-2", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "priority": 5 + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#service-3", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["DADEajsDSaksLng9h"], + "routingKeys": ["DADEajsDSaksLng9h"], + "priority": 10 + } + ], + "authentication": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#verkey", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#authentication-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "assertionMethod": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#assertionMethod-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityDelegation": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#capabilityDelegation-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityInvocation": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#capabilityInvocation-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "keyAgreement": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#keyAgreement-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#keyAgreement-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ] +} diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent.json new file mode 100644 index 0000000000..94bfaed219 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent.json @@ -0,0 +1,98 @@ +{ + "@context": ["https://w3id.org/security/suites/ed25519-2018/v1", "https://w3id.org/security/suites/x25519-2019/v1"], + "alsoKnownAs": ["did:indy:ns2:R1xKJw17sUoXhejEpugMYJ"], + "controller": ["did:indy:ns2:R1xKJw17sUoXhejEpugMYJ"], + "verificationMethod": [ + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-2", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC X..." + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-3", + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyBase58": "-----BEGIN PUBLIC 9..." + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-4", + "type": "Secp256k1VerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyHex": "-----BEGIN PUBLIC A..." + } + ], + "service": [ + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#service-1", + "type": "Mediator", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#service-2", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "priority": 5 + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#service-3", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["DADEajsDSaksLng9h"], + "routingKeys": ["DADEajsDSaksLng9h"], + "priority": 10 + } + ], + "authentication": [ + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#authentication-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "assertionMethod": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#assertionMethod-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityDelegation": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#capabilityDelegation-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityInvocation": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#capabilityInvocation-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "keyAgreement": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#keyAgreement-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#keyAgreement-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ] +} diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json new file mode 100644 index 0000000000..68874b6fc2 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json @@ -0,0 +1,13 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/suites/ed25519-2018/v1"], + "id": "did:indy:ns1:R1xKJw17sUoXhejEpugMYJ", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns1:R1xKJw17sUoXhejEpugMYJ", + "id": "did:indy:ns1:R1xKJw17sUoXhejEpugMYJ#verkey", + "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" + } + ], + "authentication": ["did:indy:ns1:R1xKJw17sUoXhejEpugMYJ#verkey"] +} diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json new file mode 100644 index 0000000000..2a58c356ca --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json @@ -0,0 +1,48 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1", + "https://didcomm.org/messaging/contexts/v2" + ], + "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo", + "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#verkey", + "publicKeyBase58": "GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8" + }, + { + "type": "X25519KeyAgreementKey2019", + "controller": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo", + "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#key-agreement-1", + "publicKeyBase58": "S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud" + } + ], + "authentication": ["did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#verkey"], + "keyAgreement": ["did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "service": [ + { + "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#endpoint", + "type": "endpoint", + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#did-communication", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "routingKeys": ["routingKey1", "routingKey2"], + "accept": ["didcomm/aip2;env=rfc19"], + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#didcomm-1", + "type": "DIDComm", + "serviceEndpoint": "https://agent.com", + "accept": ["didcomm/v2"], + "routingKeys": ["routingKey1", "routingKey2"] + } + ] +} diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json new file mode 100644 index 0000000000..6a6e4ed706 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json @@ -0,0 +1,51 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:R1xKJw17sUoXhejEpugMYJ", + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#key-1", + "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" + }, + { + "type": "X25519KeyAgreementKey2019", + "controller": "did:sov:R1xKJw17sUoXhejEpugMYJ", + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1", + "publicKeyBase58": "Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt" + } + ], + "authentication": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-1"], + "assertionMethod": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-1"], + "keyAgreement": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], + "service": [ + { + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#endpoint", + "type": "endpoint", + "serviceEndpoint": "https://ssi.com" + }, + { + "accept": ["didcomm/aip2;env=rfc19"], + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#did-communication", + "priority": 0, + "recipientKeys": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], + "routingKeys": [], + "serviceEndpoint": "https://ssi.com", + "type": "did-communication" + }, + { + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#profile", + "serviceEndpoint": "https://profile.com", + "type": "profile" + }, + { + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#hub", + "serviceEndpoint": "https://hub.com", + "type": "hub" + } + ] +} diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json new file mode 100644 index 0000000000..7b74e0587f --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json @@ -0,0 +1,49 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1", + "https://didcomm.org/messaging/contexts/v2" + ], + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:WJz9mHyW9BZksioQnRsrAo", + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#key-1", + "publicKeyBase58": "GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8" + }, + { + "type": "X25519KeyAgreementKey2019", + "controller": "did:sov:WJz9mHyW9BZksioQnRsrAo", + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1", + "publicKeyBase58": "S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud" + } + ], + "authentication": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-1"], + "assertionMethod": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-1"], + "keyAgreement": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "service": [ + { + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#endpoint", + "type": "endpoint", + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#did-communication", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "routingKeys": ["routingKey1", "routingKey2"], + "accept": ["didcomm/aip2;env=rfc19"], + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#didcomm-1", + "type": "DIDComm", + "serviceEndpoint": "https://agent.com", + "accept": ["didcomm/v2"], + "routingKeys": ["routingKey1", "routingKey2"] + } + ] +} diff --git a/packages/indy-vdr/src/dids/__tests__/didIndyUtil.test.ts b/packages/indy-vdr/src/dids/__tests__/didIndyUtil.test.ts new file mode 100644 index 0000000000..b45b81b763 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/didIndyUtil.test.ts @@ -0,0 +1,23 @@ +import { DidDocument, JsonTransformer } from '@credo-ts/core' + +import { combineDidDocumentWithJson, didDocDiff } from '../didIndyUtil' + +import didExample123Fixture from './__fixtures__/didExample123.json' +import didExample123Base from './__fixtures__/didExample123base.json' +import didExample123Extra from './__fixtures__/didExample123extracontent.json' + +describe('didIndyUtil', () => { + describe('combineDidDocumentWithJson', () => { + it('should correctly combine a base DIDDoc with extra contents from a JSON object', async () => { + const didDocument = JsonTransformer.fromJSON(didExample123Base, DidDocument) + + expect(combineDidDocumentWithJson(didDocument, didExample123Extra).toJSON()).toEqual(didExample123Fixture) + }) + }) + + describe('deepObjectDiff', () => { + it('should correctly show the diff between a base DidDocument and a full DidDocument', async () => { + expect(didDocDiff(didExample123Fixture, didExample123Base)).toMatchObject(didExample123Extra) + }) + }) +}) diff --git a/packages/indy-vdr/src/dids/__tests__/didSovUtil.test.ts b/packages/indy-vdr/src/dids/__tests__/didSovUtil.test.ts new file mode 100644 index 0000000000..f09f8060bc --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/didSovUtil.test.ts @@ -0,0 +1,5 @@ +describe('didSovUtil', () => { + describe('endpointsAttribFromServices', () => { + it.todo('should correctly transform DidDocumentService instances to endpoint Attrib') + }) +}) diff --git a/packages/indy-vdr/src/dids/didIndyUtil.ts b/packages/indy-vdr/src/dids/didIndyUtil.ts new file mode 100644 index 0000000000..ebdfbd98ae --- /dev/null +++ b/packages/indy-vdr/src/dids/didIndyUtil.ts @@ -0,0 +1,277 @@ +import type { GetNymResponseData, IndyEndpointAttrib } from './didSovUtil' +import type { IndyVdrPool } from '../pool' +import type { AgentContext } from '@credo-ts/core' + +import { parseIndyDid } from '@credo-ts/anoncreds' +import { + CredoError, + DidDocument, + DidDocumentBuilder, + DidsApi, + Hasher, + JsonTransformer, + Key, + KeyType, + TypedArrayEncoder, + convertPublicKeyToX25519, + getKeyFromVerificationMethod, +} from '@credo-ts/core' +import { GetAttribRequest, GetNymRequest } from '@hyperledger/indy-vdr-shared' + +import { IndyVdrError, IndyVdrNotFoundError } from '../error' + +import { addServicesFromEndpointsAttrib, getFullVerkey } from './didSovUtil' + +// Create a base DIDDoc template according to https://hyperledger.github.io/indy-did-method/#base-diddoc-template +export function indyDidDocumentFromDid(did: string, verKeyBase58: string) { + const verificationMethodId = `${did}#verkey` + + const publicKeyBase58 = verKeyBase58 + + const builder = new DidDocumentBuilder(did) + .addContext('https://w3id.org/security/suites/ed25519-2018/v1') + .addVerificationMethod({ + controller: did, + id: verificationMethodId, + publicKeyBase58, + type: 'Ed25519VerificationKey2018', + }) + .addAuthentication(verificationMethodId) + + return builder +} + +export function createKeyAgreementKey(verkey: string) { + return TypedArrayEncoder.toBase58(convertPublicKeyToX25519(TypedArrayEncoder.fromBase58(verkey))) +} + +const deepMerge = (a: Record, b: Record) => { + const output: Record = {} + + ;[...new Set([...Object.keys(a), ...Object.keys(b)])].forEach((key) => { + // Only an object includes a given key: just output it + if (a[key] && !b[key]) { + output[key] = a[key] + } else if (!a[key] && b[key]) { + output[key] = b[key] + } else { + // Both objects do include the key + // Some or both are arrays + if (Array.isArray(a[key])) { + if (Array.isArray(b[key])) { + const element = new Set() + ;(a[key] as Array).forEach((item: unknown) => element.add(item)) + ;(b[key] as Array).forEach((item: unknown) => element.add(item)) + output[key] = Array.from(element) + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const arr = a[key] as Array + output[key] = Array.from(new Set(...arr, b[key])) + } + } else if (Array.isArray(b[key])) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const arr = b[key] as Array + output[key] = Array.from(new Set(...arr, a[key])) + // Both elements are objects: recursive merge + } else if (typeof a[key] == 'object' && typeof b[key] == 'object') { + output[key] = deepMerge(a, b) + } + } + }) + return output +} + +/** + * Combine a JSON content with the contents of a DidDocument + * @param didDoc object containing original DIDDocument + * @param json object containing extra DIDDoc contents + * + * @returns a DidDocument object resulting from the combination of both + */ +export function combineDidDocumentWithJson(didDoc: DidDocument, json: Record) { + const didDocJson = didDoc.toJSON() + const combinedJson = deepMerge(didDocJson, json) + return JsonTransformer.fromJSON(combinedJson, DidDocument) +} + +/** + * Processes the difference between a base DidDocument and a complete DidDocument + * + * Note: it does deep comparison based only on "id" field to determine whether is + * the same object or is a different one + * + * @param extra complete DidDocument + * @param base base DidDocument + * @returns diff object + */ +export function didDocDiff(extra: Record, base: Record) { + const output: Record = {} + for (const key in extra) { + if (!(key in base)) { + output[key] = extra[key] + } else { + // They are arrays: compare elements + if (Array.isArray(extra[key]) && Array.isArray(base[key])) { + // Different types: return the extra + output[key] = [] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const baseAsArray = base[key] as Array + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const extraAsArray = extra[key] as Array + for (const element of extraAsArray) { + if (!baseAsArray.find((item) => item.id === element.id)) { + ;(output[key] as Array).push(element) + } + } + } // They are both objects: do recursive diff + else if (typeof extra[key] == 'object' && typeof base[key] == 'object') { + output[key] = didDocDiff(extra[key] as Record, base[key] as Record) + } else { + output[key] = extra[key] + } + } + } + return output +} + +/** + * Check whether the did is a self certifying did. If the verkey is abbreviated this method + * will always return true. Make sure that the verkey you pass in this method belongs to the + * did passed in + * + * @return Boolean indicating whether the did is self certifying + */ +export function isSelfCertifiedIndyDid(did: string, verkey: string): boolean { + const { namespace } = parseIndyDid(did) + const { did: didFromVerkey } = indyDidFromNamespaceAndInitialKey( + namespace, + Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) + ) + + if (didFromVerkey === did) { + return true + } + + return false +} + +export function indyDidFromNamespaceAndInitialKey(namespace: string, initialKey: Key) { + const buffer = Hasher.hash(initialKey.publicKey, 'sha-256') + + const id = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + const verkey = initialKey.publicKeyBase58 + const did = `did:indy:${namespace}:${id}` + + return { did, id, verkey } +} + +/** + * Fetches the verification key for a given did:indy did and returns the key as a {@link Key} object. + * + * @throws {@link CredoError} if the did could not be resolved or the key could not be extracted + */ +export async function verificationKeyForIndyDid(agentContext: AgentContext, did: string) { + // FIXME: we should store the didDocument in the DidRecord so we don't have to fetch our own did + // from the ledger to know which key is associated with the did + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didResult = await didsApi.resolve(did) + + if (!didResult.didDocument) { + throw new CredoError( + `Could not resolve did ${did}. ${didResult.didResolutionMetadata.error} ${didResult.didResolutionMetadata.message}` + ) + } + + // did:indy dids MUST have a verificationMethod with #verkey + const verificationMethod = didResult.didDocument.dereferenceKey(`${did}#verkey`) + const key = getKeyFromVerificationMethod(verificationMethod) + + return key +} + +export async function getPublicDid(pool: IndyVdrPool, unqualifiedDid: string) { + const request = new GetNymRequest({ dest: unqualifiedDid }) + + const didResponse = await pool.submitRequest(request) + + if (!didResponse.result.data) { + throw new IndyVdrNotFoundError(`DID ${unqualifiedDid} not found in indy namespace ${pool.indyNamespace}`) + } + return JSON.parse(didResponse.result.data) as GetNymResponseData +} + +export async function getEndpointsForDid(agentContext: AgentContext, pool: IndyVdrPool, unqualifiedDid: string) { + try { + agentContext.config.logger.debug(`Get endpoints for did '${unqualifiedDid}' from ledger '${pool.indyNamespace}'`) + + const request = new GetAttribRequest({ targetDid: unqualifiedDid, raw: 'endpoint' }) + + agentContext.config.logger.debug( + `Submitting get endpoint ATTRIB request for did '${unqualifiedDid}' to ledger '${pool.indyNamespace}'` + ) + const response = await pool.submitRequest(request) + + if (!response.result.data) { + return null + } + + const endpoints = JSON.parse(response.result.data as string)?.endpoint as IndyEndpointAttrib + agentContext.config.logger.debug( + `Got endpoints '${JSON.stringify(endpoints)}' for did '${unqualifiedDid}' from ledger '${pool.indyNamespace}'`, + { + response, + endpoints, + } + ) + + return endpoints + } catch (error) { + agentContext.config.logger.error( + `Error retrieving endpoints for did '${unqualifiedDid}' from ledger '${pool.indyNamespace}'`, + { + error, + } + ) + + throw new IndyVdrError(error) + } +} + +export async function buildDidDocument(agentContext: AgentContext, pool: IndyVdrPool, did: string) { + const { namespaceIdentifier } = parseIndyDid(did) + + const nym = await getPublicDid(pool, namespaceIdentifier) + + // Create base Did Document + + // For modern did:indy DIDs, we assume that GET_NYM is always a full verkey in base58. + // For backwards compatibility, we accept a shortened verkey and convert it using previous convention + const verkey = getFullVerkey(namespaceIdentifier, nym.verkey) + + const builder = indyDidDocumentFromDid(did, verkey) + + // If GET_NYM does not return any diddocContent, fallback to legacy GET_ATTRIB endpoint + if (!nym.diddocContent) { + const keyAgreementId = `${did}#key-agreement-1` + const endpoints = await getEndpointsForDid(agentContext, pool, namespaceIdentifier) + + if (endpoints) { + builder + .addContext('https://w3id.org/security/suites/x25519-2019/v1') + .addVerificationMethod({ + controller: did, + id: keyAgreementId, + publicKeyBase58: createKeyAgreementKey(verkey), + type: 'X25519KeyAgreementKey2019', + }) + .addKeyAgreement(keyAgreementId) + + // Process endpoint attrib following the same rules as for did:sov + addServicesFromEndpointsAttrib(builder, did, endpoints, keyAgreementId) + } + return builder.build() + } else { + // Combine it with didDoc (TODO: Check if diddocContent is returned as a JSON object or a string) + return combineDidDocumentWithJson(builder.build(), nym.diddocContent) + } +} diff --git a/packages/indy-vdr/src/dids/didSovUtil.ts b/packages/indy-vdr/src/dids/didSovUtil.ts new file mode 100644 index 0000000000..1ec6e52444 --- /dev/null +++ b/packages/indy-vdr/src/dids/didSovUtil.ts @@ -0,0 +1,204 @@ +import { + TypedArrayEncoder, + DidDocumentService, + DidDocumentBuilder, + DidCommV1Service, + DidCommV2Service, + convertPublicKeyToX25519, + CredoError, + Buffer, +} from '@credo-ts/core' + +export type CommEndpointType = 'endpoint' | 'did-communication' | 'DIDComm' + +export interface IndyEndpointAttrib { + endpoint?: string + types?: Array + routingKeys?: string[] + [key: string]: unknown +} + +export interface GetNymResponseData { + did: string + verkey: string + role: string + alias?: string + diddocContent?: Record +} + +export const FULL_VERKEY_REGEX = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/ + +/** + * Check a base58 encoded string against a regex expression to determine if it is a full valid verkey + * @param verkey Base58 encoded string representation of a verkey + * @return Boolean indicating if the string is a valid verkey + */ +export function isFullVerkey(verkey: string): boolean { + return FULL_VERKEY_REGEX.test(verkey) +} + +export function getFullVerkey(did: string, verkey: string) { + if (isFullVerkey(verkey)) return verkey + + // Did could have did:xxx prefix, only take the last item after : + const id = did.split(':').pop() ?? did + // Verkey is prefixed with ~ if abbreviated + const verkeyWithoutTilde = verkey.slice(1) + + // Create base58 encoded public key (32 bytes) + return TypedArrayEncoder.toBase58( + Buffer.concat([ + // Take did identifier (16 bytes) + TypedArrayEncoder.fromBase58(id), + // Concat the abbreviated verkey (16 bytes) + TypedArrayEncoder.fromBase58(verkeyWithoutTilde), + ]) + ) +} + +export function sovDidDocumentFromDid(fullDid: string, verkey: string) { + const verificationMethodId = `${fullDid}#key-1` + const keyAgreementId = `${fullDid}#key-agreement-1` + + const publicKeyBase58 = getFullVerkey(fullDid, verkey) + const publicKeyX25519 = TypedArrayEncoder.toBase58( + convertPublicKeyToX25519(TypedArrayEncoder.fromBase58(publicKeyBase58)) + ) + + const builder = new DidDocumentBuilder(fullDid) + .addContext('https://w3id.org/security/suites/ed25519-2018/v1') + .addContext('https://w3id.org/security/suites/x25519-2019/v1') + .addVerificationMethod({ + controller: fullDid, + id: verificationMethodId, + publicKeyBase58: publicKeyBase58, + type: 'Ed25519VerificationKey2018', + }) + .addVerificationMethod({ + controller: fullDid, + id: keyAgreementId, + publicKeyBase58: publicKeyX25519, + type: 'X25519KeyAgreementKey2019', + }) + .addAuthentication(verificationMethodId) + .addAssertionMethod(verificationMethodId) + .addKeyAgreement(keyAgreementId) + + return builder +} + +// Process Indy Attrib Endpoint Types according to: https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html > Read (Resolve) > DID Service Endpoint +function processEndpointTypes(types?: string[]) { + const expectedTypes = ['endpoint', 'did-communication', 'DIDComm'] + const defaultTypes = ['endpoint', 'did-communication'] + + // Return default types if types "is NOT present [or] empty" + if (!types || types.length <= 0) { + return defaultTypes + } + + // Return default types if types "contain any other values" + for (const type of types) { + if (!expectedTypes.includes(type)) { + return defaultTypes + } + } + + // Return provided types + return types +} + +export function endpointsAttribFromServices(services: DidDocumentService[]): IndyEndpointAttrib { + const commTypes: CommEndpointType[] = ['endpoint', 'did-communication', 'DIDComm'] + const commServices = services.filter((item) => commTypes.includes(item.type as CommEndpointType)) + + // Check that all services use the same endpoint, as only one is accepted + if (!commServices.every((item) => item.serviceEndpoint === services[0].serviceEndpoint)) { + throw new CredoError('serviceEndpoint for all services must match') + } + + const types: CommEndpointType[] = [] + const routingKeys = new Set() + + for (const commService of commServices) { + const commServiceType = commService.type as CommEndpointType + if (types.includes(commServiceType)) { + throw new CredoError('Only a single communication service per type is supported') + } + + types.push(commServiceType) + + if ( + (commService instanceof DidCommV1Service || commService instanceof DidCommV2Service) && + commService.routingKeys + ) { + commService.routingKeys.forEach((item) => routingKeys.add(item)) + } + } + + return { endpoint: services[0].serviceEndpoint as any, types, routingKeys: Array.from(routingKeys) } +} + +export function addServicesFromEndpointsAttrib( + builder: DidDocumentBuilder, + did: string, + endpoints: IndyEndpointAttrib, + keyAgreementId: string +) { + const { endpoint, routingKeys, types, ...otherEndpoints } = endpoints + + if (endpoint) { + const processedTypes = processEndpointTypes(types) + + // If 'endpoint' included in types, add id to the services array + if (processedTypes.includes('endpoint')) { + builder.addService( + new DidDocumentService({ + id: `${did}#endpoint`, + serviceEndpoint: endpoint, + type: 'endpoint', + }) + ) + } + + // If 'did-communication' included in types, add DIDComm v1 entry + if (processedTypes.includes('did-communication')) { + builder.addService( + new DidCommV1Service({ + id: `${did}#did-communication`, + serviceEndpoint: endpoint, + priority: 0, + routingKeys: routingKeys ?? [], + recipientKeys: [keyAgreementId], + accept: ['didcomm/aip2;env=rfc19'], + }) + ) + + // If 'DIDComm' included in types, add DIDComm v2 entry + // TODO: should it be DIDComm or DIDCommMessaging? (see https://github.com/sovrin-foundation/sovrin/issues/343) + if (processedTypes.includes('DIDComm')) { + builder + .addService( + new DidCommV2Service({ + id: `${did}#didcomm-1`, + serviceEndpoint: endpoint, + routingKeys: routingKeys ?? [], + accept: ['didcomm/v2'], + }) + ) + .addContext('https://didcomm.org/messaging/contexts/v2') + } + } + } + + // Add other endpoint types + for (const [type, endpoint] of Object.entries(otherEndpoints)) { + builder.addService( + new DidDocumentService({ + id: `${did}#${type}`, + serviceEndpoint: endpoint as string, + type, + }) + ) + } +} \ No newline at end of file diff --git a/packages/indy-vdr/src/dids/index.ts b/packages/indy-vdr/src/dids/index.ts new file mode 100644 index 0000000000..09983bad74 --- /dev/null +++ b/packages/indy-vdr/src/dids/index.ts @@ -0,0 +1,3 @@ +export { IndyVdrIndyDidRegistrar, IndyVdrDidCreateResult, IndyVdrDidCreateOptions } from './IndyVdrIndyDidRegistrar' +export { IndyVdrIndyDidResolver } from './IndyVdrIndyDidResolver' +export { IndyVdrSovDidResolver } from './IndyVdrSovDidResolver' diff --git a/packages/indy-vdr/src/error/IndyVdrError.ts b/packages/indy-vdr/src/error/IndyVdrError.ts new file mode 100644 index 0000000000..a54d960105 --- /dev/null +++ b/packages/indy-vdr/src/error/IndyVdrError.ts @@ -0,0 +1,7 @@ +import { CredoError } from '@credo-ts/core' + +export class IndyVdrError extends CredoError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/indy-vdr/src/error/IndyVdrNotConfiguredError.ts b/packages/indy-vdr/src/error/IndyVdrNotConfiguredError.ts new file mode 100644 index 0000000000..75cf40c9f6 --- /dev/null +++ b/packages/indy-vdr/src/error/IndyVdrNotConfiguredError.ts @@ -0,0 +1,7 @@ +import { IndyVdrError } from './IndyVdrError' + +export class IndyVdrNotConfiguredError extends IndyVdrError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/indy-vdr/src/error/IndyVdrNotFound.ts b/packages/indy-vdr/src/error/IndyVdrNotFound.ts new file mode 100644 index 0000000000..00b1b94c47 --- /dev/null +++ b/packages/indy-vdr/src/error/IndyVdrNotFound.ts @@ -0,0 +1,7 @@ +import { IndyVdrError } from './IndyVdrError' + +export class IndyVdrNotFoundError extends IndyVdrError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/indy-vdr/src/error/index.ts b/packages/indy-vdr/src/error/index.ts new file mode 100644 index 0000000000..f062bfbed0 --- /dev/null +++ b/packages/indy-vdr/src/error/index.ts @@ -0,0 +1,3 @@ +export * from './IndyVdrError' +export * from './IndyVdrNotFound' +export * from './IndyVdrNotConfiguredError' diff --git a/packages/indy-vdr/src/index.ts b/packages/indy-vdr/src/index.ts new file mode 100644 index 0000000000..ecd29dbc80 --- /dev/null +++ b/packages/indy-vdr/src/index.ts @@ -0,0 +1,11 @@ +export { + IndyVdrIndyDidRegistrar, + IndyVdrIndyDidResolver, + IndyVdrSovDidResolver, + IndyVdrDidCreateResult, + IndyVdrDidCreateOptions, +} from './dids' +export { IndyVdrPoolConfig, IndyVdrPoolService } from './pool' +export * from './IndyVdrModule' +export * from './IndyVdrModuleConfig' +export * from './anoncreds' diff --git a/packages/indy-vdr/src/pool/IndyVdrPool.ts b/packages/indy-vdr/src/pool/IndyVdrPool.ts new file mode 100644 index 0000000000..e2d3a8c57e --- /dev/null +++ b/packages/indy-vdr/src/pool/IndyVdrPool.ts @@ -0,0 +1,216 @@ +import type { AgentContext, Key } from '@credo-ts/core' +import type { IndyVdrRequest, RequestResponseType, IndyVdrPool as indyVdrPool } from '@hyperledger/indy-vdr-shared' + +import { parseIndyDid } from '@credo-ts/anoncreds' +import { TypedArrayEncoder } from '@credo-ts/core' +import { + GetTransactionAuthorAgreementRequest, + GetAcceptanceMechanismsRequest, + PoolCreate, + indyVdr, +} from '@hyperledger/indy-vdr-shared' + +import { IndyVdrError } from '../error' + +export interface TransactionAuthorAgreement { + version?: `${number}.${number}` | `${number}` + acceptanceMechanism: string +} + +export interface AuthorAgreement { + digest: string + version: string + text: string + ratification_ts: number + acceptanceMechanisms: AcceptanceMechanisms +} + +export interface AcceptanceMechanisms { + aml: Record + amlContext: string + version: string +} + +export interface IndyVdrPoolConfig { + genesisTransactions: string + isProduction: boolean + indyNamespace: string + transactionAuthorAgreement?: TransactionAuthorAgreement + connectOnStartup?: boolean +} + +export class IndyVdrPool { + private _pool?: indyVdrPool + private poolConfig: IndyVdrPoolConfig + public authorAgreement?: AuthorAgreement | null + + public constructor(poolConfig: IndyVdrPoolConfig) { + this.poolConfig = poolConfig + } + + public get indyNamespace(): string { + return this.poolConfig.indyNamespace + } + + public get config() { + return this.poolConfig + } + + public connect() { + if (this._pool) { + throw new IndyVdrError('Cannot connect to pool, already connected.') + } + + this._pool = new PoolCreate({ + parameters: { + transactions: this.config.genesisTransactions, + }, + }) + } + + /** + * Refreshes the connection to the pool. + */ + public async refreshConnection(): Promise { + if (this._pool) { + await this._pool.refresh() + } + } + + /** + * Get the transactions for a pool + */ + public get transactions() { + return this.pool.transactions + } + + private get pool(): indyVdrPool { + if (!this._pool) this.connect() + if (!this._pool) throw new IndyVdrError('Pool is not connected.') + + return this._pool + } + + public close() { + if (!this._pool) { + throw new IndyVdrError("Can't close pool. Pool is not connected") + } + + // FIXME: this method doesn't work?? + // this.pool.close() + } + + public async prepareWriteRequest( + agentContext: AgentContext, + request: Request, + signingKey: Key, + endorserDid?: string + ) { + await this.appendTaa(request) + + if (endorserDid) { + request.setEndorser({ endorser: parseIndyDid(endorserDid).namespaceIdentifier }) + } + + const signature = await agentContext.wallet.sign({ + data: TypedArrayEncoder.fromString(request.signatureInput), + key: signingKey, + }) + + request.setSignature({ + signature, + }) + + return request + } + + /** + * This method submits a request to the ledger. + * It does only submit the request. It does not modify it in any way. + * To create the request, use the `prepareWriteRequest` method. + * @param writeRequest + */ + + public async submitRequest( + writeRequest: Request + ): Promise> { + return await this.pool.submitRequest(writeRequest) + } + + private async appendTaa(request: IndyVdrRequest) { + const authorAgreement = await this.getTransactionAuthorAgreement() + const poolTaa = this.config.transactionAuthorAgreement + + // If ledger does not have TAA, we can just send request + if (authorAgreement == null) { + return request + } + + // Ledger has taa but user has not specified which one to use + if (!poolTaa) { + throw new IndyVdrError( + `Please, specify a transaction author agreement with version and acceptance mechanism. ${JSON.stringify( + authorAgreement + )}` + ) + } + + // Throw an error if the pool doesn't have the specified version and acceptance mechanism + if ( + authorAgreement.version !== poolTaa.version || + !authorAgreement.acceptanceMechanisms.aml[poolTaa.acceptanceMechanism] + ) { + // Throw an error with a helpful message + const errMessage = `Unable to satisfy matching TAA with mechanism ${JSON.stringify( + poolTaa.acceptanceMechanism + )} and version ${poolTaa.version} in pool.\n Found ${JSON.stringify( + authorAgreement.acceptanceMechanisms.aml + )} and version ${authorAgreement.version} in pool.` + throw new IndyVdrError(errMessage) + } + + const acceptance = indyVdr.prepareTxnAuthorAgreementAcceptance({ + text: authorAgreement.text, + version: authorAgreement.version, + taaDigest: authorAgreement.digest, + time: Math.floor(new Date().getTime() / 1000), + acceptanceMechanismType: poolTaa.acceptanceMechanism, + }) + + request.setTransactionAuthorAgreementAcceptance({ + acceptance: JSON.parse(acceptance), + }) + } + + private async getTransactionAuthorAgreement(): Promise { + // TODO Replace this condition with memoization + if (this.authorAgreement !== undefined) { + return this.authorAgreement + } + + const taaRequest = new GetTransactionAuthorAgreementRequest({}) + const taaResponse = await this.submitRequest(taaRequest) + + const acceptanceMechanismRequest = new GetAcceptanceMechanismsRequest({}) + const acceptanceMechanismResponse = await this.submitRequest(acceptanceMechanismRequest) + + const taaData = taaResponse.result.data + + // TAA can be null + if (taaData == null) { + this.authorAgreement = null + return null + } + + // If TAA is not null, we can be sure AcceptanceMechanisms is also not null + const authorAgreement = taaData as Omit + + const acceptanceMechanisms = acceptanceMechanismResponse.result.data as AcceptanceMechanisms + this.authorAgreement = { + ...authorAgreement, + acceptanceMechanisms, + } + + return this.authorAgreement + } +} diff --git a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts new file mode 100644 index 0000000000..0eea7b1239 --- /dev/null +++ b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts @@ -0,0 +1,224 @@ +import type { AgentContext } from '@credo-ts/core' +import type { GetNymResponse } from '@hyperledger/indy-vdr-shared' + +import { didIndyRegex } from '@credo-ts/anoncreds' +import { Logger, InjectionSymbols, injectable, inject, CacheModuleConfig } from '@credo-ts/core' +import { GetNymRequest } from '@hyperledger/indy-vdr-shared' + +import { IndyVdrModuleConfig } from '../IndyVdrModuleConfig' +import { IndyVdrError, IndyVdrNotFoundError, IndyVdrNotConfiguredError } from '../error' +import { isSelfCertifiedDid } from '../utils/did' +import { allSettled, onlyFulfilled, onlyRejected } from '../utils/promises' + +import { IndyVdrPool } from './IndyVdrPool' + +export interface CachedDidResponse { + nymResponse: { + did: string + verkey: string + } + indyNamespace: string +} +@injectable() +export class IndyVdrPoolService { + public pools: IndyVdrPool[] = [] + private logger: Logger + private indyVdrModuleConfig: IndyVdrModuleConfig + + public constructor(@inject(InjectionSymbols.Logger) logger: Logger, indyVdrModuleConfig: IndyVdrModuleConfig) { + this.logger = logger + this.indyVdrModuleConfig = indyVdrModuleConfig + + this.pools = this.indyVdrModuleConfig.networks.map((poolConfig) => new IndyVdrPool(poolConfig)) + } + + /** + * Get the most appropriate pool for the given did. + * If the did is a qualified indy did, the pool will be determined based on the namespace. + * If it is a legacy unqualified indy did, the pool will be determined based on the algorithm as described in this document: + * https://docs.google.com/document/d/109C_eMsuZnTnYe2OAd02jAts1vC4axwEKIq7_4dnNVA/edit + * + * This method will optionally return a nym response when the did has been resolved to determine the ledger + * either now or in the past. The nymResponse can be used to prevent multiple ledger quries fetching the same + * did + */ + public async getPoolForDid( + agentContext: AgentContext, + did: string + ): Promise<{ pool: IndyVdrPool; nymResponse?: CachedDidResponse['nymResponse'] }> { + // Check if the did starts with did:indy + const match = did.match(didIndyRegex) + + if (match) { + const [, namespace] = match + + const pool = this.getPoolForNamespace(namespace) + + if (pool) return { pool } + + throw new IndyVdrError(`Pool for indy namespace '${namespace}' not found`) + } else { + return await this.getPoolForLegacyDid(agentContext, did) + } + } + + private async getPoolForLegacyDid( + agentContext: AgentContext, + did: string + ): Promise<{ pool: IndyVdrPool; nymResponse?: CachedDidResponse['nymResponse'] }> { + const pools = this.pools + + if (pools.length === 0) { + throw new IndyVdrNotConfiguredError( + 'No indy ledgers configured. Provide at least one pool configuration in IndyVdrModuleConfigOptions.networks' + ) + } + + const cache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache + const cacheKey = `IndyVdrPoolService:${did}` + + const cachedNymResponse = await cache.get(agentContext, cacheKey) + const pool = this.pools.find((pool) => pool.indyNamespace === cachedNymResponse?.indyNamespace) + + // If we have the nym response with associated pool in the cache, we'll use that + if (cachedNymResponse && pool) { + this.logger.trace(`Found ledger id '${pool.indyNamespace}' for did '${did}' in cache`) + return { pool, nymResponse: cachedNymResponse.nymResponse } + } + + const { successful, rejected } = await this.getSettledDidResponsesFromPools(did, pools) + + if (successful.length === 0) { + const allNotFound = rejected.every((e) => e.reason instanceof IndyVdrNotFoundError) + const rejectedOtherThanNotFound = rejected.filter((e) => !(e.reason instanceof IndyVdrNotFoundError)) + + // All ledgers returned response that the did was not found + if (allNotFound) { + throw new IndyVdrNotFoundError(`Did '${did}' not found on any of the ledgers (total ${this.pools.length}).`) + } + + // one or more of the ledgers returned an unknown error + throw new IndyVdrError( + `Unknown error retrieving did '${did}' from '${rejectedOtherThanNotFound.length}' of '${pools.length}' ledgers. ${rejectedOtherThanNotFound[0].reason}`, + { cause: rejectedOtherThanNotFound[0].reason } + ) + } + + // If there are self certified DIDs we always prefer it over non self certified DIDs + // We take the first self certifying DID as we take the order in the + // IndyVdrModuleConfigOptions.networks config as the order of preference of ledgers + let value = successful.find((response) => + isSelfCertifiedDid(response.value.did.nymResponse.did, response.value.did.nymResponse.verkey) + )?.value + + if (!value) { + // Split between production and nonProduction ledgers. If there is at least one + // successful response from a production ledger, only keep production ledgers + // otherwise we only keep the non production ledgers. + const production = successful.filter((s) => s.value.pool.config.isProduction) + const nonProduction = successful.filter((s) => !s.value.pool.config.isProduction) + const productionOrNonProduction = production.length >= 1 ? production : nonProduction + + // We take the first value as we take the order in the IndyVdrModuleConfigOptions.networks + // config as the order of preference of ledgers + value = productionOrNonProduction[0].value + } + + await cache.set(agentContext, cacheKey, { + nymResponse: { + did: value.did.nymResponse.did, + verkey: value.did.nymResponse.verkey, + }, + indyNamespace: value.did.indyNamespace, + }) + return { pool: value.pool, nymResponse: value.did.nymResponse } + } + + private async getSettledDidResponsesFromPools(did: string, pools: IndyVdrPool[]) { + this.logger.trace(`Retrieving did '${did}' from ${pools.length} ledgers`) + const didResponses = await allSettled(pools.map((pool) => this.getDidFromPool(did, pool))) + + const successful = onlyFulfilled(didResponses) + this.logger.trace(`Retrieved ${successful.length} responses from ledgers for did '${did}'`) + + const rejected = onlyRejected(didResponses) + + return { + rejected, + successful, + } + } + + /** + * Refresh the pool connections asynchronously + */ + public refreshPoolConnections() { + return Promise.allSettled(this.pools.map((pool) => pool.refreshConnection())) + } + + /** + * Get all pool transactions + */ + public getAllPoolTransactions() { + return Promise.allSettled( + this.pools.map(async (pool) => { + return { config: pool.config, transactions: await pool.transactions } + }) + ) + } + + /** + * Get the most appropriate pool for the given indyNamespace + */ + public getPoolForNamespace(indyNamespace: string) { + if (this.pools.length === 0) { + throw new IndyVdrNotConfiguredError( + 'No indy ledgers configured. Provide at least one pool configuration in IndyVdrModuleConfigOptions.networks' + ) + } + + const pool = this.pools.find((pool) => pool.indyNamespace === indyNamespace) + + if (!pool) { + throw new IndyVdrError(`No ledgers found for indy namespace '${indyNamespace}'.`) + } + + return pool + } + + private async getDidFromPool(did: string, pool: IndyVdrPool): Promise { + try { + this.logger.trace(`Get public did '${did}' from ledger '${pool.indyNamespace}'`) + const request = new GetNymRequest({ dest: did }) + + this.logger.trace(`Submitting get did request for did '${did}' to ledger '${pool.indyNamespace}'`) + const response = await pool.submitRequest(request) + + if (!response.result.data) { + throw new IndyVdrNotFoundError(`Did ${did} not found on indy pool with namespace ${pool.indyNamespace}`) + } + + const result = JSON.parse(response.result.data) + + this.logger.trace(`Retrieved did '${did}' from ledger '${pool.indyNamespace}'`, result) + + return { + did: { nymResponse: { did: result.dest, verkey: result.verkey }, indyNamespace: pool.indyNamespace }, + pool, + response, + } + } catch (error) { + this.logger.trace(`Error retrieving did '${did}' from ledger '${pool.indyNamespace}'`, { + error, + did, + }) + throw error + } + } +} + +export interface PublicDidRequest { + did: CachedDidResponse + pool: IndyVdrPool + response: GetNymResponse +} diff --git a/packages/indy-vdr/src/pool/index.ts b/packages/indy-vdr/src/pool/index.ts new file mode 100644 index 0000000000..ec4bc06677 --- /dev/null +++ b/packages/indy-vdr/src/pool/index.ts @@ -0,0 +1,2 @@ +export * from './IndyVdrPool' +export * from './IndyVdrPoolService' diff --git a/packages/indy-vdr/src/utils/did.ts b/packages/indy-vdr/src/utils/did.ts new file mode 100644 index 0000000000..7f73dd75c3 --- /dev/null +++ b/packages/indy-vdr/src/utils/did.ts @@ -0,0 +1,60 @@ +/** + * Based on DidUtils implementation in Aries Framework .NET + * @see: https://github.com/hyperledger/aries-framework-dotnet/blob/f90eaf9db8548f6fc831abea917e906201755763/src/Hyperledger.Aries/Utils/DidUtils.cs + * + * Some context about full verkeys versus abbreviated verkeys: + * A standard verkey is 32 bytes, and by default in Indy the DID is chosen as the first 16 bytes of that key, before base58 encoding. + * An abbreviated verkey replaces the first 16 bytes of the verkey with ~ when it matches the DID. + * + * When a full verkey is used to register on the ledger, this is stored as a full verkey on the ledger and also returned from the ledger as a full verkey. + * The same applies to an abbreviated verkey. If an abbreviated verkey is used to register on the ledger, this is stored as an abbreviated verkey on the ledger and also returned from the ledger as an abbreviated verkey. + * + * For this reason we need some methods to check whether verkeys are full or abbreviated, so we can align this with `indy.abbreviateVerkey` + * + * Aries Framework .NET also abbreviates verkey before sending to ledger: + * https://github.com/hyperledger/aries-framework-dotnet/blob/f90eaf9db8548f6fc831abea917e906201755763/src/Hyperledger.Aries/Ledger/DefaultLedgerService.cs#L139-L147 + */ + +import { TypedArrayEncoder } from '@credo-ts/core' + +export const ABBREVIATED_VERKEY_REGEX = /^~[1-9A-HJ-NP-Za-km-z]{21,22}$/ + +/** + * Check whether the did is a self certifying did. If the verkey is abbreviated this method + * will always return true. Make sure that the verkey you pass in this method belongs to the + * did passed in + * + * @return Boolean indicating whether the did is self certifying + */ +export function isSelfCertifiedDid(did: string, verkey: string): boolean { + // If the verkey is Abbreviated, it means the full verkey + // is the did + the verkey + if (isAbbreviatedVerkey(verkey)) { + return true + } + + const didFromVerkey = indyDidFromPublicKeyBase58(verkey) + + if (didFromVerkey === did) { + return true + } + + return false +} + +export function indyDidFromPublicKeyBase58(publicKeyBase58: string): string { + const buffer = TypedArrayEncoder.fromBase58(publicKeyBase58) + + const did = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + + return did +} + +/** + * Check a base58 encoded string against a regex expression to determine if it is a valid abbreviated verkey + * @param verkey Base58 encoded string representation of an abbreviated verkey + * @returns Boolean indicating if the string is a valid abbreviated verkey + */ +export function isAbbreviatedVerkey(verkey: string): boolean { + return ABBREVIATED_VERKEY_REGEX.test(verkey) +} diff --git a/packages/indy-vdr/src/utils/promises.ts b/packages/indy-vdr/src/utils/promises.ts new file mode 100644 index 0000000000..0e843d73b5 --- /dev/null +++ b/packages/indy-vdr/src/utils/promises.ts @@ -0,0 +1,44 @@ +// This file polyfills the allSettled method introduced in ESNext + +export type AllSettledFulfilled = { + status: 'fulfilled' + value: T +} + +export type AllSettledRejected = { + status: 'rejected' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reason: any +} + +export function allSettled(promises: Promise[]) { + return Promise.all( + promises.map((p) => + p + .then( + (value) => + ({ + status: 'fulfilled', + value, + } as AllSettledFulfilled) + ) + .catch( + (reason) => + ({ + status: 'rejected', + reason, + } as AllSettledRejected) + ) + ) + ) +} + +export function onlyFulfilled(entries: Array | AllSettledRejected>) { + // We filter for only the rejected values, so we can safely cast the type + return entries.filter((e) => e.status === 'fulfilled') as AllSettledFulfilled[] +} + +export function onlyRejected(entries: Array | AllSettledRejected>) { + // We filter for only the rejected values, so we can safely cast the type + return entries.filter((e) => e.status === 'rejected') as AllSettledRejected[] +} diff --git a/packages/indy-vdr/src/utils/sign.ts b/packages/indy-vdr/src/utils/sign.ts new file mode 100644 index 0000000000..1ca51c118a --- /dev/null +++ b/packages/indy-vdr/src/utils/sign.ts @@ -0,0 +1,38 @@ +import type { IndyVdrPool } from '../pool' +import type { AgentContext, Key } from '@credo-ts/core' +import type { IndyVdrRequest } from '@hyperledger/indy-vdr-shared' + +import { TypedArrayEncoder } from '@credo-ts/core' + +import { verificationKeyForIndyDid } from '../dids/didIndyUtil' + +export async function multiSignRequest( + agentContext: AgentContext, + request: Request, + signingKey: Key, + identifier: string +) { + const signature = await agentContext.wallet.sign({ + data: TypedArrayEncoder.fromString(request.signatureInput), + key: signingKey, + }) + + request.setMultiSignature({ + signature, + identifier, + }) + + return request +} + +export async function signRequest( + agentContext: AgentContext, + pool: IndyVdrPool, + request: Request, + submitterDid: string +) { + const signingKey = await verificationKeyForIndyDid(agentContext, submitterDid) + const signedRequest = await pool.prepareWriteRequest(agentContext, request, signingKey) + + return signedRequest +} diff --git a/packages/indy-vdr/tests/__fixtures__/anoncreds.ts b/packages/indy-vdr/tests/__fixtures__/anoncreds.ts new file mode 100644 index 0000000000..f4b57c21f0 --- /dev/null +++ b/packages/indy-vdr/tests/__fixtures__/anoncreds.ts @@ -0,0 +1,41 @@ +export const credentialDefinitionValue = { + primary: { + n: '96517142458750088826087901549537285521906361834839650465292394026155791790248920518228426560592477800345470631128393537910767968076647428853737338120375137978526133371095345886547568849980095910835456337942570110635942227498396677781945046904040000347997661394155645138402989185582727368743644878567330299129483548946710969360956979880962101169330048328620192831242584775824654760726417810662811409929761424969870024291961980782988854217354212087291593903213167261779548063894662259300608395552269380441482047725811646638173390809967510159302372018819245039226007682154490256871635806558216146474297742733244470144481', + s: '20992997088800769394205042281221010730843336204635587269131066142238627416871294692123680065003125450990475247419429111144686875080339959479648984195457400282722471552678361441816569115316390063503704185107464429408708889920969284364549487320740759452356010336698287092961864738455949515401889999320804333605635972368885179914619910494573144273759358510644118555354521660927445864167887629319425342133470781407706668100509422240127902573158722086763638357241708157836231326104213948080124231104027985997092193458353052131052627451830345602820935886233072722689872803371231173593216542422645374438328309647440653637339', + r: { + master_secret: + '96243300745227716230048295249700256382424379142767068560156597061550615821183969840133023439359733351013932957841392861447122785423145599004240865527901625751619237368187131360686977600247815596986496835118582544022443932674638843143227258367859921648385998241629365673854479167826898057354386557912400420925145402535066400276579674049751639901555837852972622061540154688641944145082381483273814616102862399655638465723909813901943343059991047747289931252070264205125933226649905593045675877143065756794349492159868513288280364195700788501708587588090219665708038121636837649207584981238653023213330207384929738192210', + age: '73301750658973501389860306433954162777688414647250690792688553201037736559940890441467927863421690990807820789906540409252803697381653459639864945429958798104818241892796218340966964349674689564019059435289373607451125919476002261041343187491848656595845611576458601110066647002078334660251906541846222115184239401618625285703919125402959929850028352261117167621349930047514115676870868726855651130262227714591240534532398809967792128535084773798290351459391475237061458901325844643172504167457543287673202618731404966555015061917662865397763636445953946274068384614117513804834235388565249331682010365807270858083546', + }, + rctxt: + '37788128721284563440858950515231840450431543928224096081933216180465915572829884228780081835462293611329848268384962871736884632087015070623933628853658097637604059748079512999518737243304794110313829761155878287344472916564970806851294430356498883927870926898737394894892797927804721407643833828162246495645836390303263072281761384240973982733122383052566872688887552226083782030670443318152427129452272570595367287061688769394567289624972332234661767648489253220495098949161964171486245324730862072203259801377135500275012560207100571502032523912388082460843991502336467718632746396226650194750972544436894286230063', + z: '43785356695890052462955676926428400928903479009358861113206349419200366390858322895540291303484939601128045362682307382393826375825484851021601464391509750565285197155653613669680662395620338416776539485377195826876505126073018100680273457526216247879013350460071029101583221000647494610122617904515744711339846577920055655093367012508192004131719432915903924789974568341538556528133188398290594619318653419602058489178526243446782729272985727332736198326183868783570550373552407121582843992983431205917273352230155794805507408743590383242904107596623095433284330566906935063373759426916339149701872288610119965287995', + }, + revocation: { + g: '1 0A84C28144BC8B677839038FFFA824AB5ADE517F8DD4A89F092FAF9A3560C62D 1 00FD708E112EEA5D89AF9D0559795E6DBCF56D3B8CDF79EFF34A72EB741F896F 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + g_dash: + '1 201F3E23CC7E9284F3EFCF9500F1E2537C398EAB2E94D2EB801AECC7FBFBDC01 1 08132C7723CF9861D4CC24B56555EF1CBD9AE746C97B3ADFA36C669F2DCE09B6 1 1B2397FB2A1ADE704E2A1E4C242612F4677F9F1BD09E6B14C2E77E25EDA4C62E 1 00CDC2CF5F278D699D52223577AB032C150A3CB4C8E8AB07AB9D592772910E95 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + h: '1 072E0A505004F2F32B4210E72FA18A2ADF17F31479BD2059B7A8C0BA58F2ACB3 1 05C70F039E60317003C41C319753ECACC629791FDB06D6ADC5B06DD94501B973 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + h0: '1 03CBE26D18118E9770D4A0B3E8607B3B3A8D3D3CA81FF8D41862430CC583156E 1 004A2A57E0A826AEFF007EDDAF89B02F054050843689167B10127FE9EDEEEDA9 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + h1: '1 10C9F9DE537994E4FEF2625AFA78342C8A096238A875F6899DD500230E6022E5 1 0C0A88F53D020557377B4ED9C3826E9B8F918DD03E23B0F8ECD922F8333359D3 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + h2: '1 017F748AEEC1DDE4E4C3FBAE771C041F0A6FAEAF34FD02AF773AC4B75025147B 1 1298DBD9A4BEE6AD54E060A57BCE932735B7738C30A9ADAEFE2F38E1858A0183 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + htilde: + '1 0C471F0451D6AC352E28B6ECDE8D7233B75530AE59276DF0F4B9A8B0C5C7E5DB 1 24CE4461910AA5D60C09C24EE0FE51E1B1600D8BA6E483E9050EF897CA3E3C8A 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + h_cap: + '1 225B2106DEBD353AABDFC4C7F7E8660D308FB514EA9DAE0533DDEB65CF796159 1 1F6093622F439FC22C64F157F4F35F7C592EC0169C6F0026BC44CD3E375974A7 1 142126FAC3657AD846D394E1F72FD01ECC15E84416713CD133980E324B24F4BC 1 0357995DBDCD4385E59E607761AB30AE8D9DDE005A777EE846EF51AE2816CD33 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + u: '1 00D8DDC2EB6536CA320EE035D099937E59B11678162C1BFEB30C58FCA9F84650 1 1557A5B05A1A30D63322E187D323C9CA431BC5E811E68D4703933D9DDA26D299 1 10E8AB93AA87839B757521742EBA23C3B257C91F61A93D37AEC4C0A011B5F073 1 1DA65E40406A7875DA8CFCE9FD7F283145C166382A937B72819BDC335FE9A734 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + pk: '1 1A7EBBE3E7F8ED50959851364B20997944FA8AE5E3FC0A2BB531BAA17179D320 1 02C55FE6F64A2A4FF49B37C513C39E56ECD565CFAD6CA46DC6D8095179351863 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + y: '1 1BF97F07270EC21A89E43BCA645D86A755F846B547238F1DA379E088CDD9B40D 1 146BB00F56FFC0DEF6541CEB484C718559B398DB1547B52850E46B23144161F1 1 079A1BEF8DFFA4E6352F701D476664340E7FBE5D3F46B897412BD2B5F10E33D7 1 02FDC508AEF90FB11961AF332BE4037973C76B954FFA48848F7E0588E93FCA8C 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + }, +} + +export const revocationRegistryDefinitionValue = { + maxCredNum: 11, + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', +} diff --git a/packages/indy-vdr/tests/helpers.ts b/packages/indy-vdr/tests/helpers.ts new file mode 100644 index 0000000000..992cfdfc4b --- /dev/null +++ b/packages/indy-vdr/tests/helpers.ts @@ -0,0 +1,77 @@ +import type { IndyVdrDidCreateOptions } from '../src/dids/IndyVdrIndyDidRegistrar' +import type { Agent } from '@credo-ts/core' + +import { + DidCommV1Service, + NewDidCommV2Service, + DidDocumentService, + KeyType, + NewDidCommV2ServiceEndpoint, +} from '@credo-ts/core' +import { indyVdr } from '@hyperledger/indy-vdr-nodejs' + +import { sleep } from '../../core/src/utils/sleep' +import { genesisTransactions } from '../../core/tests/helpers' +import { IndyVdrModuleConfig } from '../src/IndyVdrModuleConfig' + +export const indyVdrModuleConfig = new IndyVdrModuleConfig({ + indyVdr, + networks: [ + { + genesisTransactions, + indyNamespace: 'pool:localtest', + isProduction: false, + transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, + }, + ], +}) + +export async function createDidOnLedger(agent: Agent, endorserDid: string) { + const key = await agent.wallet.createKey({ keyType: KeyType.Ed25519 }) + + const createResult = await agent.dids.create({ + method: 'indy', + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + alias: 'Alias', + role: 'TRUSTEE', + verkey: key.publicKeyBase58, + useEndpointAttrib: true, + services: [ + new DidDocumentService({ + id: `#endpoint`, + serviceEndpoint: 'http://localhost:3000', + type: 'endpoint', + }), + new DidCommV1Service({ + id: `#did-communication`, + priority: 0, + recipientKeys: [`#key-agreement-1`], + routingKeys: ['a-routing-key'], + serviceEndpoint: 'http://localhost:3000', + accept: ['didcomm/aip2;env=rfc19'], + }), + new NewDidCommV2Service({ + id: `#didcomm--messaging-1`, + serviceEndpoint: new NewDidCommV2ServiceEndpoint({ + accept: ['didcomm/v2'], + routingKeys: ['a-routing-key'], + uri: 'http://localhost:3000', + }), + }), + ], + }, + }) + + if (!createResult.didState.did) { + throw new Error( + `Did was not created. ${createResult.didState.state === 'failed' ? createResult.didState.reason : 'Not finished'}` + ) + } + + // Wait some time pass to let ledger settle the object + await sleep(1000) + + return { did: createResult.didState.did, key } +} diff --git a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts new file mode 100644 index 0000000000..d0647dc599 --- /dev/null +++ b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts @@ -0,0 +1,1329 @@ +import type { IndyVdrDidCreateOptions, IndyVdrDidCreateResult } from '../src/dids/IndyVdrIndyDidRegistrar' + +import { + getUnqualifiedRevocationRegistryDefinitionId, + parseIndyDid, + parseIndyRevocationRegistryId, +} from '@credo-ts/anoncreds' +import { Agent, DidsModule, TypedArrayEncoder } from '@credo-ts/core' +import { indyVdr } from '@hyperledger/indy-vdr-nodejs' + +import { getInMemoryAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' +import { IndyVdrIndyDidResolver, IndyVdrModule, IndyVdrSovDidResolver } from '../src' +import { IndyVdrAnonCredsRegistry } from '../src/anoncreds/IndyVdrAnonCredsRegistry' +import { IndyVdrIndyDidRegistrar } from '../src/dids/IndyVdrIndyDidRegistrar' +import { IndyVdrPoolService } from '../src/pool' + +import { credentialDefinitionValue, revocationRegistryDefinitionValue } from './__fixtures__/anoncreds' +import { indyVdrModuleConfig } from './helpers' + +const indyVdrAnonCredsRegistry = new IndyVdrAnonCredsRegistry() + +const endorser = new Agent( + getInMemoryAgentOptions( + 'IndyVdrAnonCredsRegistryEndorser', + {}, + { + indyVdr: new IndyVdrModule({ + indyVdr, + networks: indyVdrModuleConfig.networks, + }), + dids: new DidsModule({ + registrars: [new IndyVdrIndyDidRegistrar()], + resolvers: [new IndyVdrSovDidResolver(), new IndyVdrIndyDidResolver()], + }), + } + ) +) + +const agent = new Agent( + getInMemoryAgentOptions( + 'IndyVdrAnonCredsRegistryAgent', + {}, + { + indyVdr: new IndyVdrModule({ + indyVdr, + networks: indyVdrModuleConfig.networks, + }), + dids: new DidsModule({ + registrars: [new IndyVdrIndyDidRegistrar()], + resolvers: [new IndyVdrSovDidResolver(), new IndyVdrIndyDidResolver()], + }), + } + ) +) + +const indyVdrPoolService = endorser.dependencyManager.resolve(IndyVdrPoolService) + +// FIXME: this test is very slow, probably due to the sleeps. Can we speed it up? +describe('IndyVdrAnonCredsRegistry', () => { + let endorserDid: string + let agentDid: string + beforeAll(async () => { + await endorser.initialize() + await agent.initialize() + const unqualifiedSubmitterDid = await importExistingIndyDidFromPrivateKey( + endorser, + TypedArrayEncoder.fromString('00000000000000000000000Endorser9') + ) + endorserDid = `did:indy:pool:localtest:${unqualifiedSubmitterDid}` + const agentUnqualifiedSubmitterDid = await importExistingIndyDidFromPrivateKey( + agent, + TypedArrayEncoder.fromString('00000000000000000000000Endorser9') + ) + agentDid = `did:indy:pool:localtest:${agentUnqualifiedSubmitterDid}` + }) + + afterAll(async () => { + for (const pool of indyVdrPoolService.pools) { + pool.close() + } + + await endorser.shutdown() + await endorser.wallet.delete() + await agent.shutdown() + await agent.wallet.delete() + }) + + test('register and resolve a schema and credential definition (internal, issuerDid != endorserDid)', async () => { + const didCreateResult = (await endorser.dids.create({ + method: 'indy', + options: { + endorserMode: 'internal', + endorserDid, + }, + })) as IndyVdrDidCreateResult + + if (didCreateResult.didState.state !== 'finished') throw Error('did was not successfully created') + + const didIndyIssuerId = didCreateResult.didState.did + const { namespaceIdentifier: legacyIssuerId } = parseIndyDid(didIndyIssuerId) + const dynamicVersion = `1.${Math.random() * 100}` + const legacySchemaId = `${legacyIssuerId}:2:test:${dynamicVersion}` + const didIndySchemaId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/SCHEMA/test/${dynamicVersion}` + + const schemaResult = await indyVdrAnonCredsRegistry.registerSchema(endorser.context, { + options: { + endorserMode: 'internal', + endorserDid, + }, + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + }) + const schemaSeqNo = schemaResult.schemaMetadata.indyLedgerSeqNo as number + + expect(schemaResult).toMatchObject({ + schemaState: { + state: 'finished', + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + schemaId: didIndySchemaId, + }, + registrationMetadata: {}, + schemaMetadata: { + indyLedgerSeqNo: expect.any(Number), + }, + }) + + // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + + const legacySchema = await indyVdrAnonCredsRegistry.getSchema(endorser.context, legacySchemaId) + expect(legacySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: legacyIssuerId, + }, + schemaId: legacySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + // Resolve using did indy schema id + const didIndySchema = await indyVdrAnonCredsRegistry.getSchema(endorser.context, didIndySchemaId) + expect(didIndySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: didIndyIssuerId, + }, + schemaId: didIndySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + const legacyCredentialDefinitionId = `${legacyIssuerId}:3:CL:${schemaSeqNo}:TAG` + const didIndyCredentialDefinitionId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/TAG` + const credentialDefinitionResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(endorser.context, { + credentialDefinition: { + issuerId: didIndyIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, + }) + + expect(credentialDefinitionResult).toMatchObject({ + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + credentialDefinition: { + issuerId: didIndyIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionId: didIndyCredentialDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + }) + + // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + + const legacyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + endorser.context, + legacyCredentialDefinitionId + ) + + expect(legacyCredentialDefinition).toMatchObject({ + credentialDefinitionId: legacyCredentialDefinitionId, + credentialDefinition: { + issuerId: legacyIssuerId, + schemaId: legacySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + // resolve using did indy credential definition id + const didIndyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + endorser.context, + didIndyCredentialDefinitionId + ) + + expect(didIndyCredentialDefinition).toMatchObject({ + credentialDefinitionId: didIndyCredentialDefinitionId, + credentialDefinition: { + issuerId: didIndyIssuerId, + schemaId: didIndySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + const { + revocationRegistryDefinitionState: { revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId }, + } = await indyVdrAnonCredsRegistry.registerRevocationRegistryDefinition(endorser.context, { + revocationRegistryDefinition: { + tag: 'REV_TAG', + issuerId: didIndyIssuerId, + credDefId: didIndyCredentialDefinitionId, + revocDefType: 'CL_ACCUM', + value: { + maxCredNum: 100, + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + }, + }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, + }) + + if (!didIndyRevocationRegistryDefinitionId) { + throw Error('revocation registry definition was not created correctly') + } + + const { credentialDefinitionTag, revocationRegistryTag } = parseIndyRevocationRegistryId( + didIndyRevocationRegistryDefinitionId + ) + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + legacyIssuerId, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + + // Wait some time before resolving revocation registry definition object + await new Promise((res) => setTimeout(res, 1000)) + + const legacyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( + endorser.context, + legacyRevocationRegistryDefinitionId + ) + + expect(legacyRevocationRegistryDefinition).toMatchObject({ + revocationRegistryDefinitionId: legacyRevocationRegistryDefinitionId, + revocationRegistryDefinition: { + issuerId: legacyIssuerId, + revocDefType: 'CL_ACCUM', + value: { + maxCredNum: 100, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + }, + tag: 'REV_TAG', + credDefId: legacyCredentialDefinitionId, + }, + revocationRegistryDefinitionMetadata: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + const didIndyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( + endorser.context, + didIndyRevocationRegistryDefinitionId + ) + + expect(didIndyRevocationRegistryDefinition).toMatchObject({ + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + revocationRegistryDefinition: { + issuerId: didIndyIssuerId, + revocDefType: 'CL_ACCUM', + value: { + maxCredNum: 100, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + }, + tag: 'REV_TAG', + credDefId: didIndyCredentialDefinitionId, + }, + revocationRegistryDefinitionMetadata: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + const registerStatusListResult = await indyVdrAnonCredsRegistry.registerRevocationStatusList(endorser.context, { + revocationStatusList: { + issuerId: didIndyIssuerId, + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: new Array(100).fill(0), + currentAccumulator: '1', + }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, + }) + + if (registerStatusListResult.revocationStatusListState.state !== 'finished') { + throw new Error(`Unable to register status list: ${JSON.stringify(registerStatusListResult)}`) + } + + // Wait some time before resolving revocation status list object + await new Promise((res) => setTimeout(res, 1000)) + + const legacyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( + endorser.context, + legacyRevocationRegistryDefinitionId, + registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp + ) + + expect(legacyRevocationStatusList).toMatchObject({ + resolutionMetadata: {}, + revocationStatusList: { + issuerId: legacyIssuerId, + currentAccumulator: '1', + revRegDefId: legacyRevocationRegistryDefinitionId, + revocationList: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + timestamp: registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp, + }, + revocationStatusListMetadata: { + didIndyNamespace: 'pool:localtest', + }, + }) + + const didIndyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( + endorser.context, + didIndyRevocationRegistryDefinitionId, + registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp + ) + + expect(didIndyRevocationStatusList).toMatchObject({ + resolutionMetadata: {}, + revocationStatusList: { + issuerId: didIndyIssuerId, + currentAccumulator: '1', + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + timestamp: registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp, + }, + revocationStatusListMetadata: { + didIndyNamespace: 'pool:localtest', + }, + }) + }) + + test('register and resolve a schema and credential definition (internal, issuerDid == endorserDid)', async () => { + const dynamicVersion = `1.${Math.random() * 100}` + + const legacyIssuerId = 'DJKobikPAaYWAu9vfhEEo5' + const didIndyIssuerId = 'did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5' + const legacySchemaId = `DJKobikPAaYWAu9vfhEEo5:2:test:${dynamicVersion}` + const didIndySchemaId = `did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5/anoncreds/v0/SCHEMA/test/${dynamicVersion}` + + const schemaResult = await indyVdrAnonCredsRegistry.registerSchema(endorser.context, { + options: { + endorserMode: 'internal', + endorserDid, + }, + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + }) + + const schemaSeqNo = schemaResult.schemaMetadata.indyLedgerSeqNo as number + + expect(schemaResult).toMatchObject({ + schemaState: { + state: 'finished', + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + schemaId: didIndySchemaId, + }, + registrationMetadata: {}, + schemaMetadata: { + indyLedgerSeqNo: expect.any(Number), + }, + }) + + // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + + const legacySchema = await indyVdrAnonCredsRegistry.getSchema(endorser.context, legacySchemaId) + expect(legacySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: legacyIssuerId, + }, + schemaId: legacySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + // Resolve using did indy schema id + const didIndySchema = await indyVdrAnonCredsRegistry.getSchema(endorser.context, didIndySchemaId) + expect(didIndySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: didIndyIssuerId, + }, + schemaId: didIndySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + const legacyCredentialDefinitionId = `DJKobikPAaYWAu9vfhEEo5:3:CL:${schemaSeqNo}:TAG` + const didIndyCredentialDefinitionId = `did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/TAG` + const credentialDefinitionResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(endorser.context, { + credentialDefinition: { + issuerId: didIndyIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, + }) + + expect(credentialDefinitionResult).toMatchObject({ + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + credentialDefinition: { + issuerId: didIndyIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionId: didIndyCredentialDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + }) + + // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + + const legacyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + endorser.context, + legacyCredentialDefinitionId + ) + + expect(legacyCredentialDefinition).toMatchObject({ + credentialDefinitionId: legacyCredentialDefinitionId, + credentialDefinition: { + issuerId: legacyIssuerId, + schemaId: legacySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + // resolve using did indy credential definition id + const didIndyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + endorser.context, + didIndyCredentialDefinitionId + ) + + expect(didIndyCredentialDefinition).toMatchObject({ + credentialDefinitionId: didIndyCredentialDefinitionId, + credentialDefinition: { + issuerId: didIndyIssuerId, + schemaId: didIndySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + const { + revocationRegistryDefinitionState: { revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId }, + } = await indyVdrAnonCredsRegistry.registerRevocationRegistryDefinition(endorser.context, { + revocationRegistryDefinition: { + tag: 'REV_TAG', + issuerId: didIndyIssuerId, + credDefId: didIndyCredentialDefinitionId, + revocDefType: 'CL_ACCUM', + value: { + maxCredNum: 100, + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + }, + }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, + }) + + if (!didIndyRevocationRegistryDefinitionId) { + throw Error('revocation registry definition was not created correctly') + } + + const { credentialDefinitionTag, revocationRegistryTag } = parseIndyRevocationRegistryId( + didIndyRevocationRegistryDefinitionId + ) + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + legacyIssuerId, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + + // Wait some time before resolving revocation registry definition object + await new Promise((res) => setTimeout(res, 1000)) + + const legacyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( + endorser.context, + legacyRevocationRegistryDefinitionId + ) + + expect(legacyRevocationRegistryDefinition).toMatchObject({ + revocationRegistryDefinitionId: legacyRevocationRegistryDefinitionId, + revocationRegistryDefinition: { + issuerId: legacyIssuerId, + revocDefType: 'CL_ACCUM', + value: { + maxCredNum: 100, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + }, + tag: 'REV_TAG', + credDefId: legacyCredentialDefinitionId, + }, + revocationRegistryDefinitionMetadata: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + const didIndyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( + endorser.context, + didIndyRevocationRegistryDefinitionId + ) + + expect(didIndyRevocationRegistryDefinition).toMatchObject({ + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + revocationRegistryDefinition: { + issuerId: didIndyIssuerId, + revocDefType: 'CL_ACCUM', + value: { + maxCredNum: 100, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + }, + tag: 'REV_TAG', + credDefId: didIndyCredentialDefinitionId, + }, + revocationRegistryDefinitionMetadata: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + const registerStatusListResult = await indyVdrAnonCredsRegistry.registerRevocationStatusList(endorser.context, { + revocationStatusList: { + issuerId: didIndyIssuerId, + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: new Array(100).fill(0), + currentAccumulator: '1', + }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, + }) + + if (registerStatusListResult.revocationStatusListState.state !== 'finished') { + throw new Error(`Unable to register status list: ${JSON.stringify(registerStatusListResult)}`) + } + + // Wait some time before resolving revocation status list object + await new Promise((res) => setTimeout(res, 1000)) + + const legacyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( + endorser.context, + legacyRevocationRegistryDefinitionId, + registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp + ) + + expect(legacyRevocationStatusList).toMatchObject({ + resolutionMetadata: {}, + revocationStatusList: { + issuerId: legacyIssuerId, + currentAccumulator: '1', + revRegDefId: legacyRevocationRegistryDefinitionId, + revocationList: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + timestamp: registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp, + }, + revocationStatusListMetadata: { + didIndyNamespace: 'pool:localtest', + }, + }) + + const didIndyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( + endorser.context, + didIndyRevocationRegistryDefinitionId, + registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp + ) + + expect(didIndyRevocationStatusList).toMatchObject({ + resolutionMetadata: {}, + revocationStatusList: { + issuerId: didIndyIssuerId, + currentAccumulator: '1', + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + timestamp: registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp, + }, + revocationStatusListMetadata: { + didIndyNamespace: 'pool:localtest', + }, + }) + }) + + test('register and resolve a schema, credential definition, revocation registry and status list (external)', async () => { + // ---- + // CREATE DID + // ---- + const didCreateTxResult = (await agent.dids.create({ + method: 'indy', + options: { + endorserMode: 'external', + endorserDid, + }, + })) as IndyVdrDidCreateResult + + const didState = didCreateTxResult.didState + if (didState.state !== 'action' || didState.action !== 'endorseIndyTransaction') throw Error('unexpected did state') + + const signedNymRequest = await endorser.modules.indyVdr.endorseTransaction( + didState.nymRequest, + didState.endorserDid + ) + const didCreateSubmitResult = await agent.dids.create({ + did: didState.did, + options: { + endorserMode: 'external', + endorsedTransaction: { + nymRequest: signedNymRequest, + }, + }, + secret: didState.secret, + }) + + if (!didCreateSubmitResult.didState.did) throw Error('did was not correctly created') + + const agentDid = didCreateSubmitResult.didState.did + const { namespaceIdentifier } = parseIndyDid(agentDid) + + const dynamicVersion = `1.${Math.random() * 100}` + + const legacyIssuerId = namespaceIdentifier + const didIndyIssuerId = agentDid + + // ---- + // CREATE SCHEMA + // ---- + const legacySchemaId = `${namespaceIdentifier}:2:test:${dynamicVersion}` + const didIndySchemaId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/SCHEMA/test/${dynamicVersion}` + + const createSchemaTxResult = await indyVdrAnonCredsRegistry.registerSchema(agent.context, { + options: { + endorserMode: 'external', + endorserDid, + }, + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + }) + + const { schemaState } = createSchemaTxResult + + if (schemaState.state !== 'action' || schemaState.action !== 'endorseIndyTransaction') + throw Error('unexpected schema state') + + const endorsedTx = await endorser.modules.indyVdr.endorseTransaction(schemaState.schemaRequest, endorserDid) + + const submitSchemaTxResult = await indyVdrAnonCredsRegistry.registerSchema(agent.context, { + schema: schemaState.schema, + options: { + endorserMode: 'external', + endorsedTransaction: endorsedTx, + }, + }) + + expect(submitSchemaTxResult).toMatchObject({ + schemaState: { + state: 'finished', + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + schemaId: didIndySchemaId, + }, + registrationMetadata: {}, + schemaMetadata: { + indyLedgerSeqNo: expect.any(Number), + }, + }) + + // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + + const legacySchema = await indyVdrAnonCredsRegistry.getSchema(agent.context, legacySchemaId) + expect(legacySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: legacyIssuerId, + }, + schemaId: legacySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + // Resolve using did indy schema id + const didIndySchema = await indyVdrAnonCredsRegistry.getSchema(agent.context, didIndySchemaId) + expect(didIndySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: didIndyIssuerId, + }, + schemaId: didIndySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + // ---- + // CREATE CREDENTIAL DEFINITION + // ---- + const legacyCredentialDefinitionId = `${namespaceIdentifier}:3:CL:${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}:TAG` + const didIndyCredentialDefinitionId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/CLAIM_DEF/${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}/TAG` + + const createCredDefTxResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(agent.context, { + credentialDefinition: { + issuerId: didIndyIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + options: { + endorserMode: 'external', + endorserDid, + }, + }) + + const { credentialDefinitionState } = createCredDefTxResult + + if (credentialDefinitionState.state !== 'action' || credentialDefinitionState.action !== 'endorseIndyTransaction') + throw Error('unexpected credential definition state') + + const endorsedCredDefTx = await endorser.modules.indyVdr.endorseTransaction( + credentialDefinitionState.credentialDefinitionRequest, + endorserDid + ) + const submitCredDefTxResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(agent.context, { + credentialDefinition: credentialDefinitionState.credentialDefinition, + options: { + endorserMode: 'external', + endorsedTransaction: endorsedCredDefTx, + }, + }) + + expect(submitCredDefTxResult).toMatchObject({ + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + credentialDefinition: { + issuerId: didIndyIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionId: didIndyCredentialDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + }) + + // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + + const legacyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + agent.context, + legacyCredentialDefinitionId + ) + + expect(legacyCredentialDefinition).toMatchObject({ + credentialDefinitionId: legacyCredentialDefinitionId, + credentialDefinition: { + issuerId: legacyIssuerId, + schemaId: legacySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + // resolve using did indy credential definition id + const didIndyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + agent.context, + didIndyCredentialDefinitionId + ) + + expect(didIndyCredentialDefinition).toMatchObject({ + credentialDefinitionId: didIndyCredentialDefinitionId, + credentialDefinition: { + issuerId: didIndyIssuerId, + schemaId: didIndySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + // ---- + // CREATE REVOCATION REGISTRY + // ---- + const legacyRevocationRegistryDefinitionId = `${namespaceIdentifier}:4:${namespaceIdentifier}:3:CL:${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:REV_TAG` + const didIndyRevocationRegistryDefinitionId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/REV_REG_DEF/${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}/TAG/REV_TAG` + + const createRevRegDefTxResult = await indyVdrAnonCredsRegistry.registerRevocationRegistryDefinition(agent.context, { + revocationRegistryDefinition: { + tag: 'REV_TAG', + revocDefType: 'CL_ACCUM', + credDefId: didIndyCredentialDefinitionId, + issuerId: didIndyIssuerId, + value: revocationRegistryDefinitionValue, + }, + options: { + endorserMode: 'external', + endorserDid, + }, + }) + + const { revocationRegistryDefinitionState } = createRevRegDefTxResult + + if ( + revocationRegistryDefinitionState.state !== 'action' || + revocationRegistryDefinitionState.action !== 'endorseIndyTransaction' + ) { + throw Error('unexpected revocation registry definition state') + } + + const endorsedRevRegDefTx = await endorser.modules.indyVdr.endorseTransaction( + revocationRegistryDefinitionState.revocationRegistryDefinitionRequest, + endorserDid + ) + const submitRevRegDefTxResult = await indyVdrAnonCredsRegistry.registerRevocationRegistryDefinition(agent.context, { + revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, + options: { + endorserMode: 'external', + endorsedTransaction: endorsedRevRegDefTx, + }, + }) + + expect(submitRevRegDefTxResult).toMatchObject({ + revocationRegistryDefinitionMetadata: {}, + revocationRegistryDefinitionState: { + revocationRegistryDefinition: { + credDefId: didIndyCredentialDefinitionId, + issuerId: didIndyIssuerId, + tag: 'REV_TAG', + value: revocationRegistryDefinitionValue, + }, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + }) + + await new Promise((res) => setTimeout(res, 1000)) + + const legacyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( + agent.context, + legacyRevocationRegistryDefinitionId + ) + + expect(legacyRevocationRegistryDefinition).toMatchObject({ + revocationRegistryDefinitionId: legacyRevocationRegistryDefinitionId, + revocationRegistryDefinition: { + issuerId: legacyIssuerId, + revocDefType: 'CL_ACCUM', + value: revocationRegistryDefinitionValue, + tag: 'REV_TAG', + credDefId: legacyCredentialDefinitionId, + }, + revocationRegistryDefinitionMetadata: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + const didIndyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( + agent.context, + didIndyRevocationRegistryDefinitionId + ) + + expect(didIndyRevocationRegistryDefinition).toMatchObject({ + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + revocationRegistryDefinition: { + issuerId: didIndyIssuerId, + revocDefType: 'CL_ACCUM', + value: revocationRegistryDefinitionValue, + tag: 'REV_TAG', + credDefId: didIndyCredentialDefinitionId, + }, + revocationRegistryDefinitionMetadata: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + // ---- + // CREATE REVOCATION STATUS LIST + // ---- + const revocationStatusListValue = { + issuerId: didIndyIssuerId, + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: [1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0], + currentAccumulator: '1', + } + + const createRevStatusListTxResult = await indyVdrAnonCredsRegistry.registerRevocationStatusList(agent.context, { + options: { + endorserMode: 'external', + endorserDid, + }, + revocationStatusList: revocationStatusListValue, + }) + + const { revocationStatusListState } = createRevStatusListTxResult + + if (revocationStatusListState.state !== 'action' || revocationStatusListState.action !== 'endorseIndyTransaction') { + throw Error('unexpected revocation status list state') + } + + const endorsedRevStatusListTx = await endorser.modules.indyVdr.endorseTransaction( + revocationStatusListState.revocationStatusListRequest, + endorserDid + ) + + const submitRevStatusListTxResult = await indyVdrAnonCredsRegistry.registerRevocationStatusList(agent.context, { + revocationStatusList: revocationStatusListState.revocationStatusList, + options: { + endorserMode: 'external', + endorsedTransaction: endorsedRevStatusListTx, + }, + }) + + expect(submitRevStatusListTxResult).toMatchObject({ + revocationStatusListMetadata: {}, + revocationStatusListState: { + revocationStatusList: { ...revocationStatusListValue, timestamp: expect.any(Number) }, + state: 'finished', + }, + registrationMetadata: {}, + }) + + // Wait some time before resolving status list + await new Promise((res) => setTimeout(res, 1000)) + + const legacyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( + agent.context, + legacyRevocationRegistryDefinitionId, + submitRevStatusListTxResult.revocationStatusListState.revocationStatusList?.timestamp as number + ) + + expect(legacyRevocationStatusList).toMatchObject({ + resolutionMetadata: {}, + revocationStatusList: { + issuerId: legacyIssuerId, + currentAccumulator: '1', + revRegDefId: legacyRevocationRegistryDefinitionId, + revocationList: revocationStatusListValue.revocationList, + timestamp: submitRevStatusListTxResult.revocationStatusListState.revocationStatusList?.timestamp, + }, + revocationStatusListMetadata: { + didIndyNamespace: 'pool:localtest', + }, + }) + + const didIndyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( + agent.context, + didIndyRevocationRegistryDefinitionId, + submitRevStatusListTxResult.revocationStatusListState.revocationStatusList?.timestamp as number + ) + + expect(didIndyRevocationStatusList).toMatchObject({ + resolutionMetadata: {}, + revocationStatusList: { + issuerId: didIndyIssuerId, + currentAccumulator: '1', + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: revocationStatusListValue.revocationList, + timestamp: submitRevStatusListTxResult.revocationStatusListState.revocationStatusList?.timestamp, + }, + revocationStatusListMetadata: { + didIndyNamespace: 'pool:localtest', + }, + }) + }) + + test('register and resolve a credential definition (internal,credDefDid != schemaDid)', async () => { + const didCreateResult = (await endorser.dids.create({ + method: 'indy', + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, + })) as IndyVdrDidCreateResult + + if (didCreateResult.didState.state !== 'finished') throw Error('did was not successfully created') + endorser.config.logger.debug(`didIndyIssuerId:: ${didCreateResult.didState.did}`) + const didIndyIssuerId = didCreateResult.didState.did + const { namespaceIdentifier: legacyIssuerId } = parseIndyDid(didIndyIssuerId) + const dynamicVersion = `1.${Math.random() * 100}` + const legacySchemaId = `${legacyIssuerId}:2:test:${dynamicVersion}` + const didIndySchemaId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/SCHEMA/test/${dynamicVersion}` + + const schemaResult = await indyVdrAnonCredsRegistry.registerSchema(endorser.context, { + options: { + endorserMode: 'internal', + endorserDid, + }, + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + }) + const schemaSeqNo = schemaResult.schemaMetadata.indyLedgerSeqNo as number + expect(schemaResult).toMatchObject({ + schemaState: { + state: 'finished', + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + schemaId: didIndySchemaId, + }, + registrationMetadata: {}, + schemaMetadata: { + indyLedgerSeqNo: expect.any(Number), + }, + }) + // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + const legacySchema = await indyVdrAnonCredsRegistry.getSchema(endorser.context, legacySchemaId) + expect(legacySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: legacyIssuerId, + }, + schemaId: legacySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + // Resolve using did indy schema id + const didIndySchema = await indyVdrAnonCredsRegistry.getSchema(endorser.context, didIndySchemaId) + expect(didIndySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: didIndyIssuerId, + }, + schemaId: didIndySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + const agentDidCreateResult = (await agent.dids.create({ + method: 'indy', + options: { + endorserDid: agentDid, + endorserMode: 'internal', + }, + })) as IndyVdrDidCreateResult + + if (agentDidCreateResult.didState.state !== 'finished') throw Error('did was not successfully created') + const didIndyAgentIssuerId = agentDidCreateResult.didState.did + const { namespaceIdentifier: agentLegacyIssuerId } = parseIndyDid(didIndyAgentIssuerId) + + const legacyCredentialDefinitionId = `${agentLegacyIssuerId}:3:CL:${schemaSeqNo}:TAG` + const didIndyCredentialDefinitionId = `did:indy:pool:localtest:${agentLegacyIssuerId}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/TAG` + + const credentialDefinitionResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(agent.context, { + credentialDefinition: { + issuerId: didIndyAgentIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + options: { + endorserMode: 'internal', + endorserDid: agentDid, + }, + }) + + expect(credentialDefinitionResult).toMatchObject({ + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + credentialDefinition: { + issuerId: didIndyAgentIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionId: didIndyCredentialDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + }) + + // // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + const legacyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + agent.context, + legacyCredentialDefinitionId + ) + + expect(legacyCredentialDefinition).toMatchObject({ + credentialDefinitionId: legacyCredentialDefinitionId, + credentialDefinition: { + issuerId: agentLegacyIssuerId, + schemaId: legacySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + // resolve using did indy credential definition id + const didIndyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + agent.context, + didIndyCredentialDefinitionId + ) + + expect(didIndyCredentialDefinition).toMatchObject({ + credentialDefinitionId: didIndyCredentialDefinitionId, + credentialDefinition: { + issuerId: didIndyAgentIssuerId, + schemaId: didIndySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + }) +}) diff --git a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts new file mode 100644 index 0000000000..dc5521653c --- /dev/null +++ b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts @@ -0,0 +1,624 @@ +import type { IndyVdrDidCreateOptions, IndyVdrDidCreateResult } from '../src/dids/IndyVdrIndyDidRegistrar' + +import { didIndyRegex } from '@credo-ts/anoncreds' +import { + Key, + JsonTransformer, + KeyType, + TypedArrayEncoder, + DidCommV1Service, + NewDidCommV2Service, + DidDocumentService, + Agent, + DidsModule, + NewDidCommV2ServiceEndpoint, +} from '@credo-ts/core' +import { indyVdr } from '@hyperledger/indy-vdr-nodejs' +import { convertPublicKeyToX25519, generateKeyPairFromSeed } from '@stablelib/ed25519' + +import { + getInMemoryAgentOptions, + importExistingIndyDidFromPrivateKey, + retryUntilResult, +} from '../../core/tests/helpers' +import { IndyVdrModule, IndyVdrSovDidResolver } from '../src' +import { IndyVdrIndyDidRegistrar } from '../src/dids/IndyVdrIndyDidRegistrar' +import { IndyVdrIndyDidResolver } from '../src/dids/IndyVdrIndyDidResolver' +import { indyDidFromNamespaceAndInitialKey } from '../src/dids/didIndyUtil' + +import { indyVdrModuleConfig } from './helpers' + +const endorser = new Agent( + getInMemoryAgentOptions( + 'Indy VDR Indy DID Registrar', + {}, + { + indyVdr: new IndyVdrModule({ + networks: indyVdrModuleConfig.networks, + indyVdr, + }), + dids: new DidsModule({ + registrars: [new IndyVdrIndyDidRegistrar()], + resolvers: [new IndyVdrIndyDidResolver(), new IndyVdrSovDidResolver()], + }), + } + ) +) +const agent = new Agent( + getInMemoryAgentOptions( + 'Indy VDR Indy DID Registrar', + {}, + { + indyVdr: new IndyVdrModule({ + indyVdr, + networks: indyVdrModuleConfig.networks, + }), + dids: new DidsModule({ + registrars: [new IndyVdrIndyDidRegistrar()], + resolvers: [new IndyVdrIndyDidResolver(), new IndyVdrSovDidResolver()], + }), + } + ) +) + +describe('Indy VDR Indy Did Registrar', () => { + let endorserDid: string + + beforeAll(async () => { + await endorser.initialize() + const unqualifiedSubmitterDid = await importExistingIndyDidFromPrivateKey( + endorser, + TypedArrayEncoder.fromString('00000000000000000000000Endorser9') + ) + endorserDid = `did:indy:pool:localtest:${unqualifiedSubmitterDid}` + + await agent.initialize() + }) + + afterAll(async () => { + await endorser.shutdown() + await endorser.wallet.delete() + await agent.shutdown() + await agent.wallet.delete() + }) + + test('can register a did:indy without services', async () => { + const didRegistrationResult = await endorser.dids.create({ + method: 'indy', + options: { + endorserDid, + endorserMode: 'internal', + }, + }) + + expect(JsonTransformer.toJSON(didRegistrationResult)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: expect.stringMatching(didIndyRegex), + didDocument: { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: expect.stringMatching(didIndyRegex), + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: expect.stringMatching(didIndyRegex), + id: expect.stringContaining('#verkey'), + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [expect.stringContaining('#verkey')], + service: undefined, + }, + }, + }) + + const did = didRegistrationResult.didState.did + if (!did) throw Error('did not defined') + + // Tries to call it in an interval until it succeeds (with maxAttempts) + // As the ledger write is not always consistent in how long it takes + // to write the data, we need to retry until we get a result. + const didDocument = await retryUntilResult(async () => { + const result = await endorser.dids.resolve(did) + return result.didDocument + }) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject({ + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: undefined, + }) + }) + + test('cannot create a did with TRUSTEE role', async () => { + const didRegistrationResult = await endorser.dids.create({ + method: 'indy', + options: { + endorserDid, + endorserMode: 'internal', + role: 'TRUSTEE', + }, + }) + + expect(JsonTransformer.toJSON(didRegistrationResult.didState.state)).toBe('failed') + }) + + test('can register an endorsed did:indy without services - did and verkey specified', async () => { + // Generate a seed and the indy did. This allows us to create a new did every time + // but still check if the created output document is as expected. + const seed = Array(32 + 1) + .join((Math.random().toString(36) + '00000000000000000').slice(2, 18)) + .slice(0, 32) + + const keyPair = generateKeyPairFromSeed(TypedArrayEncoder.fromString(seed)) + const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(keyPair.publicKey) + + const { did, verkey } = indyDidFromNamespaceAndInitialKey( + 'pool:localtest', + Key.fromPublicKey(keyPair.publicKey, KeyType.Ed25519) + ) + + const didCreateTobeEndorsedResult = (await agent.dids.create({ + did, + options: { + endorserDid, + endorserMode: 'external', + verkey, + }, + })) as IndyVdrDidCreateResult + + const didState = didCreateTobeEndorsedResult.didState + if (didState.state !== 'action' || didState.action !== 'endorseIndyTransaction') throw Error('unexpected did state') + + const signedNymRequest = await endorser.modules.indyVdr.endorseTransaction( + didState.nymRequest, + didState.endorserDid + ) + const didCreateSubmitResult = await agent.dids.create({ + did: didState.did, + options: { + endorserMode: 'external', + endorsedTransaction: { + nymRequest: signedNymRequest, + }, + }, + secret: didState.secret, + }) + + if (didCreateSubmitResult.didState.state !== 'finished') { + throw new Error(`Unexpected did state, ${JSON.stringify(didCreateSubmitResult.didState, null, 2)}`) + } + expect(JsonTransformer.toJSON(didCreateSubmitResult)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did, + didDocument: { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: ed25519PublicKeyBase58, + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: undefined, + }, + }, + }) + + // Tries to call it in an interval until it succeeds (with maxAttempts) + // As the ledger write is not always consistent in how long it takes + // to write the data, we need to retry until we get a result. + const didDocument = await retryUntilResult(async () => { + const result = await endorser.dids.resolve(did) + return result.didDocument + }) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject({ + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: ed25519PublicKeyBase58, + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: undefined, + }) + }) + + test('can register a did:indy without services - did and verkey specified', async () => { + // Generate a seed and the indy did. This allows us to create a new did every time + // but still check if the created output document is as expected. + const seed = Array(32 + 1) + .join((Math.random().toString(36) + '00000000000000000').slice(2, 18)) + .slice(0, 32) + + const keyPair = generateKeyPairFromSeed(TypedArrayEncoder.fromString(seed)) + const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(keyPair.publicKey) + + const { did, verkey } = indyDidFromNamespaceAndInitialKey( + 'pool:localtest', + Key.fromPublicKey(keyPair.publicKey, KeyType.Ed25519) + ) + const didRegistrationResult = await endorser.dids.create({ + did, + options: { + endorserDid, + endorserMode: 'internal', + verkey, + }, + }) + + expect(JsonTransformer.toJSON(didRegistrationResult)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did, + didDocument: { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: ed25519PublicKeyBase58, + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: undefined, + }, + }, + }) + + // Tries to call it in an interval until it succeeds (with maxAttempts) + // As the ledger write is not always consistent in how long it takes + // to write the data, we need to retry until we get a result. + const didDocument = await retryUntilResult(async () => { + const result = await endorser.dids.resolve(did) + return result.didDocument + }) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject({ + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: ed25519PublicKeyBase58, + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: undefined, + }) + }) + + test('can register a did:indy with services - did and verkey specified - using attrib endpoint', async () => { + // Generate a private key and the indy did. This allows us to create a new did every time + // but still check if the created output document is as expected. + const privateKey = TypedArrayEncoder.fromString( + Array(32 + 1) + .join((Math.random().toString(36) + '00000000000000000').slice(2, 18)) + .slice(0, 32) + ) + + const key = await endorser.wallet.createKey({ privateKey, keyType: KeyType.Ed25519 }) + const x25519PublicKeyBase58 = TypedArrayEncoder.toBase58(convertPublicKeyToX25519(key.publicKey)) + const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(key.publicKey) + + const { did, verkey } = indyDidFromNamespaceAndInitialKey( + 'pool:localtest', + Key.fromPublicKey(key.publicKey, KeyType.Ed25519) + ) + + const didRegistrationResult = await endorser.dids.create({ + did, + options: { + endorserDid, + endorserMode: 'internal', + useEndpointAttrib: true, + verkey, + services: [ + new DidDocumentService({ + id: `${did}#endpoint`, + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }), + new DidCommV1Service({ + id: `${did}#did-communication`, + priority: 0, + recipientKeys: [`${did}#key-agreement-1`], + routingKeys: ['a-routing-key'], + serviceEndpoint: 'https://example.com/endpoint', + accept: ['didcomm/aip2;env=rfc19'], + }), + new NewDidCommV2Service({ + id: `${did}#didcomm-messaging-1`, + serviceEndpoint: new NewDidCommV2ServiceEndpoint({ + accept: ['didcomm/v2'], + routingKeys: ['a-routing-key'], + uri: 'https://example.com/endpoint', + }), + }), + ], + }, + }) + + const expectedDidDocument = { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + 'https://didcomm.org/messaging/contexts/v2', + ], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: ed25519PublicKeyBase58, + }, + { + type: 'X25519KeyAgreementKey2019', + controller: did, + id: `${did}#key-agreement-1`, + publicKeyBase58: x25519PublicKeyBase58, + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: [ + { + id: `${did}#endpoint`, + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }, + { + accept: ['didcomm/aip2;env=rfc19'], + id: `${did}#did-communication`, + priority: 0, + recipientKeys: [`${did}#key-agreement-1`], + routingKeys: ['a-routing-key'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + { + id: `${did}#didcomm-messaging-1`, + serviceEndpoint: { + uri: 'https://example.com/endpoint', + accept: ['didcomm/v2'], + routingKeys: ['a-routing-key'], + }, + type: 'DIDCommMessaging', + }, + ], + } + + expect(JsonTransformer.toJSON(didRegistrationResult)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did, + didDocument: expectedDidDocument, + }, + }) + + // Tries to call it in an interval until it succeeds (with maxAttempts) + // As the ledger write is not always consistent in how long it takes + // to write the data, we need to retry until we get a result. + const didDocument = await retryUntilResult(async () => { + const result = await endorser.dids.resolve(did) + return result.didDocument + }) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(expectedDidDocument) + }) + + test('can register an endorsed did:indy with services - did and verkey specified - using attrib endpoint', async () => { + // Generate a private key and the indy did. This allows us to create a new did every time + // but still check if the created output document is as expected. + const privateKey = TypedArrayEncoder.fromString( + Array(32 + 1) + .join((Math.random().toString(36) + '00000000000000000').slice(2, 18)) + .slice(0, 32) + ) + + const key = await endorser.wallet.createKey({ privateKey, keyType: KeyType.Ed25519 }) + const x25519PublicKeyBase58 = TypedArrayEncoder.toBase58(convertPublicKeyToX25519(key.publicKey)) + const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(key.publicKey) + + const { did, verkey } = indyDidFromNamespaceAndInitialKey( + 'pool:localtest', + Key.fromPublicKey(key.publicKey, KeyType.Ed25519) + ) + + const didCreateTobeEndorsedResult = (await endorser.dids.create({ + did, + options: { + endorserMode: 'external', + endorserDid: endorserDid, + useEndpointAttrib: true, + verkey, + services: [ + new DidDocumentService({ + id: `${did}#endpoint`, + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }), + new DidCommV1Service({ + id: `${did}#did-communication`, + priority: 0, + recipientKeys: [`${did}#key-agreement-1`], + routingKeys: ['a-routing-key'], + serviceEndpoint: 'https://example.com/endpoint', + accept: ['didcomm/aip2;env=rfc19'], + }), + new NewDidCommV2Service({ + id: `${did}#didcomm-messaging-1`, + serviceEndpoint: new NewDidCommV2ServiceEndpoint({ + accept: ['didcomm/v2'], + routingKeys: ['a-routing-key'], + uri: 'https://example.com/endpoint', + }), + }), + ], + }, + })) as IndyVdrDidCreateResult + + const didState = didCreateTobeEndorsedResult.didState + if (didState.state !== 'action' || didState.action !== 'endorseIndyTransaction') throw Error('unexpected did state') + + const signedNymRequest = await endorser.modules.indyVdr.endorseTransaction( + didState.nymRequest, + didState.endorserDid + ) + + if (!didState.attribRequest) throw Error('attrib request not found') + const endorsedAttribRequest = await endorser.modules.indyVdr.endorseTransaction( + didState.attribRequest, + didState.endorserDid + ) + + const didCreateSubmitResult = await agent.dids.create({ + did: didState.did, + options: { + endorserMode: 'external', + endorsedTransaction: { + nymRequest: signedNymRequest, + attribRequest: endorsedAttribRequest, + }, + }, + secret: didState.secret, + }) + + const expectedDidDocument = { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + 'https://didcomm.org/messaging/contexts/v2', + ], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: ed25519PublicKeyBase58, + }, + { + type: 'X25519KeyAgreementKey2019', + controller: did, + id: `${did}#key-agreement-1`, + publicKeyBase58: x25519PublicKeyBase58, + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: [ + { + id: `${did}#endpoint`, + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }, + { + accept: ['didcomm/aip2;env=rfc19'], + id: `${did}#did-communication`, + priority: 0, + recipientKeys: [`${did}#key-agreement-1`], + routingKeys: ['a-routing-key'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + { + id: `${did}#didcomm-messaging-1`, + serviceEndpoint: { + uri: 'https://example.com/endpoint', + routingKeys: ['a-routing-key'], + accept: ['didcomm/v2'], + }, + type: 'DIDCommMessaging', + }, + ], + } + + if (didCreateSubmitResult.didState.state !== 'finished') { + throw new Error(`Unexpected did state, ${JSON.stringify(didCreateSubmitResult.didState, null, 2)}`) + } + + expect(JsonTransformer.toJSON(didCreateSubmitResult)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did, + didDocument: expectedDidDocument, + }, + }) + + // Tries to call it in an interval until it succeeds (with maxAttempts) + // As the ledger write is not always consistent in how long it takes + // to write the data, we need to retry until we get a result. + const didDocument = await retryUntilResult(async () => { + const result = await endorser.dids.resolve(did) + return result.didDocument + }) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(expectedDidDocument) + }) +}) diff --git a/packages/indy-vdr/tests/indy-vdr-indy-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-indy-did-resolver.e2e.test.ts new file mode 100644 index 0000000000..b5386cfe79 --- /dev/null +++ b/packages/indy-vdr/tests/indy-vdr-indy-did-resolver.e2e.test.ts @@ -0,0 +1,140 @@ +import { DidsModule, Agent, TypedArrayEncoder, JsonTransformer } from '@credo-ts/core' +import { indyVdr } from '@hyperledger/indy-vdr-nodejs' + +import { getInMemoryAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' +import { IndyVdrModule } from '../src' +import { IndyVdrIndyDidRegistrar, IndyVdrIndyDidResolver, IndyVdrSovDidResolver } from '../src/dids' + +import { createDidOnLedger, indyVdrModuleConfig } from './helpers' + +const agent = new Agent( + getInMemoryAgentOptions( + 'Indy VDR Indy DID resolver', + {}, + { + indyVdr: new IndyVdrModule({ + indyVdr, + networks: indyVdrModuleConfig.networks, + }), + dids: new DidsModule({ + registrars: [new IndyVdrIndyDidRegistrar()], + resolvers: [new IndyVdrIndyDidResolver(), new IndyVdrSovDidResolver()], + }), + } + ) +) + +describe('indy-vdr DID Resolver E2E', () => { + beforeAll(async () => { + await agent.initialize() + }) + + afterAll(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + test('resolve a did:indy did', async () => { + const did = 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt' + const didResult = await agent.dids.resolve(did) + + expect(JsonTransformer.toJSON(didResult)).toMatchObject({ + didDocument: { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + assertionMethod: undefined, + keyAgreement: undefined, + service: undefined, + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + test('resolve a did with endpoints', async () => { + const unqualifiedSubmitterDid = await importExistingIndyDidFromPrivateKey( + agent, + TypedArrayEncoder.fromString('000000000000000000000000Trustee9') + ) + + // First we need to create a new DID and add ATTRIB endpoint to it + const { did } = await createDidOnLedger(agent, `did:indy:pool:localtest:${unqualifiedSubmitterDid}`) + + // DID created. Now resolve it + const didResult = await agent.dids.resolve(did, { + useLocalCreatedDidRecord: false, + }) + expect(JsonTransformer.toJSON(didResult)).toMatchObject({ + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + 'https://didcomm.org/messaging/contexts/v2', + ], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: expect.any(String), + }, + { + controller: did, + type: 'X25519KeyAgreementKey2019', + id: `${did}#key-agreement-1`, + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + assertionMethod: undefined, + keyAgreement: [`${did}#key-agreement-1`], + service: [ + { + id: `${did}#endpoint`, + serviceEndpoint: 'http://localhost:3000', + type: 'endpoint', + }, + { + id: `${did}#did-communication`, + accept: ['didcomm/aip2;env=rfc19'], + priority: 0, + recipientKeys: [`${did}#key-agreement-1`], + routingKeys: ['a-routing-key'], + serviceEndpoint: 'http://localhost:3000', + type: 'did-communication', + }, + { + id: `${did}#didcomm-messaging-1`, + serviceEndpoint: { uri: 'http://localhost:3000', accept: ['didcomm/v2'], routingKeys: ['a-routing-key'] }, + type: 'DIDCommMessaging', + }, + ], + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) +}) diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts new file mode 100644 index 0000000000..ba23c7b384 --- /dev/null +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -0,0 +1,224 @@ +import type { Key } from '@credo-ts/core' + +import { TypedArrayEncoder, KeyType } from '@credo-ts/core' +import { GetNymRequest, NymRequest, SchemaRequest, CredentialDefinitionRequest } from '@hyperledger/indy-vdr-shared' + +import { InMemoryWallet } from '../../../tests/InMemoryWallet' +import { genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' +import { IndyVdrPool } from '../src/pool' +import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' +import { indyDidFromPublicKeyBase58 } from '../src/utils/did' + +import { indyVdrModuleConfig } from './helpers' + +const indyVdrPoolService = new IndyVdrPoolService(testLogger, indyVdrModuleConfig) +const wallet = new InMemoryWallet() +const agentConfig = getAgentConfig('IndyVdrPoolService') +const agentContext = getAgentContext({ wallet, agentConfig }) + +const config = { + isProduction: false, + genesisTransactions, + indyNamespace: `pool:localtest`, + transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, +} as const + +let signerKey: Key + +describe('IndyVdrPoolService', () => { + beforeAll(async () => { + await wallet.createAndOpen(agentConfig.walletConfig) + + signerKey = await wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('000000000000000000000000Trustee9'), + keyType: KeyType.Ed25519, + }) + }) + + afterAll(async () => { + for (const pool of indyVdrPoolService.pools) { + pool.close() + } + + await wallet.delete() + }) + + describe('DIDs', () => { + test('can get a pool based on the namespace', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + expect(pool).toBeInstanceOf(IndyVdrPool) + expect(pool.config).toEqual(config) + }) + + test('can resolve a did using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + const request = new GetNymRequest({ + dest: 'TL1EaPFCZ8Si5aUrqScBDt', + }) + + const response = await pool.submitRequest(request) + + expect(response).toMatchObject({ + op: 'REPLY', + result: { + dest: 'TL1EaPFCZ8Si5aUrqScBDt', + type: '105', + data: expect.any(String), + identifier: 'LibindyDid111111111111', + reqId: expect.any(Number), + seqNo: expect.any(Number), + txnTime: expect.any(Number), + state_proof: expect.any(Object), + }, + }) + + expect(JSON.parse(response.result.data as string)).toMatchObject({ + dest: 'TL1EaPFCZ8Si5aUrqScBDt', + identifier: 'V4SGRU86Z58d6TV7PBUe6f', + role: '0', + seqNo: expect.any(Number), + txnTime: expect.any(Number), + verkey: '~43X4NhAFqREffK7eWdKgFH', + }) + }) + + test('can write a did using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + // prepare the DID we are going to write to the ledger + const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const did = indyDidFromPublicKeyBase58(key.publicKeyBase58) + + const request = new NymRequest({ + dest: did, + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + verkey: key.publicKeyBase58, + }) + + const writeRequest = await pool.prepareWriteRequest(agentContext, request, signerKey) + const response = await pool.submitRequest(writeRequest) + + expect(response).toMatchObject({ + op: 'REPLY', + result: { + txn: { + protocolVersion: 2, + metadata: expect.any(Object), + data: expect.any(Object), + type: '1', + }, + ver: '1', + rootHash: expect.any(String), + txnMetadata: expect.any(Object), + }, + }) + }) + }) + + test('can write a schema and credential definition using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + const dynamicVersion = `1.${Math.random() * 100}` + + const schemaRequest = new SchemaRequest({ + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + schema: { + id: 'test-schema-id', + name: 'test-schema', + ver: '1.0', + version: dynamicVersion, + attrNames: ['first_name', 'last_name', 'age'], + }, + }) + + const writeRequest = await pool.prepareWriteRequest(agentContext, schemaRequest, signerKey) + const schemaResponse = await pool.submitRequest(writeRequest) + + expect(schemaResponse).toMatchObject({ + op: 'REPLY', + result: { + ver: '1', + txn: { + metadata: expect.any(Object), + type: '101', + data: { + data: { + attr_names: expect.arrayContaining(['age', 'last_name', 'first_name']), + name: 'test-schema', + version: dynamicVersion, + }, + }, + }, + }, + }) + + const credentialDefinitionRequest = new CredentialDefinitionRequest({ + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + credentialDefinition: { + ver: '1.0', + id: `TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResponse.result.txnMetadata.seqNo}:TAG`, + // must be string version of the schema seqNo + schemaId: `${schemaResponse.result.txnMetadata.seqNo}`, + type: 'CL', + tag: 'TAG', + value: { + primary: { + n: '95671911213029889766246243339609567053285242961853979532076192834533577534909796042025401129640348836502648821408485216223269830089771714177855160978214805993386076928594836829216646288195127289421136294309746871614765411402917891972999085287429566166932354413679994469616357622976775651506242447852304853465380257226445481515631782793575184420720296120464167257703633829902427169144462981949944348928086406211627174233811365419264314148304536534528344413738913277713548403058098093453580992173145127632199215550027527631259565822872315784889212327945030315062879193999012349220118290071491899498795367403447663354833', + s: '1573939820553851804028472930351082111827449763317396231059458630252708273163050576299697385049087601314071156646675105028237105229428440185022593174121924731226634356276616495327358864865629675802738680754755949997611920669823449540027707876555408118172529688443208301403297680159171306000341239398135896274940688268460793682007115152428685521865921925309154307574955324973580144009271977076586453011938089159885164705002797196738438392179082905738155386545935208094240038135576042886730802817809757582039362798495805441520744154270346780731494125065136433163757697326955962282840631850597919384092584727207908978907', + r: { + master_secret: + '51468326064458249697956272807708948542001661888325200180968238787091473418947480867518174106588127385097619219536294589148765074804124925845579871788369264160902401097166484002617399484700234182426993061977152961670486891123188739266793651668791365808983166555735631354925174224786218771453042042304773095663181121735652667614424198057134974727791329623974680096491276337756445057223988781749506082654194307092164895251308088903000573135447235553684949564809677864522417041639512933806794232354223826262154508950271949764583849083972967642587778197779127063591201123312548182885603427440981731822883101260509710567731', + last_name: + '35864556460959997092903171610228165251001245539613587319116151716453114432309327039517115215674024166920383445379522674504803469517283236033110568676156285676664363558333716898161685255450536856645604857714925836474250821415182026707218622134953915013803750771185050002646661004119778318524426368842019753903741998256374803456282688037624993010626333853831264356355867746685055670790915539230702546586615988121383960277550317876816983602795121749533628953449405383896799464872758725899520173321672584180060465965090049734285011738428381648013150818429882628144544132356242262467090140003979917439514443707537952643217', + first_name: + '26405366527417391838431479783966663952336302347775179063968690502492620867161212873635806190080000833725932174641667734138216137047349915190546601368424742647800764149890590518336588437317392528514313749533980651547425554257026971104775208127915118918084350210726664749850578299247705298976657301433446491575776774836993110356033664644761593799921221474617858131678955318702706530853801195330271860527250931569815553226145458665481867408279941785848264018364216087471931232367137301987457054918438087686484522112532447779498424748261678616461026788516567300969886029412198319909977473167405879110243445062391837349387', + age: '19865805272519696320755573045337531955436490760876870776207490804137339344112305203631892390827288264857621916650098902064979838987400911652887344763586495880167030031364467726355103327059673023946234460960685398768709062405377107912774045508870580108596597470880834205563197111550140867466625683117333370595295321833757429488192170551320637065066368716366317421169802474954914904380304190861641082310805418122837214965865969459724848071006870574514215255412289237027267424055400593307112849859757094597401668252862525566316402695830217450073667487951799749275437192883439584518905943435472478496028380016245355151988', + }, + rctxt: + '17146114573198643698878017247599007910707723139165264508694101989891626297408755744139587708989465136799243292477223763665064840330721616213638280284119891715514951989022398510785960099708705561761504012512387129498731093386014964896897751536856287377064154297370092339714578039195258061017640952790913108285519632654466006255438773382930416822756630391947263044087385305540191237328903426888518439803354213792647775798033294505898635058814132665832000734168261793545453678083703704122695006541391598116359796491845268631009298069826949515604008666680160398698425061157356267086946953480945396595351944425658076127674', + z: '57056568014385132434061065334124327103768023932445648883765905576432733866307137325457775876741578717650388638737098805750938053855430851133826479968450532729423746605371536096355616166421996729493639634413002114547787617999178137950004782677177313856876420539744625174205603354705595789330008560775613287118432593300023801651460885523314713996258581986238928077688246511704050386525431448517516821261983193275502089060128363906909778842476516981025598807378338053788433033754999771876361716562378445777250912525673660842724168260417083076824975992327559199634032439358787956784395443246565622469187082767614421691234', + }, + }, + }, + }) + + const credDefWriteRequest = await pool.prepareWriteRequest(agentContext, credentialDefinitionRequest, signerKey) + const response = await pool.submitRequest(credDefWriteRequest) + + expect(response).toMatchObject({ + op: 'REPLY', + result: { + ver: '1', + txn: { + metadata: expect.any(Object), + type: '102', + data: { + data: { + primary: { + r: { + last_name: + '35864556460959997092903171610228165251001245539613587319116151716453114432309327039517115215674024166920383445379522674504803469517283236033110568676156285676664363558333716898161685255450536856645604857714925836474250821415182026707218622134953915013803750771185050002646661004119778318524426368842019753903741998256374803456282688037624993010626333853831264356355867746685055670790915539230702546586615988121383960277550317876816983602795121749533628953449405383896799464872758725899520173321672584180060465965090049734285011738428381648013150818429882628144544132356242262467090140003979917439514443707537952643217', + first_name: + '26405366527417391838431479783966663952336302347775179063968690502492620867161212873635806190080000833725932174641667734138216137047349915190546601368424742647800764149890590518336588437317392528514313749533980651547425554257026971104775208127915118918084350210726664749850578299247705298976657301433446491575776774836993110356033664644761593799921221474617858131678955318702706530853801195330271860527250931569815553226145458665481867408279941785848264018364216087471931232367137301987457054918438087686484522112532447779498424748261678616461026788516567300969886029412198319909977473167405879110243445062391837349387', + age: '19865805272519696320755573045337531955436490760876870776207490804137339344112305203631892390827288264857621916650098902064979838987400911652887344763586495880167030031364467726355103327059673023946234460960685398768709062405377107912774045508870580108596597470880834205563197111550140867466625683117333370595295321833757429488192170551320637065066368716366317421169802474954914904380304190861641082310805418122837214965865969459724848071006870574514215255412289237027267424055400593307112849859757094597401668252862525566316402695830217450073667487951799749275437192883439584518905943435472478496028380016245355151988', + master_secret: + '51468326064458249697956272807708948542001661888325200180968238787091473418947480867518174106588127385097619219536294589148765074804124925845579871788369264160902401097166484002617399484700234182426993061977152961670486891123188739266793651668791365808983166555735631354925174224786218771453042042304773095663181121735652667614424198057134974727791329623974680096491276337756445057223988781749506082654194307092164895251308088903000573135447235553684949564809677864522417041639512933806794232354223826262154508950271949764583849083972967642587778197779127063591201123312548182885603427440981731822883101260509710567731', + }, + z: '57056568014385132434061065334124327103768023932445648883765905576432733866307137325457775876741578717650388638737098805750938053855430851133826479968450532729423746605371536096355616166421996729493639634413002114547787617999178137950004782677177313856876420539744625174205603354705595789330008560775613287118432593300023801651460885523314713996258581986238928077688246511704050386525431448517516821261983193275502089060128363906909778842476516981025598807378338053788433033754999771876361716562378445777250912525673660842724168260417083076824975992327559199634032439358787956784395443246565622469187082767614421691234', + rctxt: + '17146114573198643698878017247599007910707723139165264508694101989891626297408755744139587708989465136799243292477223763665064840330721616213638280284119891715514951989022398510785960099708705561761504012512387129498731093386014964896897751536856287377064154297370092339714578039195258061017640952790913108285519632654466006255438773382930416822756630391947263044087385305540191237328903426888518439803354213792647775798033294505898635058814132665832000734168261793545453678083703704122695006541391598116359796491845268631009298069826949515604008666680160398698425061157356267086946953480945396595351944425658076127674', + n: '95671911213029889766246243339609567053285242961853979532076192834533577534909796042025401129640348836502648821408485216223269830089771714177855160978214805993386076928594836829216646288195127289421136294309746871614765411402917891972999085287429566166932354413679994469616357622976775651506242447852304853465380257226445481515631782793575184420720296120464167257703633829902427169144462981949944348928086406211627174233811365419264314148304536534528344413738913277713548403058098093453580992173145127632199215550027527631259565822872315784889212327945030315062879193999012349220118290071491899498795367403447663354833', + s: '1573939820553851804028472930351082111827449763317396231059458630252708273163050576299697385049087601314071156646675105028237105229428440185022593174121924731226634356276616495327358864865629675802738680754755949997611920669823449540027707876555408118172529688443208301403297680159171306000341239398135896274940688268460793682007115152428685521865921925309154307574955324973580144009271977076586453011938089159885164705002797196738438392179082905738155386545935208094240038135576042886730802817809757582039362798495805441520744154270346780731494125065136433163757697326955962282840631850597919384092584727207908978907', + }, + }, + signature_type: 'CL', + ref: schemaResponse.result.txnMetadata.seqNo, + tag: 'TAG', + }, + }, + }, + }) + }) +}) diff --git a/packages/indy-vdr/tests/indy-vdr-sov-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-sov-did-resolver.e2e.test.ts new file mode 100644 index 0000000000..427ff542f0 --- /dev/null +++ b/packages/indy-vdr/tests/indy-vdr-sov-did-resolver.e2e.test.ts @@ -0,0 +1,152 @@ +import { parseIndyDid } from '@credo-ts/anoncreds' +import { DidsModule, Agent, TypedArrayEncoder, JsonTransformer } from '@credo-ts/core' +import { indyVdr } from '@hyperledger/indy-vdr-nodejs' + +import { getInMemoryAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' +import { IndyVdrModule } from '../src' +import { IndyVdrIndyDidRegistrar, IndyVdrIndyDidResolver, IndyVdrSovDidResolver } from '../src/dids' + +import { createDidOnLedger, indyVdrModuleConfig } from './helpers' + +const agent = new Agent( + getInMemoryAgentOptions( + 'Indy VDR Sov DID resolver', + {}, + { + indyVdr: new IndyVdrModule({ + indyVdr, + networks: indyVdrModuleConfig.networks, + }), + dids: new DidsModule({ + registrars: [new IndyVdrIndyDidRegistrar()], + resolvers: [new IndyVdrIndyDidResolver(), new IndyVdrSovDidResolver()], + }), + } + ) +) + +describe('Indy VDR Sov DID Resolver', () => { + beforeAll(async () => { + await agent.initialize() + }) + + afterAll(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + test('resolve a did:sov did', async () => { + const did = 'did:sov:TL1EaPFCZ8Si5aUrqScBDt' + const didResult = await agent.dids.resolve(did) + + expect(JsonTransformer.toJSON(didResult)).toMatchObject({ + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#key-1`, + publicKeyBase58: expect.any(String), + }, + { + controller: did, + type: 'X25519KeyAgreementKey2019', + id: `${did}#key-agreement-1`, + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#key-1`], + assertionMethod: [`${did}#key-1`], + keyAgreement: [`${did}#key-agreement-1`], + service: undefined, + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + test('resolve a did with endpoints', async () => { + const unqualifiedSubmitterDid = await importExistingIndyDidFromPrivateKey( + agent, + TypedArrayEncoder.fromString('000000000000000000000000Trustee9') + ) + + // First we need to create a new DID and add ATTRIB endpoint to it + const { did } = await createDidOnLedger(agent, `did:indy:pool:localtest:${unqualifiedSubmitterDid}`) + const { namespaceIdentifier } = parseIndyDid(did) + const sovDid = `did:sov:${namespaceIdentifier}` + + // DID created. Now resolve it + const didResult = await agent.dids.resolve(sovDid) + + expect(JsonTransformer.toJSON(didResult)).toMatchObject({ + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + 'https://didcomm.org/messaging/contexts/v2', + ], + id: sovDid, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: sovDid, + id: `${sovDid}#key-1`, + publicKeyBase58: expect.any(String), + }, + { + controller: sovDid, + type: 'X25519KeyAgreementKey2019', + id: `${sovDid}#key-agreement-1`, + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${sovDid}#key-1`], + assertionMethod: [`${sovDid}#key-1`], + keyAgreement: [`${sovDid}#key-agreement-1`], + service: [ + { + id: `${sovDid}#endpoint`, + serviceEndpoint: 'http://localhost:3000', + type: 'endpoint', + }, + { + id: `${sovDid}#did-communication`, + accept: ['didcomm/aip2;env=rfc19'], + priority: 0, + recipientKeys: [`${sovDid}#key-agreement-1`], + routingKeys: ['a-routing-key'], + serviceEndpoint: 'http://localhost:3000', + type: 'did-communication', + }, + { + id: `${sovDid}#didcomm-messaging-1`, + serviceEndpoint: { uri: 'http://localhost:3000', routingKeys: ['a-routing-key'], accept: ['didcomm/v2'] }, + type: 'DIDCommMessaging', + }, + ], + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) +}) diff --git a/packages/indy-vdr/tests/setup.ts b/packages/indy-vdr/tests/setup.ts new file mode 100644 index 0000000000..a5714a641c --- /dev/null +++ b/packages/indy-vdr/tests/setup.ts @@ -0,0 +1,3 @@ +import '@hyperledger/indy-vdr-nodejs' + +jest.setTimeout(120000) diff --git a/packages/indy-vdr/tsconfig.build.json b/packages/indy-vdr/tsconfig.build.json new file mode 100644 index 0000000000..2b75d0adab --- /dev/null +++ b/packages/indy-vdr/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build" + }, + "include": ["src/**/*"] +} diff --git a/packages/indy-vdr/tsconfig.json b/packages/indy-vdr/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/indy-vdr/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/packages/node/CHANGELOG.md b/packages/node/CHANGELOG.md new file mode 100644 index 0000000000..1ec71d7c83 --- /dev/null +++ b/packages/node/CHANGELOG.md @@ -0,0 +1,165 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release +- Updated dependencies [66e696d] + - @credo-ts/core@0.5.6 + +## 0.5.5 + +### Patch Changes + +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) +- Updated dependencies [3239ef3] +- Updated dependencies [d548fa4] +- Updated dependencies [482a630] + - @credo-ts/core@0.5.5 + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +**Note:** Version bump only for package @credo-ts/node + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +### Bug Fixes + +- node-ffi-napi compatibility ([#1821](https://github.com/openwallet-foundation/credo-ts/issues/1821)) ([81d351b](https://github.com/openwallet-foundation/credo-ts/commit/81d351bc9d4d508ebfac9e7f2b2f10276ab1404a)) + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +### Bug Fixes + +- import of websocket ([#1804](https://github.com/openwallet-foundation/credo-ts/issues/1804)) ([48b31ae](https://github.com/openwallet-foundation/credo-ts/commit/48b31ae9229cd188defb0ed3b4e64b0346013f3d)) + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +**Note:** Version bump only for package @credo-ts/node + +## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) + +### Bug Fixes + +- log and throw on WebSocket sending errors ([#1573](https://github.com/hyperledger/aries-framework-javascript/issues/1573)) ([11050af](https://github.com/hyperledger/aries-framework-javascript/commit/11050afc7965adfa9b00107ba34abfbe3afaf874)) + +## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) + +### Bug Fixes + +- **samples:** mediator wallet and http transport ([#1508](https://github.com/hyperledger/aries-framework-javascript/issues/1508)) ([04a8058](https://github.com/hyperledger/aries-framework-javascript/commit/04a80589b19725fb493e51e52a7345915b2c7341)) + +# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) + +### Bug Fixes + +- return HTTP 415 if unsupported content type ([#1313](https://github.com/hyperledger/aries-framework-javascript/issues/1313)) ([122cdde](https://github.com/hyperledger/aries-framework-javascript/commit/122cdde6982174a8e9cf70ef26a1393cb3912066)) + +- feat!: add data, cache and temp dirs to FileSystem (#1306) ([ff5596d](https://github.com/hyperledger/aries-framework-javascript/commit/ff5596d0631e93746494c017797d0191b6bdb0b1)), closes [#1306](https://github.com/hyperledger/aries-framework-javascript/issues/1306) + +### Features + +- add initial askar package ([#1211](https://github.com/hyperledger/aries-framework-javascript/issues/1211)) ([f18d189](https://github.com/hyperledger/aries-framework-javascript/commit/f18d1890546f7d66571fe80f2f3fc1fead1cd4c3)) +- **anoncreds:** legacy indy proof format service ([#1283](https://github.com/hyperledger/aries-framework-javascript/issues/1283)) ([c72fd74](https://github.com/hyperledger/aries-framework-javascript/commit/c72fd7416f2c1bc0497a84036e16adfa80585e49)) +- **askar:** import/export wallet support for SQLite ([#1377](https://github.com/hyperledger/aries-framework-javascript/issues/1377)) ([19cefa5](https://github.com/hyperledger/aries-framework-javascript/commit/19cefa54596a4e4848bdbe89306a884a5ce2e991)) + +### BREAKING CHANGES + +- Agent-produced files will now be divided in different system paths depending on their nature: data, temp and cache. Previously, they were located at a single location, defaulting to a temporary directory. + +If you specified a custom path in `FileSystem` object constructor, you now must provide an object containing `baseDataPath`, `baseTempPath` and `baseCachePath`. They can point to the same path, although it's recommended to specify different path to avoid future file clashes. + +## [0.3.3](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.2...v0.3.3) (2023-01-18) + +### Bug Fixes + +- fix typing issues with typescript 4.9 ([#1214](https://github.com/hyperledger/aries-framework-javascript/issues/1214)) ([087980f](https://github.com/hyperledger/aries-framework-javascript/commit/087980f1adf3ee0bc434ca9782243a62c6124444)) + +## [0.3.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.1...v0.3.2) (2023-01-04) + +**Note:** Version bump only for package @credo-ts/node + +## [0.3.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.0...v0.3.1) (2022-12-27) + +**Note:** Version bump only for package @credo-ts/node + +# [0.3.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.5...v0.3.0) (2022-12-22) + +- refactor!: add agent context (#920) ([b47cfcb](https://github.com/hyperledger/aries-framework-javascript/commit/b47cfcba1450cd1d6839bf8192d977bfe33f1bb0)), closes [#920](https://github.com/hyperledger/aries-framework-javascript/issues/920) + +### Features + +- add agent context provider ([#921](https://github.com/hyperledger/aries-framework-javascript/issues/921)) ([a1b1e5a](https://github.com/hyperledger/aries-framework-javascript/commit/a1b1e5a22fd4ab9ef593b5cd7b3c710afcab3142)) +- specify httpinboundtransport path ([#1115](https://github.com/hyperledger/aries-framework-javascript/issues/1115)) ([03cdf39](https://github.com/hyperledger/aries-framework-javascript/commit/03cdf397b61253d2eb20694049baf74843b7ed92)) + +### BREAKING CHANGES + +- To make AFJ multi-tenancy ready, all services and repositories have been made stateless. A new `AgentContext` is introduced that holds the current context, which is passed to each method call. The public API hasn't been affected, but due to the large impact of this change it is marked as breaking. + +## [0.2.5](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.4...v0.2.5) (2022-10-13) + +**Note:** Version bump only for package @credo-ts/node + +## [0.2.4](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.3...v0.2.4) (2022-09-10) + +**Note:** Version bump only for package @credo-ts/node + +## [0.2.3](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.2...v0.2.3) (2022-08-30) + +**Note:** Version bump only for package @credo-ts/node + +## [0.2.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.1...v0.2.2) (2022-07-15) + +**Note:** Version bump only for package @credo-ts/node + +## [0.2.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.0...v0.2.1) (2022-07-08) + +### Features + +- initial plugin api ([#907](https://github.com/hyperledger/aries-framework-javascript/issues/907)) ([6d88aa4](https://github.com/hyperledger/aries-framework-javascript/commit/6d88aa4537ab2a9494ffea8cdfb4723cf915f291)) + +# [0.2.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.1.0...v0.2.0) (2022-06-24) + +### Bug Fixes + +- close session early if no return route ([#715](https://github.com/hyperledger/aries-framework-javascript/issues/715)) ([2e65408](https://github.com/hyperledger/aries-framework-javascript/commit/2e6540806f2d67bef16004f6e8398c5bf7a05bcf)) +- **node:** allow to import node package without postgres ([#757](https://github.com/hyperledger/aries-framework-javascript/issues/757)) ([59e1058](https://github.com/hyperledger/aries-framework-javascript/commit/59e10589acee987fb46f9cbaa3583ba8dcd70b87)) +- **node:** only send 500 if no headers sent yet ([#857](https://github.com/hyperledger/aries-framework-javascript/issues/857)) ([4be8f82](https://github.com/hyperledger/aries-framework-javascript/commit/4be8f82c214f99538eaa0fd0aac5a8f7a6e1dd6b)) + +### Features + +- **core:** add support for postgres wallet type ([#699](https://github.com/hyperledger/aries-framework-javascript/issues/699)) ([83ff0f3](https://github.com/hyperledger/aries-framework-javascript/commit/83ff0f36401cbf6e95c0a1ceb9fa921a82dc6830)) +- indy revocation (prover & verifier) ([#592](https://github.com/hyperledger/aries-framework-javascript/issues/592)) ([fb19ff5](https://github.com/hyperledger/aries-framework-javascript/commit/fb19ff555b7c10c9409450dcd7d385b1eddf41ac)) + +# 0.1.0 (2021-12-23) + +### Bug Fixes + +- **node:** node v12 support for is-indy-installed ([#542](https://github.com/hyperledger/aries-framework-javascript/issues/542)) ([17e9157](https://github.com/hyperledger/aries-framework-javascript/commit/17e9157479d6bba90c2a94bce64697d7f65fac96)) + +### Features + +- add multiple inbound transports ([#433](https://github.com/hyperledger/aries-framework-javascript/issues/433)) ([56cb9f2](https://github.com/hyperledger/aries-framework-javascript/commit/56cb9f2202deb83b3c133905f21651bfefcb63f7)) +- break out indy wallet, better indy handling ([#396](https://github.com/hyperledger/aries-framework-javascript/issues/396)) ([9f1a4a7](https://github.com/hyperledger/aries-framework-javascript/commit/9f1a4a754a61573ce3fee78d52615363c7e25d58)) +- **core:** connection-less issuance and verification ([#359](https://github.com/hyperledger/aries-framework-javascript/issues/359)) ([fb46ade](https://github.com/hyperledger/aries-framework-javascript/commit/fb46ade4bc2dd4f3b63d4194bb170d2f329562b7)) +- **core:** support multiple indy ledgers ([#474](https://github.com/hyperledger/aries-framework-javascript/issues/474)) ([47149bc](https://github.com/hyperledger/aries-framework-javascript/commit/47149bc5742456f4f0b75e0944ce276972e645b8)) +- **node:** add http and ws inbound transport ([#392](https://github.com/hyperledger/aries-framework-javascript/issues/392)) ([34a6ff2](https://github.com/hyperledger/aries-framework-javascript/commit/34a6ff2699197b9d525422a0a405e241582a476c)) +- **node:** add is-indy-installed command ([#510](https://github.com/hyperledger/aries-framework-javascript/issues/510)) ([e50b821](https://github.com/hyperledger/aries-framework-javascript/commit/e50b821343970d299a4cacdcba3a051893524ed6)) diff --git a/packages/node/README.md b/packages/node/README.md new file mode 100644 index 0000000000..b1891e138d --- /dev/null +++ b/packages/node/README.md @@ -0,0 +1,31 @@ +

+
+ Credo Logo +

+

Credo - Node

+

+ License + typescript + @credo-ts/node version + +

+
+ +Credo Node provides platform specific dependencies to run Credo in [Node.JS](https://nodejs.org). See the [Getting Started Guide](https://github.com/openwallet-foundation/credo-ts#getting-started) for installation instructions. diff --git a/packages/node/jest.config.ts b/packages/node/jest.config.ts new file mode 100644 index 0000000000..2556d19c61 --- /dev/null +++ b/packages/node/jest.config.ts @@ -0,0 +1,12 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, +} + +export default config diff --git a/packages/node/package.json b/packages/node/package.json new file mode 100644 index 0000000000..efb960e0a9 --- /dev/null +++ b/packages/node/package.json @@ -0,0 +1,43 @@ +{ + "name": "@credo-ts/node", + "main": "src/index", + "types": "src/index", + "version": "0.5.6", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/node", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/node" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build", + "test": "jest" + }, + "dependencies": { + "@2060.io/ffi-napi": "^4.0.9", + "@2060.io/ref-napi": "^3.0.6", + "@credo-ts/core": "workspace:*", + "@types/express": "^4.17.15", + "express": "^4.17.1", + "ws": "^8.13.0" + }, + "devDependencies": { + "@types/node": "^18.18.8", + "@types/ws": "^8.5.4", + "nock": "^13.3.0", + "rimraf": "^4.4.0", + "typescript": "~5.5.2" + } +} diff --git a/packages/node/src/NodeFileSystem.ts b/packages/node/src/NodeFileSystem.ts new file mode 100644 index 0000000000..eb8611d7c4 --- /dev/null +++ b/packages/node/src/NodeFileSystem.ts @@ -0,0 +1,113 @@ +import type { DownloadToFileOptions, FileSystem } from '@credo-ts/core' + +import { CredoError, TypedArrayEncoder } from '@credo-ts/core' +import { createHash } from 'crypto' +import fs, { promises } from 'fs' +import http from 'http' +import https from 'https' +import { tmpdir, homedir } from 'os' +import { dirname } from 'path' + +const { access, readFile, writeFile, mkdir, rm, unlink, copyFile } = promises + +export class NodeFileSystem implements FileSystem { + public readonly dataPath + public readonly cachePath + public readonly tempPath + + /** + * Create new NodeFileSystem class instance. + * + * @param baseDataPath The base path to use for reading and writing data files used within the framework. + * Files will be created under baseDataPath/.afj directory. If not specified, it will be set to homedir() + * @param baseCachePath The base path to use for reading and writing cache files used within the framework. + * Files will be created under baseCachePath/.afj directory. If not specified, it will be set to homedir() + * @param baseTempPath The base path to use for reading and writing temporary files within the framework. + * Files will be created under baseTempPath/.afj directory. If not specified, it will be set to tmpdir() + */ + public constructor(options?: { baseDataPath?: string; baseCachePath?: string; baseTempPath?: string }) { + this.dataPath = options?.baseDataPath ? `${options?.baseDataPath}/.afj` : `${homedir()}/.afj/data` + this.cachePath = options?.baseCachePath ? `${options?.baseCachePath}/.afj` : `${homedir()}/.afj/cache` + this.tempPath = `${options?.baseTempPath ?? tmpdir()}/.afj` + } + + public async exists(path: string) { + try { + await access(path) + return true + } catch { + return false + } + } + + public async createDirectory(path: string): Promise { + await mkdir(dirname(path), { recursive: true }) + } + + public async copyFile(sourcePath: string, destinationPath: string): Promise { + await copyFile(sourcePath, destinationPath) + } + + public async write(path: string, data: string): Promise { + // Make sure parent directories exist + await mkdir(dirname(path), { recursive: true }) + + return writeFile(path, data, { encoding: 'utf-8' }) + } + + public async read(path: string): Promise { + return readFile(path, { encoding: 'utf-8' }) + } + + public async delete(path: string): Promise { + await rm(path, { recursive: true, force: true }) + } + + public async downloadToFile(url: string, path: string, options: DownloadToFileOptions) { + const httpMethod = url.startsWith('https') ? https : http + + // Make sure parent directories exist + await mkdir(dirname(path), { recursive: true }) + + const file = fs.createWriteStream(path) + const hash = options.verifyHash ? createHash('sha256') : undefined + + return new Promise((resolve, reject) => { + httpMethod + .get(url, (response) => { + // check if response is success + if (response.statusCode !== 200) { + reject(`Unable to download file from url: ${url}. Response status was ${response.statusCode}`) + } + + hash && response.pipe(hash) + response.pipe(file) + file.on('finish', async () => { + file.close() + + if (hash && options.verifyHash?.hash) { + hash.end() + const digest = hash.digest() + if (digest.compare(options.verifyHash.hash) !== 0) { + await fs.promises.unlink(path) + + reject( + new CredoError( + `Hash of downloaded file does not match expected hash. Expected: ${ + options.verifyHash.hash + }, Actual: ${TypedArrayEncoder.toUtf8String(digest)})}` + ) + ) + } + } + resolve() + }) + }) + .on('error', async (error) => { + // Handle errors + await unlink(path) // Delete the file async. (But we don't check the result) + reject(`Unable to download file from url: ${url}. ${error.message}`) + }) + }) + } +} diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts new file mode 100644 index 0000000000..b73c3bbcf1 --- /dev/null +++ b/packages/node/src/index.ts @@ -0,0 +1,17 @@ +import type { AgentDependencies } from '@credo-ts/core' + +import { EventEmitter } from 'events' +import { WebSocket } from 'ws' + +import { NodeFileSystem } from './NodeFileSystem' +import { HttpInboundTransport } from './transport/HttpInboundTransport' +import { WsInboundTransport } from './transport/WsInboundTransport' + +const agentDependencies: AgentDependencies = { + FileSystem: NodeFileSystem, + fetch, + EventEmitterClass: EventEmitter, + WebSocketClass: WebSocket, +} + +export { agentDependencies, HttpInboundTransport, WsInboundTransport } diff --git a/packages/node/src/transport/HttpInboundTransport.ts b/packages/node/src/transport/HttpInboundTransport.ts new file mode 100644 index 0000000000..ac86d80cec --- /dev/null +++ b/packages/node/src/transport/HttpInboundTransport.ts @@ -0,0 +1,117 @@ +import type { InboundTransport, Agent, TransportSession, EncryptedMessage, AgentContext } from '@credo-ts/core' +import type { Express, Request, Response } from 'express' +import type { Server } from 'http' + +import { DidCommMimeType, CredoError, TransportService, utils, MessageReceiver } from '@credo-ts/core' +import express, { text } from 'express' + +const supportedContentTypes: string[] = [DidCommMimeType.V0, DidCommMimeType.V1] + +export class HttpInboundTransport implements InboundTransport { + public readonly app: Express + private port: number + private path: string + private _server?: Server + + public get server() { + return this._server + } + + public constructor({ app, path, port }: { app?: Express; path?: string; port: number }) { + this.port = port + + // Create Express App + this.app = app ?? express() + this.path = path ?? '/' + + this.app.use(text({ type: supportedContentTypes, limit: '5mb' })) + } + + public async start(agent: Agent) { + const transportService = agent.dependencyManager.resolve(TransportService) + const messageReceiver = agent.dependencyManager.resolve(MessageReceiver) + + agent.config.logger.debug(`Starting HTTP inbound transport`, { + port: this.port, + }) + + this.app.post(this.path, async (req, res) => { + const contentType = req.headers['content-type'] + + if (!contentType || !supportedContentTypes.includes(contentType)) { + return res + .status(415) + .send('Unsupported content-type. Supported content-types are: ' + supportedContentTypes.join(', ')) + } + + const session = new HttpTransportSession(utils.uuid(), req, res) + // We want to make sure the session is removed if the connection is closed, as it + // can't be used anymore then. This could happen if the client abruptly closes the connection. + req.once('close', () => transportService.removeSession(session)) + + try { + const message = req.body + const encryptedMessage = JSON.parse(message) + await messageReceiver.receiveMessage(encryptedMessage, { + session, + }) + + // If agent did not use session when processing message we need to send response here. + if (!res.headersSent) { + res.status(200).end() + } + } catch (error) { + agent.config.logger.error(`Error processing inbound message: ${error.message}`, error) + + if (!res.headersSent) { + res.status(500).send('Error processing message') + } + } finally { + transportService.removeSession(session) + } + }) + + this._server = this.app.listen(this.port) + } + + public async stop(): Promise { + return new Promise((resolve, reject) => this._server?.close((err) => (err ? reject(err) : resolve()))) + } +} + +export class HttpTransportSession implements TransportSession { + public id: string + public readonly type = 'http' + public req: Request + public res: Response + + public constructor(id: string, req: Request, res: Response) { + this.id = id + this.req = req + this.res = res + } + + public async close(): Promise { + if (!this.res.headersSent) { + this.res.status(200).end() + } + } + + public async send(agentContext: AgentContext, encryptedMessage: EncryptedMessage): Promise { + if (this.res.headersSent) { + throw new CredoError(`${this.type} transport session has been closed.`) + } + + // By default we take the agent config's default DIDComm content-type + let responseMimeType = agentContext.config.didCommMimeType as string + + // However, if the request mime-type is a mime-type that is supported by us, we use that + // to minimize the chance of interoperability issues + const requestMimeType = this.req.headers['content-type'] + if (requestMimeType && supportedContentTypes.includes(requestMimeType)) { + responseMimeType = requestMimeType + } + + this.res.status(200).contentType(responseMimeType).json(encryptedMessage).end() + } +} diff --git a/packages/node/src/transport/WsInboundTransport.ts b/packages/node/src/transport/WsInboundTransport.ts new file mode 100644 index 0000000000..ff23807fed --- /dev/null +++ b/packages/node/src/transport/WsInboundTransport.ts @@ -0,0 +1,106 @@ +import type { Agent, InboundTransport, Logger, TransportSession, EncryptedMessage, AgentContext } from '@credo-ts/core' + +import { CredoError, TransportService, utils, MessageReceiver } from '@credo-ts/core' +// eslint-disable-next-line import/no-named-as-default +import WebSocket, { Server } from 'ws' + +export class WsInboundTransport implements InboundTransport { + private socketServer: Server + private logger!: Logger + + // We're using a `socketId` just for the prevention of calling the connection handler twice. + private socketIds: Record = {} + + public constructor({ server, port }: { server: Server; port?: undefined } | { server?: undefined; port: number }) { + this.socketServer = server ?? new Server({ port }) + } + + public async start(agent: Agent) { + const transportService = agent.dependencyManager.resolve(TransportService) + + this.logger = agent.config.logger + + const wsEndpoint = agent.config.endpoints.find((e) => e.startsWith('ws')) + this.logger.debug(`Starting WS inbound transport`, { + endpoint: wsEndpoint, + }) + + this.socketServer.on('connection', (socket: WebSocket) => { + const socketId = utils.uuid() + this.logger.debug('Socket connected.') + + if (!this.socketIds[socketId]) { + this.logger.debug(`Saving new socket with id ${socketId}.`) + this.socketIds[socketId] = socket + const session = new WebSocketTransportSession(socketId, socket, this.logger) + this.listenOnWebSocketMessages(agent, socket, session) + socket.on('close', () => { + this.logger.debug('Socket closed.') + transportService.removeSession(session) + }) + } else { + this.logger.debug(`Socket with id ${socketId} already exists.`) + } + }) + } + + public async stop() { + this.logger.debug('Closing WebSocket Server') + + return new Promise((resolve, reject) => { + this.socketServer.close((error) => { + if (error) { + reject(error) + } + resolve() + }) + }) + } + + private listenOnWebSocketMessages(agent: Agent, socket: WebSocket, session: TransportSession) { + const messageReceiver = agent.dependencyManager.resolve(MessageReceiver) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + socket.addEventListener('message', async (event: any) => { + this.logger.debug('WebSocket message event received.', { url: event.target.url }) + try { + await messageReceiver.receiveMessage(JSON.parse(event.data), { session }) + } catch (error) { + this.logger.error(`Error processing message: ${error}`) + } + }) + } +} + +export class WebSocketTransportSession implements TransportSession { + public id: string + public readonly type = 'WebSocket' + public socket: WebSocket + private logger: Logger + + public constructor(id: string, socket: WebSocket, logger: Logger) { + this.id = id + this.socket = socket + this.logger = logger + } + + public async send(agentContext: AgentContext, encryptedMessage: EncryptedMessage): Promise { + if (this.socket.readyState !== WebSocket.OPEN) { + throw new CredoError(`${this.type} transport session has been closed.`) + } + this.socket.send(JSON.stringify(encryptedMessage), (error?) => { + if (error != undefined) { + this.logger.debug(`Error sending message: ${error}`) + throw new CredoError(`${this.type} send message failed.`, { cause: error }) + } else { + this.logger.debug(`${this.type} sent message successfully.`) + } + }) + } + + public async close(): Promise { + if (this.socket.readyState === WebSocket.OPEN) { + this.socket.close() + } + } +} diff --git a/packages/node/tests/NodeFileSystem.test.ts b/packages/node/tests/NodeFileSystem.test.ts new file mode 100644 index 0000000000..c529b9d3df --- /dev/null +++ b/packages/node/tests/NodeFileSystem.test.ts @@ -0,0 +1,42 @@ +import { TypedArrayEncoder } from '@credo-ts/core' +import nock, { cleanAll, enableNetConnect } from 'nock' +import path from 'path' + +import { NodeFileSystem } from '../src/NodeFileSystem' + +describe('@credo-ts/file-system-node', () => { + describe('NodeFileSystem', () => { + const fileSystem = new NodeFileSystem() + + afterAll(() => { + cleanAll() + enableNetConnect() + }) + + describe('exists()', () => { + it('should return false if the pash does not exist', () => { + return expect(fileSystem.exists('some-random-path')).resolves.toBe(false) + }) + }) + + describe('downloadToFile()', () => { + test('should verify the hash', async () => { + // Mock tails file + nock('https://tails.prod.absa.africa') + .get('/api/public/tails/4B1NxYuGxwYMe5BAyP9NXkUmbEkDATo4oGZCgjXQ3y1p') + .replyWithFile(200, path.join(__dirname, '__fixtures__/tailsFile')) + + await fileSystem.downloadToFile( + 'https://tails.prod.absa.africa/api/public/tails/4B1NxYuGxwYMe5BAyP9NXkUmbEkDATo4oGZCgjXQ3y1p', + `${fileSystem.dataPath}/tails/4B1NxYuGxwYMe5BAyP9NXkUmbEkDATo4oGZCgjXQ3y1p`, + { + verifyHash: { + algorithm: 'sha256', + hash: TypedArrayEncoder.fromBase58('4B1NxYuGxwYMe5BAyP9NXkUmbEkDATo4oGZCgjXQ3y1p'), + }, + } + ) + }) + }) + }) +}) diff --git a/packages/node/tests/__fixtures__/tailsFile b/packages/node/tests/__fixtures__/tailsFile new file mode 100644 index 0000000000..73f0471860 Binary files /dev/null and b/packages/node/tests/__fixtures__/tailsFile differ diff --git a/packages/node/tsconfig.build.json b/packages/node/tsconfig.build.json new file mode 100644 index 0000000000..5f125502b3 --- /dev/null +++ b/packages/node/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.build.json", + + "compilerOptions": { + "outDir": "./build", + "types": ["node"] + }, + + "include": ["src/**/*"] +} diff --git a/packages/node/tsconfig.json b/packages/node/tsconfig.json new file mode 100644 index 0000000000..89375a3930 --- /dev/null +++ b/packages/node/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["node", "jest"] + } +} diff --git a/packages/openid4vc/CHANGELOG.md b/packages/openid4vc/CHANGELOG.md new file mode 100644 index 0000000000..6fd81bcfc2 --- /dev/null +++ b/packages/openid4vc/CHANGELOG.md @@ -0,0 +1,67 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release +- Updated dependencies [66e696d] + - @credo-ts/core@0.5.6 + +## 0.5.5 + +### Patch Changes + +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) +- Updated dependencies [3239ef3] +- Updated dependencies [d548fa4] +- Updated dependencies [482a630] + - @credo-ts/core@0.5.5 + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +**Note:** Version bump only for package @credo-ts/openid4vc + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +### Bug Fixes + +- access token can only be used for offer ([#1828](https://github.com/openwallet-foundation/credo-ts/issues/1828)) ([f54b90b](https://github.com/openwallet-foundation/credo-ts/commit/f54b90b0530b43a04df6299a39414a142d73276e)) +- oid4vp can be used separate from idtoken ([#1827](https://github.com/openwallet-foundation/credo-ts/issues/1827)) ([ca383c2](https://github.com/openwallet-foundation/credo-ts/commit/ca383c284e2073992a1fd280fca99bee1c2e19f8)) +- **openid4vc:** update verified state for more states ([#1831](https://github.com/openwallet-foundation/credo-ts/issues/1831)) ([958bf64](https://github.com/openwallet-foundation/credo-ts/commit/958bf647c086a2ca240e9ad140defc39b7f20f43)) + +### Features + +- add disclosures so you know which fields are disclosed ([#1834](https://github.com/openwallet-foundation/credo-ts/issues/1834)) ([6ec43eb](https://github.com/openwallet-foundation/credo-ts/commit/6ec43eb1f539bd8d864d5bbd2ab35459809255ec)) +- apply new version of SD JWT package ([#1787](https://github.com/openwallet-foundation/credo-ts/issues/1787)) ([b41e158](https://github.com/openwallet-foundation/credo-ts/commit/b41e158098773d2f59b5b5cfb82cc6be06a57acd)) +- openid4vc issued state per credential ([#1829](https://github.com/openwallet-foundation/credo-ts/issues/1829)) ([229c621](https://github.com/openwallet-foundation/credo-ts/commit/229c62177c04060c7ca4c19dfd35bab328035067)) + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +### Bug Fixes + +- **openid4vc:** several fixes and improvements ([#1795](https://github.com/openwallet-foundation/credo-ts/issues/1795)) ([b83c517](https://github.com/openwallet-foundation/credo-ts/commit/b83c5173070594448d92f801331b3a31c7ac8049)) + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +### Features + +- anoncreds w3c migration ([#1744](https://github.com/openwallet-foundation/credo-ts/issues/1744)) ([d7c2bbb](https://github.com/openwallet-foundation/credo-ts/commit/d7c2bbb4fde57cdacbbf1ed40c6bd1423f7ab015)) +- **openid4vc:** persistance and events ([#1793](https://github.com/openwallet-foundation/credo-ts/issues/1793)) ([f4c386a](https://github.com/openwallet-foundation/credo-ts/commit/f4c386a6ccf8adb829cad30b81d524e6ffddb029)) diff --git a/packages/openid4vc/README.md b/packages/openid4vc/README.md new file mode 100644 index 0000000000..4796ff9c2d --- /dev/null +++ b/packages/openid4vc/README.md @@ -0,0 +1,31 @@ +

+
+ Credo Logo +

+

Credo OpenID4VC Module

+

+ License + typescript + @credo-ts/openid4vc version + +

+
+ +Open ID For Verifiable Credentials Holder Module for [Credo](https://credo.js.org). diff --git a/packages/openid4vc/jest.config.ts b/packages/openid4vc/jest.config.ts new file mode 100644 index 0000000000..8641cf4d67 --- /dev/null +++ b/packages/openid4vc/jest.config.ts @@ -0,0 +1,14 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json new file mode 100644 index 0000000000..964363179e --- /dev/null +++ b/packages/openid4vc/package.json @@ -0,0 +1,46 @@ +{ + "name": "@credo-ts/openid4vc", + "main": "src/index", + "types": "src/index", + "version": "0.5.6", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/openid4vc", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/openid4vc" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build", + "test": "jest" + }, + "dependencies": { + "@credo-ts/core": "workspace:*", + "@sphereon/did-auth-siop": "^0.6.4", + "@sphereon/oid4vci-client": "^0.10.3", + "@sphereon/oid4vci-common": "^0.10.3", + "@sphereon/oid4vci-issuer": "^0.10.3", + "@sphereon/ssi-types": "^0.23.0", + "class-transformer": "^0.5.1", + "rxjs": "^7.8.0" + }, + "devDependencies": { + "@credo-ts/tenants": "workspace:*", + "@types/express": "^4.17.21", + "express": "^4.18.2", + "nock": "^13.3.0", + "rimraf": "^4.4.0", + "typescript": "~5.5.2" + } +} diff --git a/packages/openid4vc/src/index.ts b/packages/openid4vc/src/index.ts new file mode 100644 index 0000000000..222f8329c6 --- /dev/null +++ b/packages/openid4vc/src/index.ts @@ -0,0 +1,4 @@ +export * from './openid4vc-holder' +export * from './openid4vc-verifier' +export * from './openid4vc-issuer' +export * from './shared' diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts new file mode 100644 index 0000000000..97789677fb --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts @@ -0,0 +1,127 @@ +import type { + OpenId4VciResolvedCredentialOffer, + OpenId4VciResolvedAuthorizationRequest, + OpenId4VciAuthCodeFlowOptions, + OpenId4VciAcceptCredentialOfferOptions, +} from './OpenId4VciHolderServiceOptions' +import type { OpenId4VcSiopAcceptAuthorizationRequestOptions } from './OpenId4vcSiopHolderServiceOptions' + +import { injectable, AgentContext } from '@credo-ts/core' + +import { OpenId4VciHolderService } from './OpenId4VciHolderService' +import { OpenId4VcSiopHolderService } from './OpenId4vcSiopHolderService' + +/** + * @public + */ +@injectable() +export class OpenId4VcHolderApi { + public constructor( + private agentContext: AgentContext, + private openId4VciHolderService: OpenId4VciHolderService, + private openId4VcSiopHolderService: OpenId4VcSiopHolderService + ) {} + + /** + * Resolves the authentication request given as URI or JWT to a unified format, and + * verifies the validity of the request. + * + * The resolved request can be accepted with the @see acceptSiopAuthorizationRequest. + * + * If the authorization request uses OpenID4VP and included presentation definitions, + * a `presentationExchange` property will be defined with credentials that satisfy the + * incoming request. When `presentationExchange` is present, you MUST supply `presentationExchange` + * when calling `acceptSiopAuthorizationRequest` as well. + * + * @param requestJwtOrUri JWT or an SIOPv2 request URI + * @returns the resolved and verified authentication request. + */ + public async resolveSiopAuthorizationRequest(requestJwtOrUri: string) { + return this.openId4VcSiopHolderService.resolveAuthorizationRequest(this.agentContext, requestJwtOrUri) + } + + /** + * Accepts the authentication request after it has been resolved and verified with {@link resolveSiopAuthorizationRequest}. + * + * If the resolved authorization request included a `presentationExchange` property, you MUST supply `presentationExchange` + * in the `options` parameter. + * + * If no `presentationExchange` property is present, you MUST supply `openIdTokenIssuer` in the `options` parameter. + */ + public async acceptSiopAuthorizationRequest(options: OpenId4VcSiopAcceptAuthorizationRequestOptions) { + return await this.openId4VcSiopHolderService.acceptAuthorizationRequest(this.agentContext, options) + } + + /** + * Resolves a credential offer given as credential offer URL, or issuance initiation URL, + * into a unified format with metadata. + * + * @param credentialOffer the credential offer to resolve + * @returns The uniform credential offer payload, the issuer metadata, protocol version, and the offered credentials with metadata. + */ + public async resolveCredentialOffer(credentialOffer: string) { + return await this.openId4VciHolderService.resolveCredentialOffer(credentialOffer) + } + + /** + * This function is to be used to receive an credential in OpenID4VCI using the Authorization Code Flow. + * + * Not to be confused with the {@link resolveSiopAuthorizationRequest}, which is only used for SIOP requests. + * + * It will generate the authorization request URI based on the provided options. + * The authorization request URI is used to obtain the authorization code. Currently this needs to be done manually. + * + * Authorization to request credentials can be requested via authorization_details or scopes. + * This function automatically generates the authorization_details for all offered credentials. + * If scopes are provided, the provided scopes are sent alongside the authorization_details. + * + * @param resolvedCredentialOffer Obtained through @see resolveCredentialOffer + * @param authCodeFlowOptions + * @returns The authorization request URI alongside the code verifier and original @param authCodeFlowOptions + */ + public async resolveIssuanceAuthorizationRequest( + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, + authCodeFlowOptions: OpenId4VciAuthCodeFlowOptions + ) { + return await this.openId4VciHolderService.resolveAuthorizationRequest( + this.agentContext, + resolvedCredentialOffer, + authCodeFlowOptions + ) + } + + /** + * Accepts a credential offer using the pre-authorized code flow. + * @param resolvedCredentialOffer Obtained through @see resolveCredentialOffer + * @param acceptCredentialOfferOptions + */ + public async acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, + acceptCredentialOfferOptions: OpenId4VciAcceptCredentialOfferOptions + ) { + return this.openId4VciHolderService.acceptCredentialOffer(this.agentContext, { + resolvedCredentialOffer, + acceptCredentialOfferOptions, + }) + } + + /** + * Accepts a credential offer using the authorization code flow. + * @param resolvedCredentialOffer Obtained through @see resolveCredentialOffer + * @param resolvedAuthorizationRequest Obtained through @see resolveIssuanceAuthorizationRequest + * @param code The authorization code obtained via the authorization request URI + * @param acceptCredentialOfferOptions + */ + public async acceptCredentialOfferUsingAuthorizationCode( + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, + resolvedAuthorizationRequest: OpenId4VciResolvedAuthorizationRequest, + code: string, + acceptCredentialOfferOptions: OpenId4VciAcceptCredentialOfferOptions + ) { + return this.openId4VciHolderService.acceptCredentialOffer(this.agentContext, { + resolvedCredentialOffer, + resolvedAuthorizationRequestWithCode: { ...resolvedAuthorizationRequest, code }, + acceptCredentialOfferOptions, + }) + } +} diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts new file mode 100644 index 0000000000..64cd6b9f08 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts @@ -0,0 +1,31 @@ +import type { DependencyManager, Module } from '@credo-ts/core' + +import { AgentConfig } from '@credo-ts/core' + +import { OpenId4VcHolderApi } from './OpenId4VcHolderApi' +import { OpenId4VciHolderService } from './OpenId4VciHolderService' +import { OpenId4VcSiopHolderService } from './OpenId4vcSiopHolderService' + +/** + * @public @module OpenId4VcHolderModule + * This module provides the functionality to assume the role of owner in relation to the OpenId4VC specification suite. + */ +export class OpenId4VcHolderModule implements Module { + public readonly api = OpenId4VcHolderApi + + /** + * Registers the dependencies of the question answer module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The '@credo-ts/openid4vc' Holder module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." + ) + + // Services + dependencyManager.registerSingleton(OpenId4VciHolderService) + dependencyManager.registerSingleton(OpenId4VcSiopHolderService) + } +} diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts new file mode 100644 index 0000000000..d9fe77d11f --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts @@ -0,0 +1,710 @@ +import type { + OpenId4VciAuthCodeFlowOptions, + OpenId4VciAcceptCredentialOfferOptions, + OpenId4VciProofOfPossessionRequirements, + OpenId4VciCredentialBindingResolver, + OpenId4VciResolvedCredentialOffer, + OpenId4VciResolvedAuthorizationRequest, + OpenId4VciResolvedAuthorizationRequestWithCode, + OpenId4VciSupportedCredentialFormats, +} from './OpenId4VciHolderServiceOptions' +import type { + OpenId4VciCredentialOfferPayload, + OpenId4VciCredentialSupported, + OpenId4VciCredentialSupportedWithId, + OpenId4VciIssuerMetadata, +} from '../shared' +import type { AgentContext, JwaSignatureAlgorithm, Key, JwkJson, VerifiableCredential } from '@credo-ts/core' +import type { + AccessTokenResponse, + CredentialResponse, + Jwt, + OpenIDResponse, + PushedAuthorizationResponse, + AuthorizationDetails, + AuthorizationDetailsJwtVcJson, +} from '@sphereon/oid4vci-common' + +import { + SdJwtVcApi, + getJwkFromJson, + DidsApi, + CredoError, + Hasher, + InjectionSymbols, + JsonEncoder, + JwsService, + Logger, + SignatureSuiteRegistry, + TypedArrayEncoder, + W3cCredentialService, + W3cJsonLdVerifiableCredential, + W3cJwtVerifiableCredential, + getJwkClassFromJwaSignatureAlgorithm, + getJwkFromKey, + getKeyFromVerificationMethod, + getSupportedVerificationMethodTypesFromKeyType, + inject, + injectable, + parseDid, +} from '@credo-ts/core' +import { + AccessTokenClient, + CredentialRequestClientBuilder, + ProofOfPossessionBuilder, + formPost, + OpenID4VCIClient, +} from '@sphereon/oid4vci-client' +import { CodeChallengeMethod, ResponseType, convertJsonToURI, JsonURIMode } from '@sphereon/oid4vci-common' + +import { OpenId4VciCredentialFormatProfile } from '../shared' +import { + getTypesFromCredentialSupported, + handleAuthorizationDetails, + getOfferedCredentials, +} from '../shared/issuerMetadataUtils' +import { getSupportedJwaSignatureAlgorithms } from '../shared/utils' + +import { openId4VciSupportedCredentialFormats } from './OpenId4VciHolderServiceOptions' + +@injectable() +export class OpenId4VciHolderService { + private logger: Logger + private w3cCredentialService: W3cCredentialService + private jwsService: JwsService + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + w3cCredentialService: W3cCredentialService, + jwsService: JwsService + ) { + this.w3cCredentialService = w3cCredentialService + this.jwsService = jwsService + this.logger = logger + } + + public async resolveCredentialOffer(credentialOffer: string): Promise { + const client = await OpenID4VCIClient.fromURI({ + uri: credentialOffer, + resolveOfferUri: true, + retrieveServerMetadata: true, + // This is a separate call, so we don't fetch it here, however it may be easier to just construct it here? + createAuthorizationRequestURL: false, + }) + + if (!client.credentialOffer?.credential_offer) { + throw new CredoError(`Could not resolve credential offer from '${credentialOffer}'`) + } + const credentialOfferPayload: OpenId4VciCredentialOfferPayload = client.credentialOffer.credential_offer + + const metadata = await client.retrieveServerMetadata() + if (!metadata.credentialIssuerMetadata) { + throw new CredoError(`Could not retrieve issuer metadata from '${metadata.issuer}'`) + } + const issuerMetadata = metadata.credentialIssuerMetadata as OpenId4VciIssuerMetadata + + this.logger.info('Fetched server metadata', { + issuer: metadata.issuer, + credentialEndpoint: metadata.credential_endpoint, + tokenEndpoint: metadata.token_endpoint, + }) + + this.logger.debug('Full server metadata', metadata) + + return { + metadata: { + ...metadata, + credentialIssuerMetadata: issuerMetadata, + }, + credentialOfferPayload, + offeredCredentials: getOfferedCredentials( + credentialOfferPayload.credentials, + issuerMetadata.credentials_supported + ), + version: client.version(), + } + } + + private getAuthDetailsFromOfferedCredential( + offeredCredential: OpenId4VciCredentialSupported, + authDetailsLocation: string | undefined + ): AuthorizationDetails | undefined { + const { format } = offeredCredential + const type = 'openid_credential' + + const locations = authDetailsLocation ? [authDetailsLocation] : undefined + if (format === OpenId4VciCredentialFormatProfile.JwtVcJson) { + return { type, format, types: offeredCredential.types, locations } satisfies AuthorizationDetailsJwtVcJson + } else if ( + format === OpenId4VciCredentialFormatProfile.LdpVc || + format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd + ) { + const credential_definition = { + '@context': offeredCredential['@context'], + credentialSubject: offeredCredential.credentialSubject, + types: offeredCredential.types, + } + + return { type, format, locations, credential_definition } + } else if (format === OpenId4VciCredentialFormatProfile.SdJwtVc) { + return { + type, + format, + locations, + vct: offeredCredential.vct, + claims: offeredCredential.claims, + } + } else { + throw new CredoError(`Cannot create authorization_details. Unsupported credential format '${format}'.`) + } + } + + public async resolveAuthorizationRequest( + agentContext: AgentContext, + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, + authCodeFlowOptions: OpenId4VciAuthCodeFlowOptions + ): Promise { + const { credentialOfferPayload, metadata, offeredCredentials } = resolvedCredentialOffer + const codeVerifier = ( + await Promise.allSettled([agentContext.wallet.generateNonce(), agentContext.wallet.generateNonce()]) + ).join() + const codeVerifierSha256 = Hasher.hash(codeVerifier, 'sha-256') + const codeChallenge = TypedArrayEncoder.toBase64URL(codeVerifierSha256) + + this.logger.debug('Converted code_verifier to code_challenge', { + codeVerifier: codeVerifier, + sha256: codeVerifierSha256.toString(), + base64Url: codeChallenge, + }) + + const authDetailsLocation = metadata.credentialIssuerMetadata.authorization_server + ? metadata.credentialIssuerMetadata.authorization_server + : undefined + const authDetails = offeredCredentials + .map((credential) => this.getAuthDetailsFromOfferedCredential(credential, authDetailsLocation)) + .filter((authDetail): authDetail is AuthorizationDetails => authDetail !== undefined) + + const { clientId, redirectUri, scope } = authCodeFlowOptions + const authorizationRequestUri = await createAuthorizationRequestUri({ + clientId, + codeChallenge, + redirectUri, + credentialOffer: credentialOfferPayload, + codeChallengeMethod: CodeChallengeMethod.S256, + // TODO: Read HAIP SdJwtVc's should always be requested via scopes + // TODO: should we now always use scopes instead of authDetails? or both???? + scope: scope ?? [], + authDetails, + metadata, + }) + + return { + ...authCodeFlowOptions, + codeVerifier, + authorizationRequestUri, + } + } + + public async acceptCredentialOffer( + agentContext: AgentContext, + options: { + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer + acceptCredentialOfferOptions: OpenId4VciAcceptCredentialOfferOptions + resolvedAuthorizationRequestWithCode?: OpenId4VciResolvedAuthorizationRequestWithCode + } + ) { + const { resolvedCredentialOffer, acceptCredentialOfferOptions, resolvedAuthorizationRequestWithCode } = options + const { credentialOfferPayload, metadata, version, offeredCredentials } = resolvedCredentialOffer + + const { credentialsToRequest, userPin, credentialBindingResolver, verifyCredentialStatus } = + acceptCredentialOfferOptions + + if (credentialsToRequest?.length === 0) { + this.logger.warn(`Accepting 0 credential offers. Returning`) + return [] + } + + this.logger.info(`Accepting the following credential offers '${credentialsToRequest}'`) + + const supportedJwaSignatureAlgorithms = getSupportedJwaSignatureAlgorithms(agentContext) + + const allowedProofOfPossessionSigAlgs = acceptCredentialOfferOptions.allowedProofOfPossessionSignatureAlgorithms + const possibleProofOfPossessionSigAlgs = allowedProofOfPossessionSigAlgs + ? allowedProofOfPossessionSigAlgs.filter((algorithm) => supportedJwaSignatureAlgorithms.includes(algorithm)) + : supportedJwaSignatureAlgorithms + + if (possibleProofOfPossessionSigAlgs.length === 0) { + throw new CredoError( + [ + `No possible proof of possession signature algorithm found.`, + `Signature algorithms supported by the Agent '${supportedJwaSignatureAlgorithms.join(', ')}'`, + `Allowed Signature algorithms '${allowedProofOfPossessionSigAlgs?.join(', ')}'`, + ].join('\n') + ) + } + + // acquire the access token + let accessTokenResponse: OpenIDResponse + + const accessTokenClient = new AccessTokenClient() + if (resolvedAuthorizationRequestWithCode) { + const { code, codeVerifier, redirectUri } = resolvedAuthorizationRequestWithCode + accessTokenResponse = await accessTokenClient.acquireAccessToken({ + metadata: metadata, + credentialOffer: { credential_offer: credentialOfferPayload }, + pin: userPin, + code, + codeVerifier, + redirectUri, + }) + } else { + accessTokenResponse = await accessTokenClient.acquireAccessToken({ + metadata: metadata, + credentialOffer: { credential_offer: credentialOfferPayload }, + pin: userPin, + }) + } + + if (!accessTokenResponse.successBody) { + throw new CredoError( + `could not acquire access token from '${metadata.issuer}'. ${accessTokenResponse.errorBody?.error}: ${accessTokenResponse.errorBody?.error_description}` + ) + } + + this.logger.debug('Requested OpenId4VCI Access Token.') + + const accessToken = accessTokenResponse.successBody + const receivedCredentials: Array = [] + let newCNonce: string | undefined + + const credentialsSupportedToRequest = + credentialsToRequest + ?.map((id) => offeredCredentials.find((credential) => credential.id === id)) + .filter((c, i): c is OpenId4VciCredentialSupportedWithId => { + if (!c) { + const offeredCredentialIds = offeredCredentials.map((c) => c.id).join(', ') + throw new CredoError( + `Credential to request '${credentialsToRequest[i]}' is not present in offered credentials. Offered credentials are ${offeredCredentialIds}` + ) + } + + return true + }) ?? offeredCredentials + + for (const offeredCredential of credentialsSupportedToRequest) { + // Get all options for the credential request (such as which kid to use, the signature algorithm, etc) + const { credentialBinding, signatureAlgorithm } = await this.getCredentialRequestOptions(agentContext, { + possibleProofOfPossessionSignatureAlgorithms: possibleProofOfPossessionSigAlgs, + offeredCredential, + credentialBindingResolver, + }) + + // Create the proof of possession + const proofOfPossessionBuilder = ProofOfPossessionBuilder.fromAccessTokenResponse({ + accessTokenResponse: accessToken, + callbacks: { signCallback: this.proofOfPossessionSignCallback(agentContext) }, + version, + }) + .withEndpointMetadata(metadata) + .withAlg(signatureAlgorithm) + + if (credentialBinding.method === 'did') { + proofOfPossessionBuilder.withClientId(parseDid(credentialBinding.didUrl).did).withKid(credentialBinding.didUrl) + } else if (credentialBinding.method === 'jwk') { + proofOfPossessionBuilder.withJWK(credentialBinding.jwk.toJson()) + } + + if (newCNonce) proofOfPossessionBuilder.withAccessTokenNonce(newCNonce) + + const proofOfPossession = await proofOfPossessionBuilder.build() + this.logger.debug('Generated JWS', proofOfPossession) + + // Acquire the credential + const credentialRequestBuilder = new CredentialRequestClientBuilder() + credentialRequestBuilder + .withVersion(version) + .withCredentialEndpoint(metadata.credential_endpoint) + .withTokenFromResponse(accessToken) + + const credentialRequestClient = credentialRequestBuilder.build() + const credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ + proofInput: proofOfPossession, + credentialTypes: getTypesFromCredentialSupported(offeredCredential), + format: offeredCredential.format, + }) + + newCNonce = credentialResponse.successBody?.c_nonce + + // Create credential, but we don't store it yet (only after the user has accepted the credential) + const credential = await this.handleCredentialResponse(agentContext, credentialResponse, { + verifyCredentialStatus: verifyCredentialStatus ?? false, + }) + + this.logger.debug('Full credential', credential) + receivedCredentials.push(credential) + } + + return receivedCredentials + } + + /** + * Get the options for the credential request. Internally this will resolve the proof of possession + * requirements, and based on that it will call the proofOfPossessionVerificationMethodResolver to + * allow the caller to select the correct verification method based on the requirements for the proof + * of possession. + */ + private async getCredentialRequestOptions( + agentContext: AgentContext, + options: { + credentialBindingResolver: OpenId4VciCredentialBindingResolver + possibleProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] + offeredCredential: OpenId4VciCredentialSupportedWithId + } + ) { + const { signatureAlgorithm, supportedDidMethods, supportsAllDidMethods, supportsJwk } = + this.getProofOfPossessionRequirements(agentContext, { + credentialToRequest: options.offeredCredential, + possibleProofOfPossessionSignatureAlgorithms: options.possibleProofOfPossessionSignatureAlgorithms, + }) + + const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) + if (!JwkClass) { + throw new CredoError(`Could not determine JWK key type of the JWA signature algorithm '${signatureAlgorithm}'`) + } + + const supportedVerificationMethods = getSupportedVerificationMethodTypesFromKeyType(JwkClass.keyType) + + const format = options.offeredCredential.format as OpenId4VciSupportedCredentialFormats + + // Now we need to determine how the credential will be bound to us + const credentialBinding = await options.credentialBindingResolver({ + credentialFormat: format, + signatureAlgorithm, + supportedVerificationMethods, + keyType: JwkClass.keyType, + supportedCredentialId: options.offeredCredential.id, + supportsAllDidMethods, + supportedDidMethods, + supportsJwk, + }) + + // Make sure the issuer of proof of possession is valid according to openid issuer metadata + if ( + credentialBinding.method === 'did' && + !supportsAllDidMethods && + // If supportedDidMethods is undefined, it means the issuer didn't include the binding methods in the metadata + // The user can still select a verification method, but we can't validate it + supportedDidMethods !== undefined && + !supportedDidMethods.find((supportedDidMethod) => credentialBinding.didUrl.startsWith(supportedDidMethod)) + ) { + const { method } = parseDid(credentialBinding.didUrl) + const supportedDidMethodsString = supportedDidMethods.join(', ') + throw new CredoError( + `Resolved credential binding for proof of possession uses did method '${method}', but issuer only supports '${supportedDidMethodsString}'` + ) + } else if (credentialBinding.method === 'jwk' && !supportsJwk) { + throw new CredoError( + `Resolved credential binding for proof of possession uses jwk, but openid issuer does not support 'jwk' cryptographic binding method` + ) + } + + // FIXME: we don't have the verification method here + // Make sure the verification method uses a supported verification method type + // if (!supportedVerificationMethods.includes(verificationMethod.type)) { + // const supportedVerificationMethodsString = supportedVerificationMethods.join(', ') + // throw new CredoError( + // `Verification method uses verification method type '${verificationMethod.type}', but only '${supportedVerificationMethodsString}' verification methods are supported for key type '${JwkClass.keyType}'` + // ) + // } + + return { credentialBinding, signatureAlgorithm } + } + + /** + * Get the requirements for creating the proof of possession. Based on the allowed + * credential formats, the allowed proof of possession signature algorithms, and the + * credential type, this method will select the best credential format and signature + * algorithm to use, based on the order of preference. + */ + private getProofOfPossessionRequirements( + agentContext: AgentContext, + options: { + credentialToRequest: OpenId4VciCredentialSupportedWithId + possibleProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] + } + ): OpenId4VciProofOfPossessionRequirements { + const { credentialToRequest } = options + + if ( + !openId4VciSupportedCredentialFormats.includes(credentialToRequest.format as OpenId4VciSupportedCredentialFormats) + ) { + throw new CredoError( + [ + `Requested credential with format '${credentialToRequest.format}',`, + `for the credential with id '${credentialToRequest.id},`, + `but the wallet only supports the following formats '${openId4VciSupportedCredentialFormats.join(', ')}'`, + ].join('\n') + ) + } + + // For each of the supported algs, find the key types, then find the proof types + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + + let signatureAlgorithm: JwaSignatureAlgorithm | undefined + + const issuerSupportedCryptographicSuites = credentialToRequest.cryptographic_suites_supported + const issuerSupportedBindingMethods = credentialToRequest.cryptographic_binding_methods_supported + + // If undefined, it means the issuer didn't include the cryptographic suites in the metadata + // We just guess that the first one is supported + if (issuerSupportedCryptographicSuites === undefined) { + signatureAlgorithm = options.possibleProofOfPossessionSignatureAlgorithms[0] + } else { + switch (credentialToRequest.format) { + case OpenId4VciCredentialFormatProfile.JwtVcJson: + case OpenId4VciCredentialFormatProfile.JwtVcJsonLd: + case OpenId4VciCredentialFormatProfile.SdJwtVc: + signatureAlgorithm = options.possibleProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => + issuerSupportedCryptographicSuites.includes(signatureAlgorithm) + ) + break + case OpenId4VciCredentialFormatProfile.LdpVc: + signatureAlgorithm = options.possibleProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => { + const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) + if (!JwkClass) return false + + const matchingSuite = signatureSuiteRegistry.getAllByKeyType(JwkClass.keyType) + if (matchingSuite.length === 0) return false + + return issuerSupportedCryptographicSuites.includes(matchingSuite[0].proofType) + }) + break + default: + throw new CredoError(`Unsupported credential format.`) + } + } + + if (!signatureAlgorithm) { + throw new CredoError( + `Could not establish signature algorithm for format ${credentialToRequest.format} and id ${credentialToRequest.id}` + ) + } + + const supportsAllDidMethods = issuerSupportedBindingMethods?.includes('did') ?? false + const supportedDidMethods = issuerSupportedBindingMethods?.filter((method) => method.startsWith('did:')) + const supportsJwk = issuerSupportedBindingMethods?.includes('jwk') ?? false + + return { + signatureAlgorithm, + supportedDidMethods, + supportsAllDidMethods, + supportsJwk, + } + } + + private async handleCredentialResponse( + agentContext: AgentContext, + credentialResponse: OpenIDResponse, + options: { verifyCredentialStatus: boolean } + ): Promise { + const { verifyCredentialStatus } = options + this.logger.debug('Credential request response', credentialResponse) + + if (!credentialResponse.successBody || !credentialResponse.successBody.credential) { + throw new CredoError( + `Did not receive a successful credential response. ${credentialResponse.errorBody?.error}: ${credentialResponse.errorBody?.error_description}` + ) + } + + const format = credentialResponse.successBody.format + if (format === OpenId4VciCredentialFormatProfile.SdJwtVc) { + if (typeof credentialResponse.successBody.credential !== 'string') + throw new CredoError( + `Received a credential of format ${ + OpenId4VciCredentialFormatProfile.SdJwtVc + }, but the credential is not a string. ${JSON.stringify(credentialResponse.successBody.credential)}` + ) + + const sdJwtVcApi = agentContext.dependencyManager.resolve(SdJwtVcApi) + const verificationResult = await sdJwtVcApi.verify({ + compactSdJwtVc: credentialResponse.successBody.credential, + }) + + if (!verificationResult.isValid) { + agentContext.config.logger.error('Failed to validate credential', { verificationResult }) + throw new CredoError(`Failed to validate sd-jwt-vc credential. Results = ${JSON.stringify(verificationResult)}`) + } + + return verificationResult.sdJwtVc + } else if ( + format === OpenId4VciCredentialFormatProfile.JwtVcJson || + format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd + ) { + const credential = W3cJwtVerifiableCredential.fromSerializedJwt( + credentialResponse.successBody.credential as string + ) + const result = await this.w3cCredentialService.verifyCredential(agentContext, { + credential, + verifyCredentialStatus, + }) + if (!result.isValid) { + agentContext.config.logger.error('Failed to validate credential', { result }) + throw new CredoError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) + } + + return credential + } else if (format === OpenId4VciCredentialFormatProfile.LdpVc) { + const credential = W3cJsonLdVerifiableCredential.fromJson( + credentialResponse.successBody.credential as Record + ) + const result = await this.w3cCredentialService.verifyCredential(agentContext, { + credential, + verifyCredentialStatus, + }) + if (!result.isValid) { + agentContext.config.logger.error('Failed to validate credential', { result }) + throw new CredoError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) + } + + return credential + } + + throw new CredoError(`Unsupported credential format ${credentialResponse.successBody.format}`) + } + + private proofOfPossessionSignCallback(agentContext: AgentContext) { + return async (jwt: Jwt, kid?: string) => { + if (!jwt.header) throw new CredoError('No header present on JWT') + if (!jwt.payload) throw new CredoError('No payload present on JWT') + if (kid && jwt.header.jwk) { + throw new CredoError('Both KID and JWK are present in the callback. Only one can be present') + } + + let key: Key + + if (kid) { + if (!kid.startsWith('did:')) { + throw new CredoError(`kid '${kid}' is not a DID. Only dids are supported for kid`) + } else if (!kid.includes('#')) { + throw new CredoError( + `kid '${kid}' does not contain a fragment. kid MUST point to a specific key in the did document.` + ) + } + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(kid) + const verificationMethod = didDocument.dereferenceKey(kid, ['authentication']) + + key = getKeyFromVerificationMethod(verificationMethod) + } else if (jwt.header.jwk) { + key = getJwkFromJson(jwt.header.jwk as JwkJson).key + } else { + throw new CredoError('No KID or JWK is present in the callback') + } + + const jwk = getJwkFromKey(key) + if (!jwk.supportsSignatureAlgorithm(jwt.header.alg)) { + throw new CredoError(`key type '${jwk.keyType}', does not support the JWS signature alg '${jwt.header.alg}'`) + } + + // We don't support these properties, remove them, so we can pass all other header properties to the JWS service + if (jwt.header.x5c) throw new CredoError('x5c is not supported') + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { x5c: _x5c, ...supportedHeaderOptions } = jwt.header + + const jws = await this.jwsService.createJwsCompact(agentContext, { + key, + payload: JsonEncoder.toBuffer(jwt.payload), + protectedHeaderOptions: { + ...supportedHeaderOptions, + // only pass jwk if it was present in the header + jwk: jwt.header.jwk ? jwk : undefined, + }, + }) + + return jws + } + } +} + +// NOTE: this is also defined in the sphereon lib, but we use +// this custom method to get PAR working and because we don't +// use the oid4vci client in sphereon's lib +// Once PAR is supported in the sphereon lib, we should to try remove this +// and use the one from the sphereon lib +async function createAuthorizationRequestUri(options: { + credentialOffer: OpenId4VciCredentialOfferPayload + metadata: OpenId4VciResolvedCredentialOffer['metadata'] + clientId: string + codeChallenge: string + codeChallengeMethod: CodeChallengeMethod + authDetails?: AuthorizationDetails | AuthorizationDetails[] + redirectUri: string + scope?: string[] +}) { + const { scope, authDetails, metadata, clientId, codeChallenge, codeChallengeMethod, redirectUri } = options + let nonEmptyScope = !scope || scope.length === 0 ? undefined : scope + const nonEmptyAuthDetails = !authDetails || authDetails.length === 0 ? undefined : authDetails + + // Scope and authorization_details can be used in the same authorization request + // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param + if (!nonEmptyScope && !nonEmptyAuthDetails) { + throw new CredoError(`Please provide a 'scope' or 'authDetails' via the options.`) + } + + // Authorization servers supporting PAR SHOULD include the URL of their pushed authorization request endpoint in their authorization server metadata document + // Note that the presence of pushed_authorization_request_endpoint is sufficient for a client to determine that it may use the PAR flow. + const parEndpoint = metadata.credentialIssuerMetadata.pushed_authorization_request_endpoint + + const authorizationEndpoint = metadata.credentialIssuerMetadata?.authorization_endpoint + + if (!authorizationEndpoint && !parEndpoint) { + throw new CredoError( + "Server metadata does not contain an 'authorization_endpoint' which is required for the 'Authorization Code Flow'" + ) + } + + // add 'openid' scope if not present + if (nonEmptyScope && !nonEmptyScope?.includes('openid')) { + nonEmptyScope = ['openid', ...nonEmptyScope] + } + + const queryObj: Record = { + client_id: clientId, + response_type: ResponseType.AUTH_CODE, + code_challenge_method: codeChallengeMethod, + code_challenge: codeChallenge, + redirect_uri: redirectUri, + } + + if (nonEmptyScope) queryObj['scope'] = nonEmptyScope.join(' ') + + if (nonEmptyAuthDetails) + queryObj['authorization_details'] = JSON.stringify(handleAuthorizationDetails(nonEmptyAuthDetails, metadata)) + + const issuerState = options.credentialOffer.grants?.authorization_code?.issuer_state + if (issuerState) queryObj['issuer_state'] = issuerState + + if (parEndpoint) { + const body = new URLSearchParams(queryObj) + const response = await formPost(parEndpoint, body) + if (!response.successBody) { + throw new CredoError(`Could not acquire the authorization request uri from '${parEndpoint}'`) + } + return convertJsonToURI( + { request_uri: response.successBody.request_uri, client_id: clientId, response_type: ResponseType.AUTH_CODE }, + { + baseUrl: authorizationEndpoint, + uriTypeProperties: ['request_uri', 'client_id', 'response_type'], + mode: JsonURIMode.X_FORM_WWW_URLENCODED, + } + ) + } else { + return convertJsonToURI(queryObj, { + baseUrl: authorizationEndpoint, + uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details', 'issuer_state'], + mode: JsonURIMode.X_FORM_WWW_URLENCODED, + }) + } +} diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts new file mode 100644 index 0000000000..129040fd8b --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts @@ -0,0 +1,191 @@ +import type { + OpenId4VcCredentialHolderBinding, + OpenId4VciCredentialOfferPayload, + OpenId4VciCredentialSupportedWithId, + OpenId4VciIssuerMetadata, +} from '../shared' +import type { JwaSignatureAlgorithm, KeyType } from '@credo-ts/core' +import type { AuthorizationServerMetadata, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' + +import { OpenId4VciCredentialFormatProfile } from '../shared/models/OpenId4VciCredentialFormatProfile' + +export type OpenId4VciSupportedCredentialFormats = + | OpenId4VciCredentialFormatProfile.JwtVcJson + | OpenId4VciCredentialFormatProfile.JwtVcJsonLd + | OpenId4VciCredentialFormatProfile.SdJwtVc + | OpenId4VciCredentialFormatProfile.LdpVc + +export const openId4VciSupportedCredentialFormats: OpenId4VciSupportedCredentialFormats[] = [ + OpenId4VciCredentialFormatProfile.JwtVcJson, + OpenId4VciCredentialFormatProfile.JwtVcJsonLd, + OpenId4VciCredentialFormatProfile.SdJwtVc, + OpenId4VciCredentialFormatProfile.LdpVc, +] + +export interface OpenId4VciResolvedCredentialOffer { + metadata: EndpointMetadataResult & { + credentialIssuerMetadata: Partial & OpenId4VciIssuerMetadata + } + credentialOfferPayload: OpenId4VciCredentialOfferPayload + offeredCredentials: OpenId4VciCredentialSupportedWithId[] + version: OpenId4VCIVersion +} + +export interface OpenId4VciResolvedAuthorizationRequest extends OpenId4VciAuthCodeFlowOptions { + codeVerifier: string + authorizationRequestUri: string +} + +export interface OpenId4VciResolvedAuthorizationRequestWithCode extends OpenId4VciResolvedAuthorizationRequest { + code: string +} + +/** + * Options that are used to accept a credential offer for both the pre-authorized code flow and authorization code flow. + */ +export interface OpenId4VciAcceptCredentialOfferOptions { + /** + * String value containing a user PIN. This value MUST be present if user_pin_required was set to true in the Credential Offer. + * This parameter MUST only be used, if the grant_type is urn:ietf:params:oauth:grant-type:pre-authorized_code. + */ + userPin?: string + + /** + * This is the list of credentials that will be requested from the issuer. + * Should be a list of ids of the credentials that are included in the credential offer. + * If not provided all offered credentials will be requested. + */ + credentialsToRequest?: string[] + + verifyCredentialStatus?: boolean + + /** + * A list of allowed proof of possession signature algorithms in order of preference. + * + * Note that the signature algorithms must be supported by the wallet implementation. + * Signature algorithms that are not supported by the wallet will be ignored. + * + * The proof of possession (pop) signature algorithm is used in the credential request + * to bind the credential to a did. In most cases the JWA signature algorithm + * that is used in the pop will determine the cryptographic suite that is used + * for signing the credential, but this not a requirement for the spec. E.g. if the + * pop uses EdDsa, the credential will most commonly also use EdDsa, or Ed25519Signature2018/2020. + */ + allowedProofOfPossessionSignatureAlgorithms?: JwaSignatureAlgorithm[] + + /** + * A function that should resolve key material for binding the to-be-issued credential + * to the holder based on the options passed. This key material will be used for signing + * the proof of possession included in the credential request. + * + * This method will be called once for each of the credentials that are included + * in the credential offer. + * + * Based on the credential format, JWA signature algorithm, verification method types + * and binding methods (did methods, jwk), the resolver must return an object + * conformant to the `CredentialHolderBinding` interface, which will be used + * for the proof of possession signature. + */ + credentialBindingResolver: OpenId4VciCredentialBindingResolver +} + +/** + * Options that are used for the authorization code flow. + * Extends the pre-authorized code flow options. + */ +export interface OpenId4VciAuthCodeFlowOptions { + clientId: string + redirectUri: string + scope?: string[] +} + +export interface OpenId4VciCredentialBindingOptions { + /** + * The credential format that will be requested from the issuer. + * E.g. `jwt_vc` or `ldp_vc`. + */ + credentialFormat: OpenId4VciSupportedCredentialFormats + + /** + * The JWA Signature Algorithm that will be used in the proof of possession. + * This is based on the `allowedProofOfPossessionSignatureAlgorithms` passed + * to the request credential method, and the supported signature algorithms. + */ + signatureAlgorithm: JwaSignatureAlgorithm + + /** + * This is a list of verification methods types that are supported + * for creating the proof of possession signature. The returned + * verification method type must be of one of these types. + */ + supportedVerificationMethods: string[] + + /** + * The key type that will be used to create the proof of possession signature. + * This is related to the verification method and the signature algorithm, and + * is added for convenience. + */ + keyType: KeyType + + /** + * The credential type that will be requested from the issuer. This is + * based on the credential types that are included the credential offer. + * + * If the offered credential is an inline credential offer, the value + * will be `undefined`. + */ + supportedCredentialId?: string + + /** + * Whether the issuer supports the `did` cryptographic binding method, + * indicating they support all did methods. In most cases, they do not + * support all did methods, and it means we have to make an assumption + * about the did methods they support. + * + * If this value is `false`, the `supportedDidMethods` property will + * contain a list of supported did methods. + */ + supportsAllDidMethods: boolean + + /** + * A list of supported did methods. This is only used if the `supportsAllDidMethods` + * property is `false`. When this array is populated, the returned verification method + * MUST be based on one of these did methods. + * + * The did methods are returned in the format `did:`, e.g. `did:web`. + * + * The value is undefined in the case the supported did methods could not be extracted. + * This is the case when an inline credential was used, or when the issuer didn't include + * the supported did methods in the issuer metadata. + * + * NOTE: an empty array (no did methods supported) has a different meaning from the value + * being undefined (the supported did methods could not be extracted). If `supportsAllDidMethods` + * is true, the value of this property MUST be ignored. + */ + supportedDidMethods?: string[] + + /** + * Whether the issuer supports the `jwk` cryptographic binding method, + * indicating they support proof of possession signatures bound to a jwk. + */ + supportsJwk: boolean +} + +/** + * The proof of possession verification method resolver is a function that can be passed by the + * user of the framework and allows them to determine which verification method should be used + * for the proof of possession signature. + */ +export type OpenId4VciCredentialBindingResolver = ( + options: OpenId4VciCredentialBindingOptions +) => Promise | OpenId4VcCredentialHolderBinding + +/** + * @internal + */ +export interface OpenId4VciProofOfPossessionRequirements { + signatureAlgorithm: JwaSignatureAlgorithm + supportedDidMethods?: string[] + supportsAllDidMethods: boolean + supportsJwk: boolean +} diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts new file mode 100644 index 0000000000..28d6511449 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -0,0 +1,309 @@ +import type { + OpenId4VcSiopAcceptAuthorizationRequestOptions, + OpenId4VcSiopResolvedAuthorizationRequest, +} from './OpenId4vcSiopHolderServiceOptions' +import type { OpenId4VcJwtIssuer } from '../shared' +import type { AgentContext, VerifiablePresentation } from '@credo-ts/core' +import type { VerifiedAuthorizationRequest, PresentationExchangeResponseOpts } from '@sphereon/did-auth-siop' + +import { + Hasher, + W3cJwtVerifiablePresentation, + parseDid, + CredoError, + injectable, + W3cJsonLdVerifiablePresentation, + asArray, + DifPresentationExchangeService, + DifPresentationExchangeSubmissionLocation, + DidsApi, +} from '@credo-ts/core' +import { + CheckLinkedDomain, + OP, + ResponseIss, + ResponseMode, + SupportedVersion, + VPTokenLocation, + VerificationMode, +} from '@sphereon/did-auth-siop' + +import { getSphereonVerifiablePresentation } from '../shared/transform' +import { getSphereonDidResolver, getSphereonSuppliedSignatureFromJwtIssuer } from '../shared/utils' + +@injectable() +export class OpenId4VcSiopHolderService { + public constructor(private presentationExchangeService: DifPresentationExchangeService) {} + + public async resolveAuthorizationRequest( + agentContext: AgentContext, + requestJwtOrUri: string + ): Promise { + const openidProvider = await this.getOpenIdProvider(agentContext, {}) + + // parsing happens automatically in verifyAuthorizationRequest + const verifiedAuthorizationRequest = await openidProvider.verifyAuthorizationRequest(requestJwtOrUri, { + verification: { + // FIXME: we want custom verification, but not supported currently + // https://github.com/Sphereon-Opensource/SIOP-OID4VP/issues/55 + mode: VerificationMode.INTERNAL, + resolveOpts: { resolver: getSphereonDidResolver(agentContext), noUniversalResolverFallback: true }, + }, + }) + + agentContext.config.logger.debug( + `verified SIOP Authorization Request for issuer '${verifiedAuthorizationRequest.issuer}'` + ) + agentContext.config.logger.debug(`requestJwtOrUri '${requestJwtOrUri}'`) + + if ( + verifiedAuthorizationRequest.presentationDefinitions && + verifiedAuthorizationRequest.presentationDefinitions.length > 1 + ) { + throw new CredoError('Only a single presentation definition is supported.') + } + + const presentationDefinition = verifiedAuthorizationRequest.presentationDefinitions?.[0]?.definition + + return { + authorizationRequest: verifiedAuthorizationRequest, + + // Parameters related to DIF Presentation Exchange + presentationExchange: presentationDefinition + ? { + definition: presentationDefinition, + credentialsForRequest: await this.presentationExchangeService.getCredentialsForRequest( + agentContext, + presentationDefinition + ), + } + : undefined, + } + } + + public async acceptAuthorizationRequest( + agentContext: AgentContext, + options: OpenId4VcSiopAcceptAuthorizationRequestOptions + ) { + const { authorizationRequest, presentationExchange } = options + let openIdTokenIssuer = options.openIdTokenIssuer + let presentationExchangeOptions: PresentationExchangeResponseOpts | undefined = undefined + + // Handle presentation exchange part + if (authorizationRequest.presentationDefinitions && authorizationRequest.presentationDefinitions.length > 0) { + if (!presentationExchange) { + throw new CredoError( + 'Authorization request included presentation definition. `presentationExchange` MUST be supplied to accept authorization requests.' + ) + } + + const nonce = await authorizationRequest.authorizationRequest.getMergedProperty('nonce') + if (!nonce) { + throw new CredoError("Unable to extract 'nonce' from authorization request") + } + + const clientId = await authorizationRequest.authorizationRequest.getMergedProperty('client_id') + if (!clientId) { + throw new CredoError("Unable to extract 'client_id' from authorization request") + } + + const { verifiablePresentations, presentationSubmission } = + await this.presentationExchangeService.createPresentation(agentContext, { + credentialsForInputDescriptor: presentationExchange.credentials, + presentationDefinition: authorizationRequest.presentationDefinitions[0].definition, + challenge: nonce, + domain: clientId, + presentationSubmissionLocation: DifPresentationExchangeSubmissionLocation.EXTERNAL, + }) + + presentationExchangeOptions = { + verifiablePresentations: verifiablePresentations.map((vp) => getSphereonVerifiablePresentation(vp)), + presentationSubmission, + vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE, + } + + if (!openIdTokenIssuer) { + openIdTokenIssuer = this.getOpenIdTokenIssuerFromVerifiablePresentation(verifiablePresentations[0]) + } + } else if (options.presentationExchange) { + throw new CredoError( + '`presentationExchange` was supplied, but no presentation definition was found in the presentation request.' + ) + } + + if (!openIdTokenIssuer) { + throw new CredoError( + 'Unable to create authorization response. openIdTokenIssuer MUST be supplied when no presentation is active.' + ) + } + + this.assertValidTokenIssuer(authorizationRequest, openIdTokenIssuer) + const openidProvider = await this.getOpenIdProvider(agentContext, { + openIdTokenIssuer, + }) + + const suppliedSignature = await getSphereonSuppliedSignatureFromJwtIssuer(agentContext, openIdTokenIssuer) + const authorizationResponseWithCorrelationId = await openidProvider.createAuthorizationResponse( + authorizationRequest, + { + signature: suppliedSignature, + issuer: suppliedSignature.did, + verification: { + resolveOpts: { resolver: getSphereonDidResolver(agentContext), noUniversalResolverFallback: true }, + mode: VerificationMode.INTERNAL, + }, + presentationExchange: presentationExchangeOptions, + // https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-aud-of-a-request-object + audience: authorizationRequest.authorizationRequestPayload.client_id, + } + ) + + const response = await openidProvider.submitAuthorizationResponse(authorizationResponseWithCorrelationId) + let responseDetails: string | Record | undefined = undefined + try { + responseDetails = await response.text() + if (responseDetails.includes('{')) { + responseDetails = JSON.parse(responseDetails) + } + } catch (error) { + // no-op + } + + return { + serverResponse: { + status: response.status, + body: responseDetails, + }, + submittedResponse: authorizationResponseWithCorrelationId.response.payload, + } + } + + private async getOpenIdProvider( + agentContext: AgentContext, + options: { + openIdTokenIssuer?: OpenId4VcJwtIssuer + } = {} + ) { + const { openIdTokenIssuer } = options + + const builder = OP.builder() + .withExpiresIn(6000) + .withIssuer(ResponseIss.SELF_ISSUED_V2) + .withResponseMode(ResponseMode.POST) + .withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18]) + .withCustomResolver(getSphereonDidResolver(agentContext)) + .withCheckLinkedDomain(CheckLinkedDomain.NEVER) + .withHasher(Hasher.hash) + + if (openIdTokenIssuer) { + const suppliedSignature = await getSphereonSuppliedSignatureFromJwtIssuer(agentContext, openIdTokenIssuer) + builder.withSignature(suppliedSignature) + } + + // Add did methods + const supportedDidMethods = agentContext.dependencyManager.resolve(DidsApi).supportedResolverMethods + for (const supportedDidMethod of supportedDidMethods) { + builder.addDidMethod(supportedDidMethod) + } + + const openidProvider = builder.build() + + return openidProvider + } + + private getOpenIdTokenIssuerFromVerifiablePresentation( + verifiablePresentation: VerifiablePresentation + ): OpenId4VcJwtIssuer { + let openIdTokenIssuer: OpenId4VcJwtIssuer + + if (verifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { + const [firstProof] = asArray(verifiablePresentation.proof) + if (!firstProof) throw new CredoError('Verifiable presentation does not contain a proof') + + if (!firstProof.verificationMethod.startsWith('did:')) { + throw new CredoError( + 'Verifiable presentation proof verificationMethod is not a did. Unable to extract openIdTokenIssuer from verifiable presentation' + ) + } + + openIdTokenIssuer = { + method: 'did', + didUrl: firstProof.verificationMethod, + } + } else if (verifiablePresentation instanceof W3cJwtVerifiablePresentation) { + const kid = verifiablePresentation.jwt.header.kid + + if (!kid) throw new CredoError('Verifiable Presentation does not contain a kid in the jwt header') + if (kid.startsWith('#') && verifiablePresentation.presentation.holderId) { + openIdTokenIssuer = { + didUrl: `${verifiablePresentation.presentation.holderId}${kid}`, + method: 'did', + } + } else if (kid.startsWith('did:')) { + openIdTokenIssuer = { + didUrl: kid, + method: 'did', + } + } else { + throw new CredoError( + "JWT W3C Verifiable presentation does not include did in JWT header 'kid'. Unable to extract openIdTokenIssuer from verifiable presentation" + ) + } + } else { + const cnf = verifiablePresentation.payload.cnf + // FIXME: SD-JWT VC should have better payload typing, so this doesn't become so ugly + if ( + !cnf || + typeof cnf !== 'object' || + !('kid' in cnf) || + typeof cnf.kid !== 'string' || + !cnf.kid.startsWith('did:') || + !cnf.kid.includes('#') + ) { + throw new CredoError( + "SD-JWT Verifiable presentation has no 'cnf' claim or does not include 'cnf' claim where 'kid' is a didUrl pointing to a key. Unable to extract openIdTokenIssuer from verifiable presentation" + ) + } + + openIdTokenIssuer = { + didUrl: cnf.kid, + method: 'did', + } + } + + return openIdTokenIssuer + } + + private assertValidTokenIssuer( + authorizationRequest: VerifiedAuthorizationRequest, + openIdTokenIssuer: OpenId4VcJwtIssuer + ) { + // TODO: jwk thumbprint support + const subjectSyntaxTypesSupported = authorizationRequest.registrationMetadataPayload.subject_syntax_types_supported + if (!subjectSyntaxTypesSupported) { + throw new CredoError( + 'subject_syntax_types_supported is not supplied in the registration metadata. subject_syntax_types is REQUIRED.' + ) + } + + let allowedSubjectSyntaxTypes: string[] = [] + if (openIdTokenIssuer.method === 'did') { + const parsedDid = parseDid(openIdTokenIssuer.didUrl) + + // Either did: or did (for all did methods) is allowed + allowedSubjectSyntaxTypes = [`did:${parsedDid.method}`, 'did'] + } else { + throw new CredoError("Only 'did' is supported as openIdTokenIssuer at the moment") + } + + // At least one of the allowed subject syntax types must be supported by the RP + if (!allowedSubjectSyntaxTypes.some((allowed) => subjectSyntaxTypesSupported.includes(allowed))) { + throw new CredoError( + [ + 'The provided openIdTokenIssuer is not supported by the relying party.', + `Supported subject syntax types: '${subjectSyntaxTypesSupported.join(', ')}'`, + ].join('\n') + ) + } + } +} diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts new file mode 100644 index 0000000000..c59a9dd53f --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts @@ -0,0 +1,58 @@ +import type { + OpenId4VcJwtIssuer, + OpenId4VcSiopVerifiedAuthorizationRequest, + OpenId4VcSiopAuthorizationResponsePayload, +} from '../shared' +import type { + DifPexCredentialsForRequest, + DifPexInputDescriptorToCredentials, + DifPresentationExchangeDefinition, +} from '@credo-ts/core' + +export interface OpenId4VcSiopResolvedAuthorizationRequest { + /** + * Parameters related to DIF Presentation Exchange. Only defined when + * the request included + */ + presentationExchange?: { + definition: DifPresentationExchangeDefinition + credentialsForRequest: DifPexCredentialsForRequest + } + + /** + * The verified authorization request. + */ + authorizationRequest: OpenId4VcSiopVerifiedAuthorizationRequest +} + +export interface OpenId4VcSiopAcceptAuthorizationRequestOptions { + /** + * Parameters related to DIF Presentation Exchange. MUST be present when the resolved + * authorization request included a `presentationExchange` parameter. + */ + presentationExchange?: { + credentials: DifPexInputDescriptorToCredentials + } + + /** + * The issuer of the ID Token. + * + * REQUIRED when presentation exchange is not used. + * + * In case presentation exchange is used, and `openIdTokenIssuer` is not provided, the issuer of the ID Token + * will be extracted from the signer of the first verifiable presentation. + */ + openIdTokenIssuer?: OpenId4VcJwtIssuer + + /** + * The verified authorization request. + */ + authorizationRequest: OpenId4VcSiopVerifiedAuthorizationRequest +} + +// FIXME: rethink properties +export interface OpenId4VcSiopAuthorizationResponseSubmission { + ok: boolean + status: number + submittedResponse: OpenId4VcSiopAuthorizationResponsePayload +} diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/OpenId4VcHolderModule.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/OpenId4VcHolderModule.test.ts new file mode 100644 index 0000000000..cd56d78cf0 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/OpenId4VcHolderModule.test.ts @@ -0,0 +1,23 @@ +import type { DependencyManager } from '@credo-ts/core' + +import { OpenId4VcHolderModule } from '../OpenId4VcHolderModule' +import { OpenId4VciHolderService } from '../OpenId4VciHolderService' +import { OpenId4VcSiopHolderService } from '../OpenId4vcSiopHolderService' + +const dependencyManager = { + registerInstance: jest.fn(), + registerSingleton: jest.fn(), + registerContextScoped: jest.fn(), + resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), +} as unknown as DependencyManager + +describe('OpenId4VcHolderModule', () => { + test('registers dependencies on the dependency manager', () => { + const openId4VcClientModule = new OpenId4VcHolderModule() + openId4VcClientModule.register(dependencyManager) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VciHolderService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcSiopHolderService) + }) +}) diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts new file mode 100644 index 0000000000..ea84c90eb3 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts @@ -0,0 +1,342 @@ +export const matrrLaunchpadDraft11JwtVcJson = { + credentialOffer: + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%22613ecbbb-0a4c-4041-bb78-c64943139d5f%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22Jd6TUmLJct1DNyJpKKmt0i85scznBoJrEe_y_SlMW0j%22%7D%7D%7D', + getMetadataResponse: { + issuer: 'https://launchpad.vii.electron.mattrlabs.io', + authorization_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/authorize', + token_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/token', + jwks_uri: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/jwks', + token_endpoint_auth_methods_supported: [ + 'none', + 'client_secret_basic', + 'client_secret_jwt', + 'client_secret_post', + 'private_key_jwt', + ], + code_challenge_methods_supported: ['S256'], + grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], + response_modes_supported: ['form_post', 'fragment', 'query'], + response_types_supported: ['code id_token', 'code', 'id_token', 'none'], + scopes_supported: ['OpenBadgeCredential', 'Passport'], + token_endpoint_auth_signing_alg_values_supported: ['HS256', 'RS256', 'PS256', 'ES256', 'EdDSA'], + credential_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/credential', + credentials_supported: [ + { + id: 'd2662472-891c-413d-b3c6-e2f0109001c5', + format: 'ldp_vc', + types: ['VerifiableCredential', 'OpenBadgeCredential'], + cryptographic_binding_methods_supported: ['did:key'], + cryptographic_suites_supported: ['Ed25519Signature2018'], + display: [ + { + name: 'Example University Degree', + description: 'JFF Plugfest 3 OpenBadge Credential', + background_color: '#464c49', + logo: {}, + }, + ], + }, + { + id: 'b4c4cdf5-ccc9-4945-8c19-9334558653b2', + format: 'ldp_vc', + types: ['VerifiableCredential', 'Passport'], + cryptographic_binding_methods_supported: ['did:key'], + cryptographic_suites_supported: ['Ed25519Signature2018'], + display: [ + { + name: 'Passport', + description: 'Passport of the Kingdom of Kākāpō', + background_color: '#171717', + logo: { url: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/logo.svg' }, + }, + ], + }, + { + id: '613ecbbb-0a4c-4041-bb78-c64943139d5f', + format: 'jwt_vc_json', + types: ['VerifiableCredential', 'OpenBadgeCredential'], + cryptographic_binding_methods_supported: ['did:key'], + cryptographic_suites_supported: ['EdDSA'], + display: [ + { + name: 'Example University Degree', + description: 'JFF Plugfest 3 OpenBadge Credential', + background_color: '#464c49', + logo: {}, + }, + ], + }, + { + id: 'c3db5513-ae2b-46e9-8a0d-fbfd0ce52b6a', + format: 'jwt_vc_json', + types: ['VerifiableCredential', 'Passport'], + cryptographic_binding_methods_supported: ['did:key'], + cryptographic_suites_supported: ['EdDSA'], + display: [ + { + name: 'Passport', + description: 'Passport of the Kingdom of Kākāpō', + background_color: '#171717', + logo: { url: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/logo.svg' }, + }, + ], + }, + ], + }, + + wellKnownDid: { + id: 'did:web:launchpad.vii.electron.mattrlabs.io', + '@context': 'https://w3.org/ns/did/v1', + // Uses deprecated publicKey, but the did:web resolver transforms + // it to the newer verificationMethod + publicKey: [ + { + id: 'did:web:launchpad.vii.electron.mattrlabs.io#Ck99k8Rd75', + type: 'Ed25519VerificationKey2018', + controller: 'did:web:launchpad.vii.electron.mattrlabs.io', + publicKeyBase58: 'Ck99k8Rd75V3THNexmMYYA6McqUJi9QgcPh4B1BBUTX7', + }, + ], + keyAgreement: [ + { + id: 'did:web:launchpad.vii.electron.mattrlabs.io#Dd3FUiBvRy', + type: 'X25519KeyAgreementKey2019', + controller: 'did:web:launchpad.vii.electron.mattrlabs.io', + publicKeyBase58: 'Dd3FUiBvRyBcAbcywjGy99BtPaV2DXnvjbYPCu8MYs68', + }, + ], + authentication: ['did:web:launchpad.vii.electron.mattrlabs.io#Ck99k8Rd75'], + assertionMethod: ['did:web:launchpad.vii.electron.mattrlabs.io#Ck99k8Rd75'], + capabilityDelegation: ['did:web:launchpad.vii.electron.mattrlabs.io#Ck99k8Rd75'], + capabilityInvocation: ['did:web:launchpad.vii.electron.mattrlabs.io#Ck99k8Rd75'], + }, + + acquireAccessTokenResponse: { + access_token: 'i3iOTQe5TOskOOUnkIDX29M8AuygT7Lfv3MkaHprL4p', + expires_in: 3600, + scope: 'OpenBadgeCredential', + token_type: 'Bearer', + }, + + credentialResponse: { + credential: + 'eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8jQ2s5OWs4UmQ3NSJ9.eyJpc3MiOiJkaWQ6d2ViOmxhdW5jaHBhZC52aWkuZWxlY3Ryb24ubWF0dHJsYWJzLmlvIiwic3ViIjoiZGlkOmtleTp6Nk1rcEdSNGdzNFJjM1pwaDR2ajh3Um5qbkF4Z0FQU3hjUjhNQVZLdXRXc3BRemMiLCJuYmYiOjE3MDU4NDAzMDksImV4cCI6MTczNzQ2MjcwOSwidmMiOnsibmFtZSI6IkV4YW1wbGUgVW5pdmVyc2l0eSBEZWdyZWUiLCJkZXNjcmlwdGlvbiI6IkpGRiBQbHVnZmVzdCAzIE9wZW5CYWRnZSBDcmVkZW50aWFsIiwiY3JlZGVudGlhbEJyYW5kaW5nIjp7ImJhY2tncm91bmRDb2xvciI6IiM0NjRjNDkifSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL21hdHRyLmdsb2JhbC9jb250ZXh0cy92Yy1leHRlbnNpb25zL3YyIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9leHRlbnNpb25zLmpzb24iLCJodHRwczovL3czaWQub3JnL3ZjLXJldm9jYXRpb24tbGlzdC0yMDIwL3YxIiwiaHR0cHM6Ly93M2lkLm9yZy92Yy1yZXZvY2F0aW9uLWxpc3QtMjAyMC92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwidHlwZSI6WyJBY2hpZXZlbWVudFN1YmplY3QiXSwiYWNoaWV2ZW1lbnQiOnsiaWQiOiJodHRwczovL2V4YW1wbGUuY29tL2FjaGlldmVtZW50cy8yMXN0LWNlbnR1cnktc2tpbGxzL3RlYW13b3JrIiwibmFtZSI6IlRlYW13b3JrIiwidHlwZSI6WyJBY2hpZXZlbWVudCJdLCJpbWFnZSI6eyJpZCI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMy0yMDIzL2ltYWdlcy9KRkYtVkMtRURVLVBMVUdGRVNUMy1iYWRnZS1pbWFnZS5wbmciLCJ0eXBlIjoiSW1hZ2UifSwiY3JpdGVyaWEiOnsibmFycmF0aXZlIjoiVGVhbSBtZW1iZXJzIGFyZSBub21pbmF0ZWQgZm9yIHRoaXMgYmFkZ2UgYnkgdGhlaXIgcGVlcnMgYW5kIHJlY29nbml6ZWQgdXBvbiByZXZpZXcgYnkgRXhhbXBsZSBDb3JwIG1hbmFnZW1lbnQuIn0sImRlc2NyaXB0aW9uIjoiVGhpcyBiYWRnZSByZWNvZ25pemVzIHRoZSBkZXZlbG9wbWVudCBvZiB0aGUgY2FwYWNpdHkgdG8gY29sbGFib3JhdGUgd2l0aGluIGEgZ3JvdXAgZW52aXJvbm1lbnQuIn19LCJpc3N1ZXIiOnsiaWQiOiJkaWQ6d2ViOmxhdW5jaHBhZC52aWkuZWxlY3Ryb24ubWF0dHJsYWJzLmlvIiwibmFtZSI6IkV4YW1wbGUgVW5pdmVyc2l0eSIsImljb25VcmwiOiJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL3ZjLWVkL3BsdWdmZXN0LTEtMjAyMi9pbWFnZXMvSkZGX0xvZ29Mb2NrdXAucG5nIiwiaW1hZ2UiOiJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL3ZjLWVkL3BsdWdmZXN0LTEtMjAyMi9pbWFnZXMvSkZGX0xvZ29Mb2NrdXAucG5nIn19fQ.u33C1y8qwlKQSIq5NjgjXq-fG_u5-bP87HAZPiaTtXhUzd5hxToyrEUb3GAEa4dkLY2TVQA1LtC6sNSUmGevBQ', + format: 'jwt_vc_json', + }, +} + +export const waltIdDraft11JwtVcJson = { + credentialOffer: + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%22UniversityDegree%22%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22efc2f5dd-0f44-4f38-a902-3204e732c391%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJlZmMyZjVkZC0wZjQ0LTRmMzgtYTkwMi0zMjA0ZTczMmMzOTEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.OHzYTP_u6I95hHBmjF3RchydGidq3nsT0QHdgJ1AXyR5AFkrTfJwsW4FQIdOdda93uS7FOh_vSVGY0Qngzm7Ag%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D', + getMetadataResponse: { + issuer: 'https://issuer.portal.walt.id', + authorization_endpoint: 'https://issuer.portal.walt.id/authorize', + pushed_authorization_request_endpoint: 'https://issuer.portal.walt.id/par', + token_endpoint: 'https://issuer.portal.walt.id/token', + jwks_uri: 'https://issuer.portal.walt.id/jwks', + scopes_supported: ['openid'], + response_modes_supported: ['query', 'fragment'], + grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], + subject_types_supported: ['public'], + credential_issuer: 'https://issuer.portal.walt.id/.well-known/openid-credential-issuer', + credential_endpoint: 'https://issuer.portal.walt.id/credential', + credentials_supported: [ + { + format: 'jwt_vc_json', + id: 'BankId', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'BankId'], + }, + { + format: 'jwt_vc_json', + id: 'KycChecksCredential', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'KycChecksCredential'], + }, + { + format: 'jwt_vc_json', + id: 'KycDataCredential', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'KycDataCredential'], + }, + { + format: 'jwt_vc_json', + id: 'PassportCh', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableId', 'PassportCh'], + }, + { + format: 'jwt_vc_json', + id: 'PND91Credential', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'PND91Credential'], + }, + { + format: 'jwt_vc_json', + id: 'MortgageEligibility', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableId', 'MortgageEligibility'], + }, + { + format: 'jwt_vc_json', + id: 'PortableDocumentA1', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'PortableDocumentA1'], + }, + { + format: 'jwt_vc_json', + id: 'OpenBadgeCredential', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + { + format: 'jwt_vc_json', + id: 'VaccinationCertificate', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VaccinationCertificate'], + }, + { + format: 'jwt_vc_json', + id: 'WalletHolderCredential', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'WalletHolderCredential'], + }, + { + format: 'jwt_vc_json', + id: 'UniversityDegree', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'UniversityDegree'], + }, + { + format: 'jwt_vc_json', + id: 'VerifiableId', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableId'], + }, + ], + batch_credential_endpoint: 'https://issuer.portal.walt.id/batch_credential', + deferred_credential_endpoint: 'https://issuer.portal.walt.id/credential_deferred', + }, + + acquireAccessTokenResponse: { + access_token: + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJjMDQyMmUxMy1kNTU0LTQwMmUtOTQ0OS0yZjA0ZjAyNjMzNTMiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IkFDQ0VTUyJ9.pkNF05uUy72QAoZwdf1Uz1XRc4aGs1hhnim-x1qIeMe17TMUYV2D6BOATQtDItxnnhQz2MBfqUSQKYi7CFirDA', + token_type: 'bearer', + c_nonce: 'd4364dac-f026-4380-a4c3-2bfe2d2df52a', + c_nonce_expires_in: 27, + }, + + authorizationCode: + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJkZDYyOGQxYy1kYzg4LTQ2OGItYjI5Yi05ODQwMzFlNzg3OWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.86LfW1y7QwNObIhJej40E4Ea8PGjBbIeq1KBkOWOLNnOs5rRvtDkazA52npsKrBKqfoqCPmOHcVAvPZPWJhKAA', + + par: { + request_uri: 'urn:ietf:params:oauth:request_uri:738f2ac2-18ac-4162-b0a8-5e0e6ba2270b', + expires_in: 'PT3M46.132011234S', + }, + + credentialResponse: { + credential: + 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pUTBaUkxVNXlZVFY1Ym5sQ2MyWjRkM2szWVU1bU9HUjFRVVZWUTAxc1RVbHlVa2x5UkdjMlJFbDVOQ0lzSW5naU9pSm9OVzVpZHpaWU9VcHRTVEJDZG5WUk5VMHdTbGhtZWs4NGN6SmxSV0pRWkZZeU9YZHpTRlJNT1hCckluMCJ9.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaVEwWlJMVTV5WVRWNWJubENjMlo0ZDNrM1lVNW1PR1IxUVVWVlEwMXNUVWx5VWtseVJHYzJSRWw1TkNJc0luZ2lPaUpvTlc1aWR6WllPVXB0U1RCQ2RuVlJOVTB3U2xobWVrODRjekpsUldKUVpGWXlPWGR6U0ZSTU9YQnJJbjAiLCJzdWIiOiJkaWQ6a2V5Ono2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyN6Nk1rcEdSNGdzNFJjM1pwaDR2ajh3Um5qbkF4Z0FQU3hjUjhNQVZLdXRXc3BRemMiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoidXJuOnV1aWQ6NmU2ODVlOGUtNmRmNS00NzhkLTlkNWQtNDk2ZTcxMDJkYmFhIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlVuaXZlcnNpdHlEZWdyZWUiXSwiaXNzdWVyIjp7ImlkIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lRMFpSTFU1eVlUVjVibmxDYzJaNGQzazNZVTVtT0dSMVFVVlZRMDFzVFVseVVrbHlSR2MyUkVsNU5DSXNJbmdpT2lKb05XNWlkelpZT1VwdFNUQkNkblZSTlUwd1NsaG1lazg0Y3pKbFJXSlFaRll5T1hkelNGUk1PWEJySW4wIn0sImlzc3VhbmNlRGF0ZSI6IjIwMjQtMDEtMjFUMTI6NDU6NDYuOTU1MjU0MDg3WiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rcEdSNGdzNFJjM1pwaDR2ajh3Um5qbkF4Z0FQU3hjUjhNQVZLdXRXc3BRemMjejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwiZGVncmVlIjp7InR5cGUiOiJCYWNoZWxvckRlZ3JlZSIsIm5hbWUiOiJCYWNoZWxvciBvZiBTY2llbmNlIGFuZCBBcnRzIn19fSwianRpIjoidXJuOnV1aWQ6NmU2ODVlOGUtNmRmNS00NzhkLTlkNWQtNDk2ZTcxMDJkYmFhIiwiaWF0IjoxNzA1ODQxMTQ2LCJuYmYiOjE3MDU4NDEwNTZ9.sEudi9lL4YSvMdfjRaeDoRl2_p6dpfuxw_qkPXeBx8FRIQ41t-fyH_S_CDTVYH7wwL-RDbVMK1cza2FQH65hCg', + format: 'jwt_vc_json', + }, +} + +export const animoOpenIdPlaygroundDraft11SdJwtVc = { + credentialOffer: + 'openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221076398228999891821960009%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22AnimoOpenId4VcPlaygroundSdJwtVcJwk%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc.animo.id%2Foid4vci%2F0bbfb1c0-9f45-478c-a139-08f6ed610a37%22%7D', + getMetadataResponse: { + credential_issuer: 'https://openid4vc.animo.id/oid4vci/0bbfb1c0-9f45-478c-a139-08f6ed610a37', + token_endpoint: 'https://openid4vc.animo.id/oid4vci/0bbfb1c0-9f45-478c-a139-08f6ed610a37/token', + credential_endpoint: 'https://openid4vc.animo.id/oid4vci/0bbfb1c0-9f45-478c-a139-08f6ed610a37/credential', + credentials_supported: [ + { + id: 'AnimoOpenId4VcPlaygroundSdJwtVcDid', + format: 'vc+sd-jwt', + vct: 'AnimoOpenId4VcPlayground', + cryptographic_binding_methods_supported: ['did:key', 'did:jwk'], + cryptographic_suites_supported: ['EdDSA'], + display: [ + { + name: 'Animo OpenID4VC Playground - SD-JWT-VC (did holder binding)', + description: "Issued using Animo's OpenID4VC Playground", + background_color: '#FFFFFF', + locale: 'en', + text_color: '#E17471', + }, + ], + }, + { + id: 'AnimoOpenId4VcPlaygroundSdJwtVcJwk', + format: 'vc+sd-jwt', + vct: 'AnimoOpenId4VcPlayground', + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: ['EdDSA'], + display: [ + { + name: 'Animo OpenID4VC Playground - SD-JWT-VC (jwk holder binding)', + description: "Issued using Animo's OpenID4VC Playground", + background_color: '#FFFFFF', + locale: 'en', + text_color: '#E17471', + }, + ], + }, + { + id: 'AnimoOpenId4VcPlaygroundJwtVc', + format: 'jwt_vc_json', + types: ['AnimoOpenId4VcPlayground'], + cryptographic_binding_methods_supported: ['did:key', 'did:jwk'], + cryptographic_suites_supported: ['EdDSA'], + display: [ + { + name: 'Animo OpenID4VC Playground - JWT VC', + description: "Issued using Animo's OpenID4VC Playground", + background_color: '#FFFFFF', + locale: 'en', + text_color: '#E17471', + }, + ], + }, + ], + display: [ + { + background_color: '#FFFFFF', + description: 'Animo OpenID4VC Playground', + name: 'Animo OpenID4VC Playground', + locale: 'en', + logo: { alt_text: 'Animo logo', url: 'https://i.imgur.com/8B37E4a.png' }, + text_color: '#E17471', + }, + ], + }, + + acquireAccessTokenResponse: { + access_token: + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im5fQ05IM3c1dWpQaDNsTmVaR05Ta0hiT2pSTnNudkJpNXIzcXhINGZwd1UifX0.eyJpc3MiOiJodHRwczovL29wZW5pZDR2Yy5hbmltby5pZC9vaWQ0dmNpLzBiYmZiMWMwLTlmNDUtNDc4Yy1hMTM5LTA4ZjZlZDYxMGEzNyIsImV4cCI6MTgwMDAwLCJpYXQiOjE3MDU4NDM1NzM1ODh9.3JC_R4zXK0GLMG6MS7ClVWm9bK-9v7mA2iS_0hqYdmZRwXJI3ME6TAslPZNNdxCTp5ZYzzsFuLd2L3l7kULmBQ', + token_type: 'bearer', + expires_in: 180000, + c_nonce: '725150697872293881791236', + c_nonce_expires_in: 300000, + authorization_pending: false, + }, + + credentialResponse: { + credential: + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1raDVITlBDQ0pXWm42V1JMalJQdHR5dllaQnNrWlVkU0pmVGlad2NVU2llcXgifQ.eyJ2Y3QiOiJBbmltb09wZW5JZDRWY1BsYXlncm91bmQiLCJwbGF5Z3JvdW5kIjp7ImZyYW1ld29yayI6IkFyaWVzIEZyYW1ld29yayBKYXZhU2NyaXB0IiwiY3JlYXRlZEJ5IjoiQW5pbW8gU29sdXRpb25zIiwiX3NkIjpbImZZM0ZqUHpZSEZOcHlZZnRnVl9kX25DMlRHSVh4UnZocE00VHdrMk1yMDQiLCJwTnNqdmZJeVBZOEQwTks1c1l0alR2Nkc2R0FNVDNLTjdaZDNVNDAwZ1pZIl19LCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoia2MydGxwaGNadzFBSUt5a3pNNnBjY2k2UXNLQW9jWXpGTC01RmUzNmg2RSJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1raDVITlBDQ0pXWm42V1JMalJQdHR5dllaQnNrWlVkU0pmVGlad2NVU2llcXgiLCJpYXQiOjE3MDU4NDM1NzQsIl9zZF9hbGciOiJzaGEtMjU2In0.2iAjaCFcuiHXTfQsrxXo6BghtwzqTrfDmhmarAAJAhY8r9yKXY3d10JY1dry2KnaEYWpq2R786thjdA5BXlPAQ~WyI5MzM3MTM0NzU4NDM3MjYyODY3NTE4NzkiLCJsYW5ndWFnZSIsIlR5cGVTY3JpcHQiXQ~WyIxMTQ3MDA5ODk2Nzc2MDYzOTc1MDUwOTMxIiwidmVyc2lvbiIsIjEuMCJd~', + format: 'vc+sd-jwt', + c_nonce: '98b487cb-f6e5-4f9b-b963-ad69b8fe5e29', + c_nonce_expires_in: 300000, + }, +} diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.test.ts new file mode 100644 index 0000000000..7c9a1e64c9 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.test.ts @@ -0,0 +1,295 @@ +import type { Key, SdJwtVc } from '@credo-ts/core' + +import { + getJwkFromKey, + Agent, + DidKey, + JwaSignatureAlgorithm, + KeyType, + TypedArrayEncoder, + W3cJwtVerifiableCredential, +} from '@credo-ts/core' +import nock, { cleanAll, enableNetConnect } from 'nock' + +import { AskarModule } from '../../../../askar/src' +import { askarModuleConfig } from '../../../../askar/tests/helpers' +import { agentDependencies } from '../../../../node/src' +import { OpenId4VcHolderModule } from '../OpenId4VcHolderModule' + +import { animoOpenIdPlaygroundDraft11SdJwtVc, matrrLaunchpadDraft11JwtVcJson, waltIdDraft11JwtVcJson } from './fixtures' + +const holder = new Agent({ + config: { + label: 'OpenId4VcHolder Test28', + walletConfig: { id: 'openid4vc-holder-test27', key: 'openid4vc-holder-test27' }, + }, + dependencies: agentDependencies, + modules: { + openId4VcHolder: new OpenId4VcHolderModule(), + askar: new AskarModule(askarModuleConfig), + }, +}) + +describe('OpenId4VcHolder', () => { + let holderKey: Key + let holderDid: string + let holderVerificationMethod: string + + beforeEach(async () => { + await holder.initialize() + + holderKey = await holder.wallet.createKey({ + keyType: KeyType.Ed25519, + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + }) + const holderDidKey = new DidKey(holderKey) + holderDid = holderDidKey.did + holderVerificationMethod = `${holderDidKey.did}#${holderDidKey.key.fingerprint}` + }) + + afterEach(async () => { + await holder.shutdown() + await holder.wallet.delete() + }) + + describe('[DRAFT 11]: Pre-authorized flow', () => { + afterEach(() => { + cleanAll() + enableNetConnect() + }) + + it('Should successfully receive credential from MATTR launchpad using the pre-authorized flow using a did:key Ed25519 subject and jwt_vc_json credential', async () => { + const fixture = matrrLaunchpadDraft11JwtVcJson + + /** + * Below we're setting up some mock HTTP responses. + * These responses are based on the openid-initiate-issuance URI above + * */ + // setup temporary redirect mock + nock('https://launchpad.mattrlabs.com').get('/.well-known/openid-credential-issuer').reply(307, undefined, { + Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', + }) + + // setup server metadata response + nock('https://launchpad.vii.electron.mattrlabs.io') + .get('/.well-known/did.json') + .reply(200, fixture.wellKnownDid) + + .get('/.well-known/openid-configuration') + .reply(404) + + .get('/.well-known/oauth-authorization-server') + .reply(404) + + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + + // setup access token response + .post('/oidc/v1/auth/token') + .reply(200, fixture.acquireAccessTokenResponse) + + // setup credential request response + .post('/oidc/v1/auth/credential') + .reply(200, fixture.credentialResponse) + + .get('/.well-known/did.json') + .reply(200, fixture.wellKnownDid) + + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) + const credentials = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode(resolved, { + verifyCredentialStatus: false, + // We only allow EdDSa, as we've created a did with keyType ed25519. If we create + // or determine the did dynamically we could use any signature algorithm + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'jwt_vc_json').map((m) => m.id), + credentialBindingResolver: () => ({ method: 'did', didUrl: holderVerificationMethod }), + }) + + expect(credentials).toHaveLength(1) + const w3cCredential = credentials[0] as W3cJwtVerifiableCredential + expect(w3cCredential).toBeInstanceOf(W3cJwtVerifiableCredential) + + expect(w3cCredential.credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) + expect(w3cCredential.credential.credentialSubjectIds[0]).toEqual(holderDid) + }) + + it('Should successfully receive credential from walt.id using the pre-authorized flow using a did:key Ed25519 subject and jwt_vc_json credential', async () => { + const fixture = waltIdDraft11JwtVcJson + + // setup server metadata response + nock('https://issuer.portal.walt.id') + // openid configuration is same as issuer metadata for walt.id + .get('/.well-known/openid-configuration') + .reply(200, fixture.getMetadataResponse) + + .get('/.well-known/oauth-authorization-server') + .reply(404) + + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + + // setup access token response + .post('/token') + .reply(200, fixture.acquireAccessTokenResponse) + + // setup credential request response + .post('/credential') + .reply(200, fixture.credentialResponse) + + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) + + await expect(() => + holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode(resolved, { + verifyCredentialStatus: false, + // We only allow EdDSa, as we've created a did with keyType ed25519. If we create + // or determine the did dynamically we could use any signature algorithm + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'jwt_vc_json').map((m) => m.id), + credentialBindingResolver: () => ({ method: 'did', didUrl: holderVerificationMethod }), + }) + ) + // FIXME: walt.id issues jwt where nbf and issuanceDate do not match + .rejects.toThrowError('JWT nbf and vc.issuanceDate do not match') + }) + + it('Should successfully receive credential from animo openid4vc playground using the pre-authorized flow using a jwk EdDSA subject and vc+sd-jwt credential', async () => { + const fixture = animoOpenIdPlaygroundDraft11SdJwtVc + + // setup server metadata response + nock('https://openid4vc.animo.id/oid4vci/0bbfb1c0-9f45-478c-a139-08f6ed610a37') + .get('/.well-known/openid-configuration') + .reply(404) + + .get('/.well-known/oauth-authorization-server') + .reply(404) + + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + + // setup access token response + .post('/token') + .reply(200, fixture.acquireAccessTokenResponse) + + // setup credential request response + .post('/credential') + .reply(200, fixture.credentialResponse) + + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) + + const credentials = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode(resolved, { + verifyCredentialStatus: false, + // We only allow EdDSa, as we've created a did with keyType ed25519. If we create + // or determine the did dynamically we could use any signature algorithm + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'vc+sd-jwt').map((m) => m.id), + credentialBindingResolver: () => ({ method: 'jwk', jwk: getJwkFromKey(holderKey) }), + }) + + expect(credentials).toHaveLength(1) + const credential = credentials[0] as SdJwtVc + expect(credential).toEqual({ + compact: + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1raDVITlBDQ0pXWm42V1JMalJQdHR5dllaQnNrWlVkU0pmVGlad2NVU2llcXgifQ.eyJ2Y3QiOiJBbmltb09wZW5JZDRWY1BsYXlncm91bmQiLCJwbGF5Z3JvdW5kIjp7ImZyYW1ld29yayI6IkFyaWVzIEZyYW1ld29yayBKYXZhU2NyaXB0IiwiY3JlYXRlZEJ5IjoiQW5pbW8gU29sdXRpb25zIiwiX3NkIjpbImZZM0ZqUHpZSEZOcHlZZnRnVl9kX25DMlRHSVh4UnZocE00VHdrMk1yMDQiLCJwTnNqdmZJeVBZOEQwTks1c1l0alR2Nkc2R0FNVDNLTjdaZDNVNDAwZ1pZIl19LCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoia2MydGxwaGNadzFBSUt5a3pNNnBjY2k2UXNLQW9jWXpGTC01RmUzNmg2RSJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1raDVITlBDQ0pXWm42V1JMalJQdHR5dllaQnNrWlVkU0pmVGlad2NVU2llcXgiLCJpYXQiOjE3MDU4NDM1NzQsIl9zZF9hbGciOiJzaGEtMjU2In0.2iAjaCFcuiHXTfQsrxXo6BghtwzqTrfDmhmarAAJAhY8r9yKXY3d10JY1dry2KnaEYWpq2R786thjdA5BXlPAQ~WyI5MzM3MTM0NzU4NDM3MjYyODY3NTE4NzkiLCJsYW5ndWFnZSIsIlR5cGVTY3JpcHQiXQ~WyIxMTQ3MDA5ODk2Nzc2MDYzOTc1MDUwOTMxIiwidmVyc2lvbiIsIjEuMCJd~', + header: { + alg: 'EdDSA', + kid: '#z6Mkh5HNPCCJWZn6WRLjRPttyvYZBskZUdSJfTiZwcUSieqx', + typ: 'vc+sd-jwt', + }, + payload: { + _sd_alg: 'sha-256', + cnf: { + jwk: { + crv: 'Ed25519', + kty: 'OKP', + x: 'kc2tlphcZw1AIKykzM6pcci6QsKAocYzFL-5Fe36h6E', + }, + }, + iat: 1705843574, + iss: 'did:key:z6Mkh5HNPCCJWZn6WRLjRPttyvYZBskZUdSJfTiZwcUSieqx', + playground: { + _sd: ['fY3FjPzYHFNpyYftgV_d_nC2TGIXxRvhpM4Twk2Mr04', 'pNsjvfIyPY8D0NK5sYtjTv6G6GAMT3KN7Zd3U400gZY'], + createdBy: 'Animo Solutions', + framework: 'Aries Framework JavaScript', + }, + vct: 'AnimoOpenId4VcPlayground', + }, + prettyClaims: { + cnf: { + jwk: { + crv: 'Ed25519', + kty: 'OKP', + x: 'kc2tlphcZw1AIKykzM6pcci6QsKAocYzFL-5Fe36h6E', + }, + }, + iat: 1705843574, + iss: 'did:key:z6Mkh5HNPCCJWZn6WRLjRPttyvYZBskZUdSJfTiZwcUSieqx', + playground: { + createdBy: 'Animo Solutions', + framework: 'Aries Framework JavaScript', + language: 'TypeScript', + version: '1.0', + }, + vct: 'AnimoOpenId4VcPlayground', + }, + }) + }) + }) + + describe('[DRAFT 11]: Authorization flow', () => { + afterAll(() => { + cleanAll() + enableNetConnect() + }) + + it('Should successfully receive credential from walt.id using the authorized flow using a did:key Ed25519 subject and jwt_vc_json credential', async () => { + const fixture = waltIdDraft11JwtVcJson + + // setup temporary redirect mock + nock('https://issuer.portal.walt.id') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/oauth-authorization-server') + .reply(404) + .post('/par') + .reply(200, fixture.par) + // setup access token response + .post('/token') + .reply(200, fixture.acquireAccessTokenResponse) + // setup credential request response + .post('/credential') + .reply(200, fixture.credentialResponse) + + .get('/.well-known/oauth-authorization-server') + .reply(404) + + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) + + const resolvedAuthorizationRequest = await holder.modules.openId4VcHolder.resolveIssuanceAuthorizationRequest( + resolved, + { + clientId: 'test-client', + redirectUri: 'http://example.com', + scope: ['openid', 'UniversityDegree'], + } + ) + + await expect( + holder.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( + resolved, + resolvedAuthorizationRequest, + fixture.authorizationCode, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + credentialBindingResolver: () => ({ method: 'did', didUrl: holderVerificationMethod }), + verifyCredentialStatus: false, + } + ) + ) + // FIXME: credential returned by walt.id has nbf and issuanceDate that do not match + // but we know that we at least received the credential if we got to this error + .rejects.toThrow('JWT nbf and vc.issuanceDate do not match') + }) + }) +}) diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts new file mode 100644 index 0000000000..05f8c395eb --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts @@ -0,0 +1,114 @@ +import type { AgentType } from '../../../tests/utils' +import type { OpenId4VcVerifierRecord } from '../../openid4vc-verifier/repository' +import type { Express } from 'express' +import type { Server } from 'http' + +import express from 'express' + +import { AskarModule } from '../../../../askar/src' +import { askarModuleConfig } from '../../../../askar/tests/helpers' +import { waitForVerificationSessionRecordSubject, createAgentFromModules } from '../../../tests/utils' +import { OpenId4VcVerificationSessionState, OpenId4VcVerifierModule } from '../../openid4vc-verifier' +import { OpenId4VcHolderModule } from '../OpenId4VcHolderModule' + +const port = 3121 +const verificationEndpointPath = '/proofResponse' +const verifierBaseUrl = `http://localhost:${port}` + +const holderModules = { + openId4VcHolder: new OpenId4VcHolderModule(), + askar: new AskarModule(askarModuleConfig), +} + +const verifierModules = { + openId4VcVerifier: new OpenId4VcVerifierModule({ + baseUrl: verifierBaseUrl, + endpoints: { + authorization: { + endpointPath: verificationEndpointPath, + }, + }, + }), + askar: new AskarModule(askarModuleConfig), +} + +describe('OpenId4VcHolder | OpenID4VP', () => { + let openIdVerifier: OpenId4VcVerifierRecord + let verifier: AgentType + let holder: AgentType + let verifierApp: Express + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let verifierServer: Server + + beforeEach(async () => { + verifier = await createAgentFromModules('verifier', verifierModules, '96213c3d7fc8d4d6754c7a0fd969598f') + openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() + holder = await createAgentFromModules('holder', holderModules, '96213c3d7fc8d4d6754c7a0fd969598e') + verifierApp = express() + + verifierApp.use('/', verifier.agent.modules.openId4VcVerifier.config.router) + verifierServer = verifierApp.listen(port) + }) + + afterEach(async () => { + verifierServer?.close() + await holder.agent.shutdown() + await holder.agent.wallet.delete() + await verifier.agent.shutdown() + await verifier.agent.wallet.delete() + }) + + it('siop authorization request without presentation exchange', async () => { + const { authorizationRequest, verificationSession } = + await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'did', + didUrl: verifier.kid, + }, + verifierId: openIdVerifier.verifierId, + }) + + const resolvedAuthorizationRequest = await holder.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequest + ) + + const { submittedResponse, serverResponse } = + await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, + // When no VP is created, we need to provide the did we want to use for authentication + openIdTokenIssuer: { + method: 'did', + didUrl: holder.kid, + }, + }) + + expect(serverResponse).toEqual({ + status: 200, + body: '', + }) + + expect(submittedResponse).toMatchObject({ + expires_in: 6000, + id_token: expect.any(String), + state: expect.any(String), + }) + + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + state: OpenId4VcVerificationSessionState.ResponseVerified, + contextCorrelationId: verifier.agent.context.contextCorrelationId, + verificationSessionId: verificationSession.id, + }) + + const { idToken, presentationExchange } = + await verifier.agent.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession.id) + + expect(presentationExchange).toBeUndefined() + expect(idToken).toMatchObject({ + payload: { + state: expect.any(String), + nonce: expect.any(String), + }, + }) + }) +}) diff --git a/packages/openid4vc/src/openid4vc-holder/index.ts b/packages/openid4vc/src/openid4vc-holder/index.ts new file mode 100644 index 0000000000..2b7a8d1d5b --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/index.ts @@ -0,0 +1,6 @@ +export * from './OpenId4VcHolderApi' +export * from './OpenId4VcHolderModule' +export * from './OpenId4VciHolderService' +export * from './OpenId4VciHolderServiceOptions' +export * from './OpenId4vcSiopHolderService' +export * from './OpenId4vcSiopHolderServiceOptions' diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuanceSessionState.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuanceSessionState.ts new file mode 100644 index 0000000000..9bce616687 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuanceSessionState.ts @@ -0,0 +1,10 @@ +export enum OpenId4VcIssuanceSessionState { + OfferCreated = 'OfferCreated', + OfferUriRetrieved = 'OfferUriRetrieved', + AccessTokenRequested = 'AccessTokenRequested', + AccessTokenCreated = 'AccessTokenCreated', + CredentialRequestReceived = 'CredentialRequestReceived', + CredentialsPartiallyIssued = 'CredentialsPartiallyIssued', + Completed = 'Completed', + Error = 'Error', +} diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts new file mode 100644 index 0000000000..9011424af8 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts @@ -0,0 +1,113 @@ +import type { + OpenId4VciCreateCredentialResponseOptions, + OpenId4VciCreateCredentialOfferOptions, + OpenId4VciCreateIssuerOptions, +} from './OpenId4VcIssuerServiceOptions' +import type { OpenId4VcIssuerRecordProps } from './repository' +import type { OpenId4VciCredentialRequest } from '../shared' + +import { injectable, AgentContext } from '@credo-ts/core' + +import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' +import { OpenId4VcIssuerService } from './OpenId4VcIssuerService' + +/** + * @public + * This class represents the API for interacting with the OpenID4VC Issuer service. + * It provides methods for creating a credential offer, creating a response to a credential issuance request, + * and retrieving a credential offer from a URI. + */ +@injectable() +export class OpenId4VcIssuerApi { + public constructor( + public readonly config: OpenId4VcIssuerModuleConfig, + private agentContext: AgentContext, + private openId4VcIssuerService: OpenId4VcIssuerService + ) {} + + public async getAllIssuers() { + return this.openId4VcIssuerService.getAllIssuers(this.agentContext) + } + + /** + * @deprecated use {@link getIssuerByIssuerId} instead. + * @todo remove in 0.6 + */ + public async getByIssuerId(issuerId: string) { + return this.getIssuerByIssuerId(issuerId) + } + + public async getIssuerByIssuerId(issuerId: string) { + return this.openId4VcIssuerService.getIssuerByIssuerId(this.agentContext, issuerId) + } + + /** + * Creates an issuer and stores the corresponding issuer metadata. Multiple issuers can be created, to allow different sets of + * credentials to be issued with each issuer. + */ + public async createIssuer(options: OpenId4VciCreateIssuerOptions) { + return this.openId4VcIssuerService.createIssuer(this.agentContext, options) + } + + /** + * Rotate the key used for signing access tokens for the issuer with the given issuerId. + */ + public async rotateAccessTokenSigningKey(issuerId: string) { + const issuer = await this.openId4VcIssuerService.getIssuerByIssuerId(this.agentContext, issuerId) + return this.openId4VcIssuerService.rotateAccessTokenSigningKey(this.agentContext, issuer) + } + + public async updateIssuerMetadata( + options: Pick + ) { + const issuer = await this.openId4VcIssuerService.getIssuerByIssuerId(this.agentContext, options.issuerId) + + issuer.credentialsSupported = options.credentialsSupported + issuer.display = options.display + + return this.openId4VcIssuerService.updateIssuer(this.agentContext, issuer) + } + + /** + * Creates a credential offer. Either the preAuthorizedCodeFlowConfig or the authorizationCodeFlowConfig must be provided. + * + * @returns Object containing the payload of the credential offer and the credential offer request, which can be sent to the wallet. + */ + public async createCredentialOffer(options: OpenId4VciCreateCredentialOfferOptions & { issuerId: string }) { + const { issuerId, ...rest } = options + const issuer = await this.openId4VcIssuerService.getIssuerByIssuerId(this.agentContext, issuerId) + return await this.openId4VcIssuerService.createCredentialOffer(this.agentContext, { ...rest, issuer }) + } + + /** + * This function creates a response which can be send to the holder after receiving a credential issuance request. + */ + public async createCredentialResponse( + options: OpenId4VciCreateCredentialResponseOptions & { issuanceSessionId: string } + ) { + const { issuanceSessionId, ...rest } = options + const issuanceSession = await this.openId4VcIssuerService.getIssuanceSessionById( + this.agentContext, + issuanceSessionId + ) + + return await this.openId4VcIssuerService.createCredentialResponse(this.agentContext, { ...rest, issuanceSession }) + } + + public async findIssuanceSessionForCredentialRequest(options: { + credentialRequest: OpenId4VciCredentialRequest + issuerId?: string + }) { + const issuanceSession = await this.openId4VcIssuerService.findIssuanceSessionForCredentialRequest( + this.agentContext, + options + ) + + return issuanceSession + } + + public async getIssuerMetadata(issuerId: string) { + const issuer = await this.openId4VcIssuerService.getIssuerByIssuerId(this.agentContext, issuerId) + return this.openId4VcIssuerService.getIssuerMetadata(this.agentContext, issuer) + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerEvents.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerEvents.ts new file mode 100644 index 0000000000..79f5b7d38e --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerEvents.ts @@ -0,0 +1,15 @@ +import type { OpenId4VcIssuanceSessionState } from './OpenId4VcIssuanceSessionState' +import type { OpenId4VcIssuanceSessionRecord } from './repository' +import type { BaseEvent } from '@credo-ts/core' + +export enum OpenId4VcIssuerEvents { + IssuanceSessionStateChanged = 'OpenId4VcIssuer.IssuanceSessionStateChanged', +} + +export interface OpenId4VcIssuanceSessionStateChangedEvent extends BaseEvent { + type: typeof OpenId4VcIssuerEvents.IssuanceSessionStateChanged + payload: { + issuanceSession: OpenId4VcIssuanceSessionRecord + previousState: OpenId4VcIssuanceSessionState | null + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts new file mode 100644 index 0000000000..44f4f6e84c --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts @@ -0,0 +1,138 @@ +import type { OpenId4VcIssuerModuleConfigOptions } from './OpenId4VcIssuerModuleConfig' +import type { OpenId4VcIssuanceRequest } from './router' +import type { AgentContext, DependencyManager, Module } from '@credo-ts/core' + +import { AgentConfig } from '@credo-ts/core' + +import { getAgentContextForActorId, getRequestContext, importExpress } from '../shared/router' + +import { OpenId4VcIssuerApi } from './OpenId4VcIssuerApi' +import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' +import { OpenId4VcIssuerService } from './OpenId4VcIssuerService' +import { OpenId4VcIssuanceSessionRepository } from './repository' +import { OpenId4VcIssuerRepository } from './repository/OpenId4VcIssuerRepository' +import { + configureCredentialOfferEndpoint, + configureAccessTokenEndpoint, + configureCredentialEndpoint, + configureIssuerMetadataEndpoint, +} from './router' + +/** + * @public + */ +export class OpenId4VcIssuerModule implements Module { + public readonly api = OpenId4VcIssuerApi + public readonly config: OpenId4VcIssuerModuleConfig + + public constructor(options: OpenId4VcIssuerModuleConfigOptions) { + this.config = new OpenId4VcIssuerModuleConfig(options) + } + + /** + * Registers the dependencies of the question answer module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The '@credo-ts/openid4vc' Issuer module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." + ) + + // Register config + dependencyManager.registerInstance(OpenId4VcIssuerModuleConfig, this.config) + + // Services + dependencyManager.registerSingleton(OpenId4VcIssuerService) + + // Repository + dependencyManager.registerSingleton(OpenId4VcIssuerRepository) + dependencyManager.registerSingleton(OpenId4VcIssuanceSessionRepository) + } + + public async initialize(rootAgentContext: AgentContext): Promise { + this.configureRouter(rootAgentContext) + } + + /** + * Registers the endpoints on the router passed to this module. + */ + private configureRouter(rootAgentContext: AgentContext) { + const { Router, json, urlencoded } = importExpress() + + // TODO: it is currently not possible to initialize an agent + // shut it down, and then start it again, as the + // express router is configured with a specific `AgentContext` instance + // and dependency manager. One option is to always create a new router + // but then users cannot pass their own router implementation. + // We need to find a proper way to fix this. + + // We use separate context router and endpoint router. Context router handles the linking of the request + // to a specific agent context. Endpoint router only knows about a single context + const endpointRouter = Router() + const contextRouter = this.config.router + + // parse application/x-www-form-urlencoded + contextRouter.use(urlencoded({ extended: false })) + // parse application/json + contextRouter.use(json()) + + contextRouter.param('issuerId', async (req: OpenId4VcIssuanceRequest, _res, next, issuerId: string) => { + if (!issuerId) { + rootAgentContext.config.logger.debug('No issuerId provided for incoming oid4vci request, returning 404') + _res.status(404).send('Not found') + } + + let agentContext: AgentContext | undefined = undefined + + try { + // FIXME: should we create combined openId actor record? + agentContext = await getAgentContextForActorId(rootAgentContext, issuerId) + const issuerApi = agentContext.dependencyManager.resolve(OpenId4VcIssuerApi) + const issuer = await issuerApi.getByIssuerId(issuerId) + + req.requestContext = { + agentContext, + issuer, + } + } catch (error) { + agentContext?.config.logger.error( + 'Failed to correlate incoming oid4vci request to existing tenant and issuer', + { + error, + } + ) + // If the opening failed + await agentContext?.endSession() + + return _res.status(404).send('Not found') + } + + next() + }) + + contextRouter.use('/:issuerId', endpointRouter) + + // Configure endpoints + configureIssuerMetadataEndpoint(endpointRouter) + configureCredentialOfferEndpoint(endpointRouter, this.config.credentialOfferEndpoint) + configureAccessTokenEndpoint(endpointRouter, this.config.accessTokenEndpoint) + configureCredentialEndpoint(endpointRouter, this.config.credentialEndpoint) + + // First one will be called for all requests (when next is called) + contextRouter.use(async (req: OpenId4VcIssuanceRequest, _res: unknown, next) => { + const { agentContext } = getRequestContext(req) + await agentContext.endSession() + next() + }) + + // This one will be called for all errors that are thrown + // eslint-disable-next-line @typescript-eslint/no-explicit-any + contextRouter.use(async (_error: unknown, req: OpenId4VcIssuanceRequest, _res: unknown, next: any) => { + const { agentContext } = getRequestContext(req) + await agentContext.endSession() + next() + }) + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts new file mode 100644 index 0000000000..71eaa43c9a --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts @@ -0,0 +1,97 @@ +import type { + OpenId4VciAccessTokenEndpointConfig, + OpenId4VciCredentialEndpointConfig, + OpenId4VciCredentialOfferEndpointConfig, +} from './router' +import type { Optional } from '@credo-ts/core' +import type { Router } from 'express' + +import { importExpress } from '../shared/router' + +const DEFAULT_C_NONCE_EXPIRES_IN = 5 * 60 // 5 minutes +const DEFAULT_TOKEN_EXPIRES_IN = 3 * 60 // 3 minutes +const DEFAULT_PRE_AUTH_CODE_EXPIRES_IN = 3 * 60 // 3 minutes + +export interface OpenId4VcIssuerModuleConfigOptions { + /** + * Base url at which the issuer endpoints will be hosted. All endpoints will be exposed with + * this path as prefix. + */ + baseUrl: string + + /** + * Express router on which the openid4vci endpoints will be registered. If + * no router is provided, a new one will be created. + * + * NOTE: you must manually register the router on your express app and + * expose this on a public url that is reachable when `baseUrl` is called. + */ + router?: Router + + endpoints: { + credentialOffer?: Optional + credential: Optional + accessToken?: Optional< + OpenId4VciAccessTokenEndpointConfig, + 'cNonceExpiresInSeconds' | 'endpointPath' | 'preAuthorizedCodeExpirationInSeconds' | 'tokenExpiresInSeconds' + > + } +} + +export class OpenId4VcIssuerModuleConfig { + private options: OpenId4VcIssuerModuleConfigOptions + public readonly router: Router + + public constructor(options: OpenId4VcIssuerModuleConfigOptions) { + this.options = options + + this.router = options.router ?? importExpress().Router() + } + + public get baseUrl() { + return this.options.baseUrl + } + + /** + * Get the credential endpoint config, with default values set + */ + public get credentialEndpoint(): OpenId4VciCredentialEndpointConfig { + // Use user supplied options, or return defaults. + const userOptions = this.options.endpoints.credential + + return { + ...userOptions, + endpointPath: userOptions.endpointPath ?? '/credential', + } + } + + /** + * Get the access token endpoint config, with default values set + */ + public get accessTokenEndpoint(): OpenId4VciAccessTokenEndpointConfig { + // Use user supplied options, or return defaults. + const userOptions = this.options.endpoints.accessToken ?? {} + + return { + ...userOptions, + endpointPath: userOptions.endpointPath ?? '/token', + cNonceExpiresInSeconds: userOptions.cNonceExpiresInSeconds ?? DEFAULT_C_NONCE_EXPIRES_IN, + preAuthorizedCodeExpirationInSeconds: + userOptions.preAuthorizedCodeExpirationInSeconds ?? DEFAULT_PRE_AUTH_CODE_EXPIRES_IN, + tokenExpiresInSeconds: userOptions.tokenExpiresInSeconds ?? DEFAULT_TOKEN_EXPIRES_IN, + } + } + + /** + * Get the hosted credential offer endpoint config, with default values set + */ + public get credentialOfferEndpoint(): OpenId4VciCredentialOfferEndpointConfig { + // Use user supplied options, or return defaults. + const userOptions = this.options.endpoints.credentialOffer ?? {} + + return { + ...userOptions, + endpointPath: userOptions.endpointPath ?? '/offers', + } + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts new file mode 100644 index 0000000000..057241d86c --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -0,0 +1,603 @@ +import type { + OpenId4VciCreateCredentialResponseOptions, + OpenId4VciCreateCredentialOfferOptions, + OpenId4VciCreateIssuerOptions, + OpenId4VciPreAuthorizedCodeFlowConfig, + OpenId4VcIssuerMetadata, + OpenId4VciSignSdJwtCredential, + OpenId4VciSignW3cCredential, +} from './OpenId4VcIssuerServiceOptions' +import type { OpenId4VcIssuanceSessionRecord } from './repository' +import type { + OpenId4VcCredentialHolderBinding, + OpenId4VciCredentialOfferPayload, + OpenId4VciCredentialRequest, + OpenId4VciCredentialSupported, + OpenId4VciCredentialSupportedWithId, +} from '../shared' +import type { AgentContext, DidDocument, Query, QueryOptions } from '@credo-ts/core' +import type { Grant, JWTVerifyCallback } from '@sphereon/oid4vci-common' +import type { + CredentialDataSupplier, + CredentialDataSupplierArgs, + CredentialIssuanceInput, + CredentialSignerCallback, +} from '@sphereon/oid4vci-issuer' +import type { ICredential } from '@sphereon/ssi-types' + +import { + SdJwtVcApi, + CredoError, + ClaimFormat, + DidsApi, + equalsIgnoreOrder, + getJwkFromJson, + getJwkFromKey, + getKeyFromVerificationMethod, + injectable, + joinUriParts, + JsonEncoder, + JsonTransformer, + JwsService, + Jwt, + KeyType, + utils, + W3cCredentialService, +} from '@credo-ts/core' +import { VcIssuerBuilder } from '@sphereon/oid4vci-issuer' + +import { getOfferedCredentials, OpenId4VciCredentialFormatProfile } from '../shared' +import { storeActorIdForContextCorrelationId } from '../shared/router' +import { getSphereonVerifiableCredential } from '../shared/transform' +import { getProofTypeFromKey } from '../shared/utils' + +import { OpenId4VcIssuanceSessionState } from './OpenId4VcIssuanceSessionState' +import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' +import { OpenId4VcIssuerRepository, OpenId4VcIssuerRecord, OpenId4VcIssuanceSessionRepository } from './repository' +import { OpenId4VcCNonceStateManager } from './repository/OpenId4VcCNonceStateManager' +import { OpenId4VcCredentialOfferSessionStateManager } from './repository/OpenId4VcCredentialOfferSessionStateManager' +import { OpenId4VcCredentialOfferUriStateManager } from './repository/OpenId4VcCredentialOfferUriStateManager' +import { getCNonceFromCredentialRequest } from './util/credentialRequest' + +const w3cOpenId4VcFormats = [ + OpenId4VciCredentialFormatProfile.JwtVcJson, + OpenId4VciCredentialFormatProfile.JwtVcJsonLd, + OpenId4VciCredentialFormatProfile.LdpVc, +] + +/** + * @internal + */ +@injectable() +export class OpenId4VcIssuerService { + private w3cCredentialService: W3cCredentialService + private jwsService: JwsService + private openId4VcIssuerConfig: OpenId4VcIssuerModuleConfig + private openId4VcIssuerRepository: OpenId4VcIssuerRepository + private openId4VcIssuanceSessionRepository: OpenId4VcIssuanceSessionRepository + + public constructor( + w3cCredentialService: W3cCredentialService, + jwsService: JwsService, + openId4VcIssuerConfig: OpenId4VcIssuerModuleConfig, + openId4VcIssuerRepository: OpenId4VcIssuerRepository, + openId4VcIssuanceSessionRepository: OpenId4VcIssuanceSessionRepository + ) { + this.w3cCredentialService = w3cCredentialService + this.jwsService = jwsService + this.openId4VcIssuerConfig = openId4VcIssuerConfig + this.openId4VcIssuerRepository = openId4VcIssuerRepository + this.openId4VcIssuanceSessionRepository = openId4VcIssuanceSessionRepository + } + + public async createCredentialOffer( + agentContext: AgentContext, + options: OpenId4VciCreateCredentialOfferOptions & { issuer: OpenId4VcIssuerRecord } + ) { + const { preAuthorizedCodeFlowConfig, issuer, offeredCredentials } = options + + const vcIssuer = this.getVcIssuer(agentContext, issuer) + + // this checks if the structure of the credentials is correct + // it throws an error if a offered credential cannot be found in the credentialsSupported + getOfferedCredentials(options.offeredCredentials, vcIssuer.issuerMetadata.credentials_supported) + + const uniqueOfferedCredentials = Array.from(new Set(options.offeredCredentials)) + if (uniqueOfferedCredentials.length !== offeredCredentials.length) { + throw new CredoError('All offered credentials must have unique ids.') + } + + // We always use shortened URIs currently + const hostedCredentialOfferUri = joinUriParts(vcIssuer.issuerMetadata.credential_issuer, [ + this.openId4VcIssuerConfig.credentialOfferEndpoint.endpointPath, + // It doesn't really matter what the url is, as long as it's unique + utils.uuid(), + ]) + + let { uri } = await vcIssuer.createCredentialOfferURI({ + grants: await this.getGrantsFromConfig(agentContext, preAuthorizedCodeFlowConfig), + credentials: offeredCredentials, + credentialOfferUri: hostedCredentialOfferUri, + baseUri: options.baseUri, + credentialDataSupplierInput: options.issuanceMetadata, + }) + + // FIXME: https://github.com/Sphereon-Opensource/OID4VCI/issues/102 + if (uri.includes(hostedCredentialOfferUri)) { + uri = uri.replace(hostedCredentialOfferUri, encodeURIComponent(hostedCredentialOfferUri)) + } + + const issuanceSession = await this.openId4VcIssuanceSessionRepository.getSingleByQuery(agentContext, { + credentialOfferUri: hostedCredentialOfferUri, + }) + + return { + issuanceSession, + credentialOffer: uri, + } + } + + /** + * find the issuance session associated with a credential request. You can optionally provide a issuer id if + * the issuer that the request is associated with is already known. + */ + public async findIssuanceSessionForCredentialRequest( + agentContext: AgentContext, + { credentialRequest, issuerId }: { credentialRequest: OpenId4VciCredentialRequest; issuerId?: string } + ) { + const cNonce = getCNonceFromCredentialRequest(credentialRequest) + + const issuanceSession = await this.openId4VcIssuanceSessionRepository.findSingleByQuery(agentContext, { + issuerId, + cNonce, + }) + + return issuanceSession + } + + public async createCredentialResponse( + agentContext: AgentContext, + options: OpenId4VciCreateCredentialResponseOptions & { issuanceSession: OpenId4VcIssuanceSessionRecord } + ) { + options.issuanceSession.assertState([ + OpenId4VcIssuanceSessionState.AccessTokenCreated, + OpenId4VcIssuanceSessionState.CredentialRequestReceived, + // It is possible to issue multiple credentials in one session + OpenId4VcIssuanceSessionState.CredentialsPartiallyIssued, + ]) + const { credentialRequest, issuanceSession } = options + if (!credentialRequest.proof) throw new CredoError('No proof defined in the credentialRequest.') + + const issuer = await this.getIssuerByIssuerId(agentContext, options.issuanceSession.issuerId) + + const cNonce = getCNonceFromCredentialRequest(credentialRequest) + if (issuanceSession.cNonce !== cNonce) { + throw new CredoError('The cNonce in the credential request does not match the cNonce in the issuance session.') + } + + if (!issuanceSession.cNonceExpiresAt) { + throw new CredoError('Missing required cNonceExpiresAt in the issuance session. Assuming cNonce is not valid') + } + if (Date.now() > issuanceSession.cNonceExpiresAt.getTime()) { + throw new CredoError('The cNonce has expired.') + } + + const vcIssuer = this.getVcIssuer(agentContext, issuer) + + const credentialResponse = await vcIssuer.issueCredential({ + credentialRequest, + tokenExpiresIn: this.openId4VcIssuerConfig.accessTokenEndpoint.tokenExpiresInSeconds, + + // This can just be combined with signing callback right? + credentialDataSupplier: this.getCredentialDataSupplier(agentContext, { ...options, issuer }), + credentialDataSupplierInput: issuanceSession.issuanceMetadata, + responseCNonce: undefined, + }) + + const updatedIssuanceSession = await this.openId4VcIssuanceSessionRepository.getById( + agentContext, + issuanceSession.id + ) + if (!credentialResponse.credential) { + updatedIssuanceSession.state = OpenId4VcIssuanceSessionState.Error + updatedIssuanceSession.errorMessage = 'No credential found in the issueCredentialResponse.' + await this.openId4VcIssuanceSessionRepository.update(agentContext, updatedIssuanceSession) + throw new CredoError(updatedIssuanceSession.errorMessage) + } + + if (credentialResponse.acceptance_token) { + updatedIssuanceSession.state = OpenId4VcIssuanceSessionState.Error + updatedIssuanceSession.errorMessage = 'Acceptance token not yet supported.' + await this.openId4VcIssuanceSessionRepository.update(agentContext, updatedIssuanceSession) + throw new CredoError(updatedIssuanceSession.errorMessage) + } + + return { + credentialResponse, + issuanceSession: updatedIssuanceSession, + } + } + + public async findIssuanceSessionsByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ) { + return this.openId4VcIssuanceSessionRepository.findByQuery(agentContext, query, queryOptions) + } + + public async getIssuanceSessionById(agentContext: AgentContext, issuanceSessionId: string) { + return this.openId4VcIssuanceSessionRepository.getById(agentContext, issuanceSessionId) + } + + public async getAllIssuers(agentContext: AgentContext) { + return this.openId4VcIssuerRepository.getAll(agentContext) + } + + public async getIssuerByIssuerId(agentContext: AgentContext, issuerId: string) { + return this.openId4VcIssuerRepository.getByIssuerId(agentContext, issuerId) + } + + public async updateIssuer(agentContext: AgentContext, issuer: OpenId4VcIssuerRecord) { + return this.openId4VcIssuerRepository.update(agentContext, issuer) + } + + public async createIssuer(agentContext: AgentContext, options: OpenId4VciCreateIssuerOptions) { + // TODO: ideally we can store additional data with a key, such as: + // - createdAt + // - purpose + const accessTokenSignerKey = await agentContext.wallet.createKey({ + keyType: KeyType.Ed25519, + }) + const openId4VcIssuer = new OpenId4VcIssuerRecord({ + issuerId: options.issuerId ?? utils.uuid(), + display: options.display, + accessTokenPublicKeyFingerprint: accessTokenSignerKey.fingerprint, + credentialsSupported: options.credentialsSupported, + }) + + await this.openId4VcIssuerRepository.save(agentContext, openId4VcIssuer) + await storeActorIdForContextCorrelationId(agentContext, openId4VcIssuer.issuerId) + return openId4VcIssuer + } + + public async rotateAccessTokenSigningKey(agentContext: AgentContext, issuer: OpenId4VcIssuerRecord) { + const accessTokenSignerKey = await agentContext.wallet.createKey({ + keyType: KeyType.Ed25519, + }) + + // TODO: ideally we can remove the previous key + issuer.accessTokenPublicKeyFingerprint = accessTokenSignerKey.fingerprint + await this.openId4VcIssuerRepository.update(agentContext, issuer) + } + + public getIssuerMetadata(agentContext: AgentContext, issuerRecord: OpenId4VcIssuerRecord): OpenId4VcIssuerMetadata { + const config = agentContext.dependencyManager.resolve(OpenId4VcIssuerModuleConfig) + const issuerUrl = joinUriParts(config.baseUrl, [issuerRecord.issuerId]) + + const issuerMetadata = { + issuerUrl, + tokenEndpoint: joinUriParts(issuerUrl, [config.accessTokenEndpoint.endpointPath]), + credentialEndpoint: joinUriParts(issuerUrl, [config.credentialEndpoint.endpointPath]), + credentialsSupported: issuerRecord.credentialsSupported, + issuerDisplay: issuerRecord.display, + } satisfies OpenId4VcIssuerMetadata + + return issuerMetadata + } + + private getJwtVerifyCallback = (agentContext: AgentContext): JWTVerifyCallback => { + return async (opts) => { + let didDocument = undefined as DidDocument | undefined + const { isValid, jws } = await this.jwsService.verifyJws(agentContext, { + jws: opts.jwt, + // Only handles kid as did resolution. JWK is handled by jws service + jwkResolver: async ({ protectedHeader: { kid } }) => { + if (!kid) throw new CredoError('Missing kid in protected header.') + if (!kid.startsWith('did:')) throw new CredoError('Only did is supported for kid identifier') + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + didDocument = await didsApi.resolveDidDocument(kid) + const verificationMethod = didDocument.dereferenceKey(kid, ['authentication', 'assertionMethod']) + const key = getKeyFromVerificationMethod(verificationMethod) + return getJwkFromKey(key) + }, + }) + + if (!isValid) throw new CredoError('Could not verify JWT signature.') + + // TODO: the jws service should return some better decoded metadata also from the resolver + // as currently is less useful if you afterwards need properties from the JWS + const firstJws = jws.signatures[0] + const protectedHeader = JsonEncoder.fromBase64(firstJws.protected) + return { + jwt: { header: protectedHeader, payload: JsonEncoder.fromBase64(jws.payload) }, + kid: protectedHeader.kid, + jwk: protectedHeader.jwk ? getJwkFromJson(protectedHeader.jwk) : undefined, + did: didDocument?.id, + alg: protectedHeader.alg, + didDocument, + } + } + } + + private getVcIssuer(agentContext: AgentContext, issuer: OpenId4VcIssuerRecord) { + const issuerMetadata = this.getIssuerMetadata(agentContext, issuer) + + const builder = new VcIssuerBuilder() + .withCredentialIssuer(issuerMetadata.issuerUrl) + .withCredentialEndpoint(issuerMetadata.credentialEndpoint) + .withTokenEndpoint(issuerMetadata.tokenEndpoint) + .withCredentialsSupported(issuerMetadata.credentialsSupported) + .withCNonceStateManager(new OpenId4VcCNonceStateManager(agentContext, issuer.issuerId)) + .withCredentialOfferStateManager(new OpenId4VcCredentialOfferSessionStateManager(agentContext, issuer.issuerId)) + .withCredentialOfferURIStateManager(new OpenId4VcCredentialOfferUriStateManager(agentContext, issuer.issuerId)) + .withJWTVerifyCallback(this.getJwtVerifyCallback(agentContext)) + .withCredentialSignerCallback(() => { + throw new CredoError('Credential signer callback should be overwritten. This is a no-op') + }) + + if (issuerMetadata.authorizationServer) { + builder.withAuthorizationServer(issuerMetadata.authorizationServer) + } + + if (issuerMetadata.issuerDisplay) { + builder.withIssuerDisplay(issuerMetadata.issuerDisplay) + } + + return builder.build() + } + + private async getGrantsFromConfig( + agentContext: AgentContext, + preAuthorizedCodeFlowConfig: OpenId4VciPreAuthorizedCodeFlowConfig + ) { + const grants: Grant = { + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': + preAuthorizedCodeFlowConfig.preAuthorizedCode ?? (await agentContext.wallet.generateNonce()), + user_pin_required: preAuthorizedCodeFlowConfig.userPinRequired ?? false, + }, + } + + return grants + } + + private findOfferedCredentialsMatchingRequest( + credentialOffer: OpenId4VciCredentialOfferPayload, + credentialRequest: OpenId4VciCredentialRequest, + credentialsSupported: OpenId4VciCredentialSupported[], + issuanceSession: OpenId4VcIssuanceSessionRecord + ): OpenId4VciCredentialSupportedWithId[] { + const offeredCredentials = getOfferedCredentials(credentialOffer.credentials, credentialsSupported) + + return offeredCredentials.filter((offeredCredential) => { + if (offeredCredential.format !== credentialRequest.format) return false + if (issuanceSession.issuedCredentials.includes(offeredCredential.id)) return false + + if ( + credentialRequest.format === OpenId4VciCredentialFormatProfile.JwtVcJson && + offeredCredential.format === credentialRequest.format + ) { + return equalsIgnoreOrder(offeredCredential.types, credentialRequest.types) + } else if ( + credentialRequest.format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd && + offeredCredential.format === credentialRequest.format + ) { + return equalsIgnoreOrder(offeredCredential.types, credentialRequest.credential_definition.types) + } else if ( + credentialRequest.format === OpenId4VciCredentialFormatProfile.LdpVc && + offeredCredential.format === credentialRequest.format + ) { + return equalsIgnoreOrder(offeredCredential.types, credentialRequest.credential_definition.types) + } else if ( + credentialRequest.format === OpenId4VciCredentialFormatProfile.SdJwtVc && + offeredCredential.format === credentialRequest.format + ) { + return offeredCredential.vct === credentialRequest.vct + } + + return false + }) + } + + private getSdJwtVcCredentialSigningCallback = ( + agentContext: AgentContext, + options: OpenId4VciSignSdJwtCredential + ): CredentialSignerCallback => { + return async () => { + const sdJwtVcApi = agentContext.dependencyManager.resolve(SdJwtVcApi) + + const sdJwtVc = await sdJwtVcApi.sign(options) + return getSphereonVerifiableCredential(sdJwtVc) + } + } + + private getW3cCredentialSigningCallback = ( + agentContext: AgentContext, + options: OpenId4VciSignW3cCredential + ): CredentialSignerCallback => { + return async (opts) => { + const { jwtVerifyResult, format } = opts + const { kid, didDocument: holderDidDocument } = jwtVerifyResult + + if (!kid) throw new CredoError('Missing Kid. Cannot create the holder binding') + if (!holderDidDocument) throw new CredoError('Missing did document. Cannot create the holder binding.') + if (!format) throw new CredoError('Missing format. Cannot issue credential.') + + const formatMap: Record = { + [OpenId4VciCredentialFormatProfile.JwtVcJson]: ClaimFormat.JwtVc, + [OpenId4VciCredentialFormatProfile.JwtVcJsonLd]: ClaimFormat.JwtVc, + [OpenId4VciCredentialFormatProfile.LdpVc]: ClaimFormat.LdpVc, + } + const w3cServiceFormat = formatMap[format] + + // Set the binding on the first credential subject if not set yet + // on any subject + if (!options.credential.credentialSubjectIds.includes(holderDidDocument.id)) { + const credentialSubject = Array.isArray(options.credential.credentialSubject) + ? options.credential.credentialSubject[0] + : options.credential.credentialSubject + credentialSubject.id = holderDidDocument.id + } + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const issuerDidDocument = await didsApi.resolveDidDocument(options.verificationMethod) + const verificationMethod = issuerDidDocument.dereferenceVerificationMethod(options.verificationMethod) + + if (w3cServiceFormat === ClaimFormat.JwtVc) { + const key = getKeyFromVerificationMethod(verificationMethod) + const alg = getJwkFromKey(key).supportedSignatureAlgorithms[0] + + if (!alg) { + throw new CredoError(`No supported JWA signature algorithms for key type ${key.keyType}`) + } + + const signed = await this.w3cCredentialService.signCredential(agentContext, { + format: w3cServiceFormat, + credential: options.credential, + verificationMethod: options.verificationMethod, + alg, + }) + + return getSphereonVerifiableCredential(signed) + } else { + const key = getKeyFromVerificationMethod(verificationMethod) + const proofType = getProofTypeFromKey(agentContext, key) + + const signed = await this.w3cCredentialService.signCredential(agentContext, { + format: w3cServiceFormat, + credential: options.credential, + verificationMethod: options.verificationMethod, + proofType: proofType, + }) + + return getSphereonVerifiableCredential(signed) + } + } + } + + private async getHolderBindingFromRequest(credentialRequest: OpenId4VciCredentialRequest) { + if (!credentialRequest.proof?.jwt) throw new CredoError('Received a credential request without a proof') + + const jwt = Jwt.fromSerializedJwt(credentialRequest.proof.jwt) + + if (jwt.header.kid) { + if (!jwt.header.kid.startsWith('did:')) { + throw new CredoError("Only did is supported for 'kid' identifier") + } else if (!jwt.header.kid.includes('#')) { + throw new CredoError( + `kid containing did MUST point to a specific key within the did document: ${jwt.header.kid}` + ) + } + + return { + method: 'did', + didUrl: jwt.header.kid, + } satisfies OpenId4VcCredentialHolderBinding + } else if (jwt.header.jwk) { + return { + method: 'jwk', + jwk: getJwkFromJson(jwt.header.jwk), + } satisfies OpenId4VcCredentialHolderBinding + } else { + throw new CredoError('Either kid or jwk must be present in credential request proof header') + } + } + + private getCredentialDataSupplier = ( + agentContext: AgentContext, + options: OpenId4VciCreateCredentialResponseOptions & { + issuer: OpenId4VcIssuerRecord + issuanceSession: OpenId4VcIssuanceSessionRecord + } + ): CredentialDataSupplier => { + return async (args: CredentialDataSupplierArgs) => { + const { issuanceSession, issuer } = options + const { credentialRequest } = args + + const issuerMetadata = this.getIssuerMetadata(agentContext, issuer) + + const offeredCredentialsMatchingRequest = this.findOfferedCredentialsMatchingRequest( + options.issuanceSession.credentialOfferPayload, + credentialRequest as OpenId4VciCredentialRequest, + issuerMetadata.credentialsSupported, + issuanceSession + ) + + if (offeredCredentialsMatchingRequest.length === 0) { + throw new CredoError('No offered credentials match the credential request.') + } + + if (offeredCredentialsMatchingRequest.length > 1) { + agentContext.config.logger.debug( + 'Multiple credentials from credentials supported matching request, picking first one.' + ) + } + + const mapper = + options.credentialRequestToCredentialMapper ?? + this.openId4VcIssuerConfig.credentialEndpoint.credentialRequestToCredentialMapper + + const holderBinding = await this.getHolderBindingFromRequest(credentialRequest as OpenId4VciCredentialRequest) + const signOptions = await mapper({ + agentContext, + issuanceSession, + holderBinding, + credentialOffer: { credential_offer: issuanceSession.credentialOfferPayload }, + credentialRequest: credentialRequest as OpenId4VciCredentialRequest, + credentialsSupported: offeredCredentialsMatchingRequest, + }) + + const credentialHasAlreadyBeenIssued = issuanceSession.issuedCredentials.includes( + signOptions.credentialSupportedId + ) + if (credentialHasAlreadyBeenIssued) { + throw new CredoError( + `The requested credential with id '${signOptions.credentialSupportedId}' has already been issued.` + ) + } + + const updatedIssuanceSession = await this.openId4VcIssuanceSessionRepository.getById( + agentContext, + issuanceSession.id + ) + updatedIssuanceSession.issuedCredentials.push(signOptions.credentialSupportedId) + await this.openId4VcIssuanceSessionRepository.update(agentContext, updatedIssuanceSession) + + if (signOptions.format === ClaimFormat.JwtVc || signOptions.format === ClaimFormat.LdpVc) { + if (!w3cOpenId4VcFormats.includes(credentialRequest.format as OpenId4VciCredentialFormatProfile)) { + throw new CredoError( + `The credential to be issued does not match the request. Cannot issue a W3cCredential if the client expects a credential of format '${credentialRequest.format}'.` + ) + } + + return { + format: credentialRequest.format, + credential: JsonTransformer.toJSON(signOptions.credential) as ICredential, + signCallback: this.getW3cCredentialSigningCallback(agentContext, signOptions), + } + } else if (signOptions.format === ClaimFormat.SdJwtVc) { + if (credentialRequest.format !== OpenId4VciCredentialFormatProfile.SdJwtVc) { + throw new CredoError( + `Invalid credential format. Expected '${OpenId4VciCredentialFormatProfile.SdJwtVc}', received '${credentialRequest.format}'.` + ) + } + if (credentialRequest.vct !== signOptions.payload.vct) { + throw new CredoError( + `The types of the offered credentials do not match the types of the requested credential. Offered '${signOptions.payload.vct}' Requested '${credentialRequest.vct}'.` + ) + } + + return { + format: credentialRequest.format, + // NOTE: we don't use the credential value here as we pass the credential directly to the singer + credential: { ...signOptions.payload } as unknown as CredentialIssuanceInput, + signCallback: this.getSdJwtVcCredentialSigningCallback(agentContext, signOptions), + } + } else { + throw new CredoError(`Unsupported credential format`) + } + } + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts new file mode 100644 index 0000000000..5775d01f63 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -0,0 +1,126 @@ +import type { OpenId4VcIssuanceSessionRecord } from './repository' +import type { + OpenId4VcCredentialHolderBinding, + OpenId4VciCredentialOffer, + OpenId4VciCredentialRequest, + OpenId4VciCredentialSupported, + OpenId4VciCredentialSupportedWithId, + OpenId4VciIssuerMetadataDisplay, +} from '../shared' +import type { AgentContext, ClaimFormat, W3cCredential, SdJwtVcSignOptions } from '@credo-ts/core' + +export interface OpenId4VciPreAuthorizedCodeFlowConfig { + preAuthorizedCode?: string + userPinRequired?: boolean +} + +export type OpenId4VcIssuerMetadata = { + // The Credential Issuer's identifier. (URL using the https scheme) + issuerUrl: string + credentialEndpoint: string + tokenEndpoint: string + authorizationServer?: string + + issuerDisplay?: OpenId4VciIssuerMetadataDisplay[] + credentialsSupported: OpenId4VciCredentialSupported[] +} + +export interface OpenId4VciCreateCredentialOfferOptions { + // NOTE: v11 of OID4VCI supports both inline and referenced (to credentials_supported.id) credential offers. + // In draft 12 the inline credential offers have been removed and to make the migration to v12 easier + // we only support referenced credentials in an offer + offeredCredentials: string[] + + /** + * baseUri for the credential offer uri. By default `openid-credential-offer://` will be used + * if no value is provided. If a value is provided, make sure it contains the scheme as well as `://`. + */ + baseUri?: string + + preAuthorizedCodeFlowConfig: OpenId4VciPreAuthorizedCodeFlowConfig + + /** + * Metadata about the issuance, that will be stored in the issuance session record and + * passed to the credential request to credential mapper. This can be used to e.g. store an + * user identifier so user data can be fetched in the credential mapper, or the actual credential + * data. + */ + issuanceMetadata?: Record +} + +export interface OpenId4VciCreateCredentialResponseOptions { + credentialRequest: OpenId4VciCredentialRequest + + /** + * You can optionally provide a credential request to credential mapper that will be + * dynamically invoked to return credential data based on the credential request. + * + * If not provided, the `credentialRequestToCredentialMapper` from the agent config + * will be used. + */ + credentialRequestToCredentialMapper?: OpenId4VciCredentialRequestToCredentialMapper +} + +// FIXME: Flows: +// - provide credential data at time of offer creation (NOT SUPPORTED) +// - provide credential data at time of calling createCredentialResponse (partially supported by passing in mapper to this method -> preferred as it gives you request data dynamically) +// - provide credential data dynamically using this method (SUPPORTED) +// mapper should get input data passed (which is supplied to offer or create response) like credentialDataSupplierInput in sphereon lib +export type OpenId4VciCredentialRequestToCredentialMapper = (options: { + agentContext: AgentContext + + /** + * The issuance session associated with the credential request. You can extract the + * issuance metadata from this record if passed in the offer creation method. + */ + issuanceSession: OpenId4VcIssuanceSessionRecord + + /** + * The credential request received from the wallet + */ + credentialRequest: OpenId4VciCredentialRequest + + /** + * The offer associated with the credential request + */ + credentialOffer: OpenId4VciCredentialOffer + + /** + * Verified key binding material that should be included in the credential + * + * Can either be bound to did or a JWK (in case of for ex. SD-JWT) + */ + holderBinding: OpenId4VcCredentialHolderBinding + + /** + * The credentials supported entries from the issuer metadata that were offered + * and match the incoming request + * + * NOTE: in v12 this will probably become a single entry, as it will be matched on id + */ + credentialsSupported: OpenId4VciCredentialSupported[] +}) => Promise | OpenId4VciSignCredential + +export type OpenId4VciSignCredential = OpenId4VciSignSdJwtCredential | OpenId4VciSignW3cCredential + +export interface OpenId4VciSignSdJwtCredential extends SdJwtVcSignOptions { + credentialSupportedId: string + format: ClaimFormat.SdJwtVc | `${ClaimFormat.SdJwtVc}` +} + +export interface OpenId4VciSignW3cCredential { + credentialSupportedId: string + format: ClaimFormat.JwtVc | `${ClaimFormat.JwtVc}` | ClaimFormat.LdpVc | `${ClaimFormat.LdpVc}` + verificationMethod: string + credential: W3cCredential +} + +export interface OpenId4VciCreateIssuerOptions { + /** + * Id of the issuer, not the id of the issuer record. Will be exposed publicly + */ + issuerId?: string + + credentialsSupported: OpenId4VciCredentialSupportedWithId[] + display?: OpenId4VciIssuerMetadataDisplay[] +} diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/OpenId4VcIsserModule.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/OpenId4VcIsserModule.test.ts new file mode 100644 index 0000000000..f6351696ed --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/__tests__/OpenId4VcIsserModule.test.ts @@ -0,0 +1,52 @@ +import type { DependencyManager } from '@credo-ts/core' + +import { Router } from 'express' + +import { getAgentContext } from '../../../../core/tests' +import { OpenId4VcIssuerModule } from '../OpenId4VcIssuerModule' +import { OpenId4VcIssuerModuleConfig } from '../OpenId4VcIssuerModuleConfig' +import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' +import { OpenId4VcIssuanceSessionRepository } from '../repository' +import { OpenId4VcIssuerRepository } from '../repository/OpenId4VcIssuerRepository' + +const dependencyManager = { + registerInstance: jest.fn(), + registerSingleton: jest.fn(), + registerContextScoped: jest.fn(), + resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), +} as unknown as DependencyManager + +const agentContext = getAgentContext() + +describe('OpenId4VcIssuerModule', () => { + test('registers dependencies on the dependency manager', async () => { + const options = { + baseUrl: 'http://localhost:3000', + endpoints: { + credential: { + credentialRequestToCredentialMapper: () => { + throw new Error('Not implemented') + }, + }, + }, + router: Router(), + } as const + const openId4VcClientModule = new OpenId4VcIssuerModule(options) + openId4VcClientModule.register(dependencyManager) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith( + OpenId4VcIssuerModuleConfig, + new OpenId4VcIssuerModuleConfig(options) + ) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcIssuerService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcIssuanceSessionRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcIssuerRepository) + + await openId4VcClientModule.initialize(agentContext) + + expect(openId4VcClientModule.config.router).toBeDefined() + }) +}) diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.test.ts new file mode 100644 index 0000000000..cb5f52cd5c --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.test.ts @@ -0,0 +1,688 @@ +import type { OpenId4VciCredentialRequest, OpenId4VciCredentialSupportedWithId } from '../../shared' +import type { + OpenId4VcIssuerMetadata, + OpenId4VciCredentialRequestToCredentialMapper, +} from '../OpenId4VcIssuerServiceOptions' +import type { OpenId4VcIssuerRecord } from '../repository' +import type { + AgentContext, + KeyDidCreateOptions, + VerificationMethod, + W3cVerifiableCredential, + W3cVerifyCredentialResult, +} from '@credo-ts/core' +import type { OriginalVerifiableCredential as SphereonW3cVerifiableCredential } from '@sphereon/ssi-types' + +import { + SdJwtVcApi, + JwtPayload, + Agent, + CredoError, + DidKey, + DidsApi, + JsonTransformer, + JwsService, + KeyType, + TypedArrayEncoder, + W3cCredential, + W3cCredentialService, + W3cCredentialSubject, + W3cIssuer, + W3cJsonLdVerifiableCredential, + W3cJwtVerifiableCredential, + equalsIgnoreOrder, + getJwkFromKey, + getKeyFromVerificationMethod, + w3cDate, +} from '@credo-ts/core' + +import { AskarModule } from '../../../../askar/src' +import { askarModuleConfig } from '../../../../askar/tests/helpers' +import { agentDependencies } from '../../../../node/src' +import { OpenId4VciCredentialFormatProfile } from '../../shared' +import { OpenId4VcIssuanceSessionState } from '../OpenId4VcIssuanceSessionState' +import { OpenId4VcIssuerModule } from '../OpenId4VcIssuerModule' +import { OpenId4VcIssuanceSessionRepository } from '../repository' + +const openBadgeCredential = { + id: 'https://openid4vc-issuer.com/credentials/OpenBadgeCredential', + format: OpenId4VciCredentialFormatProfile.JwtVcJson, + types: ['VerifiableCredential', 'OpenBadgeCredential'], +} satisfies OpenId4VciCredentialSupportedWithId + +const universityDegreeCredential = { + id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredential', + format: OpenId4VciCredentialFormatProfile.JwtVcJson, + types: ['VerifiableCredential', 'UniversityDegreeCredential'], +} satisfies OpenId4VciCredentialSupportedWithId + +const universityDegreeCredentialLd = { + id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialLd', + format: OpenId4VciCredentialFormatProfile.JwtVcJsonLd, + '@context': [], + types: ['VerifiableCredential', 'UniversityDegreeCredential'], +} satisfies OpenId4VciCredentialSupportedWithId + +const universityDegreeCredentialSdJwt = { + id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt', + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + vct: 'UniversityDegreeCredential', +} satisfies OpenId4VciCredentialSupportedWithId + +const modules = { + openId4VcIssuer: new OpenId4VcIssuerModule({ + baseUrl: 'https://openid4vc-issuer.com', + endpoints: { + credential: { + credentialRequestToCredentialMapper: () => { + throw new Error('Not implemented') + }, + }, + }, + }), + askar: new AskarModule(askarModuleConfig), +} + +const jwsService = new JwsService() + +const createCredentialRequest = async ( + agentContext: AgentContext, + options: { + issuerMetadata: OpenId4VcIssuerMetadata + credentialSupported: OpenId4VciCredentialSupportedWithId + nonce: string + kid: string + clientId?: string // use with the authorization code flow, + } +): Promise => { + const { credentialSupported, kid, nonce, issuerMetadata, clientId } = options + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(kid) + if (!didDocument.verificationMethod) { + throw new CredoError(`No verification method found for kid ${kid}`) + } + + const verificationMethod = didDocument.dereferenceKey(kid, ['authentication', 'assertionMethod']) + const key = getKeyFromVerificationMethod(verificationMethod) + const jwk = getJwkFromKey(key) + + const jws = await jwsService.createJwsCompact(agentContext, { + protectedHeaderOptions: { alg: jwk.supportedSignatureAlgorithms[0], kid, typ: 'openid4vci-proof+jwt' }, + payload: new JwtPayload({ + iat: Math.floor(Date.now() / 1000), // unix time + iss: clientId, + aud: issuerMetadata.issuerUrl, + additionalClaims: { + nonce, + }, + }), + key, + }) + + if (credentialSupported.format === OpenId4VciCredentialFormatProfile.JwtVcJson) { + return { ...credentialSupported, proof: { jwt: jws, proof_type: 'jwt' } } + } else if ( + credentialSupported.format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd || + credentialSupported.format === OpenId4VciCredentialFormatProfile.LdpVc + ) { + return { + format: credentialSupported.format, + credential_definition: { + '@context': credentialSupported['@context'], + types: credentialSupported.types, + }, + + proof: { jwt: jws, proof_type: 'jwt' }, + } + } else if (credentialSupported.format === OpenId4VciCredentialFormatProfile.SdJwtVc) { + return { ...credentialSupported, proof: { jwt: jws, proof_type: 'jwt' } } + } + + throw new Error('Unsupported format') +} + +const issuer = new Agent({ + config: { + label: 'OpenId4VcIssuer Test323', + walletConfig: { + id: 'openid4vc-Issuer-test323', + key: 'openid4vc-Issuer-test323', + }, + }, + dependencies: agentDependencies, + modules, +}) + +const holder = new Agent({ + config: { + label: 'OpenId4VciIssuer(Holder) Test323', + walletConfig: { + id: 'openid4vc-Issuer(Holder)-test323', + key: 'openid4vc-Issuer(Holder)-test323', + }, + }, + dependencies: agentDependencies, + modules, +}) + +describe('OpenId4VcIssuer', () => { + let issuerVerificationMethod: VerificationMethod + let issuerDid: string + let openId4VcIssuer: OpenId4VcIssuerRecord + + let holderKid: string + let holderVerificationMethod: VerificationMethod + let holderDid: string + + beforeEach(async () => { + await issuer.initialize() + await holder.initialize() + + const holderDidCreateResult = await holder.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + }) + + holderDid = holderDidCreateResult.didState.did as string + const holderDidKey = DidKey.fromDid(holderDid) + holderKid = `${holderDid}#${holderDidKey.key.fingerprint}` + const _holderVerificationMethod = holderDidCreateResult.didState.didDocument?.dereferenceKey(holderKid, [ + 'authentication', + ]) + if (!_holderVerificationMethod) throw new Error('No verification method found') + holderVerificationMethod = _holderVerificationMethod + + const issuerDidCreateResult = await issuer.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f') }, + }) + + issuerDid = issuerDidCreateResult.didState.did as string + + const issuerDidKey = DidKey.fromDid(issuerDid) + const issuerKid = `${issuerDid}#${issuerDidKey.key.fingerprint}` + const _issuerVerificationMethod = issuerDidCreateResult.didState.didDocument?.dereferenceKey(issuerKid, [ + 'authentication', + ]) + if (!_issuerVerificationMethod) throw new Error('No verification method found') + issuerVerificationMethod = _issuerVerificationMethod + + openId4VcIssuer = await issuer.modules.openId4VcIssuer.createIssuer({ + credentialsSupported: [ + openBadgeCredential, + universityDegreeCredential, + universityDegreeCredentialLd, + universityDegreeCredentialSdJwt, + ], + }) + }) + + afterEach(async () => { + await issuer.shutdown() + await issuer.wallet.delete() + + await holder.shutdown() + await holder.wallet.delete() + }) + + // This method is available on the holder service, + // would be nice to reuse + async function handleCredentialResponse( + agentContext: AgentContext, + sphereonVerifiableCredential: SphereonW3cVerifiableCredential, + credentialSupported: OpenId4VciCredentialSupportedWithId + ) { + if (credentialSupported.format === 'vc+sd-jwt' && typeof sphereonVerifiableCredential === 'string') { + const api = agentContext.dependencyManager.resolve(SdJwtVcApi) + await api.verify({ compactSdJwtVc: sphereonVerifiableCredential }) + return + } + + const w3cCredentialService = holder.context.dependencyManager.resolve(W3cCredentialService) + + let result: W3cVerifyCredentialResult + let w3cVerifiableCredential: W3cVerifiableCredential + + if (typeof sphereonVerifiableCredential === 'string') { + if (credentialSupported.format !== 'jwt_vc_json' && credentialSupported.format !== 'jwt_vc_json-ld') { + throw new Error(`Invalid format. ${credentialSupported.format}`) + } + w3cVerifiableCredential = W3cJwtVerifiableCredential.fromSerializedJwt(sphereonVerifiableCredential) + result = await w3cCredentialService.verifyCredential(holder.context, { credential: w3cVerifiableCredential }) + } else if (credentialSupported.format === 'ldp_vc') { + if (credentialSupported.format !== 'ldp_vc') throw new Error('Invalid format') + // validate jwt credentials + + w3cVerifiableCredential = JsonTransformer.fromJSON(sphereonVerifiableCredential, W3cJsonLdVerifiableCredential) + result = await w3cCredentialService.verifyCredential(holder.context, { credential: w3cVerifiableCredential }) + } else { + throw new CredoError(`Unsupported credential format`) + } + + if (!result.isValid) { + holder.context.config.logger.error('Failed to validate credential', { result }) + throw new CredoError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) + } + + if (equalsIgnoreOrder(w3cVerifiableCredential.type, credentialSupported.types) === false) { + throw new Error('Invalid credential type') + } + return w3cVerifiableCredential + } + + it('pre authorized code flow (sd-jwt-vc)', async () => { + const preAuthorizedCode = '1234567890' + + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: openId4VcIssuer.issuerId, + offeredCredentials: [universityDegreeCredentialSdJwt.id], + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, + }) + + const issuanceSessionRepository = issuer.context.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository) + result.issuanceSession.cNonce = '1234' + result.issuanceSession.cNonceExpiresAt = new Date(Date.now() + 30000) // 30 seconds + await issuanceSessionRepository.update(issuer.context, result.issuanceSession) + + expect(result).toMatchObject({ + credentialOffer: expect.stringMatching( + new RegExp( + `^openid-credential-offer://\\?credential_offer_uri=https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%2Foffers%2F.*$` + ) + ), + issuanceSession: { + credentialOfferPayload: { + credential_issuer: `https://openid4vc-issuer.com/${openId4VcIssuer.issuerId}`, + credentials: ['https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt'], + grants: { + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': '1234567890', + user_pin_required: false, + }, + }, + }, + }, + }) + + const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) + const credentialRequest = await createCredentialRequest(holder.context, { + credentialSupported: universityDegreeCredentialSdJwt, + issuerMetadata, + kid: holderKid, + nonce: result.issuanceSession.cNonce as string, + }) + + const issuanceSession = await issuer.modules.openId4VcIssuer.findIssuanceSessionForCredentialRequest({ + credentialRequest, + issuerId: openId4VcIssuer.issuerId, + }) + + if (!issuanceSession) { + throw new Error('No issuance session found') + } + + // We need to update the state, as it is checked and we're skipping the access token step + result.issuanceSession.state = OpenId4VcIssuanceSessionState.AccessTokenCreated + await issuanceSessionRepository.update(issuer.context, result.issuanceSession) + + const { credentialResponse } = await issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuanceSessionId: issuanceSession.id, + credentialRequest, + + credentialRequestToCredentialMapper: () => ({ + format: 'vc+sd-jwt', + payload: { vct: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, + issuer: { method: 'did', didUrl: issuerVerificationMethod.id }, + holder: { method: 'did', didUrl: holderVerificationMethod.id }, + disclosureFrame: { _sd: ['university', 'degree'] }, + credentialSupportedId: universityDegreeCredentialSdJwt.id, + }), + }) + + expect(credentialResponse).toEqual({ + c_nonce: expect.any(String), + c_nonce_expires_in: 300, + credential: expect.any(String), + format: 'vc+sd-jwt', + }) + + await handleCredentialResponse( + holder.context, + credentialResponse.credential as SphereonW3cVerifiableCredential, + universityDegreeCredentialSdJwt + ) + }) + + it('pre authorized code flow (jwt-vc-json)', async () => { + const preAuthorizedCode = '1234567890' + + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: openId4VcIssuer.issuerId, + offeredCredentials: [openBadgeCredential.id], + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, + issuanceMetadata: { + myIssuance: 'metadata', + }, + }) + + const issuanceSessionRepository = issuer.context.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository) + // We need to update the state, as it is checked and we're skipping the access token step + result.issuanceSession.cNonce = '1234' + result.issuanceSession.cNonceExpiresAt = new Date(Date.now() + 30000) // 30 seconds + result.issuanceSession.state = OpenId4VcIssuanceSessionState.AccessTokenCreated + await issuanceSessionRepository.update(issuer.context, result.issuanceSession) + + expect(result.credentialOffer).toBeDefined() + + const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) + const { credentialResponse } = await issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuanceSessionId: result.issuanceSession.id, + credentialRequestToCredentialMapper: ({ issuanceSession }) => { + expect(issuanceSession.id).toEqual(result.issuanceSession.id) + expect(issuanceSession.issuanceMetadata).toEqual({ + myIssuance: 'metadata', + }) + + return { + format: 'jwt_vc', + credentialSupportedId: openBadgeCredential.id, + credential: new W3cCredential({ + type: openBadgeCredential.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }), + verificationMethod: issuerVerificationMethod.id, + } + }, + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: openBadgeCredential, + issuerMetadata, + kid: holderKid, + nonce: result.issuanceSession.cNonce as string, + }), + }) + + expect(credentialResponse).toEqual({ + c_nonce: expect.any(String), + c_nonce_expires_in: 300, + credential: expect.any(String), + format: 'jwt_vc_json', + }) + + await handleCredentialResponse( + holder.context, + credentialResponse.credential as SphereonW3cVerifiableCredential, + openBadgeCredential + ) + }) + + it('credential id not in credential supported errors', async () => { + const preAuthorizedCode = '1234567890' + + await expect( + issuer.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: openId4VcIssuer.issuerId, + offeredCredentials: ['invalid id'], + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, + }) + ).rejects.toThrow("Offered credential 'invalid id' is not part of credentials_supported of the issuer metadata.") + }) + + it('issuing non offered credential errors', async () => { + const preAuthorizedCode = '1234567890' + + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: openId4VcIssuer.issuerId, + offeredCredentials: [openBadgeCredential.id], + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, + }) + + const issuanceSessionRepository = issuer.context.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository) + // We need to update the state, as it is checked and we're skipping the access token step + result.issuanceSession.state = OpenId4VcIssuanceSessionState.AccessTokenCreated + result.issuanceSession.cNonce = '1234' + result.issuanceSession.cNonceExpiresAt = new Date(Date.now() + 30000) // 30 seconds + await issuanceSessionRepository.update(issuer.context, result.issuanceSession) + + const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) + await expect( + issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuanceSessionId: result.issuanceSession.id, + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: universityDegreeCredential, + issuerMetadata, + kid: holderKid, + nonce: result.issuanceSession.cNonce as string, + }), + credentialRequestToCredentialMapper: () => { + throw new Error('Not implemented') + }, + }) + ).rejects.toThrow('No offered credentials match the credential request.') + }) + + it('pre authorized code flow using multiple credentials_supported', async () => { + const preAuthorizedCode = '1234567890' + + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + offeredCredentials: [openBadgeCredential.id, universityDegreeCredentialLd.id], + issuerId: openId4VcIssuer.issuerId, + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, + }) + + const issuanceSessionRepository = issuer.context.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository) + // We need to update the state, as it is checked and we're skipping the access token step + result.issuanceSession.cNonce = '1234' + result.issuanceSession.cNonceExpiresAt = new Date(Date.now() + 30000) // 30 seconds + result.issuanceSession.state = OpenId4VcIssuanceSessionState.AccessTokenCreated + await issuanceSessionRepository.update(issuer.context, result.issuanceSession) + + const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) + const { credentialResponse } = await issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuanceSessionId: result.issuanceSession.id, + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: universityDegreeCredentialLd, + issuerMetadata, + kid: holderKid, + nonce: result.issuanceSession.cNonce as string, + }), + credentialRequestToCredentialMapper: () => ({ + format: 'jwt_vc', + credential: new W3cCredential({ + type: universityDegreeCredentialLd.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }), + credentialSupportedId: universityDegreeCredentialLd.id, + verificationMethod: issuerVerificationMethod.id, + }), + }) + + expect(credentialResponse).toEqual({ + c_nonce: expect.any(String), + c_nonce_expires_in: 300, + credential: expect.any(String), + format: 'jwt_vc_json-ld', + }) + + await handleCredentialResponse( + holder.context, + credentialResponse.credential as SphereonW3cVerifiableCredential, + universityDegreeCredentialLd + ) + }) + + it('requesting non offered credential errors', async () => { + const preAuthorizedCode = '1234567890' + + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + offeredCredentials: [openBadgeCredential.id], + issuerId: openId4VcIssuer.issuerId, + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, + }) + + const issuanceSessionRepository = issuer.context.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository) + // We need to update the state, as it is checked and we're skipping the access token step + result.issuanceSession.state = OpenId4VcIssuanceSessionState.AccessTokenCreated + result.issuanceSession.cNonce = '1234' + result.issuanceSession.cNonceExpiresAt = new Date(Date.now() + 30000) // 30 seconds + await issuanceSessionRepository.update(issuer.context, result.issuanceSession) + + const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) + await expect( + issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuanceSessionId: result.issuanceSession.id, + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: { + id: 'someid', + format: openBadgeCredential.format, + types: universityDegreeCredential.types, + }, + issuerMetadata, + kid: holderKid, + nonce: result.issuanceSession.cNonce as string, + }), + credentialRequestToCredentialMapper: () => { + throw new Error('Not implemented') + }, + }) + ).rejects.toThrow('No offered credentials match the credential request.') + }) + + it('create credential offer and retrieve it from the uri (pre authorized flow)', async () => { + const preAuthorizedCode = '1234567890' + + const { credentialOffer } = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: openId4VcIssuer.issuerId, + offeredCredentials: [openBadgeCredential.id], + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, + }) + + expect(credentialOffer).toMatch( + new RegExp( + `^openid-credential-offer://\\?credential_offer_uri=https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%2Foffers%2F.*$` + ) + ) + }) + + it('offer and request multiple credentials', async () => { + const preAuthorizedCode = '1234567890' + + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + offeredCredentials: [openBadgeCredential.id, universityDegreeCredential.id], + issuerId: openId4VcIssuer.issuerId, + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, + }) + + const issuanceSessionRepository = issuer.context.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository) + result.issuanceSession.cNonce = '1234' + result.issuanceSession.cNonceExpiresAt = new Date(Date.now() + 30000) // 30 seconds + await issuanceSessionRepository.update(issuer.context, result.issuanceSession) + + expect(result.issuanceSession.credentialOfferPayload?.credentials).toEqual([ + openBadgeCredential.id, + universityDegreeCredential.id, + ]) + + const credentialRequestToCredentialMapper: OpenId4VciCredentialRequestToCredentialMapper = ({ + credentialsSupported, + }) => { + const credential = + credentialsSupported[0].id === openBadgeCredential.id ? openBadgeCredential : universityDegreeCredential + return { + format: 'jwt_vc', + credential: new W3cCredential({ + type: credential.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }), + credentialSupportedId: credential.id, + + verificationMethod: issuerVerificationMethod.id, + } + } + + // We need to update the state, as it is checked and we're skipping the access token step + result.issuanceSession.state = OpenId4VcIssuanceSessionState.AccessTokenCreated + await issuanceSessionRepository.update(issuer.context, result.issuanceSession) + + const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) + const { credentialResponse } = await issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuanceSessionId: result.issuanceSession.id, + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: openBadgeCredential, + issuerMetadata, + kid: holderKid, + nonce: result.issuanceSession.cNonce as string, + }), + credentialRequestToCredentialMapper, + }) + + expect(credentialResponse).toEqual({ + c_nonce: expect.any(String), + c_nonce_expires_in: 300, + credential: expect.any(String), + format: 'jwt_vc_json', + }) + + await handleCredentialResponse( + holder.context, + credentialResponse.credential as SphereonW3cVerifiableCredential, + openBadgeCredential + ) + + const { credentialResponse: credentialResponse2 } = await issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuanceSessionId: result.issuanceSession.id, + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: universityDegreeCredential, + issuerMetadata, + kid: holderKid, + nonce: credentialResponse.c_nonce ?? (result.issuanceSession.cNonce as string), + }), + credentialRequestToCredentialMapper, + }) + + expect(credentialResponse2).toEqual({ + c_nonce: expect.any(String), + c_nonce_expires_in: 300, + credential: expect.any(String), + format: 'jwt_vc_json', + }) + + await handleCredentialResponse( + holder.context, + credentialResponse2.credential as SphereonW3cVerifiableCredential, + universityDegreeCredential + ) + }) +}) diff --git a/packages/openid4vc/src/openid4vc-issuer/index.ts b/packages/openid4vc/src/openid4vc-issuer/index.ts new file mode 100644 index 0000000000..fd4cf97c6b --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/index.ts @@ -0,0 +1,8 @@ +export * from './OpenId4VcIssuerApi' +export * from './OpenId4VcIssuerModule' +export * from './OpenId4VcIssuerService' +export * from './OpenId4VcIssuerModuleConfig' +export * from './OpenId4VcIssuerServiceOptions' +export * from './OpenId4VcIssuerEvents' +export * from './OpenId4VcIssuanceSessionState' +export { OpenId4VcIssuerRecord, OpenId4VcIssuerRecordProps, OpenId4VcIssuerRecordTags } from './repository' diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcCNonceStateManager.ts b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcCNonceStateManager.ts new file mode 100644 index 0000000000..7473719435 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcCNonceStateManager.ts @@ -0,0 +1,123 @@ +import type { AgentContext } from '@credo-ts/core' +import type { CNonceState, IStateManager } from '@sphereon/oid4vci-common' + +import { CredoError } from '@credo-ts/core' + +import { OpenId4VcIssuerModuleConfig } from '../OpenId4VcIssuerModuleConfig' + +import { OpenId4VcIssuanceSessionRepository } from './OpenId4VcIssuanceSessionRepository' + +export class OpenId4VcCNonceStateManager implements IStateManager { + private openId4VcIssuanceSessionRepository: OpenId4VcIssuanceSessionRepository + private openId4VcIssuerModuleConfig: OpenId4VcIssuerModuleConfig + + public constructor(private agentContext: AgentContext, private issuerId: string) { + this.openId4VcIssuanceSessionRepository = agentContext.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository) + this.openId4VcIssuerModuleConfig = agentContext.dependencyManager.resolve(OpenId4VcIssuerModuleConfig) + } + + public async set(cNonce: string, stateValue: CNonceState): Promise { + // Just to make sure that the cNonce is the same as the id as that's what we use to query + if (cNonce !== stateValue.cNonce) { + throw new CredoError('Expected the id of the cNonce state to be equal to the cNonce') + } + + if (!stateValue.preAuthorizedCode) { + throw new CredoError("Expected the stateValue to have a 'preAuthorizedCode' property") + } + + // Record MUST exist (otherwise there's no issuance session active yet) + const record = await this.openId4VcIssuanceSessionRepository.getSingleByQuery(this.agentContext, { + // NOTE: once we support authorized flow, we need to add an $or for the issuer state as well + issuerId: this.issuerId, + preAuthorizedCode: stateValue.preAuthorizedCode, + }) + + // cNonce already matches, no need to update + if (record.cNonce === stateValue.cNonce) { + return + } + + const expiresAtDate = new Date( + Date.now() + this.openId4VcIssuerModuleConfig.accessTokenEndpoint.cNonceExpiresInSeconds * 1000 + ) + + record.cNonce = stateValue.cNonce + record.cNonceExpiresAt = expiresAtDate + await this.openId4VcIssuanceSessionRepository.update(this.agentContext, record) + } + + public async get(cNonce: string): Promise { + const record = await this.openId4VcIssuanceSessionRepository.findSingleByQuery(this.agentContext, { + issuerId: this.issuerId, + cNonce, + }) + + if (!record) return undefined + + // NOTE: This should not happen as we query by the credential offer uri + // so it's mostly to make TS happy + if (!record.cNonce) { + throw new CredoError('No cNonce found on record.') + } + + return { + cNonce: record.cNonce, + preAuthorizedCode: record.preAuthorizedCode, + createdAt: record.createdAt.getTime(), + } + } + + public async has(cNonce: string): Promise { + const record = await this.openId4VcIssuanceSessionRepository.findSingleByQuery(this.agentContext, { + issuerId: this.issuerId, + cNonce, + }) + + return record !== undefined + } + + public async delete(cNonce: string): Promise { + const record = await this.openId4VcIssuanceSessionRepository.findSingleByQuery(this.agentContext, { + issuerId: this.issuerId, + cNonce, + }) + + if (!record) return false + + // We only remove the cNonce from the record, we don't want to remove + // the whole issuance session. + record.cNonce = undefined + record.cNonceExpiresAt = undefined + await this.openId4VcIssuanceSessionRepository.update(this.agentContext, record) + return true + } + + public async clearExpired(): Promise { + // FIXME: we should have a way to remove expired records + // or just not return the value in the get if the record is expired + throw new Error('Method not implemented.') + } + + public async clearAll(): Promise { + throw new Error('Method not implemented.') + } + + public async getAsserted(id: string): Promise { + const state = await this.get(id) + + if (!state) { + throw new CredoError(`No cNonce state found for id ${id}`) + } + + return state + } + + public async startCleanupRoutine(): Promise { + throw new Error('Method not implemented.') + } + + public async stopCleanupRoutine(): Promise { + throw new Error('Method not implemented.') + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcCredentialOfferSessionStateManager.ts b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcCredentialOfferSessionStateManager.ts new file mode 100644 index 0000000000..83cd20d27e --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcCredentialOfferSessionStateManager.ts @@ -0,0 +1,215 @@ +import type { OpenId4VcIssuanceSessionStateChangedEvent } from '../OpenId4VcIssuerEvents' +import type { AgentContext } from '@credo-ts/core' +import type { CredentialOfferSession, IStateManager } from '@sphereon/oid4vci-common' + +import { CredoError, EventEmitter } from '@credo-ts/core' +import { IssueStatus } from '@sphereon/oid4vci-common' + +import { OpenId4VcIssuanceSessionState } from '../OpenId4VcIssuanceSessionState' +import { OpenId4VcIssuerEvents } from '../OpenId4VcIssuerEvents' + +import { OpenId4VcIssuanceSessionRecord } from './OpenId4VcIssuanceSessionRecord' +import { OpenId4VcIssuanceSessionRepository } from './OpenId4VcIssuanceSessionRepository' + +export class OpenId4VcCredentialOfferSessionStateManager implements IStateManager { + private openId4VcIssuanceSessionRepository: OpenId4VcIssuanceSessionRepository + private eventEmitter: EventEmitter + + public constructor(private agentContext: AgentContext, private issuerId: string) { + this.openId4VcIssuanceSessionRepository = agentContext.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository) + this.eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + } + + public async set(preAuthorizedCode: string, stateValue: CredentialOfferSession): Promise { + // Just to make sure that the preAuthorizedCode is the same as the id as that's what we use to query + // NOTE: once we support authorized flow, we need to also allow the id to be equal to issuer state + if (preAuthorizedCode !== stateValue.preAuthorizedCode) { + throw new CredoError('Expected the id of the credential offer state to be equal to the preAuthorizedCode') + } + + if (!stateValue.preAuthorizedCode) { + throw new CredoError("Expected the stateValue to have a 'preAuthorizedCode' property") + } + + // Record may already exist + let record = await this.openId4VcIssuanceSessionRepository.findSingleByQuery(this.agentContext, { + issuerId: this.issuerId, + preAuthorizedCode: stateValue.preAuthorizedCode, + }) + + const previousState = record?.state ?? null + + let credentialOfferUri = stateValue.credentialOffer.credential_offer_uri + if (!credentialOfferUri) { + throw new CredoError("Expected the stateValue to have a 'credentialOfferUri' property") + } + + if (credentialOfferUri.includes('credential_offer_uri=')) { + // NOTE: it's a bit cumbersome, but the credential_offer_uri is the encoded uri. This seems + // odd to me, as this is the offer payload, which should only contain the hosted URI (I think + // this is a bug in OID4VCI). But for now we have to extract the uri from the payload. + credentialOfferUri = decodeURIComponent(credentialOfferUri.split('credential_offer_uri=')[1].split('=')[0]) + } + + let state = openId4VcIssuanceStateFromSphereon(stateValue.status) + + // we set the completed state manually when all credentials have been issued + if ( + state === OpenId4VcIssuanceSessionState.CredentialsPartiallyIssued && + (record?.issuedCredentials?.length ?? 0) >= stateValue.credentialOffer.credential_offer.credentials.length + ) { + state = OpenId4VcIssuanceSessionState.Completed + } + + // NOTE: we don't use clientId at the moment, will become relevant when doing the authorized flow + if (record) { + record.issuanceMetadata = stateValue.credentialDataSupplierInput + record.credentialOfferPayload = stateValue.credentialOffer.credential_offer + record.userPin = stateValue.userPin + record.preAuthorizedCode = stateValue.preAuthorizedCode + record.errorMessage = stateValue.error + record.credentialOfferUri = credentialOfferUri + record.state = state + await this.openId4VcIssuanceSessionRepository.update(this.agentContext, record) + } else { + record = new OpenId4VcIssuanceSessionRecord({ + issuerId: this.issuerId, + preAuthorizedCode: stateValue.preAuthorizedCode, + issuanceMetadata: stateValue.credentialDataSupplierInput, + credentialOfferPayload: stateValue.credentialOffer.credential_offer, + credentialOfferUri, + userPin: stateValue.userPin, + errorMessage: stateValue.error, + state: state, + }) + + await this.openId4VcIssuanceSessionRepository.save(this.agentContext, record) + } + + this.emitStateChangedEvent(this.agentContext, record, previousState) + } + + public async get(preAuthorizedCode: string): Promise { + const record = await this.openId4VcIssuanceSessionRepository.findSingleByQuery(this.agentContext, { + issuerId: this.issuerId, + preAuthorizedCode, + }) + + if (!record) return undefined + + // NOTE: This should not happen as we query by the preAuthorizedCode + // so it's mostly to make TS happy + if (!record.preAuthorizedCode) { + throw new CredoError("No 'preAuthorizedCode' found on record.") + } + + if (!record.credentialOfferPayload) { + throw new CredoError("No 'credentialOfferPayload' found on record.") + } + + return { + credentialOffer: { + credential_offer: record.credentialOfferPayload, + credential_offer_uri: record.credentialOfferUri, + }, + status: sphereonIssueStatusFromOpenId4VcIssuanceState(record.state), + preAuthorizedCode: record.preAuthorizedCode, + credentialDataSupplierInput: record.issuanceMetadata, + error: record.errorMessage, + userPin: record.userPin, + createdAt: record.createdAt.getTime(), + lastUpdatedAt: record.updatedAt?.getTime() ?? record.createdAt.getTime(), + } + } + + public async has(preAuthorizedCode: string): Promise { + const record = await this.openId4VcIssuanceSessionRepository.findSingleByQuery(this.agentContext, { + issuerId: this.issuerId, + preAuthorizedCode, + }) + + return record !== undefined + } + + public async delete(preAuthorizedCode: string): Promise { + const record = await this.openId4VcIssuanceSessionRepository.findSingleByQuery(this.agentContext, { + issuerId: this.issuerId, + preAuthorizedCode, + }) + + if (!record) return false + + await this.openId4VcIssuanceSessionRepository.deleteById(this.agentContext, record.id) + return true + } + + public async clearExpired(): Promise { + // FIXME: we should have a way to remove expired records + // or just not return the value in the get if the record is expired + throw new Error('Method not implemented.') + } + + public async clearAll(): Promise { + throw new Error('Method not implemented.') + } + + public async getAsserted(preAuthorizedCode: string): Promise { + const state = await this.get(preAuthorizedCode) + + if (!state) { + throw new CredoError(`No credential offer state found for id ${preAuthorizedCode}`) + } + + return state + } + + public async startCleanupRoutine(): Promise { + throw new Error('Method not implemented.') + } + + public async stopCleanupRoutine(): Promise { + throw new Error('Method not implemented.') + } + + protected emitStateChangedEvent( + agentContext: AgentContext, + issuanceSession: OpenId4VcIssuanceSessionRecord, + previousState: OpenId4VcIssuanceSessionState | null + ) { + this.eventEmitter.emit(agentContext, { + type: OpenId4VcIssuerEvents.IssuanceSessionStateChanged, + payload: { + issuanceSession: issuanceSession.clone(), + previousState, + }, + }) + } +} + +function openId4VcIssuanceStateFromSphereon(stateValue: IssueStatus): OpenId4VcIssuanceSessionState { + if (stateValue === IssueStatus.OFFER_CREATED) return OpenId4VcIssuanceSessionState.OfferCreated + if (stateValue === IssueStatus.OFFER_URI_RETRIEVED) return OpenId4VcIssuanceSessionState.OfferUriRetrieved + if (stateValue === IssueStatus.ACCESS_TOKEN_REQUESTED) return OpenId4VcIssuanceSessionState.AccessTokenRequested + if (stateValue === IssueStatus.ACCESS_TOKEN_CREATED) return OpenId4VcIssuanceSessionState.AccessTokenCreated + if (stateValue === IssueStatus.CREDENTIAL_REQUEST_RECEIVED) + return OpenId4VcIssuanceSessionState.CredentialRequestReceived + // we set the completed state manually when all credentials have been issued + if (stateValue === IssueStatus.CREDENTIAL_ISSUED) return OpenId4VcIssuanceSessionState.CredentialsPartiallyIssued + if (stateValue === IssueStatus.ERROR) return OpenId4VcIssuanceSessionState.Error + + throw new CredoError(`Unknown state value: ${stateValue}`) +} + +function sphereonIssueStatusFromOpenId4VcIssuanceState(state: OpenId4VcIssuanceSessionState): IssueStatus { + if (state === OpenId4VcIssuanceSessionState.OfferCreated) return IssueStatus.OFFER_CREATED + if (state === OpenId4VcIssuanceSessionState.OfferUriRetrieved) return IssueStatus.OFFER_URI_RETRIEVED + if (state === OpenId4VcIssuanceSessionState.AccessTokenRequested) return IssueStatus.ACCESS_TOKEN_REQUESTED + if (state === OpenId4VcIssuanceSessionState.AccessTokenCreated) return IssueStatus.ACCESS_TOKEN_CREATED + if (state === OpenId4VcIssuanceSessionState.CredentialRequestReceived) return IssueStatus.CREDENTIAL_REQUEST_RECEIVED + // sphereon does not have a completed state indicating that all credentials have been issued + if (state === OpenId4VcIssuanceSessionState.CredentialsPartiallyIssued) return IssueStatus.CREDENTIAL_ISSUED + if (state === OpenId4VcIssuanceSessionState.Completed) return IssueStatus.CREDENTIAL_ISSUED + if (state === OpenId4VcIssuanceSessionState.Error) return IssueStatus.ERROR + + throw new CredoError(`Unknown state value: ${state}`) +} diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcCredentialOfferUriStateManager.ts b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcCredentialOfferUriStateManager.ts new file mode 100644 index 0000000000..33b53641bf --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcCredentialOfferUriStateManager.ts @@ -0,0 +1,82 @@ +import type { AgentContext } from '@credo-ts/core' +import type { IStateManager, URIState } from '@sphereon/oid4vci-common' + +import { CredoError } from '@credo-ts/core' + +import { OpenId4VcIssuanceSessionRepository } from './OpenId4VcIssuanceSessionRepository' + +export class OpenId4VcCredentialOfferUriStateManager implements IStateManager { + private openId4VcIssuanceSessionRepository: OpenId4VcIssuanceSessionRepository + + public constructor(private agentContext: AgentContext, private issuerId: string) { + this.openId4VcIssuanceSessionRepository = agentContext.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository) + } + + public async set(uri: string, stateValue: URIState): Promise { + // Just to make sure that the uri is the same as the id as that's what we use to query + if (uri !== stateValue.uri) { + throw new CredoError('Expected the uri of the uri state to be equal to the id') + } + + // NOTE: we're currently not ding anything here, as we store the uri in the record + // when the credential offer session is stored. + } + + public async get(uri: string): Promise { + const record = await this.openId4VcIssuanceSessionRepository.findSingleByQuery(this.agentContext, { + issuerId: this.issuerId, + credentialOfferUri: uri, + }) + + if (!record) return undefined + + return { + preAuthorizedCode: record.preAuthorizedCode, + uri: record.credentialOfferUri, + createdAt: record.createdAt.getTime(), + } + } + + public async has(uri: string): Promise { + const record = await this.openId4VcIssuanceSessionRepository.findSingleByQuery(this.agentContext, { + issuerId: this.issuerId, + credentialOfferUri: uri, + }) + + return record !== undefined + } + + public async delete(): Promise { + // NOTE: we're not doing anything here as the uri is stored in the credential offer session + // Not sure how to best handle this, but for now we just don't delete it + return false + } + + public async clearExpired(): Promise { + // FIXME: we should have a way to remove expired records + // or just not return the value in the get if the record is expired + throw new Error('Method not implemented.') + } + + public async clearAll(): Promise { + throw new Error('Method not implemented.') + } + + public async getAsserted(id: string): Promise { + const state = await this.get(id) + + if (!state) { + throw new CredoError(`No uri state found for id ${id}`) + } + + return state + } + + public async startCleanupRoutine(): Promise { + throw new Error('Method not implemented.') + } + + public async stopCleanupRoutine(): Promise { + throw new Error('Method not implemented.') + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuanceSessionRecord.ts b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuanceSessionRecord.ts new file mode 100644 index 0000000000..3de3af4313 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuanceSessionRecord.ts @@ -0,0 +1,157 @@ +import type { OpenId4VciCredentialOfferPayload } from '../../shared' +import type { RecordTags, TagsBase } from '@credo-ts/core' + +import { CredoError, BaseRecord, utils, DateTransformer } from '@credo-ts/core' +import { Transform } from 'class-transformer' + +import { OpenId4VcIssuanceSessionState } from '../OpenId4VcIssuanceSessionState' + +export type OpenId4VcIssuanceSessionRecordTags = RecordTags + +export type DefaultOpenId4VcIssuanceSessionRecordTags = { + issuerId: string + cNonce?: string + preAuthorizedCode?: string + state: OpenId4VcIssuanceSessionState + credentialOfferUri: string +} + +export interface OpenId4VcIssuanceSessionRecordProps { + id?: string + createdAt?: Date + tags?: TagsBase + + issuerId: string + + cNonce?: string + cNonceExpiresAt?: Date + + preAuthorizedCode?: string + userPin?: string + + credentialOfferUri: string + credentialOfferPayload: OpenId4VciCredentialOfferPayload + + issuanceMetadata?: Record + state: OpenId4VcIssuanceSessionState + errorMessage?: string +} + +export class OpenId4VcIssuanceSessionRecord extends BaseRecord { + public static readonly type = 'OpenId4VcIssuanceSessionRecord' + public readonly type = OpenId4VcIssuanceSessionRecord.type + + /** + * The id of the issuer that this session is for. + */ + public issuerId!: string + + /** + * The state of the issuance session. + */ + @Transform(({ value }) => { + // CredentialIssued is an old state that is no longer used. It should be mapped to Error. + if (value === 'CredentialIssued') { + return OpenId4VcIssuanceSessionState.Error + } + + return value + }) + public state!: OpenId4VcIssuanceSessionState + + /** + * The credentials that were issued during this session. + */ + public issuedCredentials: string[] = [] + + /** + * cNonce that should be used in the credential request by the holder. + */ + public cNonce?: string + + /** + * The time at which the cNonce expires. + */ + @DateTransformer() + public cNonceExpiresAt?: Date + + /** + * Pre authorized code used for the issuance session. Only used when a pre-authorized credential + * offer is created. + */ + public preAuthorizedCode?: string + + /** + * Optional user pin that needs to be provided by the user in the access token request. + */ + public userPin?: string + + /** + * User-defined metadata that will be provided to the credential request to credential mapper + * to allow to retrieve the needed credential input data. Can be the credential data itself, + * or some other data that is needed to retrieve the credential data. + */ + public issuanceMetadata?: Record + + /** + * The credential offer that was used to create the issuance session. + */ + public credentialOfferPayload!: OpenId4VciCredentialOfferPayload + + /** + * URI of the credential offer. This is the url that cn can be used to retrieve + * the credential offer + */ + public credentialOfferUri!: string + + /** + * Optional error message of the error that occurred during the issuance session. Will be set when state is {@link OpenId4VcIssuanceSessionState.Error} + */ + public errorMessage?: string + + public constructor(props: OpenId4VcIssuanceSessionRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags ?? {} + + this.issuerId = props.issuerId + this.cNonce = props.cNonce + this.cNonceExpiresAt = props.cNonceExpiresAt + this.userPin = props.userPin + this.preAuthorizedCode = props.preAuthorizedCode + this.credentialOfferUri = props.credentialOfferUri + this.credentialOfferPayload = props.credentialOfferPayload + this.issuanceMetadata = props.issuanceMetadata + this.state = props.state + this.errorMessage = props.errorMessage + } + } + + public assertState(expectedStates: OpenId4VcIssuanceSessionState | OpenId4VcIssuanceSessionState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new CredoError( + `OpenId4VcIssuanceSessionRecord is in invalid state ${this.state}. Valid states are: ${expectedStates.join( + ', ' + )}.` + ) + } + } + + public getTags() { + return { + ...this._tags, + issuerId: this.issuerId, + cNonce: this.cNonce, + credentialOfferUri: this.credentialOfferUri, + preAuthorizedCode: this.preAuthorizedCode, + state: this.state, + } + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuanceSessionRepository.ts b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuanceSessionRepository.ts new file mode 100644 index 0000000000..d4ce1aab64 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuanceSessionRepository.ts @@ -0,0 +1,13 @@ +import { Repository, StorageService, InjectionSymbols, EventEmitter, inject, injectable } from '@credo-ts/core' + +import { OpenId4VcIssuanceSessionRecord } from './OpenId4VcIssuanceSessionRecord' + +@injectable() +export class OpenId4VcIssuanceSessionRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(OpenId4VcIssuanceSessionRecord, storageService, eventEmitter) + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts new file mode 100644 index 0000000000..244192dd52 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts @@ -0,0 +1,65 @@ +import type { OpenId4VciCredentialSupportedWithId, OpenId4VciIssuerMetadataDisplay } from '../../shared' +import type { RecordTags, TagsBase } from '@credo-ts/core' + +import { BaseRecord, utils } from '@credo-ts/core' + +export type OpenId4VcIssuerRecordTags = RecordTags + +export type DefaultOpenId4VcIssuerRecordTags = { + issuerId: string +} + +export interface OpenId4VcIssuerRecordProps { + id?: string + createdAt?: Date + tags?: TagsBase + + issuerId: string + + /** + * The fingerprint (multibase encoded) of the public key used to sign access tokens for + * this issuer. + */ + accessTokenPublicKeyFingerprint: string + + credentialsSupported: OpenId4VciCredentialSupportedWithId[] + display?: OpenId4VciIssuerMetadataDisplay[] +} + +/** + * For OID4VC you need to expos metadata files. Each issuer needs to host this metadata. This is not the case for DIDComm where we can just have one /didcomm endpoint. + * So we create a record per openid issuer/verifier that you want, and each tenant can create multiple issuers/verifiers which have different endpoints + * and metadata files + * */ +export class OpenId4VcIssuerRecord extends BaseRecord { + public static readonly type = 'OpenId4VcIssuerRecord' + public readonly type = OpenId4VcIssuerRecord.type + + public issuerId!: string + public accessTokenPublicKeyFingerprint!: string + + public credentialsSupported!: OpenId4VciCredentialSupportedWithId[] + public display?: OpenId4VciIssuerMetadataDisplay[] + + public constructor(props: OpenId4VcIssuerRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags ?? {} + + this.issuerId = props.issuerId + this.accessTokenPublicKeyFingerprint = props.accessTokenPublicKeyFingerprint + this.credentialsSupported = props.credentialsSupported + this.display = props.display + } + } + + public getTags() { + return { + ...this._tags, + issuerId: this.issuerId, + } + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRepository.ts b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRepository.ts new file mode 100644 index 0000000000..50d5506df3 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@credo-ts/core' + +import { Repository, StorageService, InjectionSymbols, EventEmitter, inject, injectable } from '@credo-ts/core' + +import { OpenId4VcIssuerRecord } from './OpenId4VcIssuerRecord' + +@injectable() +export class OpenId4VcIssuerRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(OpenId4VcIssuerRecord, storageService, eventEmitter) + } + + public findByIssuerId(agentContext: AgentContext, issuerId: string) { + return this.findSingleByQuery(agentContext, { issuerId }) + } + + public getByIssuerId(agentContext: AgentContext, issuerId: string) { + return this.getSingleByQuery(agentContext, { issuerId }) + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/index.ts b/packages/openid4vc/src/openid4vc-issuer/repository/index.ts new file mode 100644 index 0000000000..30854252db --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/repository/index.ts @@ -0,0 +1,4 @@ +export * from './OpenId4VcIssuerRecord' +export * from './OpenId4VcIssuerRepository' +export * from './OpenId4VcIssuanceSessionRecord' +export * from './OpenId4VcIssuanceSessionRepository' diff --git a/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts new file mode 100644 index 0000000000..a86faed9c2 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts @@ -0,0 +1,185 @@ +import type { OpenId4VcIssuanceRequest } from './requestContext' +import type { AgentContext } from '@credo-ts/core' +import type { AccessTokenRequest, JWTSignerCallback } from '@sphereon/oid4vci-common' +import type { NextFunction, Response, Router } from 'express' + +import { getJwkFromKey, CredoError, JwsService, JwtPayload, getJwkClassFromKeyType, Key } from '@credo-ts/core' +import { + GrantTypes, + IssueStatus, + PRE_AUTHORIZED_CODE_REQUIRED_ERROR, + PRE_AUTH_CODE_LITERAL, + TokenError, + TokenErrorResponse, +} from '@sphereon/oid4vci-common' +import { assertValidAccessTokenRequest, createAccessTokenResponse } from '@sphereon/oid4vci-issuer' + +import { getRequestContext, sendErrorResponse } from '../../shared/router' +import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' +import { OpenId4VcCNonceStateManager } from '../repository/OpenId4VcCNonceStateManager' +import { OpenId4VcCredentialOfferSessionStateManager } from '../repository/OpenId4VcCredentialOfferSessionStateManager' + +export interface OpenId4VciAccessTokenEndpointConfig { + /** + * The path at which the token endpoint should be made available. Note that it will be + * hosted at a subpath to take into account multiple tenants and issuers. + * + * @default /token + */ + endpointPath: string + + /** + * The maximum amount of time in seconds that the pre-authorized code is valid. + * @default 360 (5 minutes) + */ + preAuthorizedCodeExpirationInSeconds: number + + /** + * The time after which the cNonce from the access token response will + * expire. + * + * @default 360 (5 minutes) + */ + cNonceExpiresInSeconds: number + + /** + * The time after which the token will expire. + * + * @default 360 (5 minutes) + */ + tokenExpiresInSeconds: number +} + +export function configureAccessTokenEndpoint(router: Router, config: OpenId4VciAccessTokenEndpointConfig) { + router.post( + config.endpointPath, + verifyTokenRequest({ preAuthorizedCodeExpirationInSeconds: config.preAuthorizedCodeExpirationInSeconds }), + handleTokenRequest(config) + ) +} + +function getJwtSignerCallback( + agentContext: AgentContext, + signerPublicKey: Key, + config: OpenId4VciAccessTokenEndpointConfig +): JWTSignerCallback { + return async (jwt, _kid) => { + if (_kid) { + throw new CredoError('Kid should not be supplied externally.') + } + if (jwt.header.kid || jwt.header.jwk) { + throw new CredoError('kid or jwk should not be present in access token header before signing') + } + + const jwsService = agentContext.dependencyManager.resolve(JwsService) + + const alg = getJwkClassFromKeyType(signerPublicKey.keyType)?.supportedSignatureAlgorithms[0] + if (!alg) { + throw new CredoError(`No supported signature algorithms for key type: ${signerPublicKey.keyType}`) + } + + // FIXME: the iat and exp implementation in OID4VCI is incorrect so we override the values here + // https://github.com/Sphereon-Opensource/OID4VCI/pull/99 + // https://github.com/Sphereon-Opensource/OID4VCI/pull/101 + const iat = Math.floor(new Date().getTime() / 1000) + jwt.payload.iat = iat + jwt.payload.exp = iat + config.tokenExpiresInSeconds + + const jwk = getJwkFromKey(signerPublicKey) + const signedJwt = await jwsService.createJwsCompact(agentContext, { + protectedHeaderOptions: { ...jwt.header, jwk, alg }, + payload: JwtPayload.fromJson(jwt.payload), + key: signerPublicKey, + }) + + return signedJwt + } +} + +export function handleTokenRequest(config: OpenId4VciAccessTokenEndpointConfig) { + const { tokenExpiresInSeconds, cNonceExpiresInSeconds } = config + + return async (request: OpenId4VcIssuanceRequest, response: Response, next: NextFunction) => { + response.set({ 'Cache-Control': 'no-store', Pragma: 'no-cache' }) + + const requestContext = getRequestContext(request) + const { agentContext, issuer } = requestContext + + const body = request.body as AccessTokenRequest + if (body.grant_type !== GrantTypes.PRE_AUTHORIZED_CODE) { + return sendErrorResponse( + response, + agentContext.config.logger, + 400, + TokenErrorResponse.invalid_request, + PRE_AUTHORIZED_CODE_REQUIRED_ERROR + ) + } + + const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) + const issuerMetadata = openId4VcIssuerService.getIssuerMetadata(agentContext, issuer) + const accessTokenSigningKey = Key.fromFingerprint(issuer.accessTokenPublicKeyFingerprint) + + try { + const accessTokenResponse = await createAccessTokenResponse(request.body, { + credentialOfferSessions: new OpenId4VcCredentialOfferSessionStateManager(agentContext, issuer.issuerId), + tokenExpiresIn: tokenExpiresInSeconds, + accessTokenIssuer: issuerMetadata.issuerUrl, + cNonce: await agentContext.wallet.generateNonce(), + cNonceExpiresIn: cNonceExpiresInSeconds, + cNonces: new OpenId4VcCNonceStateManager(agentContext, issuer.issuerId), + accessTokenSignerCallback: getJwtSignerCallback(agentContext, accessTokenSigningKey, config), + }) + response.status(200).json(accessTokenResponse) + } catch (error) { + sendErrorResponse(response, agentContext.config.logger, 400, TokenErrorResponse.invalid_request, error) + } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() + } +} + +export function verifyTokenRequest(options: { preAuthorizedCodeExpirationInSeconds: number }) { + return async (request: OpenId4VcIssuanceRequest, response: Response, next: NextFunction) => { + const { agentContext, issuer } = getRequestContext(request) + + try { + const credentialOfferSessions = new OpenId4VcCredentialOfferSessionStateManager(agentContext, issuer.issuerId) + const credentialOfferSession = await credentialOfferSessions.getAsserted(request.body[PRE_AUTH_CODE_LITERAL]) + if (![IssueStatus.OFFER_CREATED, IssueStatus.OFFER_URI_RETRIEVED].includes(credentialOfferSession.status)) { + throw new TokenError(400, TokenErrorResponse.invalid_request, 'Access token has already been retrieved') + } + const { preAuthSession } = await assertValidAccessTokenRequest(request.body, { + // It should actually be in seconds. but the oid4vci library has some bugs related + // to seconds vs milliseconds. We pass it as ms for now, but once the fix is released + // we should pass it as seconds. We have an extra check below, so that we won't have + // an security issue once the fix is released. + // FIXME: https://github.com/Sphereon-Opensource/OID4VCI/pull/104 + expirationDuration: options.preAuthorizedCodeExpirationInSeconds * 1000, + credentialOfferSessions, + }) + + // TODO: remove once above PR is merged and released + const expiresAt = preAuthSession.createdAt + options.preAuthorizedCodeExpirationInSeconds * 1000 + if (Date.now() > expiresAt) { + throw new TokenError(400, TokenErrorResponse.invalid_grant, 'Pre-authorized code has expired') + } + } catch (error) { + if (error instanceof TokenError) { + sendErrorResponse( + response, + agentContext.config.logger, + error.statusCode, + error.responseError, + error.getDescription() + ) + } else { + sendErrorResponse(response, agentContext.config.logger, 400, TokenErrorResponse.invalid_request, error) + } + } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts new file mode 100644 index 0000000000..90ad62ee12 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts @@ -0,0 +1,84 @@ +import type { OpenId4VcIssuanceRequest } from './requestContext' +import type { OpenId4VciCredentialRequest } from '../../shared' +import type { OpenId4VciCredentialRequestToCredentialMapper } from '../OpenId4VcIssuerServiceOptions' +import type { Router, Response } from 'express' + +import { getRequestContext, sendErrorResponse } from '../../shared/router' +import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' +import { getCNonceFromCredentialRequest } from '../util/credentialRequest' + +import { verifyAccessToken } from './verifyAccessToken' + +export interface OpenId4VciCredentialEndpointConfig { + /** + * The path at which the credential endpoint should be made available. Note that it will be + * hosted at a subpath to take into account multiple tenants and issuers. + * + * @default /credential + */ + endpointPath: string + + /** + * A function mapping a credential request to the credential to be issued. + */ + credentialRequestToCredentialMapper: OpenId4VciCredentialRequestToCredentialMapper +} + +export function configureCredentialEndpoint(router: Router, config: OpenId4VciCredentialEndpointConfig) { + router.post(config.endpointPath, async (request: OpenId4VcIssuanceRequest, response: Response, next) => { + const { agentContext, issuer } = getRequestContext(request) + const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) + + let preAuthorizedCode: string + + // Verify the access token (should at some point be moved to a middleware function or something) + try { + preAuthorizedCode = (await verifyAccessToken(agentContext, issuer, request.headers.authorization)) + .preAuthorizedCode + } catch (error) { + return sendErrorResponse(response, agentContext.config.logger, 401, 'unauthorized', error) + } + + try { + const credentialRequest = request.body as OpenId4VciCredentialRequest + + const issuanceSession = await openId4VcIssuerService.findIssuanceSessionForCredentialRequest(agentContext, { + issuerId: issuer.issuerId, + credentialRequest, + }) + + if (issuanceSession?.preAuthorizedCode !== preAuthorizedCode) { + agentContext.config.logger.warn( + `Credential request used access token with for credential offer with different pre-authorized code than was used for the issuance session ${issuanceSession?.id}` + ) + return sendErrorResponse( + response, + agentContext.config.logger, + 401, + 'unauthorized', + 'Access token is not valid for this credential request' + ) + } + + if (!issuanceSession) { + const cNonce = getCNonceFromCredentialRequest(credentialRequest) + agentContext.config.logger.warn( + `No issuance session found for incoming credential request with cNonce ${cNonce} and issuer ${issuer.issuerId}` + ) + return sendErrorResponse(response, agentContext.config.logger, 404, 'invalid_request', null) + } + + const { credentialResponse } = await openId4VcIssuerService.createCredentialResponse(agentContext, { + issuanceSession, + credentialRequest, + }) + + response.json(credentialResponse) + } catch (error) { + sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) + } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() + }) +} diff --git a/packages/openid4vc/src/openid4vc-issuer/router/credentialOfferEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/credentialOfferEndpoint.ts new file mode 100644 index 0000000000..f1e316d1f3 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/router/credentialOfferEndpoint.ts @@ -0,0 +1,103 @@ +import type { OpenId4VcIssuanceRequest } from './requestContext' +import type { OpenId4VcIssuanceSessionStateChangedEvent } from '../OpenId4VcIssuerEvents' +import type { Router, Response } from 'express' + +import { joinUriParts, EventEmitter } from '@credo-ts/core' + +import { getRequestContext, sendErrorResponse } from '../../shared/router' +import { OpenId4VcIssuanceSessionState } from '../OpenId4VcIssuanceSessionState' +import { OpenId4VcIssuerEvents } from '../OpenId4VcIssuerEvents' +import { OpenId4VcIssuerModuleConfig } from '../OpenId4VcIssuerModuleConfig' +import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' +import { OpenId4VcIssuanceSessionRepository } from '../repository' + +export interface OpenId4VciCredentialOfferEndpointConfig { + /** + * The path at which the credential offer should should be made available. Note that it will be + * hosted at a subpath to take into account multiple tenants and issuers. + * + * @default /offers + */ + endpointPath: string +} + +export function configureCredentialOfferEndpoint(router: Router, config: OpenId4VciCredentialOfferEndpointConfig) { + router.get( + joinUriParts(config.endpointPath, [':credentialOfferId']), + async (request: OpenId4VcIssuanceRequest, response: Response, next) => { + const { agentContext, issuer } = getRequestContext(request) + + if (!request.params.credentialOfferId || typeof request.params.credentialOfferId !== 'string') { + return sendErrorResponse( + response, + agentContext.config.logger, + 400, + 'invalid_request', + 'Invalid credential offer url' + ) + } + + try { + const issuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) + const issuerMetadata = issuerService.getIssuerMetadata(agentContext, issuer) + const openId4VcIssuanceSessionRepository = agentContext.dependencyManager.resolve( + OpenId4VcIssuanceSessionRepository + ) + const issuerConfig = agentContext.dependencyManager.resolve(OpenId4VcIssuerModuleConfig) + + const fullCredentialOfferUri = joinUriParts(issuerMetadata.issuerUrl, [ + issuerConfig.credentialOfferEndpoint.endpointPath, + request.params.credentialOfferId, + ]) + + const openId4VcIssuanceSession = await openId4VcIssuanceSessionRepository.findSingleByQuery(agentContext, { + issuerId: issuer.issuerId, + credentialOfferUri: fullCredentialOfferUri, + }) + + if (!openId4VcIssuanceSession || !openId4VcIssuanceSession.credentialOfferPayload) { + return sendErrorResponse(response, agentContext.config.logger, 404, 'not_found', 'Credential offer not found') + } + + if ( + ![OpenId4VcIssuanceSessionState.OfferCreated, OpenId4VcIssuanceSessionState.OfferUriRetrieved].includes( + openId4VcIssuanceSession.state + ) + ) { + return sendErrorResponse( + response, + agentContext.config.logger, + 400, + 'invalid_request', + 'Invalid state for credential offer' + ) + } + + // It's okay to retrieve the offer multiple times. So we only update the state if it's not already retrieved + if (openId4VcIssuanceSession.state !== OpenId4VcIssuanceSessionState.OfferUriRetrieved) { + const previousState = openId4VcIssuanceSession.state + + openId4VcIssuanceSession.state = OpenId4VcIssuanceSessionState.OfferUriRetrieved + await openId4VcIssuanceSessionRepository.update(agentContext, openId4VcIssuanceSession) + + agentContext.dependencyManager + .resolve(EventEmitter) + .emit(agentContext, { + type: OpenId4VcIssuerEvents.IssuanceSessionStateChanged, + payload: { + issuanceSession: openId4VcIssuanceSession.clone(), + previousState, + }, + }) + } + + response.json(openId4VcIssuanceSession.credentialOfferPayload) + } catch (error) { + sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) + } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() + } + ) +} diff --git a/packages/openid4vc/src/openid4vc-issuer/router/index.ts b/packages/openid4vc/src/openid4vc-issuer/router/index.ts new file mode 100644 index 0000000000..d261c81c50 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/router/index.ts @@ -0,0 +1,5 @@ +export { configureAccessTokenEndpoint, OpenId4VciAccessTokenEndpointConfig } from './accessTokenEndpoint' +export { configureCredentialEndpoint, OpenId4VciCredentialEndpointConfig } from './credentialEndpoint' +export { configureIssuerMetadataEndpoint } from './metadataEndpoint' +export { configureCredentialOfferEndpoint, OpenId4VciCredentialOfferEndpointConfig } from './credentialOfferEndpoint' +export { OpenId4VcIssuanceRequest } from './requestContext' diff --git a/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts new file mode 100644 index 0000000000..b3ecb4edc4 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts @@ -0,0 +1,35 @@ +import type { OpenId4VcIssuanceRequest } from './requestContext' +import type { CredentialIssuerMetadata } from '@sphereon/oid4vci-common' +import type { Router, Response } from 'express' + +import { getRequestContext, sendErrorResponse } from '../../shared/router' +import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' + +export function configureIssuerMetadataEndpoint(router: Router) { + router.get( + '/.well-known/openid-credential-issuer', + (_request: OpenId4VcIssuanceRequest, response: Response, next) => { + const { agentContext, issuer } = getRequestContext(_request) + + try { + const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) + const issuerMetadata = openId4VcIssuerService.getIssuerMetadata(agentContext, issuer) + const transformedMetadata = { + credential_issuer: issuerMetadata.issuerUrl, + token_endpoint: issuerMetadata.tokenEndpoint, + credential_endpoint: issuerMetadata.credentialEndpoint, + authorization_server: issuerMetadata.authorizationServer, + credentials_supported: issuerMetadata.credentialsSupported, + display: issuerMetadata.issuerDisplay, + } satisfies CredentialIssuerMetadata + + response.status(200).json(transformedMetadata) + } catch (e) { + sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', e) + } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() + } + ) +} diff --git a/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts b/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts new file mode 100644 index 0000000000..69e0caadb3 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts @@ -0,0 +1,4 @@ +import type { OpenId4VcRequest } from '../../shared/router' +import type { OpenId4VcIssuerRecord } from '../repository' + +export type OpenId4VcIssuanceRequest = OpenId4VcRequest<{ issuer: OpenId4VcIssuerRecord }> diff --git a/packages/openid4vc/src/openid4vc-issuer/router/verifyAccessToken.ts b/packages/openid4vc/src/openid4vc-issuer/router/verifyAccessToken.ts new file mode 100644 index 0000000000..8ee900afae --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/router/verifyAccessToken.ts @@ -0,0 +1,52 @@ +import type { OpenId4VcIssuerRecord } from '../repository' +import type { AgentContext } from '@credo-ts/core' + +import { CredoError, JwsService, Jwt } from '@credo-ts/core' + +import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' + +export async function verifyAccessToken( + agentContext: AgentContext, + issuer: OpenId4VcIssuerRecord, + authorizationHeader?: string +) { + const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) + + if (!authorizationHeader || !authorizationHeader.startsWith('Bearer ')) { + throw new CredoError('No access token provided in the authorization header') + } + + const issuerMetadata = openId4VcIssuerService.getIssuerMetadata(agentContext, issuer) + const accessToken = Jwt.fromSerializedJwt(authorizationHeader.replace('Bearer ', '')) + const jwsService = agentContext.dependencyManager.resolve(JwsService) + + const { isValid, signerKeys } = await jwsService.verifyJws(agentContext, { + jws: accessToken.serializedJwt, + jwkResolver: () => { + throw new Error('No JWK resolver available for access token verification') + }, + }) + + if (!isValid) { + throw new CredoError('Signature on access token is invalid') + } + + if (!signerKeys.map((key) => key.fingerprint).includes(issuer.accessTokenPublicKeyFingerprint)) { + throw new CredoError('Access token was not signed by the expected issuer') + } + + // Finally validate the JWT payload (expiry etc..) + accessToken.payload.validate() + + if (accessToken.payload.iss !== issuerMetadata.issuerUrl) { + throw new CredoError('Access token was not issued by the expected issuer') + } + + if (typeof accessToken.payload.additionalClaims.preAuthorizedCode !== 'string') { + throw new CredoError('No preAuthorizedCode present in access token') + } + + return { + preAuthorizedCode: accessToken.payload.additionalClaims.preAuthorizedCode, + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/util/credentialRequest.ts b/packages/openid4vc/src/openid4vc-issuer/util/credentialRequest.ts new file mode 100644 index 0000000000..61614148d7 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/util/credentialRequest.ts @@ -0,0 +1,14 @@ +import type { OpenId4VciCredentialRequest } from '../../shared' + +import { Jwt, CredoError } from '@credo-ts/core' + +/** + * Extract the 'nonce' parameter from the JWT payload of the credential request. + */ +export function getCNonceFromCredentialRequest(credentialRequest: OpenId4VciCredentialRequest) { + if (!credentialRequest.proof?.jwt) throw new CredoError('No jwt in the credentialRequest proof.') + const jwt = Jwt.fromSerializedJwt(credentialRequest.proof.jwt) + if (!jwt.payload.additionalClaims.nonce || typeof jwt.payload.additionalClaims.nonce !== 'string') + throw new CredoError('No nonce in the credentialRequest JWT proof payload.') + return jwt.payload.additionalClaims.nonce +} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts new file mode 100644 index 0000000000..19f5ecba01 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -0,0 +1,545 @@ +import type { + OpenId4VcSiopCreateAuthorizationRequestOptions, + OpenId4VcSiopCreateAuthorizationRequestReturn, + OpenId4VcSiopCreateVerifierOptions, + OpenId4VcSiopVerifiedAuthorizationResponse, + OpenId4VcSiopVerifyAuthorizationResponseOptions, +} from './OpenId4VcSiopVerifierServiceOptions' +import type { OpenId4VcVerificationSessionRecord } from './repository' +import type { OpenId4VcJwtIssuer, OpenId4VcSiopAuthorizationResponsePayload } from '../shared' +import type { + AgentContext, + DifPresentationExchangeDefinition, + Query, + QueryOptions, + RecordSavedEvent, + RecordUpdatedEvent, +} from '@credo-ts/core' +import type { PresentationVerificationCallback } from '@sphereon/did-auth-siop' + +import { + EventEmitter, + RepositoryEventTypes, + CredoError, + inject, + injectable, + InjectionSymbols, + joinUriParts, + JsonTransformer, + Logger, + SdJwtVcApi, + SignatureSuiteRegistry, + utils, + W3cCredentialService, + W3cJsonLdVerifiablePresentation, + Hasher, + DidsApi, +} from '@credo-ts/core' +import { + AuthorizationRequest, + AuthorizationResponse, + CheckLinkedDomain, + PassBy, + PropertyTarget, + ResponseIss, + ResponseMode, + ResponseType, + RevocationVerification, + RP, + SupportedVersion, + VerificationMode, +} from '@sphereon/did-auth-siop' +import { extractPresentationsFromAuthorizationResponse } from '@sphereon/did-auth-siop/dist/authorization-response/OpenID4VP' +import { filter, first, firstValueFrom, map, timeout } from 'rxjs' + +import { storeActorIdForContextCorrelationId } from '../shared/router' +import { getVerifiablePresentationFromSphereonWrapped } from '../shared/transform' +import { + getSphereonDidResolver, + getSphereonSuppliedSignatureFromJwtIssuer, + getSupportedJwaSignatureAlgorithms, +} from '../shared/utils' + +import { OpenId4VcVerificationSessionState } from './OpenId4VcVerificationSessionState' +import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' +import { + OpenId4VcVerificationSessionRepository, + OpenId4VcVerifierRecord, + OpenId4VcVerifierRepository, +} from './repository' +import { OpenId4VcRelyingPartyEventHandler } from './repository/OpenId4VcRelyingPartyEventEmitter' +import { OpenId4VcRelyingPartySessionManager } from './repository/OpenId4VcRelyingPartySessionManager' + +/** + * @internal + */ +@injectable() +export class OpenId4VcSiopVerifierService { + public constructor( + @inject(InjectionSymbols.Logger) private logger: Logger, + private w3cCredentialService: W3cCredentialService, + private openId4VcVerifierRepository: OpenId4VcVerifierRepository, + private config: OpenId4VcVerifierModuleConfig, + private openId4VcVerificationSessionRepository: OpenId4VcVerificationSessionRepository + ) {} + + public async createAuthorizationRequest( + agentContext: AgentContext, + options: OpenId4VcSiopCreateAuthorizationRequestOptions & { verifier: OpenId4VcVerifierRecord } + ): Promise { + const nonce = await agentContext.wallet.generateNonce() + const state = await agentContext.wallet.generateNonce() + + // Correlation id will be the id of the verification session record + const correlationId = utils.uuid() + + const relyingParty = await this.getRelyingParty(agentContext, options.verifier.verifierId, { + presentationDefinition: options.presentationExchange?.definition, + requestSigner: options.requestSigner, + }) + + // We always use shortened URIs currently + const hostedAuthorizationRequestUri = joinUriParts(this.config.baseUrl, [ + options.verifier.verifierId, + this.config.authorizationRequestEndpoint.endpointPath, + // It doesn't really matter what the url is, as long as it's unique + utils.uuid(), + ]) + + // This is very unfortunate, but storing state in sphereon's SiOP-OID4VP library + // is done async, so we can't be certain yet that the verification session record + // is created already when we have created the authorization request. So we need to + // wait for a short while before we can be certain that the verification session record + // is created. To not use arbitrary timeouts, we wait for the specific RecordSavedEvent + // that is emitted when the verification session record is created. + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + const verificationSessionCreatedPromise = firstValueFrom( + eventEmitter + .observable>(RepositoryEventTypes.RecordSaved) + .pipe( + filter((e) => e.metadata.contextCorrelationId === agentContext.contextCorrelationId), + filter( + (e) => e.payload.record.id === correlationId && e.payload.record.verifierId === options.verifier.verifierId + ), + first(), + timeout({ + first: 10000, + meta: 'OpenId4VcSiopVerifierService.createAuthorizationRequest', + }), + map((e) => e.payload.record) + ) + ) + + const authorizationRequest = await relyingParty.createAuthorizationRequest({ + correlationId, + nonce, + state, + requestByReferenceURI: hostedAuthorizationRequestUri, + }) + + // NOTE: it's not possible to set the uri scheme when using the RP to create an auth request, only lower level + // functions allow this. So we need to replace the uri scheme manually. + let authorizationRequestUri = (await authorizationRequest.uri()).encodedUri + if (options.presentationExchange && !options.idToken) { + authorizationRequestUri = authorizationRequestUri.replace('openid://', 'openid4vp://') + } + + const verificationSession = await verificationSessionCreatedPromise + + return { + authorizationRequest: authorizationRequestUri, + verificationSession, + } + } + + public async verifyAuthorizationResponse( + agentContext: AgentContext, + options: OpenId4VcSiopVerifyAuthorizationResponseOptions & { + verificationSession: OpenId4VcVerificationSessionRecord + } + ): Promise { + // Assert state + options.verificationSession.assertState([ + OpenId4VcVerificationSessionState.RequestUriRetrieved, + OpenId4VcVerificationSessionState.RequestCreated, + ]) + + const authorizationRequest = await AuthorizationRequest.fromUriOrJwt( + options.verificationSession.authorizationRequestJwt + ) + + const requestClientId = await authorizationRequest.getMergedProperty('client_id') + const requestNonce = await authorizationRequest.getMergedProperty('nonce') + const requestState = await authorizationRequest.getMergedProperty('state') + const presentationDefinitionsWithLocation = await authorizationRequest.getPresentationDefinitions() + + if (!requestNonce || !requestClientId || !requestState) { + throw new CredoError( + `Unable to find nonce, state, or client_id in authorization request for verification session '${options.verificationSession.id}'` + ) + } + + const relyingParty = await this.getRelyingParty(agentContext, options.verificationSession.verifierId, { + presentationDefinition: presentationDefinitionsWithLocation?.[0]?.definition, + clientId: requestClientId, + }) + + // This is very unfortunate, but storing state in sphereon's SiOP-OID4VP library + // is done async, so we can't be certain yet that the verification session record + // is updated already when we have verified the authorization response. So we need to + // wait for a short while before we can be certain that the verification session record + // is updated. To not use arbitrary timeouts, we wait for the specific RecordUpdatedEvent + // that is emitted when the verification session record is updated. + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + const verificationSessionUpdatedPromise = firstValueFrom( + eventEmitter + .observable>(RepositoryEventTypes.RecordUpdated) + .pipe( + filter((e) => e.metadata.contextCorrelationId === agentContext.contextCorrelationId), + filter( + (e) => + e.payload.record.id === options.verificationSession.id && + e.payload.record.verifierId === options.verificationSession.verifierId && + (e.payload.record.state === OpenId4VcVerificationSessionState.ResponseVerified || + e.payload.record.state === OpenId4VcVerificationSessionState.Error) + ), + first(), + timeout({ + first: 10000, + meta: 'OpenId4VcSiopVerifierService.verifyAuthorizationResponse', + }), + map((e) => e.payload.record) + ) + ) + + await relyingParty.verifyAuthorizationResponse(options.authorizationResponse, { + audience: requestClientId, + correlationId: options.verificationSession.id, + state: requestState, + presentationDefinitions: presentationDefinitionsWithLocation, + verification: { + presentationVerificationCallback: this.getPresentationVerificationCallback(agentContext, { + nonce: requestNonce, + audience: requestClientId, + }), + // FIXME: Supplied mode is not implemented. + // See https://github.com/Sphereon-Opensource/SIOP-OID4VP/issues/55 + mode: VerificationMode.INTERNAL, + resolveOpts: { noUniversalResolverFallback: true, resolver: getSphereonDidResolver(agentContext) }, + }, + }) + + const verificationSession = await verificationSessionUpdatedPromise + const verifiedAuthorizationResponse = await this.getVerifiedAuthorizationResponse(verificationSession) + + return { + ...verifiedAuthorizationResponse, + + verificationSession: await verificationSessionUpdatedPromise, + } + } + + // TODO: we can also choose to store this in the verification session, however we can easily derive it + // so it's probably easier to make changes in the future if we just store the raw payload. + public async getVerifiedAuthorizationResponse( + verificationSession: OpenId4VcVerificationSessionRecord + ): Promise { + verificationSession.assertState(OpenId4VcVerificationSessionState.ResponseVerified) + + if (!verificationSession.authorizationResponsePayload) { + throw new CredoError('No authorization response payload found in the verification session.') + } + + const authorizationResponse = await AuthorizationResponse.fromPayload( + verificationSession.authorizationResponsePayload + ) + const authorizationRequest = await AuthorizationRequest.fromUriOrJwt(verificationSession.authorizationRequestJwt) + + const idToken = authorizationResponse.idToken + ? { payload: await authorizationResponse.idToken?.payload() } + : undefined + let presentationExchange: OpenId4VcSiopVerifiedAuthorizationResponse['presentationExchange'] | undefined = undefined + + const presentationDefinitions = await authorizationRequest.getPresentationDefinitions() + if (presentationDefinitions && presentationDefinitions.length > 0) { + const presentations = await extractPresentationsFromAuthorizationResponse(authorizationResponse, { + hasher: Hasher.hash, + }) + + // TODO: Probably wise to check against request for the location of the submission_data + const submission = + idToken?.payload?._vp_token?.presentation_submission ?? authorizationResponse.payload.presentation_submission + if (!submission) { + throw new CredoError('Unable to extract submission from the response.') + } + + presentationExchange = { + definition: presentationDefinitions[0].definition, + presentations: presentations.map(getVerifiablePresentationFromSphereonWrapped), + submission, + } + } + + if (!idToken && !presentationExchange) { + throw new CredoError('No idToken or presentationExchange found in the response.') + } + + return { + idToken, + presentationExchange, + } + } + + /** + * Find the verification session associated with an authorization response. You can optionally provide a verifier id + * if the verifier that the response is associated with is already known. + */ + public async findVerificationSessionForAuthorizationResponse( + agentContext: AgentContext, + { + authorizationResponse, + verifierId, + }: { + authorizationResponse: OpenId4VcSiopAuthorizationResponsePayload + verifierId?: string + } + ) { + const authorizationResponseInstance = await AuthorizationResponse.fromPayload(authorizationResponse).catch(() => { + throw new CredoError(`Unable to parse authorization response payload. ${JSON.stringify(authorizationResponse)}`) + }) + + const responseNonce = await authorizationResponseInstance.getMergedProperty('nonce', { + hasher: Hasher.hash, + }) + const responseState = await authorizationResponseInstance.getMergedProperty('state', { + hasher: Hasher.hash, + }) + + const verificationSession = await this.openId4VcVerificationSessionRepository.findSingleByQuery(agentContext, { + nonce: responseNonce, + payloadState: responseState, + verifierId, + }) + + return verificationSession + } + + public async getAllVerifiers(agentContext: AgentContext) { + return this.openId4VcVerifierRepository.getAll(agentContext) + } + + public async getVerifierByVerifierId(agentContext: AgentContext, verifierId: string) { + return this.openId4VcVerifierRepository.getByVerifierId(agentContext, verifierId) + } + + public async updateVerifier(agentContext: AgentContext, verifier: OpenId4VcVerifierRecord) { + return this.openId4VcVerifierRepository.update(agentContext, verifier) + } + + public async createVerifier(agentContext: AgentContext, options?: OpenId4VcSiopCreateVerifierOptions) { + const openId4VcVerifier = new OpenId4VcVerifierRecord({ + verifierId: options?.verifierId ?? utils.uuid(), + }) + + await this.openId4VcVerifierRepository.save(agentContext, openId4VcVerifier) + await storeActorIdForContextCorrelationId(agentContext, openId4VcVerifier.verifierId) + return openId4VcVerifier + } + + public async findVerificationSessionsByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ) { + return this.openId4VcVerificationSessionRepository.findByQuery(agentContext, query, queryOptions) + } + + public async getVerificationSessionById(agentContext: AgentContext, verificationSessionId: string) { + return this.openId4VcVerificationSessionRepository.getById(agentContext, verificationSessionId) + } + + private async getRelyingParty( + agentContext: AgentContext, + verifierId: string, + { + idToken, + presentationDefinition, + requestSigner, + clientId, + }: { + idToken?: boolean + presentationDefinition?: DifPresentationExchangeDefinition + requestSigner?: OpenId4VcJwtIssuer + clientId?: string + } + ) { + const authorizationResponseUrl = joinUriParts(this.config.baseUrl, [ + verifierId, + this.config.authorizationEndpoint.endpointPath, + ]) + + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + + const supportedAlgs = getSupportedJwaSignatureAlgorithms(agentContext) as string[] + const supportedProofTypes = signatureSuiteRegistry.supportedProofTypes + + // Check: audience must be set to the issuer with dynamic disc otherwise self-issued.me/v2. + const builder = RP.builder() + + let _clientId = clientId + if (requestSigner) { + const suppliedSignature = await getSphereonSuppliedSignatureFromJwtIssuer(agentContext, requestSigner) + builder.withSignature(suppliedSignature) + + _clientId = suppliedSignature.did + } + + if (!_clientId) { + throw new CredoError("Either 'requestSigner' or 'clientId' must be provided.") + } + + const responseTypes: ResponseType[] = [] + if (!presentationDefinition && idToken === false) { + throw new CredoError('Either `presentationExchange` or `idToken` must be enabled') + } + if (presentationDefinition) { + responseTypes.push(ResponseType.VP_TOKEN) + } + if (idToken === true || !presentationDefinition) { + responseTypes.push(ResponseType.ID_TOKEN) + } + + // FIXME: we now manually remove did:peer, we should probably allow the user to configure this + const supportedDidMethods = agentContext.dependencyManager + .resolve(DidsApi) + .supportedResolverMethods.filter((m) => m !== 'peer') + + // The OpenId4VcRelyingPartyEventHandler is a global event handler that makes sure that + // all the events are handled, and that the correct context is used for the events. + const sphereonEventEmitter = agentContext.dependencyManager + .resolve(OpenId4VcRelyingPartyEventHandler) + .getEventEmitterForVerifier(agentContext.contextCorrelationId, verifierId) + + builder + .withRedirectUri(authorizationResponseUrl) + .withIssuer(ResponseIss.SELF_ISSUED_V2) + .withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18]) + .withCustomResolver(getSphereonDidResolver(agentContext)) + .withResponseMode(ResponseMode.POST) + .withHasher(Hasher.hash) + .withCheckLinkedDomain(CheckLinkedDomain.NEVER) + // FIXME: should allow verification of revocation + // .withRevocationVerificationCallback() + .withRevocationVerification(RevocationVerification.NEVER) + .withSessionManager(new OpenId4VcRelyingPartySessionManager(agentContext, verifierId)) + .withEventEmitter(sphereonEventEmitter) + .withResponseType(responseTypes) + + // TODO: we should probably allow some dynamic values here + .withClientMetadata({ + client_id: _clientId, + passBy: PassBy.VALUE, + responseTypesSupported: [ResponseType.VP_TOKEN], + subject_syntax_types_supported: supportedDidMethods.map((m) => `did:${m}`), + vpFormatsSupported: { + jwt_vc: { + alg: supportedAlgs, + }, + jwt_vc_json: { + alg: supportedAlgs, + }, + jwt_vp: { + alg: supportedAlgs, + }, + ldp_vc: { + proof_type: supportedProofTypes, + }, + ldp_vp: { + proof_type: supportedProofTypes, + }, + 'vc+sd-jwt': { + kb_jwt_alg_values: supportedAlgs, + sd_jwt_alg_values: supportedAlgs, + }, + }, + }) + + if (presentationDefinition) { + builder.withPresentationDefinition({ definition: presentationDefinition }, [PropertyTarget.REQUEST_OBJECT]) + } + if (responseTypes.includes(ResponseType.ID_TOKEN)) { + builder.withScope('openid') + } + + for (const supportedDidMethod of supportedDidMethods) { + builder.addDidMethod(supportedDidMethod) + } + + return builder.build() + } + + private getPresentationVerificationCallback( + agentContext: AgentContext, + options: { nonce: string; audience: string } + ): PresentationVerificationCallback { + return async (encodedPresentation, presentationSubmission) => { + try { + this.logger.debug(`Presentation response`, JsonTransformer.toJSON(encodedPresentation)) + this.logger.debug(`Presentation submission`, presentationSubmission) + + if (!encodedPresentation) throw new CredoError('Did not receive a presentation for verification.') + + let isValid: boolean + + // TODO: it might be better here to look at the presentation submission to know + // If presentation includes a ~, we assume it's an SD-JWT-VC + if (typeof encodedPresentation === 'string' && encodedPresentation.includes('~')) { + const sdJwtVcApi = agentContext.dependencyManager.resolve(SdJwtVcApi) + + const verificationResult = await sdJwtVcApi.verify({ + compactSdJwtVc: encodedPresentation, + keyBinding: { + audience: options.audience, + nonce: options.nonce, + }, + }) + + isValid = verificationResult.verification.isValid + } else if (typeof encodedPresentation === 'string') { + const verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { + presentation: encodedPresentation, + challenge: options.nonce, + domain: options.audience, + }) + + isValid = verificationResult.isValid + } else { + const verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { + presentation: JsonTransformer.fromJSON(encodedPresentation, W3cJsonLdVerifiablePresentation), + challenge: options.nonce, + domain: options.audience, + }) + + isValid = verificationResult.isValid + } + + // FIXME: we throw an error here as there's a bug in sphereon library where they + // don't check the returned 'verified' property and only catch errors thrown. + // Once https://github.com/Sphereon-Opensource/SIOP-OID4VP/pull/70 is merged we + // can remove this. + if (!isValid) { + throw new CredoError('Presentation verification failed.') + } + + return { + verified: isValid, + } + } catch (error) { + agentContext.config.logger.warn('Error occurred during verification of presentation', { + error, + }) + throw error + } + } + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts new file mode 100644 index 0000000000..6229b6fc2a --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts @@ -0,0 +1,67 @@ +import type { OpenId4VcVerificationSessionRecord } from './repository' +import type { + OpenId4VcJwtIssuer, + OpenId4VcSiopAuthorizationResponsePayload, + OpenId4VcSiopIdTokenPayload, +} from '../shared' +import type { + DifPresentationExchangeDefinition, + DifPresentationExchangeSubmission, + DifPresentationExchangeDefinitionV2, + VerifiablePresentation, +} from '@credo-ts/core' + +export interface OpenId4VcSiopCreateAuthorizationRequestOptions { + /** + * Signing information for the request JWT. This will be used to sign the request JWT + * and to set the client_id for registration of client_metadata. + */ + requestSigner: OpenId4VcJwtIssuer + + /** + * Whether to reuqest an ID Token. Enabled by defualt when `presentationExchange` is not provided, + * disabled by default when `presentationExchange` is provided. + */ + idToken?: boolean + + /** + * A DIF Presentation Definition (v2) can be provided to request a Verifiable Presentation using OpenID4VP. + */ + presentationExchange?: { + definition: DifPresentationExchangeDefinitionV2 + } +} + +export interface OpenId4VcSiopVerifyAuthorizationResponseOptions { + /** + * The authorization response received from the OpenID Provider (OP). + */ + authorizationResponse: OpenId4VcSiopAuthorizationResponsePayload +} + +export interface OpenId4VcSiopCreateAuthorizationRequestReturn { + authorizationRequest: string + verificationSession: OpenId4VcVerificationSessionRecord +} + +/** + * Either `idToken` and/or `presentationExchange` will be present. + */ +export interface OpenId4VcSiopVerifiedAuthorizationResponse { + idToken?: { + payload: OpenId4VcSiopIdTokenPayload + } + + presentationExchange?: { + submission: DifPresentationExchangeSubmission + definition: DifPresentationExchangeDefinition + presentations: Array + } +} + +export interface OpenId4VcSiopCreateVerifierOptions { + /** + * Id of the verifier, not the id of the verifier record. Will be exposed publicly + */ + verifierId?: string +} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerificationSessionState.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerificationSessionState.ts new file mode 100644 index 0000000000..3e791f31b5 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerificationSessionState.ts @@ -0,0 +1,6 @@ +export enum OpenId4VcVerificationSessionState { + RequestCreated = 'RequestCreated', + RequestUriRetrieved = 'RequestUriRetrieved', + ResponseVerified = 'ResponseVerified', + Error = 'Error', +} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts new file mode 100644 index 0000000000..03a67d083e --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts @@ -0,0 +1,113 @@ +import type { + OpenId4VcSiopCreateAuthorizationRequestOptions, + OpenId4VcSiopVerifyAuthorizationResponseOptions, + OpenId4VcSiopCreateAuthorizationRequestReturn, + OpenId4VcSiopCreateVerifierOptions, +} from './OpenId4VcSiopVerifierServiceOptions' +import type { OpenId4VcVerificationSessionRecord } from './repository' +import type { OpenId4VcSiopAuthorizationResponsePayload } from '../shared' +import type { Query, QueryOptions } from '@credo-ts/core' + +import { injectable, AgentContext } from '@credo-ts/core' + +import { OpenId4VcSiopVerifierService } from './OpenId4VcSiopVerifierService' +import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' + +/** + * @public + */ +@injectable() +export class OpenId4VcVerifierApi { + public constructor( + public readonly config: OpenId4VcVerifierModuleConfig, + private agentContext: AgentContext, + private openId4VcSiopVerifierService: OpenId4VcSiopVerifierService + ) {} + + /** + * Retrieve all verifier records from storage + */ + public async getAllVerifiers() { + return this.openId4VcSiopVerifierService.getAllVerifiers(this.agentContext) + } + + /** + * Retrieve a verifier record from storage by its verified id + */ + public async getVerifierByVerifierId(verifierId: string) { + return this.openId4VcSiopVerifierService.getVerifierByVerifierId(this.agentContext, verifierId) + } + + /** + * Create a new verifier and store the new verifier record. + */ + public async createVerifier(options?: OpenId4VcSiopCreateVerifierOptions) { + return this.openId4VcSiopVerifierService.createVerifier(this.agentContext, options) + } + + public async findVerificationSessionsByQuery( + query: Query, + queryOptions?: QueryOptions + ) { + return this.openId4VcSiopVerifierService.findVerificationSessionsByQuery(this.agentContext, query, queryOptions) + } + + public async getVerificationSessionById(verificationSessionId: string) { + return this.openId4VcSiopVerifierService.getVerificationSessionById(this.agentContext, verificationSessionId) + } + + /** + * Create an authorization request, acting as a Relying Party (RP). + * + * Currently two types of requests are supported: + * - SIOP Self-Issued ID Token request: request to a Self-Issued OP from an RP + * - SIOP Verifiable Presentation Request: request to a Self-Issued OP from an RP, requesting a Verifiable Presentation using OpenID4VP + * + * Other flows (non-SIOP) are not supported at the moment, but can be added in the future. + * + * See {@link OpenId4VcSiopCreateAuthorizationRequestOptions} for detailed documentation on the options. + */ + public async createAuthorizationRequest({ + verifierId, + ...otherOptions + }: OpenId4VcSiopCreateAuthorizationRequestOptions & { + verifierId: string + }): Promise { + const verifier = await this.getVerifierByVerifierId(verifierId) + return await this.openId4VcSiopVerifierService.createAuthorizationRequest(this.agentContext, { + ...otherOptions, + verifier, + }) + } + + /** + * Verifies an authorization response, acting as a Relying Party (RP). + * + * It validates the ID Token, VP Token and the signature(s) of the received Verifiable Presentation(s) + * as well as that the structure of the Verifiable Presentation matches the provided presentation definition. + */ + public async verifyAuthorizationResponse({ + verificationSessionId, + ...otherOptions + }: OpenId4VcSiopVerifyAuthorizationResponseOptions & { + verificationSessionId: string + }) { + const verificationSession = await this.getVerificationSessionById(verificationSessionId) + return await this.openId4VcSiopVerifierService.verifyAuthorizationResponse(this.agentContext, { + ...otherOptions, + verificationSession, + }) + } + + public async getVerifiedAuthorizationResponse(verificationSessionId: string) { + const verificationSession = await this.getVerificationSessionById(verificationSessionId) + return this.openId4VcSiopVerifierService.getVerifiedAuthorizationResponse(verificationSession) + } + + public async findVerificationSessionForAuthorizationResponse(options: { + authorizationResponse: OpenId4VcSiopAuthorizationResponsePayload + verifierId?: string + }) { + return this.openId4VcSiopVerifierService.findVerificationSessionForAuthorizationResponse(this.agentContext, options) + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierEvents.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierEvents.ts new file mode 100644 index 0000000000..cbb9fb3732 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierEvents.ts @@ -0,0 +1,15 @@ +import type { OpenId4VcVerificationSessionState } from './OpenId4VcVerificationSessionState' +import type { OpenId4VcVerificationSessionRecord } from './repository' +import type { BaseEvent } from '@credo-ts/core' + +export enum OpenId4VcVerifierEvents { + VerificationSessionStateChanged = 'OpenId4VcVerifier.VerificationSessionStateChanged', +} + +export interface OpenId4VcVerificationSessionStateChangedEvent extends BaseEvent { + type: typeof OpenId4VcVerifierEvents.VerificationSessionStateChanged + payload: { + verificationSession: OpenId4VcVerificationSessionRecord + previousState: OpenId4VcVerificationSessionState | null + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts new file mode 100644 index 0000000000..4e44b2883e --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts @@ -0,0 +1,133 @@ +import type { OpenId4VcVerifierModuleConfigOptions } from './OpenId4VcVerifierModuleConfig' +import type { OpenId4VcVerificationRequest } from './router' +import type { AgentContext, DependencyManager, Module } from '@credo-ts/core' +import type { NextFunction } from 'express' + +import { AgentConfig } from '@credo-ts/core' + +import { getAgentContextForActorId, getRequestContext, importExpress } from '../shared/router' + +import { OpenId4VcSiopVerifierService } from './OpenId4VcSiopVerifierService' +import { OpenId4VcVerifierApi } from './OpenId4VcVerifierApi' +import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' +import { OpenId4VcVerifierRepository } from './repository' +import { OpenId4VcRelyingPartyEventHandler } from './repository/OpenId4VcRelyingPartyEventEmitter' +import { configureAuthorizationEndpoint } from './router' +import { configureAuthorizationRequestEndpoint } from './router/authorizationRequestEndpoint' + +/** + * @public + */ +export class OpenId4VcVerifierModule implements Module { + public readonly api = OpenId4VcVerifierApi + public readonly config: OpenId4VcVerifierModuleConfig + + public constructor(options: OpenId4VcVerifierModuleConfigOptions) { + this.config = new OpenId4VcVerifierModuleConfig(options) + } + + /** + * Registers the dependencies of the question answer module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + const logger = dependencyManager.resolve(AgentConfig).logger + logger.warn( + "The '@credo-ts/openid4vc' Verifier module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." + ) + + // Register config + dependencyManager.registerInstance(OpenId4VcVerifierModuleConfig, this.config) + + // Services + dependencyManager.registerSingleton(OpenId4VcSiopVerifierService) + + // Repository + dependencyManager.registerSingleton(OpenId4VcVerifierRepository) + + // Global event emitter + dependencyManager.registerSingleton(OpenId4VcRelyingPartyEventHandler) + } + + public async initialize(rootAgentContext: AgentContext): Promise { + this.configureRouter(rootAgentContext) + } + + /** + * Registers the endpoints on the router passed to this module. + */ + private configureRouter(rootAgentContext: AgentContext) { + const { Router, json, urlencoded } = importExpress() + + // FIXME: it is currently not possible to initialize an agent + // shut it down, and then start it again, as the + // express router is configured with a specific `AgentContext` instance + // and dependency manager. One option is to always create a new router + // but then users cannot pass their own router implementation. + // We need to find a proper way to fix this. + + // We use separate context router and endpoint router. Context router handles the linking of the request + // to a specific agent context. Endpoint router only knows about a single context + const endpointRouter = Router() + const contextRouter = this.config.router + + // parse application/x-www-form-urlencoded + contextRouter.use(urlencoded({ extended: false })) + // parse application/json + contextRouter.use(json()) + + contextRouter.param('verifierId', async (req: OpenId4VcVerificationRequest, _res, next, verifierId: string) => { + if (!verifierId) { + rootAgentContext.config.logger.debug( + 'No verifierId provided for incoming authorization response, returning 404' + ) + _res.status(404).send('Not found') + } + + let agentContext: AgentContext | undefined = undefined + + try { + agentContext = await getAgentContextForActorId(rootAgentContext, verifierId) + const verifierApi = agentContext.dependencyManager.resolve(OpenId4VcVerifierApi) + const verifier = await verifierApi.getVerifierByVerifierId(verifierId) + + req.requestContext = { + agentContext, + verifier, + } + } catch (error) { + agentContext?.config.logger.error( + 'Failed to correlate incoming openid request to existing tenant and verifier', + { + error, + } + ) + // If the opening failed + await agentContext?.endSession() + return _res.status(404).send('Not found') + } + + next() + }) + + contextRouter.use('/:verifierId', endpointRouter) + + // Configure endpoints + configureAuthorizationEndpoint(endpointRouter, this.config.authorizationEndpoint) + configureAuthorizationRequestEndpoint(endpointRouter, this.config.authorizationRequestEndpoint) + + // First one will be called for all requests (when next is called) + contextRouter.use(async (req: OpenId4VcVerificationRequest, _res: unknown, next) => { + const { agentContext } = getRequestContext(req) + await agentContext.endSession() + next() + }) + + // This one will be called for all errors that are thrown + contextRouter.use(async (_error: unknown, req: OpenId4VcVerificationRequest, _res: unknown, next: NextFunction) => { + const { agentContext } = getRequestContext(req) + await agentContext.endSession() + next() + }) + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts new file mode 100644 index 0000000000..b2ec763cbc --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts @@ -0,0 +1,63 @@ +import type { OpenId4VcSiopAuthorizationEndpointConfig } from './router/authorizationEndpoint' +import type { OpenId4VcSiopAuthorizationRequestEndpointConfig } from './router/authorizationRequestEndpoint' +import type { Optional } from '@credo-ts/core' +import type { Router } from 'express' + +import { importExpress } from '../shared/router' + +export interface OpenId4VcVerifierModuleConfigOptions { + /** + * Base url at which the verifier endpoints will be hosted. All endpoints will be exposed with + * this path as prefix. + */ + baseUrl: string + + /** + * Express router on which the verifier endpoints will be registered. If + * no router is provided, a new one will be created. + * + * NOTE: you must manually register the router on your express app and + * expose this on a public url that is reachable when `baseUrl` is called. + */ + router?: Router + + endpoints?: { + authorization?: Optional + authorizationRequest?: Optional + } +} + +export class OpenId4VcVerifierModuleConfig { + private options: OpenId4VcVerifierModuleConfigOptions + public readonly router: Router + + public constructor(options: OpenId4VcVerifierModuleConfigOptions) { + this.options = options + + this.router = options.router ?? importExpress().Router() + } + + public get baseUrl() { + return this.options.baseUrl + } + + public get authorizationRequestEndpoint(): OpenId4VcSiopAuthorizationRequestEndpointConfig { + // Use user supplied options, or return defaults. + const userOptions = this.options.endpoints?.authorizationRequest + + return { + ...userOptions, + endpointPath: this.options.endpoints?.authorizationRequest?.endpointPath ?? '/authorization-requests', + } + } + + public get authorizationEndpoint(): OpenId4VcSiopAuthorizationEndpointConfig { + // Use user supplied options, or return defaults. + const userOptions = this.options.endpoints?.authorization + + return { + ...userOptions, + endpointPath: userOptions?.endpointPath ?? '/authorize', + } + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/OpenId4VcVerifierModule.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/OpenId4VcVerifierModule.test.ts new file mode 100644 index 0000000000..b44dc41321 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/OpenId4VcVerifierModule.test.ts @@ -0,0 +1,44 @@ +import type { OpenId4VcVerifierModuleConfigOptions } from '../OpenId4VcVerifierModuleConfig' +import type { DependencyManager } from '@credo-ts/core' + +import { Router } from 'express' + +import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService' +import { OpenId4VcVerifierModule } from '../OpenId4VcVerifierModule' +import { OpenId4VcVerifierModuleConfig } from '../OpenId4VcVerifierModuleConfig' +import { OpenId4VcVerifierRepository } from '../repository' +import { OpenId4VcRelyingPartyEventHandler } from '../repository/OpenId4VcRelyingPartyEventEmitter' + +const dependencyManager = { + registerInstance: jest.fn(), + registerSingleton: jest.fn(), + registerContextScoped: jest.fn(), + resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), +} as unknown as DependencyManager + +describe('OpenId4VcVerifierModule', () => { + test('registers dependencies on the dependency manager', () => { + const options = { + baseUrl: 'http://localhost:3000', + endpoints: { + authorization: { + endpointPath: '/hello', + }, + }, + router: Router(), + } satisfies OpenId4VcVerifierModuleConfigOptions + const openId4VcClientModule = new OpenId4VcVerifierModule(options) + openId4VcClientModule.register(dependencyManager) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith( + OpenId4VcVerifierModuleConfig, + new OpenId4VcVerifierModuleConfig(options) + ) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcSiopVerifierService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcVerifierRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcRelyingPartyEventHandler) + }) +}) diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts new file mode 100644 index 0000000000..8ef0e40936 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts @@ -0,0 +1,113 @@ +import { Jwt } from '@credo-ts/core' +import { SigningAlgo } from '@sphereon/did-auth-siop' +import { cleanAll, enableNetConnect } from 'nock' + +import { AskarModule } from '../../../../askar/src' +import { askarModuleConfig } from '../../../../askar/tests/helpers' +import { createAgentFromModules, type AgentType } from '../../../tests/utils' +import { universityDegreePresentationDefinition } from '../../../tests/utilsVp' +import { OpenId4VcVerifierModule } from '../OpenId4VcVerifierModule' + +const modules = { + openId4VcVerifier: new OpenId4VcVerifierModule({ + baseUrl: 'http://redirect-uri', + }), + askar: new AskarModule(askarModuleConfig), +} + +describe('OpenId4VcVerifier', () => { + let verifier: AgentType + + beforeEach(async () => { + verifier = await createAgentFromModules('verifier', modules, '96213c3d7fc8d4d6754c7a0fd969598f') + }) + + afterEach(async () => { + await verifier.agent.shutdown() + await verifier.agent.wallet.delete() + }) + + describe('Verification', () => { + afterEach(() => { + cleanAll() + enableNetConnect() + }) + + it('check openid proof request format (vp token)', async () => { + const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() + const { authorizationRequest, verificationSession } = + await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'did', + didUrl: verifier.kid, + }, + verifierId: openIdVerifier.verifierId, + presentationExchange: { + definition: universityDegreePresentationDefinition, + }, + }) + + expect( + authorizationRequest.startsWith( + `openid4vp://?request_uri=http%3A%2F%2Fredirect-uri%2F${openIdVerifier.verifierId}%2Fauthorization-requests%2F` + ) + ).toBe(true) + + const jwt = Jwt.fromSerializedJwt(verificationSession.authorizationRequestJwt) + + expect(jwt.header.kid) + + expect(jwt.header.kid).toEqual(verifier.kid) + expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) + expect(jwt.header.typ).toEqual('JWT') + expect(jwt.payload.additionalClaims.scope).toEqual('openid') + expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.did) + expect(jwt.payload.additionalClaims.redirect_uri).toEqual( + `http://redirect-uri/${openIdVerifier.verifierId}/authorize` + ) + expect(jwt.payload.additionalClaims.response_mode).toEqual('post') + expect(jwt.payload.additionalClaims.nonce).toBeDefined() + expect(jwt.payload.additionalClaims.state).toBeDefined() + expect(jwt.payload.additionalClaims.response_type).toEqual('vp_token') + expect(jwt.payload.iss).toEqual(verifier.did) + expect(jwt.payload.sub).toEqual(verifier.did) + }) + + it('check openid proof request format (id token)', async () => { + const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() + const { authorizationRequest, verificationSession } = + await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'did', + didUrl: verifier.kid, + }, + verifierId: openIdVerifier.verifierId, + }) + + expect( + authorizationRequest.startsWith( + `openid://?request_uri=http%3A%2F%2Fredirect-uri%2F${openIdVerifier.verifierId}%2Fauthorization-requests%2F` + ) + ).toBe(true) + + const jwt = Jwt.fromSerializedJwt(verificationSession.authorizationRequestJwt) + + expect(jwt.header.kid) + + expect(jwt.header.kid).toEqual(verifier.kid) + expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) + expect(jwt.header.typ).toEqual('JWT') + expect(jwt.payload.additionalClaims.scope).toEqual('openid') + expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.did) + expect(jwt.payload.additionalClaims.redirect_uri).toEqual( + `http://redirect-uri/${openIdVerifier.verifierId}/authorize` + ) + expect(jwt.payload.additionalClaims.response_mode).toEqual('post') + expect(jwt.payload.additionalClaims.nonce).toBeDefined() + expect(jwt.payload.additionalClaims.state).toBeDefined() + expect(jwt.payload.additionalClaims.response_type).toEqual('id_token') + expect(jwt.payload.iss).toEqual(verifier.did) + expect(jwt.payload.sub).toEqual(verifier.did) + }) + }) +}) diff --git a/packages/openid4vc/src/openid4vc-verifier/index.ts b/packages/openid4vc/src/openid4vc-verifier/index.ts new file mode 100644 index 0000000000..1e82edb592 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/index.ts @@ -0,0 +1,8 @@ +export * from './OpenId4VcVerifierApi' +export * from './OpenId4VcVerifierModule' +export * from './OpenId4VcSiopVerifierService' +export * from './OpenId4VcSiopVerifierServiceOptions' +export * from './OpenId4VcVerifierModuleConfig' +export * from './repository' +export * from './OpenId4VcVerificationSessionState' +export * from './OpenId4VcVerifierEvents' diff --git a/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcRelyingPartyEventEmitter.ts b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcRelyingPartyEventEmitter.ts new file mode 100644 index 0000000000..6a3128ea45 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcRelyingPartyEventEmitter.ts @@ -0,0 +1,309 @@ +import type { OpenId4VcVerificationSessionStateChangedEvent } from '../OpenId4VcVerifierEvents' +import type { AgentContext } from '@credo-ts/core' +import type { AuthorizationEvent, AuthorizationRequest, AuthorizationResponse } from '@sphereon/did-auth-siop' + +import { + CredoError, + injectable, + AgentContextProvider, + inject, + InjectionSymbols, + EventEmitter, + AgentDependencies, +} from '@credo-ts/core' +import { AuthorizationEvents } from '@sphereon/did-auth-siop' +import { EventEmitter as NativeEventEmitter } from 'events' + +import { OpenId4VcVerificationSessionState } from '../OpenId4VcVerificationSessionState' +import { OpenId4VcVerifierEvents } from '../OpenId4VcVerifierEvents' + +import { OpenId4VcVerificationSessionRecord } from './OpenId4VcVerificationSessionRecord' +import { OpenId4VcVerificationSessionRepository } from './OpenId4VcVerificationSessionRepository' + +interface RelyingPartyEventEmitterContext { + contextCorrelationId: string + verifierId: string +} + +@injectable() +export class OpenId4VcRelyingPartyEventHandler { + public readonly nativeEventEmitter: NativeEventEmitter + + public constructor( + @inject(InjectionSymbols.AgentContextProvider) private agentContextProvider: AgentContextProvider, + @inject(InjectionSymbols.AgentDependencies) agentDependencies: AgentDependencies + ) { + this.nativeEventEmitter = new agentDependencies.EventEmitterClass() + + this.nativeEventEmitter.on( + AuthorizationEvents.ON_AUTH_REQUEST_CREATED_SUCCESS, + this.onAuthorizationRequestCreatedSuccess + ) + + // We don't want to do anything currently when a request creation failed, as then the method that + // is called to create it will throw and we won't create a session + // AuthorizationEvents.ON_AUTH_REQUEST_CREATED_FAILED, + + this.nativeEventEmitter.on(AuthorizationEvents.ON_AUTH_REQUEST_SENT_SUCCESS, this.onAuthorizationRequestSentSuccess) + + // We manually call when the request is retrieved, and there's not really a case where it can fail, and + // not really sure how to represent it in the verification session. So not doing anything here. + // AuthorizationEvents.ON_AUTH_REQUEST_SENT_FAILED + + // NOTE: the response received and response verified states are fired in such rapid succession + // that the verification session record is not updated yet to received before the verified event is + // emitted. For now we only track the verified / failed event. Otherwise we need to use record locking, which we don't have in-place yet + // AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_SUCCESS, + + this.nativeEventEmitter.on( + AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_FAILED, + this.onAuthorizationResponseReceivedFailed + ) + + this.nativeEventEmitter.on( + AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_SUCCESS, + this.onAuthorizationResponseVerifiedSuccess + ) + this.nativeEventEmitter.on( + AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_FAILED, + this.onAuthorizationResponseVerifiedFailed + ) + } + + public getEventEmitterForVerifier(contextCorrelationId: string, verifierId: string) { + return new OpenId4VcRelyingPartyEventEmitter(this.nativeEventEmitter, contextCorrelationId, verifierId) + } + + private onAuthorizationRequestCreatedSuccess = async ( + event: AuthorizationEvent, + context: RelyingPartyEventEmitterContext + ): Promise => { + const authorizationRequestJwt = await event.subject.requestObjectJwt() + if (!authorizationRequestJwt) { + throw new CredoError('Authorization request object JWT is missing') + } + + const authorizationRequestUri = event.subject.payload.request_uri + if (!authorizationRequestUri) { + throw new CredoError('Authorization request URI is missing') + } + + const verificationSession = new OpenId4VcVerificationSessionRecord({ + id: event.correlationId, + authorizationRequestJwt, + authorizationRequestUri, + state: OpenId4VcVerificationSessionState.RequestCreated, + verifierId: context.verifierId, + }) + + await this.withSession(context.contextCorrelationId, async (agentContext, verificationSessionRepository) => { + await verificationSessionRepository.save(agentContext, verificationSession) + this.emitStateChangedEvent(agentContext, verificationSession, null) + }) + } + + private onAuthorizationRequestSentSuccess = async ( + event: AuthorizationEvent, + context: RelyingPartyEventEmitterContext + ): Promise => { + await this.withSession(context.contextCorrelationId, async (agentContext, verificationSessionRepository) => { + const verificationSession = await verificationSessionRepository.getById(agentContext, event.correlationId) + + // In all other cases it doesn't make sense to update the state, as the state is already advanced beyond + // this state. + if (verificationSession.state === OpenId4VcVerificationSessionState.RequestCreated) { + verificationSession.state = OpenId4VcVerificationSessionState.RequestUriRetrieved + await verificationSessionRepository.update(agentContext, verificationSession) + this.emitStateChangedEvent(agentContext, verificationSession, OpenId4VcVerificationSessionState.RequestCreated) + } + }) + } + + private onAuthorizationResponseReceivedFailed = async ( + event: AuthorizationEvent, + context: RelyingPartyEventEmitterContext + ): Promise => { + await this.withSession(context.contextCorrelationId, async (agentContext, verificationSessionRepository) => { + const verificationSession = await verificationSessionRepository.getById(agentContext, event.correlationId) + + const previousState = verificationSession.state + verificationSession.state = OpenId4VcVerificationSessionState.Error + verificationSession.authorizationResponsePayload = event.subject.payload + verificationSession.errorMessage = event.error.message + await verificationSessionRepository.update(agentContext, verificationSession) + this.emitStateChangedEvent(agentContext, verificationSession, previousState) + }) + } + + private onAuthorizationResponseVerifiedSuccess = async ( + event: AuthorizationEvent, + context: RelyingPartyEventEmitterContext + ): Promise => { + await this.withSession(context.contextCorrelationId, async (agentContext, verificationSessionRepository) => { + const verificationSession = await verificationSessionRepository.getById(agentContext, event.correlationId) + + if ( + verificationSession.state !== OpenId4VcVerificationSessionState.Error && + verificationSession.state !== OpenId4VcVerificationSessionState.ResponseVerified + ) { + const previousState = verificationSession.state + verificationSession.authorizationResponsePayload = event.subject.payload + verificationSession.state = OpenId4VcVerificationSessionState.ResponseVerified + await verificationSessionRepository.update(agentContext, verificationSession) + this.emitStateChangedEvent(agentContext, verificationSession, previousState) + } + }) + } + + private onAuthorizationResponseVerifiedFailed = async ( + event: AuthorizationEvent, + context: RelyingPartyEventEmitterContext + ): Promise => { + await this.withSession(context.contextCorrelationId, async (agentContext, verificationSessionRepository) => { + const verificationSession = await verificationSessionRepository.getById(agentContext, event.correlationId) + + const previousState = verificationSession.state + verificationSession.state = OpenId4VcVerificationSessionState.Error + verificationSession.errorMessage = event.error.message + await verificationSessionRepository.update(agentContext, verificationSession) + this.emitStateChangedEvent(agentContext, verificationSession, previousState) + }) + } + + private async withSession( + contextCorrelationId: string, + callback: (agentContext: AgentContext, verificationSessionRepository: OpenId4VcVerificationSessionRepository) => T + ): Promise { + const agentContext = await this.agentContextProvider.getAgentContextForContextCorrelationId(contextCorrelationId) + + try { + const verificationSessionRepository = agentContext.dependencyManager.resolve( + OpenId4VcVerificationSessionRepository + ) + const result = await callback(agentContext, verificationSessionRepository) + return result + } finally { + await agentContext.endSession() + } + } + + protected emitStateChangedEvent( + agentContext: AgentContext, + verificationSession: OpenId4VcVerificationSessionRecord, + previousState: OpenId4VcVerificationSessionState | null + ) { + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + + eventEmitter.emit(agentContext, { + type: OpenId4VcVerifierEvents.VerificationSessionStateChanged, + payload: { + verificationSession: verificationSession.clone(), + previousState, + }, + }) + } +} + +/** + * Custom implementation of the event emitter so we can associate the contextCorrelationId + * and the verifierId with the events that are emitted. This allows us to only create one + * event emitter and thus not have endless event emitters and listeners for each active RP. + * + * We only modify the emit method, and add the verifierId and contextCorrelationId to the event + * this allows the listener to know which tenant and which verifier the event is associated with. + */ +class OpenId4VcRelyingPartyEventEmitter implements NativeEventEmitter { + public constructor( + private nativeEventEmitter: NativeEventEmitter, + private contextCorrelationId: string, + private verifierId: string + ) {} + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public emit(eventName: string | symbol, ...args: any[]): boolean { + return this.nativeEventEmitter.emit(eventName, ...args, { + contextCorrelationId: this.contextCorrelationId, + verifierId: this.verifierId, + } satisfies RelyingPartyEventEmitterContext) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public [NativeEventEmitter.captureRejectionSymbol]?(error: Error, event: string, ...args: any[]): void { + return this.nativeEventEmitter[NativeEventEmitter.captureRejectionSymbol]?.(error, event, ...args) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public addListener(eventName: string | symbol, listener: (...args: any[]) => void): this { + this.nativeEventEmitter.addListener(eventName, listener) + return this + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public on(eventName: string | symbol, listener: (...args: any[]) => void): this { + this.nativeEventEmitter.on(eventName, listener) + return this + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public once(eventName: string | symbol, listener: (...args: any[]) => void): this { + this.nativeEventEmitter.once(eventName, listener) + return this + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public removeListener(eventName: string | symbol, listener: (...args: any[]) => void): this { + this.nativeEventEmitter.removeListener(eventName, listener) + return this + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public off(eventName: string | symbol, listener: (...args: any[]) => void): this { + this.nativeEventEmitter.off(eventName, listener) + return this + } + + public removeAllListeners(event?: string | symbol | undefined): this { + this.nativeEventEmitter.removeAllListeners(event) + return this + } + + public setMaxListeners(n: number): this { + this.nativeEventEmitter.setMaxListeners(n) + return this + } + + public getMaxListeners(): number { + return this.nativeEventEmitter.getMaxListeners() + } + + // eslint-disable-next-line @typescript-eslint/ban-types + public listeners(eventName: string | symbol): Function[] { + return this.nativeEventEmitter.listeners(eventName) + } + + // eslint-disable-next-line @typescript-eslint/ban-types + public rawListeners(eventName: string | symbol): Function[] { + return this.nativeEventEmitter.rawListeners(eventName) + } + + // eslint-disable-next-line @typescript-eslint/ban-types + public listenerCount(eventName: string | symbol, listener?: Function | undefined): number { + return this.nativeEventEmitter.listenerCount(eventName, listener) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public prependListener(eventName: string | symbol, listener: (...args: any[]) => void): this { + this.nativeEventEmitter.prependListener(eventName, listener) + return this + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public prependOnceListener(eventName: string | symbol, listener: (...args: any[]) => void): this { + this.nativeEventEmitter.prependOnceListener(eventName, listener) + return this + } + + public eventNames(): (string | symbol)[] { + return this.nativeEventEmitter.eventNames() + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcRelyingPartySessionManager.ts b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcRelyingPartySessionManager.ts new file mode 100644 index 0000000000..bad150136d --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcRelyingPartySessionManager.ts @@ -0,0 +1,211 @@ +import type { OpenId4VcVerificationSessionRecord } from './OpenId4VcVerificationSessionRecord' +import type { AgentContext } from '@credo-ts/core' +import type { AuthorizationRequestState, AuthorizationResponseState, IRPSessionManager } from '@sphereon/did-auth-siop' + +import { CredoError } from '@credo-ts/core' +import { + AuthorizationRequest, + AuthorizationRequestStateStatus, + AuthorizationResponse, + AuthorizationResponseStateStatus, +} from '@sphereon/did-auth-siop' + +import { OpenId4VcVerificationSessionState } from '../OpenId4VcVerificationSessionState' + +import { OpenId4VcVerificationSessionRepository } from './OpenId4VcVerificationSessionRepository' + +export class OpenId4VcRelyingPartySessionManager implements IRPSessionManager { + private openId4VcVerificationSessionRepository: OpenId4VcVerificationSessionRepository + + public constructor(private agentContext: AgentContext, private verifierId: string) { + this.openId4VcVerificationSessionRepository = agentContext.dependencyManager.resolve( + OpenId4VcVerificationSessionRepository + ) + } + + public async getRequestStateByCorrelationId( + correlationId: string, + errorOnNotFound?: boolean + ): Promise { + const verificationSession = await this.openId4VcVerificationSessionRepository.findById( + this.agentContext, + correlationId + ) + + if (!verificationSession) { + if (errorOnNotFound) + throw new CredoError(`OpenID4VC Authorization request state for correlation id ${correlationId} not found`) + return undefined + } + + return this.getRequestStateFromSessionRecord(verificationSession) + } + + public async getRequestStateByNonce( + nonce: string, + errorOnNotFound?: boolean + ): Promise { + const verificationSession = await this.openId4VcVerificationSessionRepository.findSingleByQuery(this.agentContext, { + verifierId: this.verifierId, + nonce: nonce, + }) + + if (!verificationSession) { + if (errorOnNotFound) throw new CredoError(`OpenID4VC Authorization request state for nonce ${nonce} not found`) + return undefined + } + + return this.getRequestStateFromSessionRecord(verificationSession) + } + + public async getRequestStateByState( + state: string, + errorOnNotFound?: boolean + ): Promise { + const verificationSession = await this.openId4VcVerificationSessionRepository.findSingleByQuery(this.agentContext, { + verifierId: this.verifierId, + payloadState: state, + }) + + if (!verificationSession) { + if (errorOnNotFound) throw new CredoError(`OpenID4VC Authorization request state for state ${state} not found`) + return undefined + } + + return this.getRequestStateFromSessionRecord(verificationSession) + } + + public async getResponseStateByCorrelationId( + correlationId: string, + errorOnNotFound?: boolean + ): Promise { + const verificationSession = await this.openId4VcVerificationSessionRepository.findById( + this.agentContext, + correlationId + ) + + const responseState = await this.getResponseStateFromSessionRecord(verificationSession) + if (!responseState) { + if (errorOnNotFound) + throw new CredoError(`OpenID4VC Authorization response state for correlation id ${correlationId} not found`) + return undefined + } + + return responseState + } + + public async getResponseStateByNonce( + nonce: string, + errorOnNotFound?: boolean + ): Promise { + const verificationSession = await this.openId4VcVerificationSessionRepository.findSingleByQuery(this.agentContext, { + verifierId: this.verifierId, + nonce, + }) + + const responseState = await this.getResponseStateFromSessionRecord(verificationSession) + if (!responseState) { + if (errorOnNotFound) throw new CredoError(`OpenID4VC Authorization response state for nonce ${nonce} not found`) + return undefined + } + + return responseState + } + + public async getResponseStateByState( + state: string, + errorOnNotFound?: boolean + ): Promise { + const verificationSession = await this.openId4VcVerificationSessionRepository.findSingleByQuery(this.agentContext, { + verifierId: this.verifierId, + payloadState: state, + }) + + const responseState = await this.getResponseStateFromSessionRecord(verificationSession) + if (!responseState) { + if (errorOnNotFound) throw new CredoError(`OpenID4VC Authorization response state for state ${state} not found`) + return undefined + } + + return responseState + } + + public async getCorrelationIdByNonce(nonce: string, errorOnNotFound?: boolean): Promise { + const requestState = await this.getRequestStateByNonce(nonce, errorOnNotFound) + return requestState?.correlationId + } + + public async getCorrelationIdByState(state: string, errorOnNotFound?: boolean): Promise { + const requestState = await this.getRequestStateByState(state, errorOnNotFound) + return requestState?.correlationId + } + + public async deleteStateForCorrelationId() { + throw new Error('Method not implemented.') + } + + private async getRequestStateFromSessionRecord( + sessionRecord: OpenId4VcVerificationSessionRecord + ): Promise { + const lastUpdated = sessionRecord.updatedAt?.getTime() ?? sessionRecord.createdAt.getTime() + return { + lastUpdated, + timestamp: lastUpdated, + correlationId: sessionRecord.id, + // Not so nice that the session manager expects an error instance..... + error: sessionRecord.errorMessage ? new Error(sessionRecord.errorMessage) : undefined, + request: await AuthorizationRequest.fromUriOrJwt(sessionRecord.authorizationRequestJwt), + status: sphereonAuthorizationRequestStateFromOpenId4VcVerificationState(sessionRecord.state), + } + } + + private async getResponseStateFromSessionRecord( + sessionRecord: OpenId4VcVerificationSessionRecord | null + ): Promise { + if (!sessionRecord) return undefined + const lastUpdated = sessionRecord.updatedAt?.getTime() ?? sessionRecord.createdAt.getTime() + + // If we don't have the authorization response payload yet, it means we haven't + // received the response yet, and thus the response state does not exist yet + if (!sessionRecord.authorizationResponsePayload) { + return undefined + } + + return { + lastUpdated, + timestamp: lastUpdated, + correlationId: sessionRecord.id, + // Not so nice that the session manager expects an error instance..... + error: sessionRecord.errorMessage ? new Error(sessionRecord.errorMessage) : undefined, + response: await AuthorizationResponse.fromPayload(sessionRecord.authorizationResponsePayload), + status: sphereonAuthorizationResponseStateFromOpenId4VcVerificationState(sessionRecord.state), + } + } +} + +function sphereonAuthorizationResponseStateFromOpenId4VcVerificationState( + state: OpenId4VcVerificationSessionState +): AuthorizationResponseStateStatus { + if (state === OpenId4VcVerificationSessionState.Error) return AuthorizationResponseStateStatus.ERROR + if (state === OpenId4VcVerificationSessionState.ResponseVerified) return AuthorizationResponseStateStatus.VERIFIED + + throw new CredoError(`Can not map OpenId4VcVerificationSessionState ${state} to AuthorizationResponseStateStatus`) +} + +function sphereonAuthorizationRequestStateFromOpenId4VcVerificationState( + state: OpenId4VcVerificationSessionState +): AuthorizationRequestStateStatus { + if (state === OpenId4VcVerificationSessionState.Error) return AuthorizationRequestStateStatus.ERROR + + if ( + [OpenId4VcVerificationSessionState.RequestCreated, OpenId4VcVerificationSessionState.ResponseVerified].includes( + state + ) + ) { + return AuthorizationRequestStateStatus.CREATED + } + + if (state === OpenId4VcVerificationSessionState.RequestUriRetrieved) return AuthorizationRequestStateStatus.SENT + + throw new CredoError(`Can not map OpenId4VcVerificationSessionState ${state} to AuthorizationRequestStateStatus`) +} diff --git a/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerificationSessionRecord.ts b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerificationSessionRecord.ts new file mode 100644 index 0000000000..24205c1099 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerificationSessionRecord.ts @@ -0,0 +1,118 @@ +import type { OpenId4VcSiopAuthorizationResponsePayload } from '../../shared/models' +import type { OpenId4VcVerificationSessionState } from '../OpenId4VcVerificationSessionState' +import type { RecordTags, TagsBase } from '@credo-ts/core' + +import { Jwt, CredoError, BaseRecord, utils } from '@credo-ts/core' + +export type OpenId4VcVerificationSessionRecordTags = RecordTags + +export type DefaultOpenId4VcVerificationSessionRecordTags = { + verifierId: string + state: OpenId4VcVerificationSessionState + nonce: string + payloadState: string + authorizationRequestUri: string +} + +export interface OpenId4VcVerificationSessionRecordProps { + id?: string + createdAt?: Date + tags?: TagsBase + + verifierId: string + state: OpenId4VcVerificationSessionState + errorMessage?: string + + authorizationRequestUri: string + authorizationRequestJwt: string + + authorizationResponsePayload?: OpenId4VcSiopAuthorizationResponsePayload +} + +export class OpenId4VcVerificationSessionRecord extends BaseRecord { + public static readonly type = 'OpenId4VcVerificationSessionRecord' + public readonly type = OpenId4VcVerificationSessionRecord.type + + /** + * The id of the verifier that this session is for. + */ + public verifierId!: string + + /** + * The state of the verification session. + */ + public state!: OpenId4VcVerificationSessionState + + /** + * Optional error message of the error that occurred during the verification session. Will be set when state is {@link OpenId4VcVerificationSessionState.Error} + */ + public errorMessage?: string + + /** + * The signed JWT containing the authorization request + */ + public authorizationRequestJwt!: string + + /** + * URI of the authorization request. This is the url that can be used to + * retrieve the authorization request + */ + public authorizationRequestUri!: string + + /** + * The payload of the received authorization response + */ + public authorizationResponsePayload?: OpenId4VcSiopAuthorizationResponsePayload + + public constructor(props: OpenId4VcVerificationSessionRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags ?? {} + + this.verifierId = props.verifierId + this.state = props.state + this.errorMessage = props.errorMessage + this.authorizationRequestJwt = props.authorizationRequestJwt + this.authorizationRequestUri = props.authorizationRequestUri + this.authorizationResponsePayload = props.authorizationResponsePayload + } + } + + public assertState(expectedStates: OpenId4VcVerificationSessionState | OpenId4VcVerificationSessionState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new CredoError( + `OpenId4VcVerificationSessionRecord is in invalid state ${this.state}. Valid states are: ${expectedStates.join( + ', ' + )}.` + ) + } + } + + public getTags() { + const parsedAuthorizationRequest = Jwt.fromSerializedJwt(this.authorizationRequestJwt) + + const nonce = parsedAuthorizationRequest.payload.additionalClaims.nonce + if (!nonce || typeof nonce !== 'string') throw new CredoError('Expected nonce in authorization request payload') + + const payloadState = parsedAuthorizationRequest.payload.additionalClaims.state + if (!payloadState || typeof payloadState !== 'string') + throw new CredoError('Expected state in authorization request payload') + + return { + ...this._tags, + verifierId: this.verifierId, + state: this.state, + nonce, + // FIXME: how do we call this property so it doesn't conflict with the record state? + payloadState, + authorizationRequestUri: this.authorizationRequestUri, + } + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerificationSessionRepository.ts b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerificationSessionRepository.ts new file mode 100644 index 0000000000..fc582eb383 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerificationSessionRepository.ts @@ -0,0 +1,13 @@ +import { Repository, StorageService, InjectionSymbols, EventEmitter, inject, injectable } from '@credo-ts/core' + +import { OpenId4VcVerificationSessionRecord } from './OpenId4VcVerificationSessionRecord' + +@injectable() +export class OpenId4VcVerificationSessionRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(OpenId4VcVerificationSessionRecord, storageService, eventEmitter) + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRecord.ts b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRecord.ts new file mode 100644 index 0000000000..a5c90f486c --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRecord.ts @@ -0,0 +1,48 @@ +import type { RecordTags, TagsBase } from '@credo-ts/core' + +import { BaseRecord, utils } from '@credo-ts/core' + +export type OpenId4VcVerifierRecordTags = RecordTags + +export type DefaultOpenId4VcVerifierRecordTags = { + verifierId: string +} + +export interface OpenId4VcVerifierRecordProps { + id?: string + createdAt?: Date + tags?: TagsBase + + verifierId: string +} + +/** + * For OID4VC you need to expos metadata files. Each issuer needs to host this metadata. This is not the case for DIDComm where we can just have one /didcomm endpoint. + * So we create a record per openid issuer/verifier that you want, and each tenant can create multiple issuers/verifiers which have different endpoints + * and metadata files + * */ +export class OpenId4VcVerifierRecord extends BaseRecord { + public static readonly type = 'OpenId4VcVerifierRecord' + public readonly type = OpenId4VcVerifierRecord.type + + public verifierId!: string + + public constructor(props: OpenId4VcVerifierRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags ?? {} + + this.verifierId = props.verifierId + } + } + + public getTags() { + return { + ...this._tags, + verifierId: this.verifierId, + } + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRepository.ts b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRepository.ts new file mode 100644 index 0000000000..a96a533ff1 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@credo-ts/core' + +import { Repository, StorageService, InjectionSymbols, EventEmitter, inject, injectable } from '@credo-ts/core' + +import { OpenId4VcVerifierRecord } from './OpenId4VcVerifierRecord' + +@injectable() +export class OpenId4VcVerifierRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(OpenId4VcVerifierRecord, storageService, eventEmitter) + } + + public findByVerifierId(agentContext: AgentContext, verifierId: string) { + return this.findSingleByQuery(agentContext, { verifierId }) + } + + public getByVerifierId(agentContext: AgentContext, verifierId: string) { + return this.getSingleByQuery(agentContext, { verifierId }) + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/repository/index.ts b/packages/openid4vc/src/openid4vc-verifier/repository/index.ts new file mode 100644 index 0000000000..363dddfec5 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/repository/index.ts @@ -0,0 +1,4 @@ +export * from './OpenId4VcVerifierRecord' +export * from './OpenId4VcVerifierRepository' +export * from './OpenId4VcVerificationSessionRecord' +export * from './OpenId4VcVerificationSessionRepository' diff --git a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts new file mode 100644 index 0000000000..b83c7374b4 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts @@ -0,0 +1,56 @@ +import type { OpenId4VcVerificationRequest } from './requestContext' +import type { AuthorizationResponsePayload } from '@sphereon/did-auth-siop' +import type { Router, Response } from 'express' + +import { getRequestContext, sendErrorResponse } from '../../shared/router' +import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService' + +export interface OpenId4VcSiopAuthorizationEndpointConfig { + /** + * The path at which the authorization endpoint should be made available. Note that it will be + * hosted at a subpath to take into account multiple tenants and verifiers. + * + * @default /authorize + */ + endpointPath: string +} + +export function configureAuthorizationEndpoint(router: Router, config: OpenId4VcSiopAuthorizationEndpointConfig) { + router.post(config.endpointPath, async (request: OpenId4VcVerificationRequest, response: Response, next) => { + const { agentContext, verifier } = getRequestContext(request) + + try { + const openId4VcVerifierService = agentContext.dependencyManager.resolve(OpenId4VcSiopVerifierService) + const isVpRequest = request.body.presentation_submission !== undefined + + const authorizationResponse: AuthorizationResponsePayload = request.body + if (isVpRequest) authorizationResponse.presentation_submission = JSON.parse(request.body.presentation_submission) + + const verificationSession = await openId4VcVerifierService.findVerificationSessionForAuthorizationResponse( + agentContext, + { + authorizationResponse, + verifierId: verifier.verifierId, + } + ) + + if (!verificationSession) { + agentContext.config.logger.warn( + `No verification session found for incoming authorization response for verifier ${verifier.verifierId}` + ) + return sendErrorResponse(response, agentContext.config.logger, 404, 'invalid_request', null) + } + + await openId4VcVerifierService.verifyAuthorizationResponse(agentContext, { + authorizationResponse: request.body, + verificationSession, + }) + response.status(200).send() + } catch (error) { + sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) + } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() + }) +} diff --git a/packages/openid4vc/src/openid4vc-verifier/router/authorizationRequestEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/authorizationRequestEndpoint.ts new file mode 100644 index 0000000000..01c4736dd8 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/router/authorizationRequestEndpoint.ts @@ -0,0 +1,114 @@ +import type { OpenId4VcVerificationRequest } from './requestContext' +import type { OpenId4VcVerificationSessionStateChangedEvent } from '../OpenId4VcVerifierEvents' +import type { Router, Response } from 'express' + +import { EventEmitter, joinUriParts } from '@credo-ts/core' + +import { getRequestContext, sendErrorResponse } from '../../shared/router' +import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService' +import { OpenId4VcVerificationSessionState } from '../OpenId4VcVerificationSessionState' +import { OpenId4VcVerifierEvents } from '../OpenId4VcVerifierEvents' +import { OpenId4VcVerifierModuleConfig } from '../OpenId4VcVerifierModuleConfig' +import { OpenId4VcVerificationSessionRepository } from '../repository' + +export interface OpenId4VcSiopAuthorizationRequestEndpointConfig { + /** + * The path at which the authorization request should be made available. Note that it will be + * hosted at a subpath to take into account multiple tenants and verifiers. + * + * @default /authorization-requests + */ + endpointPath: string +} + +export function configureAuthorizationRequestEndpoint( + router: Router, + config: OpenId4VcSiopAuthorizationRequestEndpointConfig +) { + router.get( + joinUriParts(config.endpointPath, [':authorizationRequestId']), + async (request: OpenId4VcVerificationRequest, response: Response, next) => { + const { agentContext, verifier } = getRequestContext(request) + + if (!request.params.authorizationRequestId || typeof request.params.authorizationRequestId !== 'string') { + return sendErrorResponse( + response, + agentContext.config.logger, + 400, + 'invalid_request', + 'Invalid authorization request url' + ) + } + + try { + const verifierService = agentContext.dependencyManager.resolve(OpenId4VcSiopVerifierService) + const verificationSessionRepository = agentContext.dependencyManager.resolve( + OpenId4VcVerificationSessionRepository + ) + const verifierConfig = agentContext.dependencyManager.resolve(OpenId4VcVerifierModuleConfig) + + // We always use shortened URIs currently + const fullAuthorizationRequestUri = joinUriParts(verifierConfig.baseUrl, [ + verifier.verifierId, + verifierConfig.authorizationRequestEndpoint.endpointPath, + request.params.authorizationRequestId, + ]) + + const [verificationSession] = await verifierService.findVerificationSessionsByQuery(agentContext, { + verifierId: verifier.verifierId, + authorizationRequestUri: fullAuthorizationRequestUri, + }) + + if (!verificationSession) { + return sendErrorResponse( + response, + agentContext.config.logger, + 404, + 'not_found', + 'Authorization request not found' + ) + } + + if ( + ![ + OpenId4VcVerificationSessionState.RequestCreated, + OpenId4VcVerificationSessionState.RequestUriRetrieved, + ].includes(verificationSession.state) + ) { + return sendErrorResponse( + response, + agentContext.config.logger, + 400, + 'invalid_request', + 'Invalid state for authorization request' + ) + } + + // It's okay to retrieve the offer multiple times. So we only update the state if it's not already retrieved + if (verificationSession.state !== OpenId4VcVerificationSessionState.RequestUriRetrieved) { + const previousState = verificationSession.state + + verificationSession.state = OpenId4VcVerificationSessionState.RequestUriRetrieved + await verificationSessionRepository.update(agentContext, verificationSession) + + agentContext.dependencyManager + .resolve(EventEmitter) + .emit(agentContext, { + type: OpenId4VcVerifierEvents.VerificationSessionStateChanged, + payload: { + verificationSession: verificationSession.clone(), + previousState, + }, + }) + } + + response.status(200).send(verificationSession.authorizationRequestJwt) + } catch (error) { + sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) + } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() + } + ) +} diff --git a/packages/openid4vc/src/openid4vc-verifier/router/index.ts b/packages/openid4vc/src/openid4vc-verifier/router/index.ts new file mode 100644 index 0000000000..8242556be4 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/router/index.ts @@ -0,0 +1,2 @@ +export { configureAuthorizationEndpoint } from './authorizationEndpoint' +export { OpenId4VcVerificationRequest } from './requestContext' diff --git a/packages/openid4vc/src/openid4vc-verifier/router/requestContext.ts b/packages/openid4vc/src/openid4vc-verifier/router/requestContext.ts new file mode 100644 index 0000000000..4dcb3964d8 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/router/requestContext.ts @@ -0,0 +1,4 @@ +import type { OpenId4VcRequest } from '../../shared/router' +import type { OpenId4VcVerifierRecord } from '../repository' + +export type OpenId4VcVerificationRequest = OpenId4VcRequest<{ verifier: OpenId4VcVerifierRecord }> diff --git a/packages/openid4vc/src/shared/index.ts b/packages/openid4vc/src/shared/index.ts new file mode 100644 index 0000000000..8eacb927b2 --- /dev/null +++ b/packages/openid4vc/src/shared/index.ts @@ -0,0 +1,2 @@ +export * from './models' +export * from './issuerMetadataUtils' diff --git a/packages/openid4vc/src/shared/issuerMetadataUtils.ts b/packages/openid4vc/src/shared/issuerMetadataUtils.ts new file mode 100644 index 0000000000..f8986d1dc2 --- /dev/null +++ b/packages/openid4vc/src/shared/issuerMetadataUtils.ts @@ -0,0 +1,83 @@ +import type { OpenId4VciCredentialSupported, OpenId4VciCredentialSupportedWithId } from './models' +import type { AuthorizationDetails, CredentialOfferFormat, EndpointMetadataResult } from '@sphereon/oid4vci-common' + +import { CredoError } from '@credo-ts/core' + +/** + * Get all `types` from a `CredentialSupported` object. + * + * Depending on the format, the types may be nested, or have different a different name/type + */ +export function getTypesFromCredentialSupported(credentialSupported: OpenId4VciCredentialSupported) { + if ( + credentialSupported.format === 'jwt_vc_json-ld' || + credentialSupported.format === 'ldp_vc' || + credentialSupported.format === 'jwt_vc_json' || + credentialSupported.format === 'jwt_vc' + ) { + return credentialSupported.types + } else if (credentialSupported.format === 'vc+sd-jwt') { + return [credentialSupported.vct] + } + + throw Error(`Unable to extract types from credentials supported. Unknown format ${credentialSupported.format}`) +} + +/** + * Returns all entries from the credential offer with the associated metadata resolved. For 'id' entries, the associated `credentials_supported` object is resolved from the issuer metadata. + * For inline entries, an error is thrown. + */ +export function getOfferedCredentials( + offeredCredentials: Array, + allCredentialsSupported: OpenId4VciCredentialSupported[] +): OpenId4VciCredentialSupportedWithId[] { + const credentialsSupported: OpenId4VciCredentialSupportedWithId[] = [] + + for (const offeredCredential of offeredCredentials) { + // In draft 12 inline credential offers are removed. It's easier to already remove support now. + if (typeof offeredCredential !== 'string') { + throw new CredoError( + 'Only referenced credentials pointing to an id in credentials_supported issuer metadata are supported' + ) + } + + const foundSupportedCredential = allCredentialsSupported.find( + (supportedCredential): supportedCredential is OpenId4VciCredentialSupportedWithId => + supportedCredential.id !== undefined && supportedCredential.id === offeredCredential + ) + + // Make sure the issuer metadata includes the offered credential. + if (!foundSupportedCredential) { + throw new Error( + `Offered credential '${offeredCredential}' is not part of credentials_supported of the issuer metadata.` + ) + } + + credentialsSupported.push(foundSupportedCredential) + } + + return credentialsSupported +} + +// copied from sphereon as the method is only available on the client +export function handleAuthorizationDetails( + authorizationDetails: AuthorizationDetails | AuthorizationDetails[], + metadata: EndpointMetadataResult +): AuthorizationDetails | AuthorizationDetails[] | undefined { + if (Array.isArray(authorizationDetails)) { + return authorizationDetails.map((value) => handleLocations(value, metadata)) + } else { + return handleLocations(authorizationDetails, metadata) + } +} + +// copied from sphereon as the method is only available on the client +function handleLocations(authorizationDetails: AuthorizationDetails, metadata: EndpointMetadataResult) { + if (typeof authorizationDetails === 'string') return authorizationDetails + if (metadata.credentialIssuerMetadata?.authorization_server || metadata.authorization_endpoint) { + if (!authorizationDetails.locations) authorizationDetails.locations = [metadata.issuer] + else if (Array.isArray(authorizationDetails.locations)) authorizationDetails.locations.push(metadata.issuer) + else authorizationDetails.locations = [authorizationDetails.locations as string, metadata.issuer] + } + return authorizationDetails +} diff --git a/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts b/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts new file mode 100644 index 0000000000..2c174dab9e --- /dev/null +++ b/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts @@ -0,0 +1,13 @@ +import type { Jwk } from '@credo-ts/core' + +export type OpenId4VcCredentialHolderDidBinding = { + method: 'did' + didUrl: string +} + +export type OpenId4VcCredentialHolderJwkBinding = { + method: 'jwk' + jwk: Jwk +} + +export type OpenId4VcCredentialHolderBinding = OpenId4VcCredentialHolderDidBinding | OpenId4VcCredentialHolderJwkBinding diff --git a/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts b/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts new file mode 100644 index 0000000000..5165628db1 --- /dev/null +++ b/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts @@ -0,0 +1,13 @@ +interface OpenId4VcJwtIssuerDid { + method: 'did' + didUrl: string +} + +// TODO: enable once supported in sphereon lib +// See https://github.com/Sphereon-Opensource/SIOP-OID4VP/issues/67 +// interface OpenId4VcJwtIssuerJwk { +// method: 'jwk' +// jwk: Jwk +// } + +export type OpenId4VcJwtIssuer = OpenId4VcJwtIssuerDid diff --git a/packages/openid4vc/src/shared/models/OpenId4VciCredentialFormatProfile.ts b/packages/openid4vc/src/shared/models/OpenId4VciCredentialFormatProfile.ts new file mode 100644 index 0000000000..628e65c12e --- /dev/null +++ b/packages/openid4vc/src/shared/models/OpenId4VciCredentialFormatProfile.ts @@ -0,0 +1,6 @@ +export enum OpenId4VciCredentialFormatProfile { + JwtVcJson = 'jwt_vc_json', + JwtVcJsonLd = 'jwt_vc_json-ld', + LdpVc = 'ldp_vc', + SdJwtVc = 'vc+sd-jwt', +} diff --git a/packages/openid4vc/src/shared/models/index.ts b/packages/openid4vc/src/shared/models/index.ts new file mode 100644 index 0000000000..779881dbed --- /dev/null +++ b/packages/openid4vc/src/shared/models/index.ts @@ -0,0 +1,39 @@ +import type { + VerifiedAuthorizationRequest, + AuthorizationRequestPayload, + AuthorizationResponsePayload, + IDTokenPayload, +} from '@sphereon/did-auth-siop' +import type { + AssertedUniformCredentialOffer, + CredentialIssuerMetadata, + CredentialOfferPayloadV1_0_11, + CredentialRequestJwtVcJson, + CredentialRequestJwtVcJsonLdAndLdpVc, + CredentialRequestSdJwtVc, + CredentialSupported, + MetadataDisplay, + UniformCredentialRequest, +} from '@sphereon/oid4vci-common' + +export type OpenId4VciCredentialSupportedWithId = CredentialSupported & { id: string } +export type OpenId4VciCredentialSupported = CredentialSupported +export type OpenId4VciIssuerMetadata = CredentialIssuerMetadata +export type OpenId4VciIssuerMetadataDisplay = MetadataDisplay + +export type OpenId4VciCredentialRequest = UniformCredentialRequest + +export type OpenId4VciCredentialRequestJwtVcJson = CredentialRequestJwtVcJson +export type OpenId4VciCredentialRequestJwtVcJsonLdAndLdpVc = CredentialRequestJwtVcJsonLdAndLdpVc +export type OpenId4VciCredentialRequestSdJwtVc = CredentialRequestSdJwtVc +export type OpenId4VciCredentialOffer = AssertedUniformCredentialOffer +export type OpenId4VciCredentialOfferPayload = CredentialOfferPayloadV1_0_11 + +export type OpenId4VcSiopVerifiedAuthorizationRequest = VerifiedAuthorizationRequest +export type OpenId4VcSiopAuthorizationRequestPayload = AuthorizationRequestPayload +export type OpenId4VcSiopAuthorizationResponsePayload = AuthorizationResponsePayload +export type OpenId4VcSiopIdTokenPayload = IDTokenPayload + +export * from './OpenId4VcJwtIssuer' +export * from './CredentialHolderBinding' +export * from './OpenId4VciCredentialFormatProfile' diff --git a/packages/openid4vc/src/shared/router/context.ts b/packages/openid4vc/src/shared/router/context.ts new file mode 100644 index 0000000000..0bf538a69d --- /dev/null +++ b/packages/openid4vc/src/shared/router/context.ts @@ -0,0 +1,32 @@ +import type { AgentContext, Logger } from '@credo-ts/core' +import type { Response, Request } from 'express' + +import { CredoError } from '@credo-ts/core' + +export interface OpenId4VcRequest = Record> extends Request { + requestContext?: RC & OpenId4VcRequestContext +} + +export interface OpenId4VcRequestContext { + agentContext: AgentContext +} + +export function sendErrorResponse(response: Response, logger: Logger, code: number, message: string, error: unknown) { + const error_description = + error instanceof Error ? error.message : typeof error === 'string' ? error : 'An unknown error occurred.' + + const body = { error: message, error_description } + logger.warn(`[OID4VCI] Sending error response: ${JSON.stringify(body)}`, { + error, + }) + + return response.status(code).json(body) +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getRequestContext>(request: T): NonNullable { + const requestContext = request.requestContext + if (!requestContext) throw new CredoError('Request context not set.') + + return requestContext +} diff --git a/packages/openid4vc/src/shared/router/express.ts b/packages/openid4vc/src/shared/router/express.ts new file mode 100644 index 0000000000..43bdcf12fa --- /dev/null +++ b/packages/openid4vc/src/shared/router/express.ts @@ -0,0 +1,12 @@ +import type { default as Express } from 'express' + +export function importExpress() { + try { + // NOTE: 'express' is added as a peer-dependency, and is required when using this module + // eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-var-requires + const express = require('express') as typeof Express + return express + } catch (error) { + throw new Error('Express must be installed as a peer dependency') + } +} diff --git a/packages/openid4vc/src/shared/router/index.ts b/packages/openid4vc/src/shared/router/index.ts new file mode 100644 index 0000000000..dc3697dcc1 --- /dev/null +++ b/packages/openid4vc/src/shared/router/index.ts @@ -0,0 +1,3 @@ +export * from './express' +export * from './context' +export * from './tenants' diff --git a/packages/openid4vc/src/shared/router/tenants.ts b/packages/openid4vc/src/shared/router/tenants.ts new file mode 100644 index 0000000000..cc3a2e1198 --- /dev/null +++ b/packages/openid4vc/src/shared/router/tenants.ts @@ -0,0 +1,56 @@ +import type { AgentContext, AgentContextProvider } from '@credo-ts/core' +import type { TenantsModule } from '@credo-ts/tenants' + +import { getApiForModuleByName, InjectionSymbols } from '@credo-ts/core' + +const OPENID4VC_ACTOR_IDS_METADATA_KEY = '_openid4vc/openId4VcActorIds' + +export async function getAgentContextForActorId(rootAgentContext: AgentContext, actorId: string) { + // Check if multi-tenancy is enabled, and if so find the associated multi-tenant record + // This is a bit hacky as it uses the tenants module to store the openid4vc actor id + // but this way we don't have to expose the contextCorrelationId in the openid metadata + const tenantsApi = getApiForModuleByName(rootAgentContext, 'TenantsModule') + if (tenantsApi) { + const [tenant] = await tenantsApi.findTenantsByQuery({ + [OPENID4VC_ACTOR_IDS_METADATA_KEY]: [actorId], + }) + + if (tenant) { + const agentContextProvider = rootAgentContext.dependencyManager.resolve( + InjectionSymbols.AgentContextProvider + ) + return agentContextProvider.getAgentContextForContextCorrelationId(tenant.id) + } + } + + return rootAgentContext +} + +/** + * Store the actor id associated with a context correlation id. If multi-tenancy is not used + * this method won't do anything as we can just use the actor from the default context. However + * if multi-tenancy is used, we will store the actor id in the tenant record metadata so it can + * be queried when a request comes in for the specific actor id. + * + * The reason for doing this is that we don't want to expose the context correlation id in the + * actor metadata url, as it is then possible to see exactly which actors are registered under + * the same agent. + */ +export async function storeActorIdForContextCorrelationId(agentContext: AgentContext, actorId: string) { + // It's kind of hacky, but we add support for the tenants module specifically here to map an actorId to + // a specific tenant. Otherwise we have to expose /:contextCorrelationId/:actorId in all the public URLs + // which is of course not so nice. + const tenantsApi = getApiForModuleByName(agentContext, 'TenantsModule') + + // We don't want to query the tenant record if the current context is the root context + if (tenantsApi && tenantsApi.rootAgentContext.contextCorrelationId !== agentContext.contextCorrelationId) { + const tenantRecord = await tenantsApi.getTenantById(agentContext.contextCorrelationId) + + const currentOpenId4VcActorIds = tenantRecord.metadata.get(OPENID4VC_ACTOR_IDS_METADATA_KEY) ?? [] + const openId4VcActorIds = [...currentOpenId4VcActorIds, actorId] + + tenantRecord.metadata.set(OPENID4VC_ACTOR_IDS_METADATA_KEY, openId4VcActorIds) + tenantRecord.setTag(OPENID4VC_ACTOR_IDS_METADATA_KEY, openId4VcActorIds) + await tenantsApi.updateTenant(tenantRecord) + } +} diff --git a/packages/openid4vc/src/shared/transform.ts b/packages/openid4vc/src/shared/transform.ts new file mode 100644 index 0000000000..bf2cebf80e --- /dev/null +++ b/packages/openid4vc/src/shared/transform.ts @@ -0,0 +1,73 @@ +import type { SdJwtVc, VerifiablePresentation, VerifiableCredential } from '@credo-ts/core' +import type { + W3CVerifiableCredential as SphereonW3cVerifiableCredential, + W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, + CompactSdJwtVc as SphereonCompactSdJwtVc, + WrappedVerifiablePresentation, +} from '@sphereon/ssi-types' + +import { + JsonTransformer, + CredoError, + W3cJsonLdVerifiablePresentation, + W3cJwtVerifiablePresentation, + W3cJwtVerifiableCredential, + W3cJsonLdVerifiableCredential, + JsonEncoder, +} from '@credo-ts/core' + +export function getSphereonVerifiableCredential( + verifiableCredential: VerifiableCredential +): SphereonW3cVerifiableCredential | SphereonCompactSdJwtVc { + // encoded sd-jwt or jwt + if (typeof verifiableCredential === 'string') { + return verifiableCredential + } else if (verifiableCredential instanceof W3cJsonLdVerifiableCredential) { + return JsonTransformer.toJSON(verifiableCredential) as SphereonW3cVerifiableCredential + } else if (verifiableCredential instanceof W3cJwtVerifiableCredential) { + return verifiableCredential.serializedJwt + } else { + return verifiableCredential.compact + } +} + +export function getSphereonVerifiablePresentation( + verifiablePresentation: VerifiablePresentation +): SphereonW3cVerifiablePresentation | SphereonCompactSdJwtVc { + // encoded sd-jwt or jwt + if (typeof verifiablePresentation === 'string') { + return verifiablePresentation + } else if (verifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { + return JsonTransformer.toJSON(verifiablePresentation) as SphereonW3cVerifiablePresentation + } else if (verifiablePresentation instanceof W3cJwtVerifiablePresentation) { + return verifiablePresentation.serializedJwt + } else { + return verifiablePresentation.compact + } +} + +export function getVerifiablePresentationFromSphereonWrapped( + wrappedVerifiablePresentation: WrappedVerifiablePresentation +): VerifiablePresentation { + if (wrappedVerifiablePresentation.format === 'jwt_vp') { + if (typeof wrappedVerifiablePresentation.original !== 'string') { + throw new CredoError('Unable to transform JWT VP to W3C VP') + } + + return W3cJwtVerifiablePresentation.fromSerializedJwt(wrappedVerifiablePresentation.original) + } else if (wrappedVerifiablePresentation.format === 'ldp_vp') { + return JsonTransformer.fromJSON(wrappedVerifiablePresentation.original, W3cJsonLdVerifiablePresentation) + } else if (wrappedVerifiablePresentation.format === 'vc+sd-jwt') { + // We use some custom logic here so we don't have to re-process the encoded SD-JWT + const [encodedHeader] = wrappedVerifiablePresentation.presentation.compactSdJwtVc.split('.') + const header = JsonEncoder.fromBase64(encodedHeader) + return { + compact: wrappedVerifiablePresentation.presentation.compactSdJwtVc, + header, + payload: wrappedVerifiablePresentation.presentation.signedPayload, + prettyClaims: wrappedVerifiablePresentation.presentation.decodedPayload, + } satisfies SdJwtVc + } + + throw new CredoError(`Unsupported presentation format: ${wrappedVerifiablePresentation.format}`) +} diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts new file mode 100644 index 0000000000..171ae0d0f6 --- /dev/null +++ b/packages/openid4vc/src/shared/utils.ts @@ -0,0 +1,103 @@ +import type { OpenId4VcJwtIssuer } from './models' +import type { AgentContext, JwaSignatureAlgorithm, Key } from '@credo-ts/core' +import type { DIDDocument, SigningAlgo, SuppliedSignature } from '@sphereon/did-auth-siop' + +import { + CredoError, + DidsApi, + TypedArrayEncoder, + getKeyFromVerificationMethod, + getJwkClassFromKeyType, + SignatureSuiteRegistry, +} from '@credo-ts/core' + +/** + * Returns the JWA Signature Algorithms that are supported by the wallet. + * + * This is an approximation based on the supported key types of the wallet. + * This is not 100% correct as a supporting a key type does not mean you support + * all the algorithms for that key type. However, this needs refactoring of the wallet + * that is planned for the 0.5.0 release. + */ +export function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { + const supportedKeyTypes = agentContext.wallet.supportedKeyTypes + + // Extract the supported JWS algs based on the key types the wallet support. + const supportedJwaSignatureAlgorithms = supportedKeyTypes + // Map the supported key types to the supported JWK class + .map(getJwkClassFromKeyType) + // Filter out the undefined values + .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) + // Extract the supported JWA signature algorithms from the JWK class + .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) + + return supportedJwaSignatureAlgorithms +} + +export async function getSphereonSuppliedSignatureFromJwtIssuer( + agentContext: AgentContext, + jwtIssuer: OpenId4VcJwtIssuer +): Promise { + let key: Key + let alg: string + let kid: string | undefined + let did: string | undefined + + if (jwtIssuer.method === 'did') { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(jwtIssuer.didUrl) + const verificationMethod = didDocument.dereferenceKey(jwtIssuer.didUrl, ['authentication']) + + // get the key from the verification method and use the first supported signature algorithm + key = getKeyFromVerificationMethod(verificationMethod) + const _alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] + if (!_alg) throw new CredoError(`No supported signature algorithms for key type: ${key.keyType}`) + + alg = _alg + kid = verificationMethod.id + did = verificationMethod.controller + } else { + throw new CredoError(`Unsupported jwt issuer method '${jwtIssuer.method as string}'. Only 'did' is supported.`) + } + + return { + signature: async (data: string | Uint8Array) => { + if (typeof data !== 'string') throw new CredoError("Expected string but received 'Uint8Array'") + const signedData = await agentContext.wallet.sign({ + data: TypedArrayEncoder.fromString(data), + key, + }) + + const signature = TypedArrayEncoder.toBase64URL(signedData) + return signature + }, + alg: alg as unknown as SigningAlgo, + did, + kid, + } +} + +export function getSphereonDidResolver(agentContext: AgentContext) { + return { + resolve: async (didUrl: string) => { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const result = await didsApi.resolve(didUrl) + + return { + ...result, + didDocument: result.didDocument?.toJSON() as DIDDocument, + } + }, + } +} + +export function getProofTypeFromKey(agentContext: AgentContext, key: Key) { + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + + const supportedSignatureSuites = signatureSuiteRegistry.getAllByKeyType(key.keyType) + if (supportedSignatureSuites.length === 0) { + throw new CredoError(`Couldn't find a supported signature suite for the given key type '${key.keyType}'.`) + } + + return supportedSignatureSuites[0].proofType +} diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts new file mode 100644 index 0000000000..87bbc19628 --- /dev/null +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -0,0 +1,869 @@ +import type { AgentType, TenantType } from './utils' +import type { OpenId4VciCredentialBindingResolver } from '../src/openid4vc-holder' +import type { DifPresentationExchangeDefinitionV2, SdJwtVc } from '@credo-ts/core' +import type { Server } from 'http' + +import { + CredoError, + ClaimFormat, + DidsApi, + DifPresentationExchangeService, + getJwkFromKey, + getKeyFromVerificationMethod, + JsonEncoder, + JwaSignatureAlgorithm, + W3cCredential, + W3cCredentialSubject, + w3cDate, + W3cIssuer, +} from '@credo-ts/core' +import express, { type Express } from 'express' + +import { AskarModule } from '../../askar/src' +import { askarModuleConfig } from '../../askar/tests/helpers' +import { TenantsModule } from '../../tenants/src' +import { + OpenId4VcHolderModule, + OpenId4VcIssuanceSessionState, + OpenId4VcIssuerModule, + OpenId4VcVerificationSessionState, + OpenId4VcVerifierModule, +} from '../src' + +import { + waitForVerificationSessionRecordSubject, + waitForCredentialIssuanceSessionRecordSubject, + createAgentFromModules, + createTenantForAgent, +} from './utils' +import { universityDegreeCredentialSdJwt, universityDegreeCredentialSdJwt2 } from './utilsVci' +import { openBadgePresentationDefinition, universityDegreePresentationDefinition } from './utilsVp' + +const serverPort = 1234 +const baseUrl = `http://localhost:${serverPort}` +const issuanceBaseUrl = `${baseUrl}/oid4vci` +const verificationBaseUrl = `${baseUrl}/oid4vp` + +describe('OpenId4Vc', () => { + let expressApp: Express + let expressServer: Server + + let issuer: AgentType<{ + openId4VcIssuer: OpenId4VcIssuerModule + tenants: TenantsModule<{ openId4VcIssuer: OpenId4VcIssuerModule }> + }> + let issuer1: TenantType + let issuer2: TenantType + + let holder: AgentType<{ + openId4VcHolder: OpenId4VcHolderModule + tenants: TenantsModule<{ openId4VcHolder: OpenId4VcHolderModule }> + }> + let holder1: TenantType + + let verifier: AgentType<{ + openId4VcVerifier: OpenId4VcVerifierModule + tenants: TenantsModule<{ openId4VcVerifier: OpenId4VcVerifierModule }> + }> + let verifier1: TenantType + let verifier2: TenantType + + beforeEach(async () => { + expressApp = express() + + issuer = (await createAgentFromModules( + 'issuer', + { + openId4VcIssuer: new OpenId4VcIssuerModule({ + baseUrl: issuanceBaseUrl, + endpoints: { + credential: { + credentialRequestToCredentialMapper: async ({ agentContext, credentialRequest, holderBinding }) => { + // We sign the request with the first did:key did we have + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const [firstDidKeyDid] = await didsApi.getCreatedDids({ method: 'key' }) + const didDocument = await didsApi.resolveDidDocument(firstDidKeyDid.did) + const verificationMethod = didDocument.verificationMethod?.[0] + if (!verificationMethod) { + throw new Error('No verification method found') + } + + if (credentialRequest.format === 'vc+sd-jwt') { + return { + credentialSupportedId: + credentialRequest.vct === 'UniversityDegreeCredential' + ? universityDegreeCredentialSdJwt.id + : universityDegreeCredentialSdJwt2.id, + format: credentialRequest.format, + payload: { vct: credentialRequest.vct, university: 'innsbruck', degree: 'bachelor' }, + holder: holderBinding, + issuer: { + method: 'did', + didUrl: verificationMethod.id, + }, + disclosureFrame: { _sd: ['university', 'degree'] }, + } + } + + throw new Error('Invalid request') + }, + }, + }, + }), + askar: new AskarModule(askarModuleConfig), + tenants: new TenantsModule(), + }, + '96213c3d7fc8d4d6754c7a0fd969598g' + )) as unknown as typeof issuer + issuer1 = await createTenantForAgent(issuer.agent, 'iTenant1') + issuer2 = await createTenantForAgent(issuer.agent, 'iTenant2') + + holder = (await createAgentFromModules( + 'holder', + { + openId4VcHolder: new OpenId4VcHolderModule(), + askar: new AskarModule(askarModuleConfig), + tenants: new TenantsModule(), + }, + '96213c3d7fc8d4d6754c7a0fd969598e' + )) as unknown as typeof holder + holder1 = await createTenantForAgent(holder.agent, 'hTenant1') + + verifier = (await createAgentFromModules( + 'verifier', + { + openId4VcVerifier: new OpenId4VcVerifierModule({ + baseUrl: verificationBaseUrl, + }), + askar: new AskarModule(askarModuleConfig), + tenants: new TenantsModule(), + }, + '96213c3d7fc8d4d6754c7a0fd969598f' + )) as unknown as typeof verifier + verifier1 = await createTenantForAgent(verifier.agent, 'vTenant1') + verifier2 = await createTenantForAgent(verifier.agent, 'vTenant2') + + // We let AFJ create the router, so we have a fresh one each time + expressApp.use('/oid4vci', issuer.agent.modules.openId4VcIssuer.config.router) + expressApp.use('/oid4vp', verifier.agent.modules.openId4VcVerifier.config.router) + + expressServer = expressApp.listen(serverPort) + }) + + afterEach(async () => { + expressServer?.close() + + await issuer.agent.shutdown() + await issuer.agent.wallet.delete() + + await holder.agent.shutdown() + await holder.agent.wallet.delete() + }) + + const credentialBindingResolver: OpenId4VciCredentialBindingResolver = ({ supportsJwk, supportedDidMethods }) => { + // prefer did:key + if (supportedDidMethods?.includes('did:key')) { + return { + method: 'did', + didUrl: holder1.verificationMethod.id, + } + } + + // otherwise fall back to JWK + if (supportsJwk) { + return { + method: 'jwk', + jwk: getJwkFromKey(getKeyFromVerificationMethod(holder1.verificationMethod)), + } + } + + // otherwise throw an error + throw new CredoError('Issuer does not support did:key or JWK for credential binding') + } + + it('e2e flow with tenants, issuer endpoints requesting a sd-jwt-vc', async () => { + const issuerTenant1 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer1.tenantId }) + const issuerTenant2 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer2.tenantId }) + + const openIdIssuerTenant1 = await issuerTenant1.modules.openId4VcIssuer.createIssuer({ + credentialsSupported: [universityDegreeCredentialSdJwt], + }) + + const openIdIssuerTenant2 = await issuerTenant2.modules.openId4VcIssuer.createIssuer({ + credentialsSupported: [universityDegreeCredentialSdJwt2], + }) + + const { issuanceSession: issuanceSession1, credentialOffer: credentialOffer1 } = + await issuerTenant1.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: openIdIssuerTenant1.issuerId, + offeredCredentials: [universityDegreeCredentialSdJwt.id], + preAuthorizedCodeFlowConfig: { userPinRequired: false }, + }) + + const { issuanceSession: issuanceSession2, credentialOffer: credentialOffer2 } = + await issuerTenant2.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: openIdIssuerTenant2.issuerId, + offeredCredentials: [universityDegreeCredentialSdJwt2.id], + preAuthorizedCodeFlowConfig: { userPinRequired: false }, + }) + + await issuerTenant1.endSession() + await issuerTenant2.endSession() + + await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { + state: OpenId4VcIssuanceSessionState.OfferCreated, + issuanceSessionId: issuanceSession1.id, + contextCorrelationId: issuer1.tenantId, + }) + await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { + state: OpenId4VcIssuanceSessionState.OfferCreated, + issuanceSessionId: issuanceSession2.id, + contextCorrelationId: issuer2.tenantId, + }) + + const holderTenant1 = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) + + const resolvedCredentialOffer1 = await holderTenant1.modules.openId4VcHolder.resolveCredentialOffer( + credentialOffer1 + ) + + await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { + state: OpenId4VcIssuanceSessionState.OfferUriRetrieved, + issuanceSessionId: issuanceSession1.id, + contextCorrelationId: issuer1.tenantId, + }) + + expect(resolvedCredentialOffer1.credentialOfferPayload.credential_issuer).toEqual( + `${issuanceBaseUrl}/${openIdIssuerTenant1.issuerId}` + ) + expect(resolvedCredentialOffer1.metadata.credentialIssuerMetadata?.token_endpoint).toEqual( + `${issuanceBaseUrl}/${openIdIssuerTenant1.issuerId}/token` + ) + expect(resolvedCredentialOffer1.metadata.credentialIssuerMetadata?.credential_endpoint).toEqual( + `${issuanceBaseUrl}/${openIdIssuerTenant1.issuerId}/credential` + ) + + // Bind to JWK + const credentialsTenant1 = await holderTenant1.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer1, + { + credentialBindingResolver, + } + ) + + // Wait for all events + await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { + state: OpenId4VcIssuanceSessionState.AccessTokenRequested, + issuanceSessionId: issuanceSession1.id, + contextCorrelationId: issuer1.tenantId, + }) + await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { + state: OpenId4VcIssuanceSessionState.AccessTokenCreated, + issuanceSessionId: issuanceSession1.id, + contextCorrelationId: issuer1.tenantId, + }) + await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { + state: OpenId4VcIssuanceSessionState.CredentialRequestReceived, + issuanceSessionId: issuanceSession1.id, + contextCorrelationId: issuer1.tenantId, + }) + await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { + state: OpenId4VcIssuanceSessionState.Completed, + issuanceSessionId: issuanceSession1.id, + contextCorrelationId: issuer1.tenantId, + }) + + expect(credentialsTenant1).toHaveLength(1) + const compactSdJwtVcTenant1 = (credentialsTenant1[0] as SdJwtVc).compact + const sdJwtVcTenant1 = holderTenant1.sdJwtVc.fromCompact(compactSdJwtVcTenant1) + expect(sdJwtVcTenant1.payload.vct).toEqual('UniversityDegreeCredential') + + const resolvedCredentialOffer2 = await holderTenant1.modules.openId4VcHolder.resolveCredentialOffer( + credentialOffer2 + ) + + await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { + state: OpenId4VcIssuanceSessionState.OfferUriRetrieved, + issuanceSessionId: issuanceSession2.id, + contextCorrelationId: issuer2.tenantId, + }) + + expect(resolvedCredentialOffer2.credentialOfferPayload.credential_issuer).toEqual( + `${issuanceBaseUrl}/${openIdIssuerTenant2.issuerId}` + ) + expect(resolvedCredentialOffer2.metadata.credentialIssuerMetadata?.token_endpoint).toEqual( + `${issuanceBaseUrl}/${openIdIssuerTenant2.issuerId}/token` + ) + expect(resolvedCredentialOffer2.metadata.credentialIssuerMetadata?.credential_endpoint).toEqual( + `${issuanceBaseUrl}/${openIdIssuerTenant2.issuerId}/credential` + ) + + // Bind to did + const credentialsTenant2 = await holderTenant1.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer2, + { + credentialBindingResolver, + } + ) + + // Wait for all events + await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { + state: OpenId4VcIssuanceSessionState.AccessTokenRequested, + issuanceSessionId: issuanceSession2.id, + contextCorrelationId: issuer2.tenantId, + }) + await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { + state: OpenId4VcIssuanceSessionState.AccessTokenCreated, + issuanceSessionId: issuanceSession2.id, + contextCorrelationId: issuer2.tenantId, + }) + await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { + state: OpenId4VcIssuanceSessionState.CredentialRequestReceived, + issuanceSessionId: issuanceSession2.id, + contextCorrelationId: issuer2.tenantId, + }) + await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { + state: OpenId4VcIssuanceSessionState.Completed, + issuanceSessionId: issuanceSession2.id, + contextCorrelationId: issuer2.tenantId, + }) + + expect(credentialsTenant2).toHaveLength(1) + const compactSdJwtVcTenant2 = (credentialsTenant2[0] as SdJwtVc).compact + const sdJwtVcTenant2 = holderTenant1.sdJwtVc.fromCompact(compactSdJwtVcTenant2) + expect(sdJwtVcTenant2.payload.vct).toEqual('UniversityDegreeCredential2') + + await holderTenant1.endSession() + }) + + it('e2e flow with tenants only requesting an id-token', async () => { + const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) + const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + + const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier() + + const { authorizationRequest: authorizationRequestUri1, verificationSession: verificationSession } = + await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ + verifierId: openIdVerifierTenant1.verifierId, + requestSigner: { + method: 'did', + didUrl: verifier1.verificationMethod.id, + }, + }) + + expect(authorizationRequestUri1).toEqual( + `openid://?request_uri=${encodeURIComponent(verificationSession.authorizationRequestUri)}` + ) + + await verifierTenant1.endSession() + + const resolvedAuthorizationRequest = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri1 + ) + + expect(resolvedAuthorizationRequest.presentationExchange).toBeUndefined() + + const { submittedResponse: submittedResponse1, serverResponse: serverResponse1 } = + await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, + openIdTokenIssuer: { + method: 'did', + didUrl: holder1.verificationMethod.id, + }, + }) + + expect(submittedResponse1).toEqual({ + expires_in: 6000, + id_token: expect.any(String), + state: expect.any(String), + }) + expect(serverResponse1).toMatchObject({ + status: 200, + }) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const verifierTenant1_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + contextCorrelationId: verifierTenant1_2.context.contextCorrelationId, + state: OpenId4VcVerificationSessionState.ResponseVerified, + verificationSessionId: verificationSession.id, + }) + + const { idToken, presentationExchange } = + await verifierTenant1_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession.id) + + const requestObjectPayload = JsonEncoder.fromBase64( + verificationSession.authorizationRequestJwt?.split('.')[1] as string + ) + expect(idToken?.payload).toMatchObject({ + state: requestObjectPayload.state, + nonce: requestObjectPayload.nonce, + }) + + expect(presentationExchange).toBeUndefined() + }) + + it('e2e flow with tenants, verifier endpoints verifying a jwt-vc', async () => { + const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) + const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + const verifierTenant2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + + const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier() + const openIdVerifierTenant2 = await verifierTenant2.modules.openId4VcVerifier.createVerifier() + + const signedCredential1 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'OpenBadgeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + const signedCredential2 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential1 }) + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential2 }) + + const { authorizationRequest: authorizationRequestUri1, verificationSession: verificationSession1 } = + await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ + verifierId: openIdVerifierTenant1.verifierId, + requestSigner: { + method: 'did', + didUrl: verifier1.verificationMethod.id, + }, + presentationExchange: { + definition: openBadgePresentationDefinition, + }, + }) + + expect(authorizationRequestUri1).toEqual( + `openid4vp://?request_uri=${encodeURIComponent(verificationSession1.authorizationRequestUri)}` + ) + + const { authorizationRequest: authorizationRequestUri2, verificationSession: verificationSession2 } = + await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'did', + didUrl: verifier2.verificationMethod.id, + }, + presentationExchange: { + definition: universityDegreePresentationDefinition, + }, + verifierId: openIdVerifierTenant2.verifierId, + }) + + expect(authorizationRequestUri2).toEqual( + `openid4vp://?request_uri=${encodeURIComponent(verificationSession2.authorizationRequestUri)}` + ) + + await verifierTenant1.endSession() + await verifierTenant2.endSession() + + const resolvedProofRequest1 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri1 + ) + + expect(resolvedProofRequest1.presentationExchange?.credentialsForRequest).toMatchObject({ + areRequirementsSatisfied: true, + requirements: [ + { + submissionEntry: [ + { + verifiableCredentials: [ + { + type: ClaimFormat.JwtVc, + credentialRecord: { + credential: { + type: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + }, + }, + ], + }, + ], + }, + ], + }) + + const resolvedProofRequest2 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri2 + ) + + expect(resolvedProofRequest2.presentationExchange?.credentialsForRequest).toMatchObject({ + areRequirementsSatisfied: true, + requirements: [ + { + submissionEntry: [ + { + verifiableCredentials: [ + { + type: ClaimFormat.JwtVc, + credentialRecord: { + credential: { + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + }, + }, + ], + }, + ], + }, + ], + }) + + if (!resolvedProofRequest1.presentationExchange || !resolvedProofRequest2.presentationExchange) { + throw new Error('Presentation exchange not defined') + } + + const presentationExchangeService = holderTenant.dependencyManager.resolve(DifPresentationExchangeService) + const selectedCredentials = presentationExchangeService.selectCredentialsForRequest( + resolvedProofRequest1.presentationExchange.credentialsForRequest + ) + + const { submittedResponse: submittedResponse1, serverResponse: serverResponse1 } = + await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedProofRequest1.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials, + }, + }) + + expect(submittedResponse1).toEqual({ + expires_in: 6000, + presentation_submission: { + definition_id: 'OpenBadgeCredential', + descriptor_map: [ + { + format: 'jwt_vp', + id: 'OpenBadgeCredentialDescriptor', + path: '$', + path_nested: { + format: 'jwt_vc', + id: 'OpenBadgeCredentialDescriptor', + path: '$.vp.verifiableCredential[0]', + }, + }, + ], + id: expect.any(String), + }, + state: expect.any(String), + vp_token: expect.any(String), + }) + expect(serverResponse1).toMatchObject({ + status: 200, + }) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const verifierTenant1_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + contextCorrelationId: verifierTenant1_2.context.contextCorrelationId, + state: OpenId4VcVerificationSessionState.ResponseVerified, + verificationSessionId: verificationSession1.id, + }) + + const { idToken: idToken1, presentationExchange: presentationExchange1 } = + await verifierTenant1_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession1.id) + + expect(idToken1).toBeUndefined() + expect(presentationExchange1).toMatchObject({ + definition: openBadgePresentationDefinition, + submission: { + definition_id: 'OpenBadgeCredential', + }, + presentations: [ + { + verifiableCredential: [ + { + type: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + ], + }, + ], + }) + + const selectedCredentials2 = presentationExchangeService.selectCredentialsForRequest( + resolvedProofRequest2.presentationExchange.credentialsForRequest + ) + + const { serverResponse: serverResponse2 } = + await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedProofRequest2.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials2, + }, + }) + expect(serverResponse2).toMatchObject({ + status: 200, + }) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const verifierTenant2_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + contextCorrelationId: verifierTenant2_2.context.contextCorrelationId, + state: OpenId4VcVerificationSessionState.ResponseVerified, + verificationSessionId: verificationSession2.id, + }) + const { idToken: idToken2, presentationExchange: presentationExchange2 } = + await verifierTenant2_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession2.id) + expect(idToken2).toBeUndefined() + + expect(presentationExchange2).toMatchObject({ + definition: universityDegreePresentationDefinition, + submission: { + definition_id: 'UniversityDegreeCredential', + }, + presentations: [ + { + verifiableCredential: [ + { + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + ], + }, + ], + }) + }) + + it('e2e flow with verifier endpoints verifying a sd-jwt-vc with selective disclosure', async () => { + const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() + + const signedSdJwtVc = await issuer.agent.sdJwtVc.sign({ + holder: { + method: 'did', + didUrl: holder.kid, + }, + issuer: { + method: 'did', + didUrl: issuer.kid, + }, + payload: { + vct: 'OpenBadgeCredential', + university: 'innsbruck', + degree: 'bachelor', + name: 'John Doe', + }, + disclosureFrame: { + _sd: ['university', 'name'], + }, + }) + + await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) + + const presentationDefinition = { + id: 'OpenBadgeCredential', + input_descriptors: [ + { + id: 'OpenBadgeCredentialDescriptor', + format: { + 'vc+sd-jwt': { + 'sd-jwt_alg_values': ['EdDSA'], + }, + }, + constraints: { + limit_disclosure: 'required', + fields: [ + { + path: ['$.vct'], + filter: { + type: 'string', + const: 'OpenBadgeCredential', + }, + }, + { + path: ['$.university'], + }, + ], + }, + }, + ], + } satisfies DifPresentationExchangeDefinitionV2 + + const { authorizationRequest, verificationSession } = + await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + verifierId: openIdVerifier.verifierId, + requestSigner: { + method: 'did', + didUrl: verifier.kid, + }, + presentationExchange: { + definition: presentationDefinition, + }, + }) + + expect(authorizationRequest).toEqual( + `openid4vp://?request_uri=${encodeURIComponent(verificationSession.authorizationRequestUri)}` + ) + + const resolvedAuthorizationRequest = await holder.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequest + ) + + expect(resolvedAuthorizationRequest.presentationExchange?.credentialsForRequest).toEqual({ + areRequirementsSatisfied: true, + name: undefined, + purpose: undefined, + requirements: [ + { + isRequirementSatisfied: true, + needsCount: 1, + rule: 'pick', + submissionEntry: [ + { + name: undefined, + purpose: undefined, + inputDescriptorId: 'OpenBadgeCredentialDescriptor', + verifiableCredentials: [ + { + type: ClaimFormat.SdJwtVc, + credentialRecord: expect.objectContaining({ + compactSdJwtVc: signedSdJwtVc.compact, + }), + // Name is NOT in here + disclosedPayload: { + cnf: { + kid: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + }, + degree: 'bachelor', + iat: expect.any(Number), + iss: 'did:key:z6MkrzQPBr4pyqC776KKtrz13SchM5ePPbssuPuQZb5t4uKQ', + university: 'innsbruck', + vct: 'OpenBadgeCredential', + }, + }, + ], + }, + ], + }, + ], + }) + + if (!resolvedAuthorizationRequest.presentationExchange) { + throw new Error('Presentation exchange not defined') + } + + // TODO: better way to auto-select + const presentationExchangeService = holder.agent.dependencyManager.resolve(DifPresentationExchangeService) + const selectedCredentials = presentationExchangeService.selectCredentialsForRequest( + resolvedAuthorizationRequest.presentationExchange.credentialsForRequest + ) + + const { serverResponse, submittedResponse } = + await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials, + }, + }) + + // path_nested should not be used for sd-jwt + expect(submittedResponse.presentation_submission?.descriptor_map[0].path_nested).toBeUndefined() + expect(submittedResponse).toEqual({ + expires_in: 6000, + presentation_submission: { + definition_id: 'OpenBadgeCredential', + descriptor_map: [ + { + format: 'vc+sd-jwt', + id: 'OpenBadgeCredentialDescriptor', + path: '$', + }, + ], + id: expect.any(String), + }, + state: expect.any(String), + vp_token: expect.any(String), + }) + expect(serverResponse).toMatchObject({ + status: 200, + }) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + contextCorrelationId: verifier.agent.context.contextCorrelationId, + state: OpenId4VcVerificationSessionState.ResponseVerified, + verificationSessionId: verificationSession.id, + }) + const { idToken, presentationExchange } = + await verifier.agent.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession.id) + + expect(idToken).toBeUndefined() + + const presentation = presentationExchange?.presentations[0] as SdJwtVc + + // name SHOULD NOT be disclosed + expect(presentation.prettyClaims).not.toHaveProperty('name') + + // university and name SHOULD NOT be in the signed payload + expect(presentation.payload).not.toHaveProperty('university') + expect(presentation.payload).not.toHaveProperty('name') + + expect(presentationExchange).toEqual({ + definition: presentationDefinition, + submission: { + definition_id: 'OpenBadgeCredential', + descriptor_map: [ + { + format: 'vc+sd-jwt', + id: 'OpenBadgeCredentialDescriptor', + path: '$', + }, + ], + id: expect.any(String), + }, + presentations: [ + { + compact: expect.any(String), + header: { + alg: 'EdDSA', + kid: '#z6MkrzQPBr4pyqC776KKtrz13SchM5ePPbssuPuQZb5t4uKQ', + typ: 'vc+sd-jwt', + }, + payload: { + _sd: [expect.any(String), expect.any(String)], + _sd_alg: 'sha-256', + cnf: { + kid: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + }, + iat: expect.any(Number), + iss: 'did:key:z6MkrzQPBr4pyqC776KKtrz13SchM5ePPbssuPuQZb5t4uKQ', + vct: 'OpenBadgeCredential', + degree: 'bachelor', + }, + // university SHOULD be disclosed + prettyClaims: { + cnf: { + kid: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + }, + iat: expect.any(Number), + iss: 'did:key:z6MkrzQPBr4pyqC776KKtrz13SchM5ePPbssuPuQZb5t4uKQ', + vct: 'OpenBadgeCredential', + degree: 'bachelor', + university: 'innsbruck', + }, + }, + ], + }) + }) +}) diff --git a/packages/openid4vc/tests/setup.ts b/packages/openid4vc/tests/setup.ts new file mode 100644 index 0000000000..34e38c9705 --- /dev/null +++ b/packages/openid4vc/tests/setup.ts @@ -0,0 +1 @@ +jest.setTimeout(120000) diff --git a/packages/openid4vc/tests/utils.ts b/packages/openid4vc/tests/utils.ts new file mode 100644 index 0000000000..cb63da0ec6 --- /dev/null +++ b/packages/openid4vc/tests/utils.ts @@ -0,0 +1,164 @@ +import type { + OpenId4VcIssuanceSessionState, + OpenId4VcIssuanceSessionStateChangedEvent, + OpenId4VcVerificationSessionState, + OpenId4VcVerificationSessionStateChangedEvent, +} from '../src' +import type { BaseEvent, ModulesMap } from '@credo-ts/core' +import type { TenantsModule } from '@credo-ts/tenants' +import type { Observable } from 'rxjs' + +import { Agent, LogLevel, utils } from '@credo-ts/core' +import { ReplaySubject, lastValueFrom, filter, timeout, catchError, take, map } from 'rxjs' + +import { + TestLogger, + agentDependencies, + createDidKidVerificationMethod, + setupEventReplaySubjects, +} from '../../core/tests' +import { OpenId4VcVerifierEvents, OpenId4VcIssuerEvents } from '../src' + +export async function createAgentFromModules(label: string, modulesMap: MM, secretKey: string) { + const agent = new Agent({ + config: { label, walletConfig: { id: utils.uuid(), key: utils.uuid() }, logger: new TestLogger(LogLevel.off) }, + dependencies: agentDependencies, + modules: modulesMap, + }) + + await agent.initialize() + const data = await createDidKidVerificationMethod(agent.context, secretKey) + + const [replaySubject] = setupEventReplaySubjects( + [agent], + [OpenId4VcIssuerEvents.IssuanceSessionStateChanged, OpenId4VcVerifierEvents.VerificationSessionStateChanged] + ) + + return { + ...data, + agent, + replaySubject, + } +} + +export type AgentType = Awaited>> + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AgentWithTenantsModule = Agent<{ tenants: TenantsModule }> + +export async function createTenantForAgent( + // FIXME: we need to make some improvements on the agent typing. It'a quite hard + // to get it right at the moment + // eslint-disable-next-line @typescript-eslint/no-explicit-any + agent: AgentWithTenantsModule & any, + label: string +) { + const tenantRecord = await agent.modules.tenants.createTenant({ + config: { + label, + }, + }) + + const tenant = await agent.modules.tenants.getTenantAgent({ tenantId: tenantRecord.id }) + const data = await createDidKidVerificationMethod(tenant) + await tenant.endSession() + + return { + ...data, + tenantId: tenantRecord.id, + } +} + +export type TenantType = Awaited> + +export function waitForCredentialIssuanceSessionRecordSubject( + subject: ReplaySubject | Observable, + { + state, + previousState, + timeoutMs = 10000, + count = 1, + contextCorrelationId, + issuanceSessionId, + }: { + state?: OpenId4VcIssuanceSessionState + previousState?: OpenId4VcIssuanceSessionState | null + timeoutMs?: number + count?: number + contextCorrelationId?: string + issuanceSessionId?: string + } +) { + const observable: Observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return lastValueFrom( + observable.pipe( + filter((e) => contextCorrelationId === undefined || e.metadata.contextCorrelationId === contextCorrelationId), + filter( + (event): event is OpenId4VcIssuanceSessionStateChangedEvent => + event.type === OpenId4VcIssuerEvents.IssuanceSessionStateChanged + ), + filter((e) => previousState === undefined || e.payload.previousState === previousState), + filter((e) => state === undefined || e.payload.issuanceSession.state === state), + filter((e) => issuanceSessionId === undefined || e.payload.issuanceSession.id === issuanceSessionId), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `OpenId4VcIssuanceSessionStateChangedEvent event not emitted within specified timeout: ${timeoutMs} + contextCorrelationId: ${contextCorrelationId}, + issuanceSessionId: ${issuanceSessionId} + previousState: ${previousState}, + state: ${state} + }` + ) + }), + take(count), + map((e) => e.payload.issuanceSession) + ) + ) +} + +export function waitForVerificationSessionRecordSubject( + subject: ReplaySubject | Observable, + { + state, + previousState, + timeoutMs = 10000, + count = 1, + contextCorrelationId, + verificationSessionId, + }: { + state?: OpenId4VcVerificationSessionState + previousState?: OpenId4VcVerificationSessionState | null + timeoutMs?: number + count?: number + contextCorrelationId?: string + verificationSessionId?: string + } +) { + const observable: Observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return lastValueFrom( + observable.pipe( + filter((e) => contextCorrelationId === undefined || e.metadata.contextCorrelationId === contextCorrelationId), + filter( + (event): event is OpenId4VcVerificationSessionStateChangedEvent => + event.type === OpenId4VcVerifierEvents.VerificationSessionStateChanged + ), + filter((e) => previousState === undefined || e.payload.previousState === previousState), + filter((e) => state === undefined || e.payload.verificationSession.state === state), + filter((e) => verificationSessionId === undefined || e.payload.verificationSession.id === verificationSessionId), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `OpenId4VcVerificationSessionStateChangedEvent event not emitted within specified timeout: ${timeoutMs} + contextCorrelationId: ${contextCorrelationId}, + verificationSessionId: ${verificationSessionId} + previousState: ${previousState}, + state: ${state} + }` + ) + }), + take(count), + map((e) => e.payload.verificationSession) + ) + ) +} diff --git a/packages/openid4vc/tests/utilsVci.ts b/packages/openid4vc/tests/utilsVci.ts new file mode 100644 index 0000000000..798c9f76af --- /dev/null +++ b/packages/openid4vc/tests/utilsVci.ts @@ -0,0 +1,45 @@ +import type { OpenId4VciCredentialSupportedWithId } from '../src' + +import { OpenId4VciCredentialFormatProfile } from '../src' + +export const openBadgeCredential: OpenId4VciCredentialSupportedWithId = { + id: `/credentials/OpenBadgeCredential`, + format: OpenId4VciCredentialFormatProfile.JwtVcJson, + types: ['VerifiableCredential', 'OpenBadgeCredential'], +} + +export const universityDegreeCredential: OpenId4VciCredentialSupportedWithId = { + id: `/credentials/UniversityDegreeCredential`, + format: OpenId4VciCredentialFormatProfile.JwtVcJson, + types: ['VerifiableCredential', 'UniversityDegreeCredential'], +} + +export const universityDegreeCredentialLd: OpenId4VciCredentialSupportedWithId = { + id: `/credentials/UniversityDegreeCredentialLd`, + format: OpenId4VciCredentialFormatProfile.JwtVcJsonLd, + types: ['VerifiableCredential', 'UniversityDegreeCredential'], + '@context': ['context'], +} + +export const universityDegreeCredentialSdJwt = { + id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt', + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + vct: 'UniversityDegreeCredential', + cryptographic_binding_methods_supported: ['did:key'], +} satisfies OpenId4VciCredentialSupportedWithId + +export const universityDegreeCredentialSdJwt2 = { + id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt2', + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + vct: 'UniversityDegreeCredential2', + // FIXME: should this be dynamically generated? I think static is fine for now + cryptographic_binding_methods_supported: ['jwk'], +} satisfies OpenId4VciCredentialSupportedWithId + +export const allCredentialsSupported = [ + openBadgeCredential, + universityDegreeCredential, + universityDegreeCredentialLd, + universityDegreeCredentialSdJwt, + universityDegreeCredentialSdJwt2, +] diff --git a/packages/openid4vc/tests/utilsVp.ts b/packages/openid4vc/tests/utilsVp.ts new file mode 100644 index 0000000000..d92ecc1918 --- /dev/null +++ b/packages/openid4vc/tests/utilsVp.ts @@ -0,0 +1,121 @@ +import type { AgentContext, DifPresentationExchangeDefinitionV2, VerificationMethod } from '@credo-ts/core' + +import { + getKeyFromVerificationMethod, + W3cCredential, + W3cIssuer, + W3cCredentialSubject, + W3cCredentialService, + ClaimFormat, + CREDENTIALS_CONTEXT_V1_URL, +} from '@credo-ts/core' + +import { getProofTypeFromKey } from '../src/shared/utils' + +export const waltPortalOpenBadgeJwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6e319LCJpc3MiOiJkaWQ6a2V5Ono2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSIsInN1YiI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwibmJmIjoxNzAwNzQzMzM1fQ.OcKPyaWeVV-78BWr8N4h2Cyvjtc9jzknAqvTA77hTbKCNCEbhGboo-S6yXHLC-3NWYQ1vVcqZmdPlIOrHZ7MDw' + +export const waltUniversityDegreeJwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnt9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdGlRUUVxbTJ5YXBYQkR0MVdFVkIzZHFndnl6aTk2RnVGQU5ZbXJnVHJLVjkiLCJzdWIiOiJkaWQ6a2V5Ono2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyIsIm5iZiI6MTcwMDc0MzM5NH0.EhMnE349oOvzbu0rFl-m_7FOoRsB5VucLV5tUUIW0jPxkJ7J0qVLOJTXVX4KNv_N9oeP8pgTUvydd6nxB_0KCQ' + +export const getOpenBadgeCredentialLdpVc = async ( + agentContext: AgentContext, + issuerVerificationMethod: VerificationMethod, + holderVerificationMethod: VerificationMethod +) => { + const credential = new W3cCredential({ + context: [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'OpenBadgeCredential'], + id: 'http://example.edu/credentials/3732', + issuer: new W3cIssuer({ + id: issuerVerificationMethod.controller, + }), + issuanceDate: '2017-10-22T12:23:48Z', + expirationDate: '2027-10-22T12:23:48Z', + credentialSubject: new W3cCredentialSubject({ + id: holderVerificationMethod.controller, + }), + }) + + const w3cs = agentContext.dependencyManager.resolve(W3cCredentialService) + const key = getKeyFromVerificationMethod(holderVerificationMethod) + const proofType = getProofTypeFromKey(agentContext, key) + const signedLdpVc = await w3cs.signCredential(agentContext, { + format: ClaimFormat.LdpVc, + credential, + verificationMethod: issuerVerificationMethod.id, + proofType, + }) + + return signedLdpVc +} +export const openBadgeCredentialPresentationDefinitionLdpVc: DifPresentationExchangeDefinitionV2 = { + id: 'OpenBadgeCredential', + input_descriptors: [ + { + id: 'OpenBadgeCredential', + // changed jwt_vc_json to jwt_vc + format: { ldp_vc: { proof_type: ['Ed25519Signature2018'] } }, + // changed $.type to $.vc.type + constraints: { + fields: [{ path: ['$.type.*', '$.vc.type'], filter: { type: 'string', pattern: 'OpenBadgeCredential' } }], + }, + }, + ], +} + +export const universityDegreePresentationDefinition: DifPresentationExchangeDefinitionV2 = { + id: 'UniversityDegreeCredential', + input_descriptors: [ + { + id: 'UniversityDegree', + // changed jwt_vc_json to jwt_vc + format: { jwt_vc: { alg: ['EdDSA'] } }, + // changed $.type to $.vc.type + constraints: { + fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'UniversityDegree' } }], + }, + }, + ], +} + +export const openBadgePresentationDefinition: DifPresentationExchangeDefinitionV2 = { + id: 'OpenBadgeCredential', + input_descriptors: [ + { + id: 'OpenBadgeCredentialDescriptor', + // changed jwt_vc_json to jwt_vc + format: { jwt_vc: { alg: ['EdDSA'] } }, + // changed $.type to $.vc.type + constraints: { + fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'OpenBadgeCredential' } }], + }, + }, + ], +} + +export const combinePresentationDefinitions = ( + presentationDefinitions: DifPresentationExchangeDefinitionV2[] +): DifPresentationExchangeDefinitionV2 => { + return { + id: 'Combined', + input_descriptors: presentationDefinitions.flatMap((p) => p.input_descriptors), + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function waitForMockFunction(mockFn: jest.Mock) { + return new Promise((resolve, reject) => { + const intervalId = setInterval(() => { + if (mockFn.mock.calls.length > 0) { + clearInterval(intervalId) + resolve(0) + } + }, 100) + + setTimeout(() => { + clearInterval(intervalId) + reject(new Error('Timeout Callback')) + }, 10000) + }) +} diff --git a/packages/openid4vc/tsconfig.build.json b/packages/openid4vc/tsconfig.build.json new file mode 100644 index 0000000000..2b75d0adab --- /dev/null +++ b/packages/openid4vc/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build" + }, + "include": ["src/**/*"] +} diff --git a/packages/openid4vc/tsconfig.json b/packages/openid4vc/tsconfig.json new file mode 100644 index 0000000000..c1aca0e050 --- /dev/null +++ b/packages/openid4vc/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"], + "skipLibCheck": true + } +} diff --git a/packages/question-answer/CHANGELOG.md b/packages/question-answer/CHANGELOG.md new file mode 100644 index 0000000000..7de8be720e --- /dev/null +++ b/packages/question-answer/CHANGELOG.md @@ -0,0 +1,148 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release +- Updated dependencies [66e696d] + - @credo-ts/core@0.5.6 + +## 0.5.5 + +### Patch Changes + +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) +- Updated dependencies [3239ef3] +- Updated dependencies [d548fa4] +- Updated dependencies [482a630] + - @credo-ts/core@0.5.5 + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +**Note:** Version bump only for package @credo-ts/question-answer + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +**Note:** Version bump only for package @credo-ts/question-answer + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +**Note:** Version bump only for package @credo-ts/question-answer + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +### Features + +- **openid4vc:** persistance and events ([#1793](https://github.com/openwallet-foundation/credo-ts/issues/1793)) ([f4c386a](https://github.com/openwallet-foundation/credo-ts/commit/f4c386a6ccf8adb829cad30b81d524e6ffddb029)) + +## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) + +**Note:** Version bump only for package @credo-ts/question-answer + +## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) + +### Features + +- oob without handhsake improvements and routing ([#1511](https://github.com/hyperledger/aries-framework-javascript/issues/1511)) ([9e69cf4](https://github.com/hyperledger/aries-framework-javascript/commit/9e69cf441a75bf7a3c5556cf59e730ee3fce8c28)) + +# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) + +### Bug Fixes + +- thread id improvements ([#1311](https://github.com/hyperledger/aries-framework-javascript/issues/1311)) ([229ed1b](https://github.com/hyperledger/aries-framework-javascript/commit/229ed1b9540ca0c9380b5cca6c763fefd6628960)) + +- refactor!: remove Dispatcher.registerMessageHandler (#1354) ([78ecf1e](https://github.com/hyperledger/aries-framework-javascript/commit/78ecf1ed959c9daba1c119d03f4596f1db16c57c)), closes [#1354](https://github.com/hyperledger/aries-framework-javascript/issues/1354) + +### Features + +- outbound message send via session ([#1335](https://github.com/hyperledger/aries-framework-javascript/issues/1335)) ([582c711](https://github.com/hyperledger/aries-framework-javascript/commit/582c711728db12b7d38a0be2e9fa78dbf31b34c6)) + +### BREAKING CHANGES + +- `Dispatcher.registerMessageHandler` has been removed in favour of `MessageHandlerRegistry.registerMessageHandler`. If you want to register message handlers in an extension module, you can use directly `agentContext.dependencyManager.registerMessageHandlers`. + +Signed-off-by: Ariel Gentile + +## [0.3.3](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.2...v0.3.3) (2023-01-18) + +### Bug Fixes + +- fix typing issues with typescript 4.9 ([#1214](https://github.com/hyperledger/aries-framework-javascript/issues/1214)) ([087980f](https://github.com/hyperledger/aries-framework-javascript/commit/087980f1adf3ee0bc434ca9782243a62c6124444)) + +### Features + +- **indy-sdk:** add indy-sdk package ([#1200](https://github.com/hyperledger/aries-framework-javascript/issues/1200)) ([9933b35](https://github.com/hyperledger/aries-framework-javascript/commit/9933b35a6aa4524caef8a885e71b742cd0d7186b)) + +## [0.3.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.1...v0.3.2) (2023-01-04) + +**Note:** Version bump only for package @credo-ts/question-answer + +## [0.3.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.0...v0.3.1) (2022-12-27) + +**Note:** Version bump only for package @credo-ts/question-answer + +# [0.3.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.5...v0.3.0) (2022-12-22) + +- refactor!: rename Handler to MessageHandler (#1161) ([5e48696](https://github.com/hyperledger/aries-framework-javascript/commit/5e48696ec16d88321f225628e6cffab243718b4c)), closes [#1161](https://github.com/hyperledger/aries-framework-javascript/issues/1161) +- feat(action-menu)!: move to separate package (#1049) ([e0df0d8](https://github.com/hyperledger/aries-framework-javascript/commit/e0df0d884b1a7816c7c638406606e45f6e169ff4)), closes [#1049](https://github.com/hyperledger/aries-framework-javascript/issues/1049) +- feat(question-answer)!: separate logic to a new module (#1040) ([97d3073](https://github.com/hyperledger/aries-framework-javascript/commit/97d3073aa9300900740c3e8aee8233d38849293d)), closes [#1040](https://github.com/hyperledger/aries-framework-javascript/issues/1040) + +### BREAKING CHANGES + +- Handler has been renamed to MessageHandler to be more descriptive, along with related types and methods. This means: + +Handler is now MessageHandler +HandlerInboundMessage is now MessageHandlerInboundMessage +Dispatcher.registerHandler is now Dispatcher.registerMessageHandlers + +- action-menu module has been removed from the core and moved to a separate package. To integrate it in an Agent instance, it can be injected in constructor like this: + +```ts +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + actionMenu: new ActionMenuModule(), + /* other custom modules */ + }, +}) +``` + +Then, module API can be accessed in `agent.modules.actionMenu`. + +- question-answer module has been removed from the core and moved to a separate package. To integrate it in an Agent instance, it can be injected in constructor like this: + +```ts +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + questionAnswer: new QuestionAnswerModule(), + /* other custom modules */ + }, +}) +``` + +Then, module API can be accessed in `agent.modules.questionAnswer`. diff --git a/packages/question-answer/README.md b/packages/question-answer/README.md new file mode 100644 index 0000000000..008d9931dd --- /dev/null +++ b/packages/question-answer/README.md @@ -0,0 +1,65 @@ +

+
+ Credo Logo +

+

Credo Question Answer Module

+

+ License + typescript + @credo-ts/question-answer version + +

+
+ +Question Answer module for [Credo](https://github.com/openwallet-foundation/credo-ts.git). Implements [Aries RFC 0113](https://github.com/hyperledger/aries-rfcs/blob/1795d5c2d36f664f88f5e8045042ace8e573808c/features/0113-question-answer/README.md). + +### Quick start + +In order for this module to work, we have to inject it into the agent to access agent functionality. See the example for more information. + +### Example of usage + +```ts +import { QuestionAnswerModule } from '@credo-ts/question-answer' + +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + questionAnswer: new QuestionAnswerModule(), + /* other custom modules */ + }, +}) + +await agent.initialize() + +// To send a question to a given connection +await agent.modules.questionAnswer.sendQuestion(connectionId, { + question: 'Do you want to play?', + validResponses: [{ text: 'Yes' }, { text: 'No' }], +}) + +// Questions and Answers are received as QuestionAnswerStateChangedEvent + +// To send an answer related to a given question answer record +await agent.modules.questionAnswer.sendAnswer(questionAnswerRecordId, 'Yes') +``` diff --git a/packages/question-answer/jest.config.ts b/packages/question-answer/jest.config.ts new file mode 100644 index 0000000000..93c0197296 --- /dev/null +++ b/packages/question-answer/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/question-answer/package.json b/packages/question-answer/package.json new file mode 100644 index 0000000000..91d0a072bc --- /dev/null +++ b/packages/question-answer/package.json @@ -0,0 +1,40 @@ +{ + "name": "@credo-ts/question-answer", + "main": "src/index", + "types": "src/index", + "version": "0.5.6", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/question-answer", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/question-answer" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build", + "test": "jest" + }, + "dependencies": { + "@credo-ts/core": "workspace:*", + "class-transformer": "0.5.1", + "class-validator": "0.14.1", + "rxjs": "^7.8.0" + }, + "devDependencies": { + "@credo-ts/node": "workspace:*", + "reflect-metadata": "^0.1.13", + "rimraf": "^4.4.0", + "typescript": "~5.5.2" + } +} diff --git a/packages/question-answer/src/QuestionAnswerApi.ts b/packages/question-answer/src/QuestionAnswerApi.ts new file mode 100644 index 0000000000..6d2ebefa5f --- /dev/null +++ b/packages/question-answer/src/QuestionAnswerApi.ts @@ -0,0 +1,130 @@ +import type { QuestionAnswerRecord } from './repository' +import type { Query, QueryOptions } from '@credo-ts/core' + +import { getOutboundMessageContext, AgentContext, ConnectionService, injectable, MessageSender } from '@credo-ts/core' + +import { AnswerMessageHandler, QuestionMessageHandler } from './handlers' +import { ValidResponse } from './models' +import { QuestionAnswerService } from './services' + +@injectable() +export class QuestionAnswerApi { + private questionAnswerService: QuestionAnswerService + private messageSender: MessageSender + private connectionService: ConnectionService + private agentContext: AgentContext + + public constructor( + questionAnswerService: QuestionAnswerService, + messageSender: MessageSender, + connectionService: ConnectionService, + agentContext: AgentContext + ) { + this.questionAnswerService = questionAnswerService + this.messageSender = messageSender + this.connectionService = connectionService + this.agentContext = agentContext + + this.agentContext.dependencyManager.registerMessageHandlers([ + new QuestionMessageHandler(this.questionAnswerService), + new AnswerMessageHandler(this.questionAnswerService), + ]) + } + + /** + * Create a question message with possible valid responses, then send message to the + * holder + * + * @param connectionId connection to send the question message to + * @param config config for creating question message + * @returns QuestionAnswer record + */ + public async sendQuestion( + connectionId: string, + config: { + question: string + validResponses: ValidResponse[] + detail?: string + } + ) { + const connection = await this.connectionService.getById(this.agentContext, connectionId) + connection.assertReady() + + const { questionMessage, questionAnswerRecord } = await this.questionAnswerService.createQuestion( + this.agentContext, + connectionId, + { + question: config.question, + validResponses: config.validResponses.map((item) => new ValidResponse(item)), + detail: config?.detail, + } + ) + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message: questionMessage, + associatedRecord: questionAnswerRecord, + connectionRecord: connection, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + + return questionAnswerRecord + } + + /** + * Create an answer message as the holder and send it in response to a question message + * + * @param questionRecordId the id of the questionAnswer record + * @param response response included in the answer message + * @returns QuestionAnswer record + */ + public async sendAnswer(questionRecordId: string, response: string) { + const questionRecord = await this.questionAnswerService.getById(this.agentContext, questionRecordId) + + const { answerMessage, questionAnswerRecord } = await this.questionAnswerService.createAnswer( + this.agentContext, + questionRecord, + response + ) + + const connection = await this.connectionService.getById(this.agentContext, questionRecord.connectionId) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message: answerMessage, + associatedRecord: questionAnswerRecord, + connectionRecord: connection, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + + return questionAnswerRecord + } + + /** + * Get all QuestionAnswer records + * + * @returns list containing all QuestionAnswer records + */ + public getAll() { + return this.questionAnswerService.getAll(this.agentContext) + } + + /** + * Get all QuestionAnswer records by specified query params + * + * @returns list containing all QuestionAnswer records matching specified query params + */ + public findAllByQuery(query: Query, queryOptions?: QueryOptions) { + return this.questionAnswerService.findAllByQuery(this.agentContext, query, queryOptions) + } + + /** + * Retrieve a question answer record by id + * + * @param questionAnswerId The questionAnswer record id + * @return The question answer record or null if not found + * + */ + public findById(questionAnswerId: string) { + return this.questionAnswerService.findById(this.agentContext, questionAnswerId) + } +} diff --git a/packages/question-answer/src/QuestionAnswerEvents.ts b/packages/question-answer/src/QuestionAnswerEvents.ts new file mode 100644 index 0000000000..4e770ad296 --- /dev/null +++ b/packages/question-answer/src/QuestionAnswerEvents.ts @@ -0,0 +1,14 @@ +import type { QuestionAnswerState } from './models' +import type { QuestionAnswerRecord } from './repository' +import type { BaseEvent } from '@credo-ts/core' + +export enum QuestionAnswerEventTypes { + QuestionAnswerStateChanged = 'QuestionAnswerStateChanged', +} +export interface QuestionAnswerStateChangedEvent extends BaseEvent { + type: typeof QuestionAnswerEventTypes.QuestionAnswerStateChanged + payload: { + previousState: QuestionAnswerState | null + questionAnswerRecord: QuestionAnswerRecord + } +} diff --git a/packages/question-answer/src/QuestionAnswerModule.ts b/packages/question-answer/src/QuestionAnswerModule.ts new file mode 100644 index 0000000000..5bb6873f2b --- /dev/null +++ b/packages/question-answer/src/QuestionAnswerModule.ts @@ -0,0 +1,31 @@ +import type { DependencyManager, FeatureRegistry, Module } from '@credo-ts/core' + +import { Protocol } from '@credo-ts/core' + +import { QuestionAnswerApi } from './QuestionAnswerApi' +import { QuestionAnswerRole } from './QuestionAnswerRole' +import { QuestionAnswerRepository } from './repository' +import { QuestionAnswerService } from './services' + +export class QuestionAnswerModule implements Module { + public readonly api = QuestionAnswerApi + + /** + * Registers the dependencies of the question answer module on the dependency manager. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Services + dependencyManager.registerSingleton(QuestionAnswerService) + + // Repositories + dependencyManager.registerSingleton(QuestionAnswerRepository) + + // Feature Registry + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/questionanswer/1.0', + roles: [QuestionAnswerRole.Questioner, QuestionAnswerRole.Responder], + }) + ) + } +} diff --git a/packages/question-answer/src/QuestionAnswerRole.ts b/packages/question-answer/src/QuestionAnswerRole.ts new file mode 100644 index 0000000000..8cd47e9271 --- /dev/null +++ b/packages/question-answer/src/QuestionAnswerRole.ts @@ -0,0 +1,4 @@ +export enum QuestionAnswerRole { + Questioner = 'questioner', + Responder = 'responder', +} diff --git a/packages/question-answer/src/__tests__/QuestionAnswerModule.test.ts b/packages/question-answer/src/__tests__/QuestionAnswerModule.test.ts new file mode 100644 index 0000000000..d789f7e114 --- /dev/null +++ b/packages/question-answer/src/__tests__/QuestionAnswerModule.test.ts @@ -0,0 +1,39 @@ +import type { DependencyManager, FeatureRegistry } from '@credo-ts/core' + +import { Protocol } from '@credo-ts/core' + +import { + QuestionAnswerModule, + QuestionAnswerRepository, + QuestionAnswerRole, + QuestionAnswerService, +} from '@credo-ts/question-answer' + +const dependencyManager = { + registerInstance: jest.fn(), + registerSingleton: jest.fn(), + registerContextScoped: jest.fn(), +} as unknown as DependencyManager + +const featureRegistry = { + register: jest.fn(), +} as unknown as FeatureRegistry + +describe('QuestionAnswerModule', () => { + test('registers dependencies on the dependency manager', () => { + const questionAnswerModule = new QuestionAnswerModule() + questionAnswerModule.register(dependencyManager, featureRegistry) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(QuestionAnswerService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(QuestionAnswerRepository) + + expect(featureRegistry.register).toHaveBeenCalledTimes(1) + expect(featureRegistry.register).toHaveBeenCalledWith( + new Protocol({ + id: 'https://didcomm.org/questionanswer/1.0', + roles: [QuestionAnswerRole.Questioner, QuestionAnswerRole.Responder], + }) + ) + }) +}) diff --git a/packages/question-answer/src/__tests__/QuestionAnswerService.test.ts b/packages/question-answer/src/__tests__/QuestionAnswerService.test.ts new file mode 100644 index 0000000000..1a45b7c173 --- /dev/null +++ b/packages/question-answer/src/__tests__/QuestionAnswerService.test.ts @@ -0,0 +1,326 @@ +import type { AgentConfig, AgentContext, Repository, Wallet } from '@credo-ts/core' +import type { QuestionAnswerStateChangedEvent, ValidResponse } from '@credo-ts/question-answer' + +import { EventEmitter, InboundMessageContext, DidExchangeState } from '@credo-ts/core' +import { agentDependencies } from '@credo-ts/node' +import { Subject } from 'rxjs' + +import { InMemoryWallet } from '../../../../tests/InMemoryWallet' +import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../core/tests/helpers' + +import { + QuestionAnswerRecord, + QuestionAnswerRepository, + QuestionAnswerEventTypes, + QuestionAnswerRole, + QuestionAnswerService, + QuestionAnswerState, + QuestionMessage, + AnswerMessage, +} from '@credo-ts/question-answer' + +jest.mock('../repository/QuestionAnswerRepository') +const QuestionAnswerRepositoryMock = QuestionAnswerRepository as jest.Mock + +describe('QuestionAnswerService', () => { + const mockConnectionRecord = getMockConnection({ + id: 'd3849ac3-c981-455b-a1aa-a10bea6cead8', + did: 'did:sov:C2SsBf5QUQpqSAQfhu3sd2', + state: DidExchangeState.Completed, + }) + + let wallet: Wallet + let agentConfig: AgentConfig + let questionAnswerRepository: Repository + let questionAnswerService: QuestionAnswerService + let eventEmitter: EventEmitter + let agentContext: AgentContext + + const mockQuestionAnswerRecord = (options: { + questionText: string + questionDetail?: string + connectionId: string + role: QuestionAnswerRole + signatureRequired: boolean + state: QuestionAnswerState + threadId: string + validResponses: ValidResponse[] + }) => { + return new QuestionAnswerRecord({ + questionText: options.questionText, + questionDetail: options.questionDetail, + connectionId: options.connectionId, + role: options.role, + signatureRequired: options.signatureRequired, + state: options.state, + threadId: options.threadId, + validResponses: options.validResponses, + }) + } + + beforeAll(async () => { + agentConfig = getAgentConfig('QuestionAnswerServiceTest') + wallet = new InMemoryWallet() + agentContext = getAgentContext() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await wallet.createAndOpen(agentConfig.walletConfig!) + }) + + beforeEach(async () => { + questionAnswerRepository = new QuestionAnswerRepositoryMock() + eventEmitter = new EventEmitter(agentDependencies, new Subject()) + questionAnswerService = new QuestionAnswerService(questionAnswerRepository, eventEmitter, agentConfig.logger) + }) + + afterAll(async () => { + await wallet.delete() + }) + + describe('create question', () => { + it(`emits a question with question text, valid responses, and question answer record`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on( + QuestionAnswerEventTypes.QuestionAnswerStateChanged, + eventListenerMock + ) + + const questionMessage = new QuestionMessage({ + questionText: 'Alice, are you on the phone with Bob?', + signatureRequired: false, + validResponses: [{ text: 'Yes' }, { text: 'No' }], + }) + + await questionAnswerService.createQuestion(agentContext, mockConnectionRecord.id, { + question: questionMessage.questionText, + validResponses: questionMessage.validResponses, + }) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'QuestionAnswerStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: null, + questionAnswerRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + questionText: questionMessage.questionText, + role: QuestionAnswerRole.Questioner, + state: QuestionAnswerState.QuestionSent, + validResponses: questionMessage.validResponses, + }), + }, + }) + }) + }) + describe('create answer', () => { + let mockRecord: QuestionAnswerRecord + + beforeAll(() => { + mockRecord = mockQuestionAnswerRecord({ + questionText: 'Alice, are you on the phone with Bob?', + connectionId: mockConnectionRecord.id, + role: QuestionAnswerRole.Responder, + signatureRequired: false, + state: QuestionAnswerState.QuestionReceived, + threadId: '123', + validResponses: [{ text: 'Yes' }, { text: 'No' }], + }) + }) + + it(`throws an error when invalid response is provided`, async () => { + expect(questionAnswerService.createAnswer(agentContext, mockRecord, 'Maybe')).rejects.toThrowError( + `Response does not match valid responses` + ) + }) + + it(`emits an answer with a valid response and question answer record`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on( + QuestionAnswerEventTypes.QuestionAnswerStateChanged, + eventListenerMock + ) + + mockFunction(questionAnswerRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + await questionAnswerService.createAnswer(agentContext, mockRecord, 'Yes') + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'QuestionAnswerStateChanged', + metadata: { + contextCorrelationId: 'mock', + }, + payload: { + previousState: QuestionAnswerState.QuestionReceived, + questionAnswerRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + role: QuestionAnswerRole.Responder, + state: QuestionAnswerState.AnswerSent, + response: 'Yes', + }), + }, + }) + }) + }) + + describe('processReceiveQuestion', () => { + let mockRecord: QuestionAnswerRecord + + beforeAll(() => { + mockRecord = mockQuestionAnswerRecord({ + questionText: 'Alice, are you on the phone with Bob?', + connectionId: mockConnectionRecord.id, + role: QuestionAnswerRole.Responder, + signatureRequired: false, + state: QuestionAnswerState.QuestionReceived, + threadId: '123', + validResponses: [{ text: 'Yes' }, { text: 'No' }], + }) + }) + + it('creates record when no previous question with that thread exists', async () => { + const questionMessage = new QuestionMessage({ + questionText: 'Alice, are you on the phone with Bob?', + validResponses: [{ text: 'Yes' }, { text: 'No' }], + }) + + const messageContext = new InboundMessageContext(questionMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + const questionAnswerRecord = await questionAnswerService.processReceiveQuestion(messageContext) + + expect(questionAnswerRecord).toMatchObject( + expect.objectContaining({ + role: QuestionAnswerRole.Responder, + state: QuestionAnswerState.QuestionReceived, + threadId: questionMessage.id, + questionText: 'Alice, are you on the phone with Bob?', + validResponses: [{ text: 'Yes' }, { text: 'No' }], + }) + ) + }) + + it(`throws an error when question from the same thread exists `, async () => { + mockFunction(questionAnswerRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + const questionMessage = new QuestionMessage({ + id: '123', + questionText: 'Alice, are you on the phone with Bob?', + validResponses: [{ text: 'Yes' }, { text: 'No' }], + }) + + const messageContext = new InboundMessageContext(questionMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + expect(questionAnswerService.processReceiveQuestion(messageContext)).rejects.toThrowError( + `Question answer record with thread Id ${questionMessage.id} already exists.` + ) + jest.resetAllMocks() + }) + }) + + describe('receiveAnswer', () => { + let mockRecord: QuestionAnswerRecord + + beforeAll(() => { + mockRecord = mockQuestionAnswerRecord({ + questionText: 'Alice, are you on the phone with Bob?', + connectionId: mockConnectionRecord.id, + role: QuestionAnswerRole.Questioner, + signatureRequired: false, + state: QuestionAnswerState.QuestionReceived, + threadId: '123', + validResponses: [{ text: 'Yes' }, { text: 'No' }], + }) + }) + + it('updates state and emits event when valid response is received', async () => { + mockRecord.state = QuestionAnswerState.QuestionSent + mockFunction(questionAnswerRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + const answerMessage = new AnswerMessage({ + response: 'Yes', + threadId: '123', + }) + + const messageContext = new InboundMessageContext(answerMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + const questionAnswerRecord = await questionAnswerService.receiveAnswer(messageContext) + + expect(questionAnswerRecord).toMatchObject( + expect.objectContaining({ + role: QuestionAnswerRole.Questioner, + state: QuestionAnswerState.AnswerReceived, + threadId: '123', + questionText: 'Alice, are you on the phone with Bob?', + validResponses: [{ text: 'Yes' }, { text: 'No' }], + }) + ) + jest.resetAllMocks() + }) + + it(`throws an error when no existing question is found`, async () => { + const answerMessage = new AnswerMessage({ + response: 'Yes', + threadId: '123', + }) + + const messageContext = new InboundMessageContext(answerMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + expect(questionAnswerService.receiveAnswer(messageContext)).rejects.toThrowError( + `Question Answer record with thread Id ${answerMessage.threadId} not found.` + ) + }) + + it(`throws an error when record is in invalid state`, async () => { + mockRecord.state = QuestionAnswerState.AnswerReceived + mockFunction(questionAnswerRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + const answerMessage = new AnswerMessage({ + response: 'Yes', + threadId: '123', + }) + + const messageContext = new InboundMessageContext(answerMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + expect(questionAnswerService.receiveAnswer(messageContext)).rejects.toThrowError( + `Question answer record is in invalid state ${mockRecord.state}. Valid states are: ${QuestionAnswerState.QuestionSent}` + ) + jest.resetAllMocks() + }) + + it(`throws an error when record is in invalid role`, async () => { + mockRecord.state = QuestionAnswerState.QuestionSent + mockRecord.role = QuestionAnswerRole.Responder + mockFunction(questionAnswerRepository.findSingleByQuery).mockReturnValue(Promise.resolve(mockRecord)) + + const answerMessage = new AnswerMessage({ + response: 'Yes', + threadId: '123', + }) + + const messageContext = new InboundMessageContext(answerMessage, { + agentContext, + connection: mockConnectionRecord, + }) + + expect(questionAnswerService.receiveAnswer(messageContext)).rejects.toThrowError( + `Invalid question answer record role ${mockRecord.role}, expected is ${QuestionAnswerRole.Questioner}` + ) + }) + jest.resetAllMocks() + }) +}) diff --git a/packages/question-answer/src/handlers/AnswerMessageHandler.ts b/packages/question-answer/src/handlers/AnswerMessageHandler.ts new file mode 100644 index 0000000000..ab9be50dd8 --- /dev/null +++ b/packages/question-answer/src/handlers/AnswerMessageHandler.ts @@ -0,0 +1,17 @@ +import type { QuestionAnswerService } from '../services' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { AnswerMessage } from '../messages' + +export class AnswerMessageHandler implements MessageHandler { + private questionAnswerService: QuestionAnswerService + public supportedMessages = [AnswerMessage] + + public constructor(questionAnswerService: QuestionAnswerService) { + this.questionAnswerService = questionAnswerService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.questionAnswerService.receiveAnswer(messageContext) + } +} diff --git a/packages/question-answer/src/handlers/QuestionMessageHandler.ts b/packages/question-answer/src/handlers/QuestionMessageHandler.ts new file mode 100644 index 0000000000..12af8e3f27 --- /dev/null +++ b/packages/question-answer/src/handlers/QuestionMessageHandler.ts @@ -0,0 +1,17 @@ +import type { QuestionAnswerService } from '../services' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { QuestionMessage } from '../messages' + +export class QuestionMessageHandler implements MessageHandler { + private questionAnswerService: QuestionAnswerService + public supportedMessages = [QuestionMessage] + + public constructor(questionAnswerService: QuestionAnswerService) { + this.questionAnswerService = questionAnswerService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.questionAnswerService.processReceiveQuestion(messageContext) + } +} diff --git a/packages/question-answer/src/handlers/index.ts b/packages/question-answer/src/handlers/index.ts new file mode 100644 index 0000000000..c89dcd4455 --- /dev/null +++ b/packages/question-answer/src/handlers/index.ts @@ -0,0 +1,2 @@ +export * from './QuestionMessageHandler' +export * from './AnswerMessageHandler' diff --git a/packages/question-answer/src/index.ts b/packages/question-answer/src/index.ts new file mode 100644 index 0000000000..e4b16be20f --- /dev/null +++ b/packages/question-answer/src/index.ts @@ -0,0 +1,8 @@ +export * from './messages' +export * from './models' +export * from './services' +export * from './repository' +export * from './QuestionAnswerEvents' +export * from './QuestionAnswerApi' +export * from './QuestionAnswerRole' +export * from './QuestionAnswerModule' diff --git a/packages/question-answer/src/messages/AnswerMessage.ts b/packages/question-answer/src/messages/AnswerMessage.ts new file mode 100644 index 0000000000..5ae9c157c9 --- /dev/null +++ b/packages/question-answer/src/messages/AnswerMessage.ts @@ -0,0 +1,27 @@ +import { AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' +import { Expose } from 'class-transformer' +import { IsString } from 'class-validator' + +export class AnswerMessage extends AgentMessage { + /** + * Create new AnswerMessage instance. + * @param options + */ + public constructor(options: { id?: string; response: string; threadId: string }) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.setThread({ threadId: options.threadId }) + this.response = options.response + } + } + + @IsValidMessageType(AnswerMessage.type) + public readonly type = AnswerMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/questionanswer/1.0/answer') + + @Expose({ name: 'response' }) + @IsString() + public response!: string +} diff --git a/packages/question-answer/src/messages/QuestionMessage.ts b/packages/question-answer/src/messages/QuestionMessage.ts new file mode 100644 index 0000000000..74897f02c3 --- /dev/null +++ b/packages/question-answer/src/messages/QuestionMessage.ts @@ -0,0 +1,59 @@ +import { AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' +import { Expose, Type } from 'class-transformer' +import { IsBoolean, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { ValidResponse } from '../models' + +export class QuestionMessage extends AgentMessage { + /** + * Create new QuestionMessage instance. + * @param options + */ + public constructor(options: { + questionText: string + questionDetail?: string + validResponses: ValidResponse[] + signatureRequired?: boolean + id?: string + nonce?: string + }) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.nonce = options.nonce + this.questionText = options.questionText + this.questionDetail = options.questionDetail + this.signatureRequired = options.signatureRequired + this.validResponses = options.validResponses + } + } + + @IsValidMessageType(QuestionMessage.type) + public readonly type = QuestionMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/questionanswer/1.0/question') + + @IsOptional() + @IsString() + public nonce?: string + + @IsOptional() + @IsBoolean() + @Expose({ name: 'signature_required' }) + public signatureRequired?: boolean + + @Expose({ name: 'valid_responses' }) + @Type(() => ValidResponse) + @ValidateNested({ each: true }) + @IsInstance(ValidResponse, { each: true }) + public validResponses!: ValidResponse[] + + @Expose({ name: 'question_text' }) + @IsString() + public questionText!: string + + @IsOptional() + @Expose({ name: 'question_detail' }) + @IsString() + public questionDetail?: string +} diff --git a/packages/question-answer/src/messages/index.ts b/packages/question-answer/src/messages/index.ts new file mode 100644 index 0000000000..ba0ce549ad --- /dev/null +++ b/packages/question-answer/src/messages/index.ts @@ -0,0 +1,2 @@ +export * from './QuestionMessage' +export * from './AnswerMessage' diff --git a/packages/question-answer/src/models/QuestionAnswerState.ts b/packages/question-answer/src/models/QuestionAnswerState.ts new file mode 100644 index 0000000000..572ec2045b --- /dev/null +++ b/packages/question-answer/src/models/QuestionAnswerState.ts @@ -0,0 +1,11 @@ +/** + * QuestionAnswer states inferred from RFC 0113. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0113-question-answer/README.md + */ +export enum QuestionAnswerState { + QuestionSent = 'question-sent', + QuestionReceived = 'question-received', + AnswerReceived = 'answer-received', + AnswerSent = 'answer-sent', +} diff --git a/packages/question-answer/src/models/ValidResponse.ts b/packages/question-answer/src/models/ValidResponse.ts new file mode 100644 index 0000000000..d3db926afa --- /dev/null +++ b/packages/question-answer/src/models/ValidResponse.ts @@ -0,0 +1,9 @@ +export class ValidResponse { + public constructor(options: ValidResponse) { + if (options) { + this.text = options.text + } + } + + public text!: string +} diff --git a/packages/question-answer/src/models/index.ts b/packages/question-answer/src/models/index.ts new file mode 100644 index 0000000000..58adb2f788 --- /dev/null +++ b/packages/question-answer/src/models/index.ts @@ -0,0 +1,2 @@ +export * from './QuestionAnswerState' +export * from './ValidResponse' diff --git a/packages/question-answer/src/repository/QuestionAnswerRecord.ts b/packages/question-answer/src/repository/QuestionAnswerRecord.ts new file mode 100644 index 0000000000..868eed97d0 --- /dev/null +++ b/packages/question-answer/src/repository/QuestionAnswerRecord.ts @@ -0,0 +1,94 @@ +import type { QuestionAnswerRole } from '../QuestionAnswerRole' +import type { QuestionAnswerState, ValidResponse } from '../models' +import type { RecordTags, TagsBase } from '@credo-ts/core' + +import { CredoError, utils, BaseRecord } from '@credo-ts/core' + +export type CustomQuestionAnswerTags = TagsBase +export type DefaultQuestionAnswerTags = { + connectionId: string + role: QuestionAnswerRole + state: QuestionAnswerState + threadId: string +} + +export type QuestionAnswerTags = RecordTags + +export interface QuestionAnswerStorageProps { + id?: string + createdAt?: Date + connectionId: string + role: QuestionAnswerRole + signatureRequired: boolean + state: QuestionAnswerState + tags?: CustomQuestionAnswerTags + threadId: string + + questionText: string + questionDetail?: string + validResponses: ValidResponse[] + + response?: string +} + +export class QuestionAnswerRecord extends BaseRecord { + public questionText!: string + public questionDetail?: string + public validResponses!: ValidResponse[] + public connectionId!: string + public role!: QuestionAnswerRole + public signatureRequired!: boolean + public state!: QuestionAnswerState + public threadId!: string + public response?: string + + public static readonly type = 'QuestionAnswerRecord' + public readonly type = QuestionAnswerRecord.type + + public constructor(props: QuestionAnswerStorageProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this.questionText = props.questionText + this.questionDetail = props.questionDetail + this.validResponses = props.validResponses + this.connectionId = props.connectionId + this._tags = props.tags ?? {} + this.role = props.role + this.signatureRequired = props.signatureRequired + this.state = props.state + this.threadId = props.threadId + this.response = props.response + } + } + + public getTags() { + return { + ...this._tags, + connectionId: this.connectionId, + role: this.role, + state: this.state, + threadId: this.threadId, + } + } + + public assertRole(expectedRole: QuestionAnswerRole) { + if (this.role !== expectedRole) { + throw new CredoError(`Invalid question answer record role ${this.role}, expected is ${expectedRole}.`) + } + } + + public assertState(expectedStates: QuestionAnswerState | QuestionAnswerState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new CredoError( + `Question answer record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.` + ) + } + } +} diff --git a/packages/question-answer/src/repository/QuestionAnswerRepository.ts b/packages/question-answer/src/repository/QuestionAnswerRepository.ts new file mode 100644 index 0000000000..d6a529f09e --- /dev/null +++ b/packages/question-answer/src/repository/QuestionAnswerRepository.ts @@ -0,0 +1,13 @@ +import { EventEmitter, inject, injectable, InjectionSymbols, Repository, StorageService } from '@credo-ts/core' + +import { QuestionAnswerRecord } from './QuestionAnswerRecord' + +@injectable() +export class QuestionAnswerRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(QuestionAnswerRecord, storageService, eventEmitter) + } +} diff --git a/packages/question-answer/src/repository/index.ts b/packages/question-answer/src/repository/index.ts new file mode 100644 index 0000000000..dbafad3c45 --- /dev/null +++ b/packages/question-answer/src/repository/index.ts @@ -0,0 +1,2 @@ +export * from './QuestionAnswerRecord' +export * from './QuestionAnswerRepository' diff --git a/packages/question-answer/src/services/QuestionAnswerService.ts b/packages/question-answer/src/services/QuestionAnswerService.ts new file mode 100644 index 0000000000..bde0575409 --- /dev/null +++ b/packages/question-answer/src/services/QuestionAnswerService.ts @@ -0,0 +1,298 @@ +import type { QuestionAnswerStateChangedEvent } from '../QuestionAnswerEvents' +import type { ValidResponse } from '../models' +import type { AgentContext, InboundMessageContext, Query, QueryOptions } from '@credo-ts/core' + +import { CredoError, EventEmitter, inject, injectable, InjectionSymbols, Logger } from '@credo-ts/core' + +import { QuestionAnswerEventTypes } from '../QuestionAnswerEvents' +import { QuestionAnswerRole } from '../QuestionAnswerRole' +import { AnswerMessage, QuestionMessage } from '../messages' +import { QuestionAnswerState } from '../models' +import { QuestionAnswerRepository, QuestionAnswerRecord } from '../repository' + +@injectable() +export class QuestionAnswerService { + private questionAnswerRepository: QuestionAnswerRepository + private eventEmitter: EventEmitter + private logger: Logger + + public constructor( + questionAnswerRepository: QuestionAnswerRepository, + eventEmitter: EventEmitter, + @inject(InjectionSymbols.Logger) logger: Logger + ) { + this.questionAnswerRepository = questionAnswerRepository + this.eventEmitter = eventEmitter + this.logger = logger + } + /** + * Create a question message and a new QuestionAnswer record for the questioner role + * + * @param question text for question message + * @param details optional details for question message + * @param connectionId connection for QuestionAnswer record + * @param validResponses array of valid responses for question + * @returns question message and QuestionAnswer record + */ + public async createQuestion( + agentContext: AgentContext, + connectionId: string, + config: { + question: string + validResponses: ValidResponse[] + detail?: string + } + ) { + const questionMessage = new QuestionMessage({ + questionText: config.question, + questionDetail: config?.detail, + signatureRequired: false, + validResponses: config.validResponses, + }) + + const questionAnswerRecord = await this.createRecord({ + questionText: questionMessage.questionText, + questionDetail: questionMessage.questionDetail, + threadId: questionMessage.threadId, + connectionId: connectionId, + role: QuestionAnswerRole.Questioner, + signatureRequired: false, + state: QuestionAnswerState.QuestionSent, + validResponses: questionMessage.validResponses, + }) + + await this.questionAnswerRepository.save(agentContext, questionAnswerRecord) + + this.eventEmitter.emit(agentContext, { + type: QuestionAnswerEventTypes.QuestionAnswerStateChanged, + payload: { previousState: null, questionAnswerRecord }, + }) + + return { questionMessage, questionAnswerRecord } + } + + /** + * receive question message and create record for responder role + * + * @param messageContext the message context containing a question message + * @returns QuestionAnswer record + */ + public async processReceiveQuestion( + messageContext: InboundMessageContext + ): Promise { + const { message: questionMessage } = messageContext + + this.logger.debug(`Receiving question message with id ${questionMessage.id}`) + + const connection = messageContext.assertReadyConnection() + const questionRecord = await this.findByThreadAndConnectionId( + messageContext.agentContext, + connection.id, + questionMessage.id + ) + if (questionRecord) { + throw new CredoError(`Question answer record with thread Id ${questionMessage.id} already exists.`) + } + const questionAnswerRecord = await this.createRecord({ + questionText: questionMessage.questionText, + questionDetail: questionMessage.questionDetail, + connectionId: connection?.id, + threadId: questionMessage.threadId, + role: QuestionAnswerRole.Responder, + signatureRequired: false, + state: QuestionAnswerState.QuestionReceived, + validResponses: questionMessage.validResponses, + }) + + await this.questionAnswerRepository.save(messageContext.agentContext, questionAnswerRecord) + + this.eventEmitter.emit(messageContext.agentContext, { + type: QuestionAnswerEventTypes.QuestionAnswerStateChanged, + payload: { previousState: null, questionAnswerRecord }, + }) + + return questionAnswerRecord + } + + /** + * create answer message, check that response is valid + * + * @param questionAnswerRecord record containing question and valid responses + * @param response response used in answer message + * @returns answer message and QuestionAnswer record + */ + public async createAnswer(agentContext: AgentContext, questionAnswerRecord: QuestionAnswerRecord, response: string) { + const answerMessage = new AnswerMessage({ response: response, threadId: questionAnswerRecord.threadId }) + + questionAnswerRecord.assertState(QuestionAnswerState.QuestionReceived) + + questionAnswerRecord.response = response + + if (questionAnswerRecord.validResponses.some((e) => e.text === response)) { + await this.updateState(agentContext, questionAnswerRecord, QuestionAnswerState.AnswerSent) + } else { + throw new CredoError(`Response does not match valid responses`) + } + return { answerMessage, questionAnswerRecord } + } + + /** + * receive answer as questioner + * + * @param messageContext the message context containing an answer message message + * @returns QuestionAnswer record + */ + public async receiveAnswer(messageContext: InboundMessageContext): Promise { + const { message: answerMessage } = messageContext + + this.logger.debug(`Receiving answer message with id ${answerMessage.id}`) + + const connection = messageContext.assertReadyConnection() + const questionAnswerRecord = await this.findByThreadAndConnectionId( + messageContext.agentContext, + connection.id, + answerMessage.threadId + ) + if (!questionAnswerRecord) { + throw new CredoError(`Question Answer record with thread Id ${answerMessage.threadId} not found.`) + } + questionAnswerRecord.assertState(QuestionAnswerState.QuestionSent) + questionAnswerRecord.assertRole(QuestionAnswerRole.Questioner) + + questionAnswerRecord.response = answerMessage.response + + await this.updateState(messageContext.agentContext, questionAnswerRecord, QuestionAnswerState.AnswerReceived) + + return questionAnswerRecord + } + + /** + * Update the record to a new state and emit an state changed event. Also updates the record + * in storage. + * + * @param questionAnswerRecord The question answer record to update the state for + * @param newState The state to update to + * + */ + private async updateState( + agentContext: AgentContext, + questionAnswerRecord: QuestionAnswerRecord, + newState: QuestionAnswerState + ) { + const previousState = questionAnswerRecord.state + questionAnswerRecord.state = newState + await this.questionAnswerRepository.update(agentContext, questionAnswerRecord) + + this.eventEmitter.emit(agentContext, { + type: QuestionAnswerEventTypes.QuestionAnswerStateChanged, + payload: { + previousState, + questionAnswerRecord: questionAnswerRecord, + }, + }) + } + + private async createRecord(options: { + questionText: string + questionDetail?: string + connectionId: string + role: QuestionAnswerRole + signatureRequired: boolean + state: QuestionAnswerState + threadId: string + validResponses: ValidResponse[] + }): Promise { + const questionMessageRecord = new QuestionAnswerRecord({ + questionText: options.questionText, + questionDetail: options.questionDetail, + connectionId: options.connectionId, + threadId: options.threadId, + role: options.role, + signatureRequired: options.signatureRequired, + state: options.state, + validResponses: options.validResponses, + }) + + return questionMessageRecord + } + + /** + * Retrieve a question answer record by connection id and thread id + * + * @param connectionId The connection id + * @param threadId The thread id + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * @returns The question answer record + */ + public getByThreadAndConnectionId( + agentContext: AgentContext, + connectionId: string, + threadId: string + ): Promise { + return this.questionAnswerRepository.getSingleByQuery(agentContext, { + connectionId, + threadId, + }) + } + + /** + * Retrieve a question answer record by thread id + * + * @param connectionId The connection id + * @param threadId The thread id + * @returns The question answer record or null if not found + */ + public findByThreadAndConnectionId( + agentContext: AgentContext, + connectionId: string, + threadId: string + ): Promise { + return this.questionAnswerRepository.findSingleByQuery(agentContext, { + connectionId, + threadId, + }) + } + + /** + * Retrieve a question answer record by id + * + * @param questionAnswerId The questionAnswer record id + * @throws {RecordNotFoundError} If no record is found + * @return The question answer record + * + */ + public getById(agentContext: AgentContext, questionAnswerId: string): Promise { + return this.questionAnswerRepository.getById(agentContext, questionAnswerId) + } + + /** + * Retrieve a question answer record by id + * + * @param questionAnswerId The questionAnswer record id + * @return The question answer record or null if not found + * + */ + public findById(agentContext: AgentContext, questionAnswerId: string): Promise { + return this.questionAnswerRepository.findById(agentContext, questionAnswerId) + } + + /** + * Retrieve a question answer record by id + * + * @param questionAnswerId The questionAnswer record id + * @return The question answer record or null if not found + * + */ + public getAll(agentContext: AgentContext) { + return this.questionAnswerRepository.getAll(agentContext) + } + + public async findAllByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ) { + return this.questionAnswerRepository.findByQuery(agentContext, query, queryOptions) + } +} diff --git a/packages/question-answer/src/services/index.ts b/packages/question-answer/src/services/index.ts new file mode 100644 index 0000000000..e7db249b2d --- /dev/null +++ b/packages/question-answer/src/services/index.ts @@ -0,0 +1 @@ +export * from './QuestionAnswerService' diff --git a/packages/question-answer/tests/helpers.ts b/packages/question-answer/tests/helpers.ts new file mode 100644 index 0000000000..02bdeb4cf6 --- /dev/null +++ b/packages/question-answer/tests/helpers.ts @@ -0,0 +1,66 @@ +import type { Agent } from '@credo-ts/core' +import type { + QuestionAnswerRole, + QuestionAnswerState, + QuestionAnswerStateChangedEvent, +} from '@credo-ts/question-answer' +import type { Observable } from 'rxjs' + +import { catchError, filter, firstValueFrom, map, ReplaySubject, timeout } from 'rxjs' + +import { QuestionAnswerEventTypes } from '@credo-ts/question-answer' + +export async function waitForQuestionAnswerRecord( + agent: Agent, + options: { + threadId?: string + role?: QuestionAnswerRole + state?: QuestionAnswerState + previousState?: QuestionAnswerState | null + timeoutMs?: number + } +) { + const observable = agent.events.observable( + QuestionAnswerEventTypes.QuestionAnswerStateChanged + ) + + return waitForQuestionAnswerRecordSubject(observable, options) +} + +export function waitForQuestionAnswerRecordSubject( + subject: ReplaySubject | Observable, + { + threadId, + role, + state, + previousState, + timeoutMs = 10000, + }: { + threadId?: string + role?: QuestionAnswerRole + state?: QuestionAnswerState + previousState?: QuestionAnswerState | null + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( + observable.pipe( + filter((e) => previousState === undefined || e.payload.previousState === previousState), + filter((e) => threadId === undefined || e.payload.questionAnswerRecord.threadId === threadId), + filter((e) => role === undefined || e.payload.questionAnswerRecord.role === role), + filter((e) => state === undefined || e.payload.questionAnswerRecord.state === state), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `QuestionAnswerChangedEvent event not emitted within specified timeout: { + previousState: ${previousState}, + threadId: ${threadId}, + state: ${state} + }` + ) + }), + map((e) => e.payload.questionAnswerRecord) + ) + ) +} diff --git a/packages/question-answer/tests/question-answer.test.ts b/packages/question-answer/tests/question-answer.test.ts new file mode 100644 index 0000000000..dafbc55cdd --- /dev/null +++ b/packages/question-answer/tests/question-answer.test.ts @@ -0,0 +1,89 @@ +import type { ConnectionRecord } from '@credo-ts/core' + +import { Agent } from '@credo-ts/core' + +import { setupSubjectTransports, testLogger, makeConnection, getInMemoryAgentOptions } from '../../core/tests' + +import { waitForQuestionAnswerRecord } from './helpers' + +import { QuestionAnswerModule, QuestionAnswerRole, QuestionAnswerState } from '@credo-ts/question-answer' + +const modules = { + questionAnswer: new QuestionAnswerModule(), +} + +const bobAgentOptions = getInMemoryAgentOptions( + 'Bob Question Answer', + { + endpoints: ['rxjs:bob'], + }, + modules +) + +const aliceAgentOptions = getInMemoryAgentOptions( + 'Alice Question Answer', + { + endpoints: ['rxjs:alice'], + }, + modules +) + +describe('Question Answer', () => { + let bobAgent: Agent + let aliceAgent: Agent + let aliceConnection: ConnectionRecord + + beforeEach(async () => { + bobAgent = new Agent(bobAgentOptions) + aliceAgent = new Agent(aliceAgentOptions) + setupSubjectTransports([bobAgent, aliceAgent]) + + await bobAgent.initialize() + await aliceAgent.initialize() + ;[aliceConnection] = await makeConnection(aliceAgent, bobAgent) + }) + + afterEach(async () => { + await bobAgent.shutdown() + await bobAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice sends a question and Bob answers', async () => { + testLogger.test('Alice sends question to Bob') + let aliceQuestionAnswerRecord = await aliceAgent.modules.questionAnswer.sendQuestion(aliceConnection.id, { + question: 'Do you want to play?', + validResponses: [{ text: 'Yes' }, { text: 'No' }], + }) + + testLogger.test('Bob waits for question from Alice') + const bobQuestionAnswerRecord = await waitForQuestionAnswerRecord(bobAgent, { + threadId: aliceQuestionAnswerRecord.threadId, + state: QuestionAnswerState.QuestionReceived, + }) + + expect(bobQuestionAnswerRecord.questionText).toEqual('Do you want to play?') + expect(bobQuestionAnswerRecord.validResponses).toEqual([{ text: 'Yes' }, { text: 'No' }]) + testLogger.test('Bob sends answer to Alice') + await bobAgent.modules.questionAnswer.sendAnswer(bobQuestionAnswerRecord.id, 'Yes') + + testLogger.test('Alice waits until Bob answers') + aliceQuestionAnswerRecord = await waitForQuestionAnswerRecord(aliceAgent, { + threadId: aliceQuestionAnswerRecord.threadId, + state: QuestionAnswerState.AnswerReceived, + }) + + expect(aliceQuestionAnswerRecord.response).toEqual('Yes') + + const retrievedRecord = await aliceAgent.modules.questionAnswer.findById(aliceQuestionAnswerRecord.id) + expect(retrievedRecord).toMatchObject( + expect.objectContaining({ + id: aliceQuestionAnswerRecord.id, + threadId: aliceQuestionAnswerRecord.threadId, + state: QuestionAnswerState.AnswerReceived, + role: QuestionAnswerRole.Questioner, + }) + ) + }) +}) diff --git a/packages/question-answer/tests/setup.ts b/packages/question-answer/tests/setup.ts new file mode 100644 index 0000000000..78143033f2 --- /dev/null +++ b/packages/question-answer/tests/setup.ts @@ -0,0 +1,3 @@ +import 'reflect-metadata' + +jest.setTimeout(120000) diff --git a/packages/question-answer/tsconfig.build.json b/packages/question-answer/tsconfig.build.json new file mode 100644 index 0000000000..2b75d0adab --- /dev/null +++ b/packages/question-answer/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build" + }, + "include": ["src/**/*"] +} diff --git a/packages/question-answer/tsconfig.json b/packages/question-answer/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/question-answer/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/packages/react-native/CHANGELOG.md b/packages/react-native/CHANGELOG.md new file mode 100644 index 0000000000..b4e3c2db28 --- /dev/null +++ b/packages/react-native/CHANGELOG.md @@ -0,0 +1,162 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release +- Updated dependencies [66e696d] + - @credo-ts/core@0.5.6 + +## 0.5.5 + +### Patch Changes + +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) +- Updated dependencies [3239ef3] +- Updated dependencies [d548fa4] +- Updated dependencies [482a630] + - @credo-ts/core@0.5.5 + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +**Note:** Version bump only for package @credo-ts/react-native + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +**Note:** Version bump only for package @credo-ts/react-native + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +**Note:** Version bump only for package @credo-ts/react-native + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +### Bug Fixes + +- **rn:** more flexible react native version ([#1760](https://github.com/openwallet-foundation/credo-ts/issues/1760)) ([af82918](https://github.com/openwallet-foundation/credo-ts/commit/af82918f5401bad113dfc32fc903d981e4389c4e)) + +## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) + +**Note:** Version bump only for package @credo-ts/react-native + +## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) + +### Bug Fixes + +- check if URL already encoded ([#1485](https://github.com/hyperledger/aries-framework-javascript/issues/1485)) ([38a0578](https://github.com/hyperledger/aries-framework-javascript/commit/38a0578011896cfcf217713d34f285cd381ad72c)) +- encode tails url ([#1479](https://github.com/hyperledger/aries-framework-javascript/issues/1479)) ([fd190b9](https://github.com/hyperledger/aries-framework-javascript/commit/fd190b96106ca4916539d96ff6c4ecef7833f148)) + +# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) + +- feat!: add data, cache and temp dirs to FileSystem (#1306) ([ff5596d](https://github.com/hyperledger/aries-framework-javascript/commit/ff5596d0631e93746494c017797d0191b6bdb0b1)), closes [#1306](https://github.com/hyperledger/aries-framework-javascript/issues/1306) + +### Features + +- add fetch indy schema method ([#1290](https://github.com/hyperledger/aries-framework-javascript/issues/1290)) ([1d782f5](https://github.com/hyperledger/aries-framework-javascript/commit/1d782f54bbb4abfeb6b6db6cd4f7164501b6c3d9)) +- add initial askar package ([#1211](https://github.com/hyperledger/aries-framework-javascript/issues/1211)) ([f18d189](https://github.com/hyperledger/aries-framework-javascript/commit/f18d1890546f7d66571fe80f2f3fc1fead1cd4c3)) +- **anoncreds:** legacy indy proof format service ([#1283](https://github.com/hyperledger/aries-framework-javascript/issues/1283)) ([c72fd74](https://github.com/hyperledger/aries-framework-javascript/commit/c72fd7416f2c1bc0497a84036e16adfa80585e49)) +- **askar:** import/export wallet support for SQLite ([#1377](https://github.com/hyperledger/aries-framework-javascript/issues/1377)) ([19cefa5](https://github.com/hyperledger/aries-framework-javascript/commit/19cefa54596a4e4848bdbe89306a884a5ce2e991)) + +### BREAKING CHANGES + +- Agent-produced files will now be divided in different system paths depending on their nature: data, temp and cache. Previously, they were located at a single location, defaulting to a temporary directory. + +If you specified a custom path in `FileSystem` object constructor, you now must provide an object containing `baseDataPath`, `baseTempPath` and `baseCachePath`. They can point to the same path, although it's recommended to specify different path to avoid future file clashes. + +## [0.3.3](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.2...v0.3.3) (2023-01-18) + +### Bug Fixes + +- fix typing issues with typescript 4.9 ([#1214](https://github.com/hyperledger/aries-framework-javascript/issues/1214)) ([087980f](https://github.com/hyperledger/aries-framework-javascript/commit/087980f1adf3ee0bc434ca9782243a62c6124444)) + +### Features + +- **indy-sdk:** add indy-sdk package ([#1200](https://github.com/hyperledger/aries-framework-javascript/issues/1200)) ([9933b35](https://github.com/hyperledger/aries-framework-javascript/commit/9933b35a6aa4524caef8a885e71b742cd0d7186b)) + +## [0.3.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.1...v0.3.2) (2023-01-04) + +**Note:** Version bump only for package @credo-ts/react-native + +## [0.3.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.0...v0.3.1) (2022-12-27) + +**Note:** Version bump only for package @credo-ts/react-native + +# [0.3.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.5...v0.3.0) (2022-12-22) + +### Bug Fixes + +- peer dependency for rn bbs signatures ([#785](https://github.com/hyperledger/aries-framework-javascript/issues/785)) ([c751e28](https://github.com/hyperledger/aries-framework-javascript/commit/c751e286aa11a1d2b9424ae23de5647efc5d536f)) +- **react-native:** move bbs dep to bbs package ([#1076](https://github.com/hyperledger/aries-framework-javascript/issues/1076)) ([c6762bb](https://github.com/hyperledger/aries-framework-javascript/commit/c6762bbe9d64ac5220915af3425d493e505dcc2c)) + +### Features + +- bbs createKey, sign and verify ([#684](https://github.com/hyperledger/aries-framework-javascript/issues/684)) ([5f91738](https://github.com/hyperledger/aries-framework-javascript/commit/5f91738337fac1efbbb4597e7724791e542f0762)) +- **dids:** add did registrar ([#953](https://github.com/hyperledger/aries-framework-javascript/issues/953)) ([93f3c93](https://github.com/hyperledger/aries-framework-javascript/commit/93f3c93310f9dae032daa04a920b7df18e2f8a65)) +- jsonld-credential support ([#718](https://github.com/hyperledger/aries-framework-javascript/issues/718)) ([ea34c47](https://github.com/hyperledger/aries-framework-javascript/commit/ea34c4752712efecf3367c5a5fc4b06e66c1e9d7)) + +## [0.2.5](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.4...v0.2.5) (2022-10-13) + +**Note:** Version bump only for package @credo-ts/react-native + +## [0.2.4](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.3...v0.2.4) (2022-09-10) + +**Note:** Version bump only for package @credo-ts/react-native + +## [0.2.3](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.2...v0.2.3) (2022-08-30) + +**Note:** Version bump only for package @credo-ts/react-native + +## [0.2.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.1...v0.2.2) (2022-07-15) + +**Note:** Version bump only for package @credo-ts/react-native + +## [0.2.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.0...v0.2.1) (2022-07-08) + +**Note:** Version bump only for package @credo-ts/react-native + +# [0.2.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.1.0...v0.2.0) (2022-06-24) + +- chore!: update indy-sdk-react-native version to 0.2.0 (#754) ([4146778](https://github.com/hyperledger/aries-framework-javascript/commit/414677828be7f6c08fa02905d60d6555dc4dd438)), closes [#754](https://github.com/hyperledger/aries-framework-javascript/issues/754) + +### Features + +- add generic did resolver ([#554](https://github.com/hyperledger/aries-framework-javascript/issues/554)) ([8e03f35](https://github.com/hyperledger/aries-framework-javascript/commit/8e03f35f8e1cd02dac4df02d1f80f2c5a921dfef)) +- add update assistant for storage migrations ([#690](https://github.com/hyperledger/aries-framework-javascript/issues/690)) ([c9bff93](https://github.com/hyperledger/aries-framework-javascript/commit/c9bff93cfac43c4ae2cbcad1f96c1a74cde39602)) +- delete credential from wallet ([#691](https://github.com/hyperledger/aries-framework-javascript/issues/691)) ([abec3a2](https://github.com/hyperledger/aries-framework-javascript/commit/abec3a2c95815d1c54b22a6370222f024eefb060)) +- indy revocation (prover & verifier) ([#592](https://github.com/hyperledger/aries-framework-javascript/issues/592)) ([fb19ff5](https://github.com/hyperledger/aries-framework-javascript/commit/fb19ff555b7c10c9409450dcd7d385b1eddf41ac)) +- ledger connections happen on agent init in background ([#580](https://github.com/hyperledger/aries-framework-javascript/issues/580)) ([61695ce](https://github.com/hyperledger/aries-framework-javascript/commit/61695ce7737ffef363b60e341ae5b0e67e0e2c90)) +- support wallet key rotation ([#672](https://github.com/hyperledger/aries-framework-javascript/issues/672)) ([5cd1598](https://github.com/hyperledger/aries-framework-javascript/commit/5cd1598b496a832c82f35a363fabe8f408abd439)) + +### BREAKING CHANGES + +- indy-sdk-react-native has been updated to 0.2.0. The new version now depends on libindy version 1.16 and requires you to update the binaries in your react-native application. See the [indy-sdk-react-native](https://github.com/hyperledger/indy-sdk-react-native) repository for instructions on how to get the latest binaries for both iOS and Android. + +# 0.1.0 (2021-12-23) + +### Bug Fixes + +- **core:** using query-string to parse URLs ([#457](https://github.com/hyperledger/aries-framework-javascript/issues/457)) ([78e5057](https://github.com/hyperledger/aries-framework-javascript/commit/78e505750557f296cc72ef19c0edd8db8e1eaa7d)) +- monorepo release issues ([#386](https://github.com/hyperledger/aries-framework-javascript/issues/386)) ([89a628f](https://github.com/hyperledger/aries-framework-javascript/commit/89a628f7c3ea9e5730d2ba5720819ac6283ee404)) + +### Features + +- **core:** d_m invitation parameter and invitation image ([#456](https://github.com/hyperledger/aries-framework-javascript/issues/456)) ([f92c322](https://github.com/hyperledger/aries-framework-javascript/commit/f92c322b97be4a4867a82c3a35159d6068951f0b)) +- **core:** ledger module registerPublicDid implementation ([#398](https://github.com/hyperledger/aries-framework-javascript/issues/398)) ([5f2d512](https://github.com/hyperledger/aries-framework-javascript/commit/5f2d5126baed2ff58268c38755c2dbe75a654947)) diff --git a/packages/react-native/README.md b/packages/react-native/README.md new file mode 100644 index 0000000000..1ea8e50cae --- /dev/null +++ b/packages/react-native/README.md @@ -0,0 +1,31 @@ +

+
+ Credo Logo +

+

Credo - React Native

+

+ License + typescript + @credo-ts/react-native version + +

+
+ +Credo React Native provides platform specific dependencies to run Credo in [React Native](https://reactnative.dev). See the [Getting Started Guide](https://github.com/openwallet-foundation/credo-ts#getting-started) for installation instructions. diff --git a/packages/react-native/jest.config.ts b/packages/react-native/jest.config.ts new file mode 100644 index 0000000000..2556d19c61 --- /dev/null +++ b/packages/react-native/jest.config.ts @@ -0,0 +1,12 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, +} + +export default config diff --git a/packages/react-native/package.json b/packages/react-native/package.json new file mode 100644 index 0000000000..da0686327a --- /dev/null +++ b/packages/react-native/package.json @@ -0,0 +1,45 @@ +{ + "name": "@credo-ts/react-native", + "main": "src/index", + "types": "src/index", + "version": "0.5.6", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/react-native", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/react-native" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build", + "test": "jest" + }, + "dependencies": { + "@azure/core-asynciterator-polyfill": "^1.0.2", + "@credo-ts/core": "workspace:*", + "events": "^3.3.0" + }, + "devDependencies": { + "react-native": "^0.71.4", + "react-native-fs": "^2.20.0", + "react-native-get-random-values": "^1.8.0", + "rimraf": "^4.4.0", + "typescript": "~5.5.2" + }, + "peerDependencies": { + "react-native": ">=0.71.4", + "react-native-fs": "^2.20.0", + "react-native-get-random-values": "^1.8.0" + } +} diff --git a/packages/react-native/src/ReactNativeFileSystem.ts b/packages/react-native/src/ReactNativeFileSystem.ts new file mode 100644 index 0000000000..6711400698 --- /dev/null +++ b/packages/react-native/src/ReactNativeFileSystem.ts @@ -0,0 +1,100 @@ +import type { FileSystem, DownloadToFileOptions } from '@credo-ts/core' + +import { TypedArrayEncoder, CredoError, getDirFromFilePath, Buffer } from '@credo-ts/core' +import { Platform } from 'react-native' +import * as RNFS from 'react-native-fs' + +export class ReactNativeFileSystem implements FileSystem { + public readonly dataPath + public readonly cachePath + public readonly tempPath + + /** + * Create new ReactNativeFileSystem class instance. + * + * @param baseDataPath The base path to use for reading and writing data files used within the framework. + * Files will be created under baseDataPath/.afj directory. If not specified, it will be set to + * RNFS.DocumentDirectoryPath + * @param baseCachePath The base path to use for reading and writing cache files used within the framework. + * Files will be created under baseCachePath/.afj directory. If not specified, it will be set to + * RNFS.CachesDirectoryPath + * @param baseTempPath The base path to use for reading and writing temporary files within the framework. + * Files will be created under baseTempPath/.afj directory. If not specified, it will be set to + * RNFS.TemporaryDirectoryPath + * + * @see https://github.com/itinance/react-native-fs#constants + */ + public constructor(options?: { baseDataPath?: string; baseCachePath?: string; baseTempPath?: string }) { + this.dataPath = `${options?.baseDataPath ?? RNFS.DocumentDirectoryPath}/.afj` + // In Android, TemporaryDirectoryPath falls back to CachesDirectoryPath + this.cachePath = options?.baseCachePath + ? `${options?.baseCachePath}/.afj` + : `${RNFS.CachesDirectoryPath}/.afj${Platform.OS === 'android' ? '/cache' : ''}` + this.tempPath = options?.baseTempPath + ? `${options?.baseTempPath}/.afj` + : `${RNFS.TemporaryDirectoryPath}/.afj${Platform.OS === 'android' ? '/temp' : ''}` + } + + public async exists(path: string): Promise { + return RNFS.exists(path) + } + + public async createDirectory(path: string): Promise { + await RNFS.mkdir(getDirFromFilePath(path)) + } + + public async copyFile(sourcePath: string, destinationPath: string): Promise { + await RNFS.copyFile(sourcePath, destinationPath) + } + + public async write(path: string, data: string): Promise { + // Make sure parent directories exist + await RNFS.mkdir(getDirFromFilePath(path)) + + return RNFS.writeFile(path, data, 'utf8') + } + + public async read(path: string): Promise { + return RNFS.readFile(path, 'utf8') + } + + public async delete(path: string): Promise { + await RNFS.unlink(path) + } + + public async downloadToFile(url: string, path: string, options?: DownloadToFileOptions) { + // Make sure parent directories exist + await RNFS.mkdir(getDirFromFilePath(path)) + + const fromUrl = this.encodeUriIfRequired(url) + + const { promise } = RNFS.downloadFile({ + fromUrl, + toFile: path, + }) + + await promise + + if (options?.verifyHash) { + // RNFS returns hash as HEX + const fileHash = await RNFS.hash(path, options.verifyHash.algorithm) + const fileHashBuffer = Buffer.from(fileHash, 'hex') + + // If hash doesn't match, remove file and throw error + if (fileHashBuffer.compare(options.verifyHash.hash) !== 0) { + await RNFS.unlink(path) + throw new CredoError( + `Hash of downloaded file does not match expected hash. Expected: ${TypedArrayEncoder.toBase58( + options.verifyHash.hash + )}, Actual: ${TypedArrayEncoder.toBase58(fileHashBuffer)}` + ) + } + } + } + + private encodeUriIfRequired(uri: string) { + // Some characters in the URL might be invalid for + // the native os to handle. Only encode if necessary. + return uri === decodeURI(uri) ? encodeURI(uri) : uri + } +} diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts new file mode 100644 index 0000000000..d1da43a1ce --- /dev/null +++ b/packages/react-native/src/index.ts @@ -0,0 +1,20 @@ +import 'react-native-get-random-values' +import '@azure/core-asynciterator-polyfill' + +import type { AgentDependencies } from '@credo-ts/core' + +import { EventEmitter } from 'events' + +import { ReactNativeFileSystem } from './ReactNativeFileSystem' + +const fetch = global.fetch as unknown as AgentDependencies['fetch'] +const WebSocket = global.WebSocket as unknown as AgentDependencies['WebSocketClass'] + +const agentDependencies: AgentDependencies = { + FileSystem: ReactNativeFileSystem, + fetch, + EventEmitterClass: EventEmitter, + WebSocketClass: WebSocket, +} + +export { agentDependencies } diff --git a/packages/react-native/tests/index.test.ts b/packages/react-native/tests/index.test.ts new file mode 100644 index 0000000000..38211e84f4 --- /dev/null +++ b/packages/react-native/tests/index.test.ts @@ -0,0 +1,3 @@ +describe('@credo-ts/react-native', () => { + it.todo('React Native tests (need babel-jest)') +}) diff --git a/packages/react-native/tsconfig.build.json b/packages/react-native/tsconfig.build.json new file mode 100644 index 0000000000..b916e24804 --- /dev/null +++ b/packages/react-native/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build", + "types": ["react-native"] + }, + "include": ["src/**/*"] +} diff --git a/packages/react-native/tsconfig.json b/packages/react-native/tsconfig.json new file mode 100644 index 0000000000..cbea2d06db --- /dev/null +++ b/packages/react-native/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + // FIXME https://github.com/openwallet-foundation/credo-ts/pull/327 + "skipLibCheck": true, + "types": ["react-native", "jest"] + } +} diff --git a/packages/tenants/CHANGELOG.md b/packages/tenants/CHANGELOG.md new file mode 100644 index 0000000000..e1560acacf --- /dev/null +++ b/packages/tenants/CHANGELOG.md @@ -0,0 +1,99 @@ +# Changelog + +## 0.5.6 + +### Patch Changes + +- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release +- Updated dependencies [66e696d] + - @credo-ts/core@0.5.6 + +## 0.5.5 + +### Patch Changes + +- 482a630: - feat: allow serving dids from did record (#1856) + - fix: set created at for anoncreds records (#1862) + - feat: add goal to public api for credential and proof (#1867) + - fix(oob): only reuse connection if enabled (#1868) + - fix: issuer id query anoncreds w3c (#1870) + - feat: sd-jwt issuance without holder binding (#1871) + - chore: update oid4vci deps (#1873) + - fix: query for qualified/unqualified forms in revocation notification (#1866) + - fix: wrong schema id is stored for credentials (#1884) + - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) + - fix: unqualified indy revRegDefId in migration (#1887) + - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) + - fix(anoncreds): combine creds into one proof (#1893) + - fix: AnonCreds proof requests with unqualified dids (#1891) + - fix: WebSocket priority in Message Pick Up V2 (#1888) + - fix: anoncreds predicate only proof with unqualified dids (#1907) + - feat: add pagination params to storage service (#1883) + - feat: add message handler middleware and fallback (#1894) +- Updated dependencies [3239ef3] +- Updated dependencies [d548fa4] +- Updated dependencies [482a630] + - @credo-ts/core@0.5.5 + +## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) + +**Note:** Version bump only for package @credo-ts/tenants + +## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) + +### Bug Fixes + +- close tenant session after migration ([#1835](https://github.com/openwallet-foundation/credo-ts/issues/1835)) ([eb2c513](https://github.com/openwallet-foundation/credo-ts/commit/eb2c51384c077038e6cd38c1ab737d0d47c1b81e)) + +### Features + +- **tenants:** return value from withTenatnAgent ([#1832](https://github.com/openwallet-foundation/credo-ts/issues/1832)) ([8371d87](https://github.com/openwallet-foundation/credo-ts/commit/8371d8728685295a1f648ca677cc6de2cb873c09)) + +## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) + +**Note:** Version bump only for package @credo-ts/tenants + +# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) + +### Features + +- **openid4vc:** persistance and events ([#1793](https://github.com/openwallet-foundation/credo-ts/issues/1793)) ([f4c386a](https://github.com/openwallet-foundation/credo-ts/commit/f4c386a6ccf8adb829cad30b81d524e6ffddb029)) +- optional backup on storage migration ([#1745](https://github.com/openwallet-foundation/credo-ts/issues/1745)) ([81ff63c](https://github.com/openwallet-foundation/credo-ts/commit/81ff63ccf7c71eccf342899d298a780d66045534)) +- **tenants:** expose get all tenants on public API ([#1731](https://github.com/openwallet-foundation/credo-ts/issues/1731)) ([f11f8fd](https://github.com/openwallet-foundation/credo-ts/commit/f11f8fdf7748b015a6f321fb16da2b075e1267ca)) +- **tenants:** support for tenant storage migration ([#1747](https://github.com/openwallet-foundation/credo-ts/issues/1747)) ([12c617e](https://github.com/openwallet-foundation/credo-ts/commit/12c617efb45d20fda8965b9b4da24c92e975c9a2)) + +## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) + +**Note:** Version bump only for package @credo-ts/tenants + +## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) + +### Features + +- support askar profiles for multi-tenancy ([#1538](https://github.com/hyperledger/aries-framework-javascript/issues/1538)) ([e448a2a](https://github.com/hyperledger/aries-framework-javascript/commit/e448a2a58dddff2cdf80c4549ea2d842a54b43d1)) + +# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) + +### Bug Fixes + +- **tenant:** Correctly configure storage for multi tenant agents ([#1359](https://github.com/hyperledger/aries-framework-javascript/issues/1359)) ([7795975](https://github.com/hyperledger/aries-framework-javascript/commit/779597563a4236fdab851df9e102dca18ce2d4e4)), closes [hyperledger#1353](https://github.com/hyperledger/issues/1353) + +## [0.3.3](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.2...v0.3.3) (2023-01-18) + +### Bug Fixes + +- fix typing issues with typescript 4.9 ([#1214](https://github.com/hyperledger/aries-framework-javascript/issues/1214)) ([087980f](https://github.com/hyperledger/aries-framework-javascript/commit/087980f1adf3ee0bc434ca9782243a62c6124444)) + +## [0.3.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.1...v0.3.2) (2023-01-04) + +### Bug Fixes + +- **credentials:** typing if no modules provided ([#1188](https://github.com/hyperledger/aries-framework-javascript/issues/1188)) ([541356e](https://github.com/hyperledger/aries-framework-javascript/commit/541356e866bcd3ce06c69093d8cb6100dca4d09f)) + +## [0.3.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.0...v0.3.1) (2022-12-27) + +**Note:** Version bump only for package @credo-ts/tenants + +# [0.3.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.2.5...v0.3.0) (2022-12-22) + +**Note:** Version bump only for package @credo-ts/tenants diff --git a/packages/tenants/README.md b/packages/tenants/README.md new file mode 100644 index 0000000000..813be5bacb --- /dev/null +++ b/packages/tenants/README.md @@ -0,0 +1,31 @@ +

+
+ Credo Logo +

+

Credo Tenants Module

+

+ License + typescript + @credo-ts/tenants version + +

+
+ +Credo Tenant Module provides an optional addon to Credo to use an agent with multiple tenants. diff --git a/packages/tenants/jest.config.ts b/packages/tenants/jest.config.ts new file mode 100644 index 0000000000..93c0197296 --- /dev/null +++ b/packages/tenants/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/tenants/package.json b/packages/tenants/package.json new file mode 100644 index 0000000000..8e4290ab5f --- /dev/null +++ b/packages/tenants/package.json @@ -0,0 +1,38 @@ +{ + "name": "@credo-ts/tenants", + "main": "src/index", + "types": "src/index", + "version": "0.5.6", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "main": "build/index", + "types": "build/index", + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/tenants", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "packages/tenants" + }, + "scripts": { + "build": "pnpm run clean && pnpm run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "pnpm run build", + "test": "jest" + }, + "dependencies": { + "@credo-ts/core": "workspace:*", + "async-mutex": "^0.4.0" + }, + "devDependencies": { + "@credo-ts/node": "workspace:*", + "reflect-metadata": "^0.1.13", + "rimraf": "^4.4.0", + "typescript": "~5.5.2" + } +} diff --git a/packages/tenants/src/TenantAgent.ts b/packages/tenants/src/TenantAgent.ts new file mode 100644 index 0000000000..f1ffeab4d0 --- /dev/null +++ b/packages/tenants/src/TenantAgent.ts @@ -0,0 +1,29 @@ +import type { AgentContext, DefaultAgentModules, ModulesMap } from '@credo-ts/core' + +import { CredoError, BaseAgent } from '@credo-ts/core' + +export class TenantAgent extends BaseAgent { + private sessionHasEnded = false + + public constructor(agentContext: AgentContext) { + super(agentContext.config, agentContext.dependencyManager) + } + + public async initialize() { + if (this.sessionHasEnded) { + throw new CredoError("Can't initialize agent after tenant sessions has been ended.") + } + + await super.initialize() + this._isInitialized = true + } + + public async endSession() { + this.logger.trace( + `Ending session for agent context with contextCorrelationId '${this.agentContext.contextCorrelationId}'` + ) + await this.agentContext.endSession() + this._isInitialized = false + this.sessionHasEnded = true + } +} diff --git a/packages/tenants/src/TenantsApi.ts b/packages/tenants/src/TenantsApi.ts new file mode 100644 index 0000000000..8063e5e989 --- /dev/null +++ b/packages/tenants/src/TenantsApi.ts @@ -0,0 +1,145 @@ +import type { + CreateTenantOptions, + GetTenantAgentOptions, + UpdateTenantStorageOptions, + WithTenantAgentCallback, +} from './TenantsApiOptions' +import type { TenantRecord } from './repository' +import type { DefaultAgentModules, ModulesMap, Query, QueryOptions } from '@credo-ts/core' + +import { + isStorageUpToDate, + AgentContext, + inject, + injectable, + InjectionSymbols, + Logger, + UpdateAssistant, +} from '@credo-ts/core' + +import { TenantAgent } from './TenantAgent' +import { TenantAgentContextProvider } from './context/TenantAgentContextProvider' +import { TenantRecordService } from './services' + +@injectable() +export class TenantsApi { + public readonly rootAgentContext: AgentContext + private tenantRecordService: TenantRecordService + private agentContextProvider: TenantAgentContextProvider + private logger: Logger + + public constructor( + tenantRecordService: TenantRecordService, + rootAgentContext: AgentContext, + @inject(InjectionSymbols.AgentContextProvider) agentContextProvider: TenantAgentContextProvider, + @inject(InjectionSymbols.Logger) logger: Logger + ) { + this.tenantRecordService = tenantRecordService + this.rootAgentContext = rootAgentContext + this.agentContextProvider = agentContextProvider + this.logger = logger + } + + public async getTenantAgent({ tenantId }: GetTenantAgentOptions): Promise> { + this.logger.debug(`Getting tenant agent for tenant '${tenantId}'`) + const tenantContext = await this.agentContextProvider.getAgentContextForContextCorrelationId(tenantId) + + this.logger.trace(`Got tenant context for tenant '${tenantId}'`) + const tenantAgent = new TenantAgent(tenantContext) + await tenantAgent.initialize() + this.logger.trace(`Initializing tenant agent for tenant '${tenantId}'`) + + return tenantAgent + } + + public async withTenantAgent( + options: GetTenantAgentOptions, + withTenantAgentCallback: WithTenantAgentCallback + ): Promise { + this.logger.debug(`Getting tenant agent for tenant '${options.tenantId}' in with tenant agent callback`) + const tenantAgent = await this.getTenantAgent(options) + + try { + this.logger.debug(`Calling tenant agent callback for tenant '${options.tenantId}'`) + const result = await withTenantAgentCallback(tenantAgent) + return result + } catch (error) { + this.logger.error(`Error in tenant agent callback for tenant '${options.tenantId}'`, { error }) + throw error + } finally { + this.logger.debug(`Ending tenant agent session for tenant '${options.tenantId}'`) + await tenantAgent.endSession() + } + } + + public async createTenant(options: CreateTenantOptions) { + this.logger.debug(`Creating tenant with label ${options.config.label}`) + const tenantRecord = await this.tenantRecordService.createTenant(this.rootAgentContext, options.config) + + // This initializes the tenant agent, creates the wallet etc... + const tenantAgent = await this.getTenantAgent({ tenantId: tenantRecord.id }) + await tenantAgent.endSession() + + this.logger.info(`Successfully created tenant '${tenantRecord.id}'`) + + return tenantRecord + } + + public async getTenantById(tenantId: string) { + this.logger.debug(`Getting tenant by id '${tenantId}'`) + return this.tenantRecordService.getTenantById(this.rootAgentContext, tenantId) + } + + public async findTenantsByLabel(label: string) { + this.logger.debug(`Finding tenants by label '${label}'`) + return this.tenantRecordService.findTenantsByLabel(this.rootAgentContext, label) + } + + public async deleteTenantById(tenantId: string) { + this.logger.debug(`Deleting tenant by id '${tenantId}'`) + // TODO: force remove context from the context provider (or session manager) + const tenantAgent = await this.getTenantAgent({ tenantId }) + + this.logger.trace(`Deleting wallet for tenant '${tenantId}'`) + await tenantAgent.wallet.delete() + this.logger.trace(`Shutting down agent for tenant '${tenantId}'`) + await tenantAgent.endSession() + + return this.tenantRecordService.deleteTenantById(this.rootAgentContext, tenantId) + } + + public async updateTenant(tenant: TenantRecord) { + await this.tenantRecordService.updateTenant(this.rootAgentContext, tenant) + } + + public async findTenantsByQuery(query: Query, queryOptions?: QueryOptions) { + return this.tenantRecordService.findTenantsByQuery(this.rootAgentContext, query, queryOptions) + } + + public async getAllTenants() { + this.logger.debug('Getting all tenants') + return this.tenantRecordService.getAllTenants(this.rootAgentContext) + } + + public async updateTenantStorage({ tenantId, updateOptions }: UpdateTenantStorageOptions) { + this.logger.debug(`Updating tenant storage for tenant '${tenantId}'`) + const tenantRecord = await this.tenantRecordService.getTenantById(this.rootAgentContext, tenantId) + + if (isStorageUpToDate(tenantRecord.storageVersion)) { + this.logger.debug(`Tenant storage for tenant '${tenantId}' is already up to date. Skipping update`) + return + } + + await this.agentContextProvider.updateTenantStorage(tenantRecord, updateOptions) + } + + public async getTenantsWithOutdatedStorage() { + const outdatedTenants = await this.tenantRecordService.findTenantsByQuery(this.rootAgentContext, { + $not: { + storageVersion: UpdateAssistant.frameworkStorageVersion, + }, + }) + + return outdatedTenants + } +} diff --git a/packages/tenants/src/TenantsApiOptions.ts b/packages/tenants/src/TenantsApiOptions.ts new file mode 100644 index 0000000000..99c188dad5 --- /dev/null +++ b/packages/tenants/src/TenantsApiOptions.ts @@ -0,0 +1,20 @@ +import type { TenantAgent } from './TenantAgent' +import type { TenantConfig } from './models/TenantConfig' +import type { ModulesMap, UpdateAssistantUpdateOptions } from '@credo-ts/core' + +export interface GetTenantAgentOptions { + tenantId: string +} + +export type WithTenantAgentCallback = ( + tenantAgent: TenantAgent +) => Promise + +export interface CreateTenantOptions { + config: Omit +} + +export interface UpdateTenantStorageOptions { + tenantId: string + updateOptions?: UpdateAssistantUpdateOptions +} diff --git a/packages/tenants/src/TenantsModule.ts b/packages/tenants/src/TenantsModule.ts new file mode 100644 index 0000000000..d45dd87f36 --- /dev/null +++ b/packages/tenants/src/TenantsModule.ts @@ -0,0 +1,59 @@ +import type { TenantsModuleConfigOptions } from './TenantsModuleConfig' +import type { Constructor, ModulesMap, DependencyManager, Module, EmptyModuleMap, Update } from '@credo-ts/core' + +import { AgentConfig, InjectionSymbols } from '@credo-ts/core' + +import { TenantsApi } from './TenantsApi' +import { TenantsModuleConfig } from './TenantsModuleConfig' +import { TenantAgentContextProvider } from './context/TenantAgentContextProvider' +import { TenantSessionCoordinator } from './context/TenantSessionCoordinator' +import { TenantRepository, TenantRoutingRepository } from './repository' +import { TenantRecordService } from './services' +import { updateTenantsModuleV0_4ToV0_5 } from './updates/0.4-0.5' + +export class TenantsModule implements Module { + public readonly config: TenantsModuleConfig + + public readonly api: Constructor> = TenantsApi + + public constructor(config?: TenantsModuleConfigOptions) { + this.config = new TenantsModuleConfig(config) + } + + /** + * Registers the dependencies of the tenants module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The '@credo-ts/tenants' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." + ) + + // Api + // NOTE: this is a singleton because tenants can't have their own tenants. This makes sure the tenants api is always used in the root agent context. + dependencyManager.registerSingleton(TenantsApi) + + // Config + dependencyManager.registerInstance(TenantsModuleConfig, this.config) + + // Services + dependencyManager.registerSingleton(TenantRecordService) + + // Repositories + dependencyManager.registerSingleton(TenantRepository) + dependencyManager.registerSingleton(TenantRoutingRepository) + + dependencyManager.registerSingleton(InjectionSymbols.AgentContextProvider, TenantAgentContextProvider) + dependencyManager.registerSingleton(TenantSessionCoordinator) + } + + public updates = [ + { + fromVersion: '0.4', + toVersion: '0.5', + doUpdate: updateTenantsModuleV0_4ToV0_5, + }, + ] satisfies Update[] +} diff --git a/packages/tenants/src/TenantsModuleConfig.ts b/packages/tenants/src/TenantsModuleConfig.ts new file mode 100644 index 0000000000..7fc8b7c84c --- /dev/null +++ b/packages/tenants/src/TenantsModuleConfig.ts @@ -0,0 +1,42 @@ +/** + * TenantsModuleConfigOptions defines the interface for the options of the TenantsModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface TenantsModuleConfigOptions { + /** + * Maximum number of concurrent tenant sessions that can be active at the same time. Defaults to + * 100 concurrent sessions. The default is low on purpose, to make sure deployments determine their own + * session limit based on the hardware and usage of the tenants module. Use `Infinity` to allow unlimited + * concurrent sessions. + * + * @default 100 + */ + sessionLimit?: number + + /** + * Timeout in milliseconds for acquiring a tenant session. If the {@link TenantsModuleConfigOptions.maxNumberOfSessions} is reached and + * a tenant sessions couldn't be acquired within the specified timeout, an error will be thrown and the session creation will be aborted. + * Use `Infinity` to disable the timeout. + * + * @default 1000 + */ + sessionAcquireTimeout?: number +} + +export class TenantsModuleConfig { + private options: TenantsModuleConfigOptions + + public constructor(options?: TenantsModuleConfigOptions) { + this.options = options ?? {} + } + + /** See {@link TenantsModuleConfigOptions.sessionLimit} */ + public get sessionLimit(): number { + return this.options.sessionLimit ?? 100 + } + + /** See {@link TenantsModuleConfigOptions.sessionAcquireTimeout} */ + public get sessionAcquireTimeout(): number { + return this.options.sessionAcquireTimeout ?? 1000 + } +} diff --git a/packages/tenants/src/__tests__/TenantAgent.test.ts b/packages/tenants/src/__tests__/TenantAgent.test.ts new file mode 100644 index 0000000000..6989bfc47c --- /dev/null +++ b/packages/tenants/src/__tests__/TenantAgent.test.ts @@ -0,0 +1,22 @@ +import { Agent, AgentContext } from '@credo-ts/core' + +import { getAgentConfig, getAgentContext, getInMemoryAgentOptions } from '../../../core/tests/helpers' +import { TenantAgent } from '../TenantAgent' + +describe('TenantAgent', () => { + test('possible to construct a TenantAgent instance', () => { + const agent = new Agent(getInMemoryAgentOptions('TenantAgentRoot')) + + const tenantDependencyManager = agent.dependencyManager.createChild() + + const agentContext = getAgentContext({ + agentConfig: getAgentConfig('TenantAgent'), + dependencyManager: tenantDependencyManager, + }) + tenantDependencyManager.registerInstance(AgentContext, agentContext) + + const tenantAgent = new TenantAgent(agentContext) + + expect(tenantAgent).toBeInstanceOf(TenantAgent) + }) +}) diff --git a/packages/tenants/src/__tests__/TenantsApi.test.ts b/packages/tenants/src/__tests__/TenantsApi.test.ts new file mode 100644 index 0000000000..2943f4ab8f --- /dev/null +++ b/packages/tenants/src/__tests__/TenantsApi.test.ts @@ -0,0 +1,224 @@ +import { Agent, AgentContext, InjectionSymbols } from '@credo-ts/core' + +import { getAgentContext, getInMemoryAgentOptions, mockFunction } from '../../../core/tests' +import { TenantAgent } from '../TenantAgent' +import { TenantsApi } from '../TenantsApi' +import { TenantAgentContextProvider } from '../context/TenantAgentContextProvider' +import { TenantRecord } from '../repository' +import { TenantRecordService } from '../services/TenantRecordService' + +jest.mock('../services/TenantRecordService') +const TenantRecordServiceMock = TenantRecordService as jest.Mock + +jest.mock('../context/TenantAgentContextProvider') +const AgentContextProviderMock = TenantAgentContextProvider as jest.Mock + +const tenantRecordService = new TenantRecordServiceMock() +const agentContextProvider = new AgentContextProviderMock() +const agentOptions = getInMemoryAgentOptions('TenantsApi') +const rootAgent = new Agent(agentOptions) +rootAgent.dependencyManager.registerInstance(InjectionSymbols.AgentContextProvider, agentContextProvider) + +const tenantsApi = new TenantsApi(tenantRecordService, rootAgent.context, agentContextProvider, rootAgent.config.logger) + +describe('TenantsApi', () => { + describe('getTenantAgent', () => { + test('gets context from agent context provider and initializes tenant agent instance', async () => { + const tenantDependencyManager = rootAgent.dependencyManager.createChild() + const tenantAgentContext = getAgentContext({ + contextCorrelationId: 'tenant-id', + dependencyManager: tenantDependencyManager, + agentConfig: rootAgent.config.extend({ + label: 'tenant-agent', + walletConfig: { + id: 'Wallet: TenantsApi: tenant-id', + key: 'Wallet: TenantsApi: tenant-id', + }, + }), + }) + tenantDependencyManager.registerInstance(AgentContext, tenantAgentContext) + + mockFunction(agentContextProvider.getAgentContextForContextCorrelationId).mockResolvedValue(tenantAgentContext) + + const tenantAgent = await tenantsApi.getTenantAgent({ tenantId: 'tenant-id' }) + + expect(tenantAgent.isInitialized).toBe(true) + expect(tenantAgent.wallet.walletConfig).toEqual({ + id: 'Wallet: TenantsApi: tenant-id', + key: 'Wallet: TenantsApi: tenant-id', + }) + + expect(agentContextProvider.getAgentContextForContextCorrelationId).toHaveBeenCalledWith('tenant-id') + expect(tenantAgent).toBeInstanceOf(TenantAgent) + expect(tenantAgent.context).toBe(tenantAgentContext) + + await tenantAgent.wallet.delete() + await tenantAgent.endSession() + }) + }) + + describe('withTenantAgent', () => { + test('gets context from agent context provider and initializes tenant agent instance', async () => { + expect.assertions(6) + + const tenantDependencyManager = rootAgent.dependencyManager.createChild() + const tenantAgentContext = getAgentContext({ + contextCorrelationId: 'tenant-id', + dependencyManager: tenantDependencyManager, + agentConfig: rootAgent.config.extend({ + label: 'tenant-agent', + walletConfig: { + id: 'Wallet: TenantsApi: tenant-id', + key: 'Wallet: TenantsApi: tenant-id', + }, + }), + }) + tenantDependencyManager.registerInstance(AgentContext, tenantAgentContext) + + mockFunction(agentContextProvider.getAgentContextForContextCorrelationId).mockResolvedValue(tenantAgentContext) + + let endSessionSpy: jest.SpyInstance | undefined = undefined + await tenantsApi.withTenantAgent({ tenantId: 'tenant-id' }, async (tenantAgent) => { + endSessionSpy = jest.spyOn(tenantAgent, 'endSession') + expect(tenantAgent.isInitialized).toBe(true) + expect(tenantAgent.wallet.walletConfig).toEqual({ + id: 'Wallet: TenantsApi: tenant-id', + key: 'Wallet: TenantsApi: tenant-id', + }) + + expect(agentContextProvider.getAgentContextForContextCorrelationId).toHaveBeenCalledWith('tenant-id') + expect(tenantAgent).toBeInstanceOf(TenantAgent) + expect(tenantAgent.context).toBe(tenantAgentContext) + + await tenantAgent.wallet.delete() + }) + + expect(endSessionSpy).toHaveBeenCalled() + }) + + test('endSession is called even if the tenant agent callback throws an error', async () => { + expect.assertions(7) + + const tenantDependencyManager = rootAgent.dependencyManager.createChild() + const tenantAgentContext = getAgentContext({ + contextCorrelationId: 'tenant-id', + dependencyManager: tenantDependencyManager, + agentConfig: rootAgent.config.extend({ + label: 'tenant-agent', + walletConfig: { + id: 'Wallet: TenantsApi: tenant-id', + key: 'Wallet: TenantsApi: tenant-id', + }, + }), + }) + tenantDependencyManager.registerInstance(AgentContext, tenantAgentContext) + + mockFunction(agentContextProvider.getAgentContextForContextCorrelationId).mockResolvedValue(tenantAgentContext) + + let endSessionSpy: jest.SpyInstance | undefined = undefined + await expect( + tenantsApi.withTenantAgent({ tenantId: 'tenant-id' }, async (tenantAgent) => { + endSessionSpy = jest.spyOn(tenantAgent, 'endSession') + expect(tenantAgent.isInitialized).toBe(true) + expect(tenantAgent.wallet.walletConfig).toEqual({ + id: 'Wallet: TenantsApi: tenant-id', + key: 'Wallet: TenantsApi: tenant-id', + }) + + expect(agentContextProvider.getAgentContextForContextCorrelationId).toHaveBeenCalledWith('tenant-id') + expect(tenantAgent).toBeInstanceOf(TenantAgent) + expect(tenantAgent.context).toBe(tenantAgentContext) + + await tenantAgent.wallet.delete() + + throw new Error('Uh oh something went wrong') + }) + ).rejects.toThrow('Uh oh something went wrong') + + // endSession should have been called + expect(endSessionSpy).toHaveBeenCalled() + }) + }) + + describe('createTenant', () => { + test('create tenant in the service and get the tenant agent to initialize', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant-id', + config: { + label: 'test', + walletConfig: { + id: 'Wallet: TenantsApi: tenant-id', + key: 'Wallet: TenantsApi: tenant-id', + }, + }, + storageVersion: '0.5', + }) + + const tenantAgentMock = { + wallet: { + delete: jest.fn(), + }, + endSession: jest.fn(), + } as unknown as TenantAgent + + mockFunction(tenantRecordService.createTenant).mockResolvedValue(tenantRecord) + const getTenantAgentSpy = jest.spyOn(tenantsApi, 'getTenantAgent').mockResolvedValue(tenantAgentMock) + + const createdTenantRecord = await tenantsApi.createTenant({ + config: { + label: 'test', + }, + }) + + expect(getTenantAgentSpy).toHaveBeenCalledWith({ tenantId: 'tenant-id' }) + expect(createdTenantRecord).toBe(tenantRecord) + expect(tenantAgentMock.endSession).toHaveBeenCalled() + expect(tenantRecordService.createTenant).toHaveBeenCalledWith(rootAgent.context, { + label: 'test', + }) + }) + }) + + describe('getTenantById', () => { + test('calls get tenant by id on tenant service', async () => { + const tenantRecord = jest.fn() as unknown as TenantRecord + mockFunction(tenantRecordService.getTenantById).mockResolvedValue(tenantRecord) + + const actualTenantRecord = await tenantsApi.getTenantById('tenant-id') + + expect(tenantRecordService.getTenantById).toHaveBeenCalledWith(rootAgent.context, 'tenant-id') + expect(actualTenantRecord).toBe(tenantRecord) + }) + }) + + describe('deleteTenantById', () => { + test('deletes the tenant and removes the wallet', async () => { + const tenantAgentMock = { + wallet: { + delete: jest.fn(), + }, + endSession: jest.fn(), + } as unknown as TenantAgent + const getTenantAgentSpy = jest.spyOn(tenantsApi, 'getTenantAgent').mockResolvedValue(tenantAgentMock) + + await tenantsApi.deleteTenantById('tenant-id') + + expect(getTenantAgentSpy).toHaveBeenCalledWith({ tenantId: 'tenant-id' }) + expect(tenantAgentMock.wallet.delete).toHaveBeenCalled() + expect(tenantAgentMock.endSession).toHaveBeenCalled() + expect(tenantRecordService.deleteTenantById).toHaveBeenCalledWith(rootAgent.context, 'tenant-id') + }) + }) + + describe('getAllTenants', () => { + test('calls get all tenants on tenant service', async () => { + const tenantRecords = jest.fn() as unknown as Array + mockFunction(tenantRecordService.getAllTenants).mockResolvedValue(tenantRecords) + + const actualTenantRecords = await tenantsApi.getAllTenants() + + expect(tenantRecordService.getAllTenants).toHaveBeenCalledWith(rootAgent.context) + expect(actualTenantRecords).toBe(tenantRecords) + }) + }) +}) diff --git a/packages/tenants/src/__tests__/TenantsModule.test.ts b/packages/tenants/src/__tests__/TenantsModule.test.ts new file mode 100644 index 0000000000..d89873590a --- /dev/null +++ b/packages/tenants/src/__tests__/TenantsModule.test.ts @@ -0,0 +1,39 @@ +import { InjectionSymbols } from '@credo-ts/core' + +import { DependencyManager } from '../../../core/src/plugins/DependencyManager' +import { mockFunction } from '../../../core/tests' +import { TenantsApi } from '../TenantsApi' +import { TenantsModule } from '../TenantsModule' +import { TenantsModuleConfig } from '../TenantsModuleConfig' +import { TenantAgentContextProvider } from '../context/TenantAgentContextProvider' +import { TenantSessionCoordinator } from '../context/TenantSessionCoordinator' +import { TenantRepository, TenantRoutingRepository } from '../repository' +import { TenantRecordService } from '../services' + +jest.mock('../../../core/src/plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +mockFunction(dependencyManager.resolve).mockReturnValue({ logger: { warn: jest.fn() } }) + +describe('TenantsModule', () => { + test('registers dependencies on the dependency manager', () => { + const tenantsModule = new TenantsModule() + tenantsModule.register(dependencyManager) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(6) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TenantsApi) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TenantRecordService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TenantRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TenantRoutingRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( + InjectionSymbols.AgentContextProvider, + TenantAgentContextProvider + ) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TenantSessionCoordinator) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(TenantsModuleConfig, tenantsModule.config) + }) +}) diff --git a/packages/tenants/src/__tests__/TenantsModuleConfig.test.ts b/packages/tenants/src/__tests__/TenantsModuleConfig.test.ts new file mode 100644 index 0000000000..b942434bad --- /dev/null +++ b/packages/tenants/src/__tests__/TenantsModuleConfig.test.ts @@ -0,0 +1,20 @@ +import { TenantsModuleConfig } from '../TenantsModuleConfig' + +describe('TenantsModuleConfig', () => { + test('sets default values', () => { + const config = new TenantsModuleConfig() + + expect(config.sessionLimit).toBe(100) + expect(config.sessionAcquireTimeout).toBe(1000) + }) + + test('sets values', () => { + const config = new TenantsModuleConfig({ + sessionAcquireTimeout: 12, + sessionLimit: 42, + }) + + expect(config.sessionAcquireTimeout).toBe(12) + expect(config.sessionLimit).toBe(42) + }) +}) diff --git a/packages/tenants/src/context/TenantAgentContextProvider.ts b/packages/tenants/src/context/TenantAgentContextProvider.ts new file mode 100644 index 0000000000..3925179e38 --- /dev/null +++ b/packages/tenants/src/context/TenantAgentContextProvider.ts @@ -0,0 +1,230 @@ +import type { TenantRecord } from '../repository' +import type { + AgentContextProvider, + RoutingCreatedEvent, + EncryptedMessage, + UpdateAssistantUpdateOptions, +} from '@credo-ts/core' + +import { + isStorageUpToDate, + UpdateAssistant, + CredoError, + injectable, + AgentContext, + EventEmitter, + inject, + Logger, + RoutingEventTypes, + InjectionSymbols, + KeyType, + Key, + isValidJweStructure, + JsonEncoder, + isJsonObject, +} from '@credo-ts/core' + +import { TenantAgent } from '../TenantAgent' +import { TenantRecordService } from '../services' + +import { TenantSessionCoordinator } from './TenantSessionCoordinator' + +@injectable() +export class TenantAgentContextProvider implements AgentContextProvider { + private tenantRecordService: TenantRecordService + private rootAgentContext: AgentContext + private eventEmitter: EventEmitter + private logger: Logger + private tenantSessionCoordinator: TenantSessionCoordinator + + public constructor( + tenantRecordService: TenantRecordService, + rootAgentContext: AgentContext, + eventEmitter: EventEmitter, + tenantSessionCoordinator: TenantSessionCoordinator, + @inject(InjectionSymbols.Logger) logger: Logger + ) { + this.tenantRecordService = tenantRecordService + this.rootAgentContext = rootAgentContext + this.eventEmitter = eventEmitter + this.tenantSessionCoordinator = tenantSessionCoordinator + this.logger = logger + + // Start listener for newly created routing keys, so we can register a mapping for each new key for the tenant + this.listenForRoutingKeyCreatedEvents() + } + + public async getAgentContextForContextCorrelationId(contextCorrelationId: string) { + // It could be that the root agent context is requested, in that case we return the root agent context + if (contextCorrelationId === this.rootAgentContext.contextCorrelationId) { + return this.rootAgentContext + } + + // TODO: maybe we can look at not having to retrieve the tenant record if there's already a context available. + const tenantRecord = await this.tenantRecordService.getTenantById(this.rootAgentContext, contextCorrelationId) + const shouldUpdate = !isStorageUpToDate(tenantRecord.storageVersion) + + // If the tenant storage is not up to date, and autoUpdate is disabled we throw an error + if (shouldUpdate && !this.rootAgentContext.config.autoUpdateStorageOnStartup) { + throw new CredoError( + `Current agent storage for tenant ${tenantRecord.id} is not up to date. ` + + `To prevent the tenant state from getting corrupted the tenant initialization is aborted. ` + + `Make sure to update the tenant storage (currently at ${tenantRecord.storageVersion}) to the latest version (${UpdateAssistant.frameworkStorageVersion}). ` + + `You can also downgrade your version of Credo.` + ) + } + + const agentContext = await this.tenantSessionCoordinator.getContextForSession(tenantRecord, { + runInMutex: shouldUpdate ? (agentContext) => this._updateTenantStorage(tenantRecord, agentContext) : undefined, + }) + + this.logger.debug(`Created tenant agent context for tenant '${contextCorrelationId}'`) + + return agentContext + } + + public async getContextForInboundMessage(inboundMessage: unknown, options?: { contextCorrelationId?: string }) { + this.logger.debug('Getting context for inbound message in tenant agent context provider', { + contextCorrelationId: options?.contextCorrelationId, + }) + + let tenantId = options?.contextCorrelationId + let recipientKeys: Key[] = [] + + if (!tenantId && isValidJweStructure(inboundMessage)) { + this.logger.trace("Inbound message is a JWE, extracting tenant id from JWE's protected header") + recipientKeys = this.getRecipientKeysFromEncryptedMessage(inboundMessage) + + this.logger.trace(`Found ${recipientKeys.length} recipient keys in JWE's protected header`) + + // FIXME: what if there are multiple recipients in the same agent? If we receive the messages twice we will process it for + // the first found recipient multiple times. This is however a case I've never seen before and will add quite some complexity + // to resolve. I think we're fine to ignore this case for now. + for (const recipientKey of recipientKeys) { + const tenantRoutingRecord = await this.tenantRecordService.findTenantRoutingRecordByRecipientKey( + this.rootAgentContext, + recipientKey + ) + + if (tenantRoutingRecord) { + this.logger.debug(`Found tenant routing record for recipient key ${recipientKeys[0].fingerprint}`, { + tenantId: tenantRoutingRecord.tenantId, + }) + tenantId = tenantRoutingRecord.tenantId + break + } + } + } + + if (!tenantId) { + this.logger.error("Couldn't determine tenant id for inbound message. Unable to create context", { + inboundMessage, + recipientKeys: recipientKeys.map((key) => key.fingerprint), + }) + throw new CredoError("Couldn't determine tenant id for inbound message. Unable to create context") + } + + const agentContext = await this.getAgentContextForContextCorrelationId(tenantId) + + return agentContext + } + + public async endSessionForAgentContext(agentContext: AgentContext) { + await this.tenantSessionCoordinator.endAgentContextSession(agentContext) + } + + private getRecipientKeysFromEncryptedMessage(jwe: EncryptedMessage): Key[] { + const jweProtected = JsonEncoder.fromBase64(jwe.protected) + if (!Array.isArray(jweProtected.recipients)) return [] + + const recipientKeys: Key[] = [] + + for (const recipient of jweProtected.recipients) { + // Check if recipient.header.kid is a string + if (isJsonObject(recipient) && isJsonObject(recipient.header) && typeof recipient.header.kid === 'string') { + // This won't work with other key types, we should detect what the encoding is of kid, and based on that + // determine how we extract the key from the message + const key = Key.fromPublicKeyBase58(recipient.header.kid, KeyType.Ed25519) + recipientKeys.push(key) + } + } + + return recipientKeys + } + + private async registerRecipientKeyForTenant(tenantId: string, recipientKey: Key) { + this.logger.debug(`Registering recipient key ${recipientKey.fingerprint} for tenant ${tenantId}`) + const tenantRecord = await this.tenantRecordService.getTenantById(this.rootAgentContext, tenantId) + await this.tenantRecordService.addTenantRoutingRecord(this.rootAgentContext, tenantRecord.id, recipientKey) + } + + private listenForRoutingKeyCreatedEvents() { + this.logger.debug('Listening for routing key created events in tenant agent context provider') + this.eventEmitter.on(RoutingEventTypes.RoutingCreatedEvent, async (event) => { + const contextCorrelationId = event.metadata.contextCorrelationId + const recipientKey = event.payload.routing.recipientKey + + // We don't want to register the key if it's for the root agent context + if (contextCorrelationId === this.rootAgentContext.contextCorrelationId) return + + this.logger.debug( + `Received routing key created event for tenant ${contextCorrelationId}, registering recipient key ${recipientKey.fingerprint} in base wallet` + ) + await this.registerRecipientKeyForTenant(contextCorrelationId, recipientKey) + }) + } + + /** + * Method to allow updating the tenant storage, this method can be called from the TenantsApi + * to update the storage for a tenant manually + */ + public async updateTenantStorage(tenantRecord: TenantRecord, updateOptions?: UpdateAssistantUpdateOptions) { + const agentContext = await this.tenantSessionCoordinator.getContextForSession(tenantRecord, { + // runInMutex allows us to run the updateTenantStorage method in a mutex lock + // prevent other sessions from being started while the update is in progress + runInMutex: (agentContext) => this._updateTenantStorage(tenantRecord, agentContext, updateOptions), + }) + + // End sesion afterwards + await agentContext.endSession() + } + + /** + * Handle the case where the tenant storage is outdated. If auto-update is disabled we will throw an error + * and not update the storage. If auto-update is enabled we will update the storage. + * + * When this method is called we can be sure that we are in the mutex runExclusive lock and thus other sessions + * will not be able to open a session for this tenant until we're done. + * + * NOTE: We don't support multi-instance locking for now. That means you can only have a single instance open and + * it will prevent multiple processes from updating the tenant storage at the same time. However if multi-instances + * are used, we can't prevent multiple instances from updating the tenant storage at the same time. + * In the future we can make the tenantSessionCoordinator an interface and allowing a instance-tenant-lock as well + * as an tenant-lock (across all instances) + */ + private async _updateTenantStorage( + tenantRecord: TenantRecord, + agentContext: AgentContext, + updateOptions?: UpdateAssistantUpdateOptions + ) { + try { + // Update the tenant storage + const tenantAgent = new TenantAgent(agentContext) + const updateAssistant = new UpdateAssistant(tenantAgent) + await updateAssistant.initialize() + await updateAssistant.update({ + ...updateOptions, + backupBeforeStorageUpdate: + updateOptions?.backupBeforeStorageUpdate ?? agentContext.config.backupBeforeStorageUpdate, + }) + + // Update the storage version in the tenant record + tenantRecord.storageVersion = await updateAssistant.getCurrentAgentStorageVersion() + const tenantRecordService = this.rootAgentContext.dependencyManager.resolve(TenantRecordService) + await tenantRecordService.updateTenant(this.rootAgentContext, tenantRecord) + } catch (error) { + this.logger.error(`Error occurred while updating tenant storage for tenant ${tenantRecord.id}`, error) + throw error + } + } +} diff --git a/packages/tenants/src/context/TenantSessionCoordinator.ts b/packages/tenants/src/context/TenantSessionCoordinator.ts new file mode 100644 index 0000000000..466ebd1a66 --- /dev/null +++ b/packages/tenants/src/context/TenantSessionCoordinator.ts @@ -0,0 +1,262 @@ +import type { TenantRecord } from '../repository' +import type { MutexInterface } from 'async-mutex' + +import { + AgentConfig, + AgentContext, + CredoError, + inject, + injectable, + InjectionSymbols, + Logger, + WalletApi, + WalletError, +} from '@credo-ts/core' +import { Mutex, withTimeout } from 'async-mutex' + +import { TenantsModuleConfig } from '../TenantsModuleConfig' + +import { TenantSessionMutex } from './TenantSessionMutex' + +/** + * Coordinates all agent context instance for tenant sessions. + * + * This class keeps a mapping of tenant ids (context correlation ids) to agent context sessions mapping. Each mapping contains the agent context, + * the current session count and a mutex for making operations against the session mapping (opening / closing an agent context). The mutex ensures + * we're not susceptible to race conditions where multiple calls to open/close an agent context are made at the same time. Even though JavaScript is + * single threaded, promises can introduce race conditions as one process can stop and another process can be picked up. + * + * NOTE: the implementation doesn't yet cache agent context objects after they aren't being used for any sessions anymore. This means if a wallet is being used + * often in a short time it will be opened/closed very often. This is an improvement to be made in the near future. + */ +@injectable() +export class TenantSessionCoordinator { + private rootAgentContext: AgentContext + private logger: Logger + private tenantAgentContextMapping: TenantAgentContextMapping = {} + private sessionMutex: TenantSessionMutex + private tenantsModuleConfig: TenantsModuleConfig + + public constructor( + rootAgentContext: AgentContext, + @inject(InjectionSymbols.Logger) logger: Logger, + tenantsModuleConfig: TenantsModuleConfig + ) { + this.rootAgentContext = rootAgentContext + this.logger = logger + this.tenantsModuleConfig = tenantsModuleConfig + + this.sessionMutex = new TenantSessionMutex( + this.logger, + this.tenantsModuleConfig.sessionLimit, + // TODO: we should probably allow a higher session acquire timeout if the storage is being updated? + this.tenantsModuleConfig.sessionAcquireTimeout + ) + } + + public getSessionCountForTenant(tenantId: string) { + return this.tenantAgentContextMapping[tenantId]?.sessionCount ?? 0 + } + + /** + * Get agent context to use for a session. If an agent context for this tenant does not exist yet + * it will create it and store it for later use. If the agent context does already exist it will + * be returned. + * + * @parm tenantRecord The tenant record for which to get the agent context + */ + public async getContextForSession( + tenantRecord: TenantRecord, + { + runInMutex, + }: { + /** optional callback that will be run inside the mutex lock */ + runInMutex?: (agentContext: AgentContext) => Promise + } = {} + ): Promise { + this.logger.debug(`Getting context for session with tenant '${tenantRecord.id}'`) + + // Wait for a session to be available + await this.sessionMutex.acquireSession() + + try { + return await this.mutexForTenant(tenantRecord.id).runExclusive(async () => { + this.logger.debug(`Acquired lock for tenant '${tenantRecord.id}' to get context`) + const tenantSessions = this.getTenantSessionsMapping(tenantRecord.id) + + // If we don't have an agent context already, create one and initialize it + if (!tenantSessions.agentContext) { + this.logger.debug(`No agent context has been initialized for tenant '${tenantRecord.id}', creating one`) + tenantSessions.agentContext = await this.createAgentContext(tenantRecord) + } + + // If we already have a context with sessions in place return the context and increment + // the session count. + tenantSessions.sessionCount++ + this.logger.debug( + `Increased agent context session count for tenant '${tenantRecord.id}' to ${tenantSessions.sessionCount}` + ) + + if (runInMutex) { + try { + await runInMutex(tenantSessions.agentContext) + } catch (error) { + // If the runInMutex failed we should release the session again + tenantSessions.sessionCount-- + this.logger.debug( + `Decreased agent context session count for tenant '${tenantSessions.agentContext.contextCorrelationId}' to ${tenantSessions.sessionCount} due to failure in mutex script`, + error + ) + + if (tenantSessions.sessionCount <= 0 && tenantSessions.agentContext) { + await this.closeAgentContext(tenantSessions.agentContext) + delete this.tenantAgentContextMapping[tenantSessions.agentContext.contextCorrelationId] + } + + throw error + } + } + + return tenantSessions.agentContext + }) + } catch (error) { + this.logger.debug( + `Releasing session because an error occurred while getting the context for tenant ${tenantRecord.id}`, + { + errorMessage: error.message, + } + ) + // If there was an error acquiring the session, we MUST release it, otherwise this will lead to deadlocks over time. + this.sessionMutex.releaseSession() + + // Re-throw error + throw error + } + } + + /** + * End a session for the provided agent context. It will decrease the session count for the agent context. + * If the number of sessions is zero after the context for this session has been ended, the agent context will be closed. + */ + public async endAgentContextSession(agentContext: AgentContext): Promise { + this.logger.debug( + `Ending session for agent context with contextCorrelationId ${agentContext.contextCorrelationId}'` + ) + const hasTenantSessionMapping = this.hasTenantSessionMapping(agentContext.contextCorrelationId) + + // Custom handling for the root agent context. We don't keep track of the total number of sessions for the root + // agent context, and we always keep the dependency manager intact. + if (!hasTenantSessionMapping && agentContext.contextCorrelationId === this.rootAgentContext.contextCorrelationId) { + this.logger.debug('Ending session for root agent context. Not disposing dependency manager') + return + } + + // This should not happen + if (!hasTenantSessionMapping) { + this.logger.error( + `Unknown agent context with contextCorrelationId '${agentContext.contextCorrelationId}'. Cannot end session` + ) + throw new CredoError( + `Unknown agent context with contextCorrelationId '${agentContext.contextCorrelationId}'. Cannot end session` + ) + } + + await this.mutexForTenant(agentContext.contextCorrelationId).runExclusive(async () => { + this.logger.debug(`Acquired lock for tenant '${agentContext.contextCorrelationId}' to end session context`) + const tenantSessions = this.getTenantSessionsMapping(agentContext.contextCorrelationId) + + // TODO: check if session count is already 0 + tenantSessions.sessionCount-- + this.logger.debug( + `Decreased agent context session count for tenant '${agentContext.contextCorrelationId}' to ${tenantSessions.sessionCount}` + ) + + if (tenantSessions.sessionCount <= 0 && tenantSessions.agentContext) { + await this.closeAgentContext(tenantSessions.agentContext) + delete this.tenantAgentContextMapping[agentContext.contextCorrelationId] + } + }) + + // Release a session so new sessions can be acquired + this.sessionMutex.releaseSession() + } + + private hasTenantSessionMapping(tenantId: T): boolean { + return this.tenantAgentContextMapping[tenantId] !== undefined + } + + private getTenantSessionsMapping(tenantId: string): TenantContextSessions { + let tenantSessionMapping = this.tenantAgentContextMapping[tenantId] + if (tenantSessionMapping) return tenantSessionMapping + + tenantSessionMapping = { + sessionCount: 0, + mutex: withTimeout( + new Mutex(), + // NOTE: It can take a while to create an indy wallet. We're using RAW key derivation which should + // be fast enough to not cause a problem. This wil also only be problem when the wallet is being created + // for the first time or being acquired while wallet initialization is in progress. + this.tenantsModuleConfig.sessionAcquireTimeout, + new CredoError(`Error acquiring lock for tenant ${tenantId}. Wallet initialization or shutdown took too long.`) + ), + } + this.tenantAgentContextMapping[tenantId] = tenantSessionMapping + + return tenantSessionMapping + } + + private mutexForTenant(tenantId: string) { + const tenantSessions = this.getTenantSessionsMapping(tenantId) + + return tenantSessions.mutex + } + + private async createAgentContext(tenantRecord: TenantRecord) { + const tenantDependencyManager = this.rootAgentContext.dependencyManager.createChild() + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, key, keyDerivationMethod, ...strippedWalletConfig } = this.rootAgentContext.config?.walletConfig ?? {} + const tenantConfig = this.rootAgentContext.config.extend({ + ...tenantRecord.config, + walletConfig: { + ...strippedWalletConfig, + ...tenantRecord.config.walletConfig, + }, + }) + + const agentContext = new AgentContext({ + contextCorrelationId: tenantRecord.id, + dependencyManager: tenantDependencyManager, + }) + + tenantDependencyManager.registerInstance(AgentContext, agentContext) + tenantDependencyManager.registerInstance(AgentConfig, tenantConfig) + + // NOTE: we're using the wallet api here because that correctly handle creating if it doesn't exist yet + // and will also write the storage version to the storage, which is needed by the update assistant. We either + // need to move this out of the module, or just keep using the module here. + const walletApi = agentContext.dependencyManager.resolve(WalletApi) + + if (!tenantConfig.walletConfig) { + throw new WalletError('Cannot initialize tenant without Wallet config.') + } + await walletApi.initialize(tenantConfig.walletConfig) + + return agentContext + } + + private async closeAgentContext(agentContext: AgentContext) { + this.logger.debug(`Closing agent context for tenant '${agentContext.contextCorrelationId}'`) + await agentContext.dependencyManager.dispose() + } +} + +interface TenantContextSessions { + sessionCount: number + agentContext?: AgentContext + mutex: MutexInterface +} + +export interface TenantAgentContextMapping { + [tenantId: string]: TenantContextSessions | undefined +} diff --git a/packages/tenants/src/context/TenantSessionMutex.ts b/packages/tenants/src/context/TenantSessionMutex.ts new file mode 100644 index 0000000000..8779990572 --- /dev/null +++ b/packages/tenants/src/context/TenantSessionMutex.ts @@ -0,0 +1,104 @@ +import type { Logger } from '@credo-ts/core' +import type { MutexInterface } from 'async-mutex' + +import { CredoError } from '@credo-ts/core' +import { withTimeout, Mutex } from 'async-mutex' + +/** + * Keep track of the total number of tenant sessions currently active. This doesn't actually manage the tenant sessions itself, or have anything to do with + * the agent context. It merely counts the current number of sessions, and provides a mutex to lock new sessions from being created once the maximum number + * of sessions has been created. Session that can't be required withing the specified sessionsAcquireTimeout will throw an error. + */ +export class TenantSessionMutex { + private _currentSessions = 0 + public readonly maxSessions = Infinity + private sessionMutex: MutexInterface + private logger: Logger + + public constructor(logger: Logger, maxSessions = Infinity, sessionAcquireTimeout: number) { + this.logger = logger + + this.maxSessions = maxSessions + // Session mutex, it can take at most sessionAcquireTimeout to acquire a session, otherwise it will fail with the error below + this.sessionMutex = withTimeout( + new Mutex(), + sessionAcquireTimeout, + new CredoError(`Failed to acquire an agent context session within ${sessionAcquireTimeout}ms`) + ) + } + + /** + * Getter to retrieve the total number of current sessions. + */ + public get currentSessions() { + return this._currentSessions + } + + private set currentSessions(value: number) { + this._currentSessions = value + } + + /** + * Wait to acquire a session. Will use the session semaphore to keep total number of sessions limited. + * For each session that is acquired using this method, the sessions MUST be closed by calling `releaseSession`. + * Failing to do so can lead to deadlocks over time. + */ + public async acquireSession() { + // TODO: We should update this to be weighted + // This will allow to weight sessions for contexts that already exist lower than sessions + // for contexts that need to be created (new injection container, wallet session etc..) + // E.g. opening a context could weigh 5, adding sessions to it would be 1 for each + this.logger.debug('Acquiring tenant session') + + // If we're out of sessions, wait for one to be released. + if (this.sessionMutex.isLocked()) { + this.logger.debug('Session mutex is locked, waiting for it to unlock') + // FIXME: waitForUnlock doesn't work with withTimeout but provides a better API (would rather not acquire and lock) + // await this.sessionMutex.waitForUnlock() + // Workaround https://github.com/MatrixAI/js-async-locks/pull/3/files#diff-4ee6a7d91cb8428765713bc3045e1dda5d43214030657a9c04804e96d68778bfR46-R61 + await this.sessionMutex.acquire() + if (this.currentSessions < this.maxSessions) { + this.sessionMutex.release() + } + } + + this.logger.debug(`Increasing current session count to ${this.currentSessions + 1} (max: ${this.maxSessions})`) + // We have waited for the session to unlock, + this.currentSessions++ + + // If we reached the limit we should lock the session mutex + if (this.currentSessions >= this.maxSessions) { + this.logger.debug(`Reached max number of sessions ${this.maxSessions}, locking mutex`) + await this.sessionMutex.acquire() + } + + this.logger.debug(`Acquired tenant session (${this.currentSessions} / ${this.maxSessions})`) + } + + /** + * Release a session from the session mutex. If the total number of current sessions drops below + * the max number of sessions, the session mutex will be released so new sessions can be started. + */ + public releaseSession() { + this.logger.debug('Releasing tenant session') + + if (this.currentSessions > 0) { + this.logger.debug(`Decreasing current sessions to ${this.currentSessions - 1} (max: ${this.maxSessions})`) + this.currentSessions-- + } else { + this.logger.warn( + 'Total sessions is already at 0, and releasing a session should not happen in this case. Not decrementing current session count.' + ) + } + + // If the number of current sessions is lower than the max number of sessions we can release the mutex + if (this.sessionMutex.isLocked() && this.currentSessions < this.maxSessions) { + this.logger.debug( + `Releasing session mutex as number of current sessions ${this.currentSessions} is below max number of sessions ${this.maxSessions}` + ) + // Even though marked as deprecated, it is not actually deprecated and will be kept + // https://github.com/DirtyHairy/async-mutex/issues/50#issuecomment-1007785141 + this.sessionMutex.release() + } + } +} diff --git a/packages/tenants/src/context/__tests__/TenantAgentContextProvider.test.ts b/packages/tenants/src/context/__tests__/TenantAgentContextProvider.test.ts new file mode 100644 index 0000000000..2b84f82b3b --- /dev/null +++ b/packages/tenants/src/context/__tests__/TenantAgentContextProvider.test.ts @@ -0,0 +1,170 @@ +import type { AgentContext } from '@credo-ts/core' + +import { Key } from '@credo-ts/core' + +import { EventEmitter } from '../../../../core/src/agent/EventEmitter' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { TenantRecord, TenantRoutingRecord } from '../../repository' +import { TenantRecordService } from '../../services/TenantRecordService' +import { TenantAgentContextProvider } from '../TenantAgentContextProvider' +import { TenantSessionCoordinator } from '../TenantSessionCoordinator' + +jest.mock('../../../../core/src/agent/EventEmitter') +jest.mock('../../services/TenantRecordService') +jest.mock('../TenantSessionCoordinator') + +const EventEmitterMock = EventEmitter as jest.Mock +const TenantRecordServiceMock = TenantRecordService as jest.Mock +const TenantSessionCoordinatorMock = TenantSessionCoordinator as jest.Mock + +const tenantRecordService = new TenantRecordServiceMock() +const tenantSessionCoordinator = new TenantSessionCoordinatorMock() + +const rootAgentContext = getAgentContext() +const agentConfig = getAgentConfig('TenantAgentContextProvider') +const eventEmitter = new EventEmitterMock() + +const tenantAgentContextProvider = new TenantAgentContextProvider( + tenantRecordService, + rootAgentContext, + eventEmitter, + tenantSessionCoordinator, + agentConfig.logger +) + +const inboundMessage = { + protected: + 'eyJlbmMiOiJ4Y2hhY2hhMjBwb2x5MTMwNV9pZXRmIiwidHlwIjoiSldNLzEuMCIsImFsZyI6IkF1dGhjcnlwdCIsInJlY2lwaWVudHMiOlt7ImVuY3J5cHRlZF9rZXkiOiIta3AzRlREbzdNTnlqSVlvWkhFdFhzMzRLTlpEODBIc2tEVTcxSVg5ejJpdVphUy1PVHhNc21KUWNCeDN1Y1lVIiwiaGVhZGVyIjp7ImtpZCI6IjdQd0ZrMXB2V2JOTkUyUDRDTFlnWW5jUXJMc0VSRUx2cDZmQVRaTktVZnpXIiwiaXYiOiJBSElTQk94MWhrWk5obkVwWndJNFlWZ09HNnQ3RDhlQiIsInNlbmRlciI6IjRMTnFHWGJ3SGlPU01uQThsV1M3bEpocER6aGY5UUIyYjNPUVZyWkkyeEctWWJkT1BUamR6WWRGamdpbFo4MF95bXhKSHpoWmp1bHhPeEVvek81VUhDQzJ3bnV3MHo3OVRRWjE5MFgzUFI2WWlrSUVIcms2N3A4V09WTT0ifX1dfQ==', + iv: 'CfsDEiS63uOJRZa-', + ciphertext: + 'V6V23nNdKSn2a0jqrjQoU8cj6Ks9w9_4eqE0_856hjd_gxYxqT4W0M9sZ5ov1zlOptrBz6wGDK-BoEbOzvgNqHmiUS5h_VvVuEIevpp9xYrCLlNigrXJEtoDGWkVVpYq3i14l5XGMYCu2zTL7QJxHqDrzRAG-5Iqht0FY45u4L471CMvj31XuZps6I_wl-TJWfeZbAS1Knp_dEnElqtbkcctOKjnvaosk2WYaIrQXRiJxk-4URGnmMAQxjSSt5KuzE3LQ_fa_u5PQLC0EaOsg5M9fYBSIB1_090fQ0QTPXLB69pyiFzLmb016vHGIG5nAbqNKS7fmkhxo7iMkOBlR5d5FpCAguXln4Fg4Q4tZgEaPXVkqmayTvLyeJqXa22dbNDhaPGrvjlNimn8moP8qSC0Avoozn4BDdLrSQxs5daYcH0JYhqz7VII2Mrb2gp2LMsQsoy-UrChTZSBeyjWdapqOzMc8yOhKYXwA_du0RfSaPFe8VJyMo4X73LC6-i1QU5xg3fZKiKJWRrTUazLvdNEXm79DV76LxylodY7OeCGH6E2amF1k10VC2eYwNCI3CfXS8uvjDEIQGgsVETCqwclWKxD-AhQEwZFRlNhZjlfbUyOKH8WAoikloN75T2AZiTivzlE5ZvnPUU_z4LJI02z-vpIMEjkHKcgjx0jDFi3VkfLPiOG4P6btZpxjISfZvWcCiebAhs5jAGX2zNjYiPErJx33zOclHYS8KEZB3fdAdpAZKdYlPyAOFpN8r21kn6HPYjm-3NmTqrHAjDIMgt0mJ6AI58XOnoqRWN7Hl1HWhy9qkt0AqRVJZIIoyFefvKRJvotsv9aj1ZGPqnrkR2Hpj7u-K8VOKreIg4kWYyVbAF8Oift9CrqJ4olOOSUOQZ_NL36qGJc1RCR_wRnTWikoRR_o4h4fDZtxTQG9nUgbAoaAumJAbp5mxrYBW6KVZ-Jm9rhdNnRRnvvd1e_uW-X66_9B5g0GM3BmsXK-ARpJP6ZYmpQYiVFjrDxOSrvq1gD3aPi0SCP6mYoNvemGoXFhGTPMTGQvy1RAwY9t_BosZNEMZMfYTzHxWhN55yXd0861uv5nFe_aLKQcdin8QySW-FS0jcExnRostY922fqT5JYPBINqAr59u8gpzX-N9DgczL1WjuKkwyezLrcCR1IaG9gZrEIJxLDRGHvBno6ZkqmLiuAx3LZxgrT5yN2fI7WjO5HHQMVLn7rVF-THmpLNTZmmsoJ2ZU9ZGeAMKBpcfIYIHgKHF1vumr_h2uCbvxlwqigm5A-dSmto0Fv9xewfDhZ5TvE-TKwHpwmb0OG4kZqC3CnMmzh_oSOK0Cc6ovldiVOUvXdVZJiSD9KEFxn1YmDNbsdMDP9GAAWAknFmdBp5x7DCCt6sMjCVuw1hbELAGXusipfdvfb4htSN5FR4k72efenEr0glFtDk7s5EvWTWsBZyv92P5et-70MjTKGtMJaC4uCBL3li3ty397yKKcJly2Fog5N0phqPHPHg_-CGZ8YpkcM_q3Ijcc8db701K2TShiG97AjOdCZCSgK8OGv_UFXxXXxiwrdQOM0Jfg0TCz_ESxQLAlepK4JQplE_kR8k3jDf5nH4SMueobioPfkLQ92lCFXBOCX3ugoJJnnb49CbQfi-49PAHsGaTopLXxZoEdf6kgJ8phFakBoMmbLE1zIV43oVR8T-zZYsr377q6c6LY46PyYusP7CB5wgXbG4nyJZ_zGZHvY_hnbcE2-EuysmzQV4-6rJdLdT8FSyX_Xo-K2ZmX-riFUcKamoFWmO3CDtexn-ZgtAIJpdjAApWHFxZWLI6xx67OgHl8GT2HIs_BdoetFvmj4tJ_Aw8_Mmb9W37B4Esom1Tg3XxxfLqj24s7UlgUwYFblkYtB1L9-9DkNlZZWkYJz-A28WW6OSqIYNw0ASyNDEp3Mwy0SHDUYh10NUmQ4C476QRNmr32Jv_6AiTGj1thibFg_Ewd_kdvvo0E7VL6gktZNh9kIT-EPgFAobR5IpG0_V1dJ7pEQPKN-n7nc6gWgry7kxNIfS4LcbPwVDsUzJiJ4Qlw=', + tag: 'goWiDaoxy4mHHRnkPiux4Q==', +} + +describe('TenantAgentContextProvider', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('getAgentContextForContextCorrelationId', () => { + test('retrieves the tenant and calls tenant session coordinator', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant1', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'test-wallet', + key: 'test-wallet-key', + }, + }, + storageVersion: '0.5', + }) + + const tenantAgentContext = jest.fn() as unknown as AgentContext + + mockFunction(tenantRecordService.getTenantById).mockResolvedValue(tenantRecord) + mockFunction(tenantSessionCoordinator.getContextForSession).mockResolvedValue(tenantAgentContext) + + const returnedAgentContext = await tenantAgentContextProvider.getAgentContextForContextCorrelationId('tenant1') + + expect(tenantRecordService.getTenantById).toHaveBeenCalledWith(rootAgentContext, 'tenant1') + expect(tenantSessionCoordinator.getContextForSession).toHaveBeenCalledWith(tenantRecord, { + runInMutex: undefined, + }) + expect(returnedAgentContext).toBe(tenantAgentContext) + }) + }) + + describe('getContextForInboundMessage', () => { + test('directly calls get agent context if tenant id has been provided in the contextCorrelationId', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant1', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'test-wallet', + key: 'test-wallet-key', + }, + }, + storageVersion: '0.5', + }) + + const tenantAgentContext = jest.fn() as unknown as AgentContext + + mockFunction(tenantRecordService.getTenantById).mockResolvedValue(tenantRecord) + mockFunction(tenantSessionCoordinator.getContextForSession).mockResolvedValue(tenantAgentContext) + + const returnedAgentContext = await tenantAgentContextProvider.getContextForInboundMessage( + {}, + { contextCorrelationId: 'tenant1' } + ) + + expect(tenantRecordService.getTenantById).toHaveBeenCalledWith(rootAgentContext, 'tenant1') + expect(tenantSessionCoordinator.getContextForSession).toHaveBeenCalledWith(tenantRecord, { + runInMutex: undefined, + }) + expect(returnedAgentContext).toBe(tenantAgentContext) + expect(tenantRecordService.findTenantRoutingRecordByRecipientKey).not.toHaveBeenCalled() + }) + + test('throws an error if not contextCorrelationId is provided and no tenant id could be extracted from the inbound message', async () => { + // no routing records found + mockFunction(tenantRecordService.findTenantRoutingRecordByRecipientKey).mockResolvedValue(null) + + await expect(tenantAgentContextProvider.getContextForInboundMessage(inboundMessage)).rejects.toThrowError( + "Couldn't determine tenant id for inbound message. Unable to create context" + ) + }) + + test('finds the tenant id based on the inbound message recipient keys and calls get agent context', async () => { + const tenantRoutingRecord = new TenantRoutingRecord({ + recipientKeyFingerprint: 'z6MkkrCJLG5Mr8rqLXDksuWXPtAQfv95q7bHW7a6HqLLPtmt', + tenantId: 'tenant1', + }) + + const tenantRecord = new TenantRecord({ + id: 'tenant1', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'test-wallet', + key: 'test-wallet-key', + }, + }, + storageVersion: '0.5', + }) + + const tenantAgentContext = jest.fn() as unknown as AgentContext + mockFunction(tenantRecordService.findTenantRoutingRecordByRecipientKey).mockResolvedValue(tenantRoutingRecord) + + mockFunction(tenantRecordService.getTenantById).mockResolvedValue(tenantRecord) + mockFunction(tenantSessionCoordinator.getContextForSession).mockResolvedValue(tenantAgentContext) + + const returnedAgentContext = await tenantAgentContextProvider.getContextForInboundMessage(inboundMessage) + + expect(tenantRecordService.getTenantById).toHaveBeenCalledWith(rootAgentContext, 'tenant1') + expect(tenantSessionCoordinator.getContextForSession).toHaveBeenCalledWith(tenantRecord, { + runInMutex: undefined, + }) + expect(returnedAgentContext).toBe(tenantAgentContext) + expect(tenantRecordService.findTenantRoutingRecordByRecipientKey).toHaveBeenCalledWith( + rootAgentContext, + expect.any(Key) + ) + + const actualKey = mockFunction(tenantRecordService.findTenantRoutingRecordByRecipientKey).mock.calls[0][1] + // Based on the recipient key from the inboundMessage protected header above + expect(actualKey.fingerprint).toBe('z6MkkrCJLG5Mr8rqLXDksuWXPtAQfv95q7bHW7a6HqLLPtmt') + }) + }) + + describe('disposeAgentContext', () => { + test('calls disposeAgentContextSession on tenant session coordinator', async () => { + const tenantAgentContext = jest.fn() as unknown as AgentContext + + await tenantAgentContextProvider.endSessionForAgentContext(tenantAgentContext) + + expect(tenantSessionCoordinator.endAgentContextSession).toHaveBeenCalledWith(tenantAgentContext) + }) + }) +}) diff --git a/packages/tenants/src/context/__tests__/TenantSessionCoordinator.test.ts b/packages/tenants/src/context/__tests__/TenantSessionCoordinator.test.ts new file mode 100644 index 0000000000..a480832174 --- /dev/null +++ b/packages/tenants/src/context/__tests__/TenantSessionCoordinator.test.ts @@ -0,0 +1,282 @@ +import type { TenantAgentContextMapping } from '../TenantSessionCoordinator' +import type { DependencyManager } from '@credo-ts/core' + +import { AgentConfig, AgentContext, WalletApi } from '@credo-ts/core' +import { Mutex, withTimeout } from 'async-mutex' + +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import testLogger from '../../../../core/tests/logger' +import { TenantsModuleConfig } from '../../TenantsModuleConfig' +import { TenantRecord } from '../../repository' +import { TenantSessionCoordinator } from '../TenantSessionCoordinator' +import { TenantSessionMutex } from '../TenantSessionMutex' + +jest.mock('../TenantSessionMutex') +const TenantSessionMutexMock = TenantSessionMutex as jest.Mock + +// tenantAgentContextMapping is private, but we need to access it to properly test this class. Adding type override to +// make sure we don't get a lot of type errors. +type PublicTenantAgentContextMapping = Omit & { + tenantAgentContextMapping: TenantAgentContextMapping +} + +const wallet = { + initialize: jest.fn(), +} as unknown as WalletApi + +const agentContext = getAgentContext({ + agentConfig: getAgentConfig('TenantSessionCoordinator'), +}) + +agentContext.dependencyManager.registerInstance(WalletApi, wallet) +const tenantSessionCoordinator = new TenantSessionCoordinator( + agentContext, + testLogger, + new TenantsModuleConfig() +) as unknown as PublicTenantAgentContextMapping + +const tenantSessionMutexMock = TenantSessionMutexMock.mock.instances[0] + +describe('TenantSessionCoordinator', () => { + afterEach(() => { + tenantSessionCoordinator.tenantAgentContextMapping = {} + jest.clearAllMocks() + }) + + describe('getContextForSession', () => { + test('returns the context from the tenantAgentContextMapping and increases the session count if already available', async () => { + const tenant1AgentContext = jest.fn() as unknown as AgentContext + + const tenant1 = { + agentContext: tenant1AgentContext, + mutex: new Mutex(), + sessionCount: 1, + } + tenantSessionCoordinator.tenantAgentContextMapping = { + tenant1, + } + + const tenantRecord = new TenantRecord({ + id: 'tenant1', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'test-wallet', + key: 'test-wallet-key', + }, + }, + storageVersion: '0.5', + }) + + const tenantAgentContext = await tenantSessionCoordinator.getContextForSession(tenantRecord) + expect(tenantSessionMutexMock.acquireSession).toHaveBeenCalledTimes(1) + expect(tenantAgentContext).toBe(tenant1AgentContext) + expect(tenant1.sessionCount).toBe(2) + }) + + test('creates a new agent context, initializes the wallet and stores it in the tenant agent context mapping', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant1', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'test-wallet', + key: 'test-wallet-key', + }, + }, + storageVersion: '0.5', + }) + const createChildSpy = jest.spyOn(agentContext.dependencyManager, 'createChild') + const extendSpy = jest.spyOn(agentContext.config, 'extend') + + const tenantDependencyManager = { + registerInstance: jest.fn(), + resolve: jest.fn(() => wallet), + } as unknown as DependencyManager + + createChildSpy.mockReturnValue(tenantDependencyManager) + + const tenantAgentContext = await tenantSessionCoordinator.getContextForSession(tenantRecord) + + expect(wallet.initialize).toHaveBeenCalledWith({ + ...tenantRecord.config.walletConfig, + storage: { config: { inMemory: true }, type: 'sqlite' }, + }) + expect(tenantSessionMutexMock.acquireSession).toHaveBeenCalledTimes(1) + expect(extendSpy).toHaveBeenCalledWith({ + ...tenantRecord.config, + walletConfig: { ...tenantRecord.config.walletConfig, storage: { config: { inMemory: true }, type: 'sqlite' } }, + }) + expect(createChildSpy).toHaveBeenCalledWith() + expect(tenantDependencyManager.registerInstance).toHaveBeenCalledWith(AgentContext, expect.any(AgentContext)) + expect(tenantDependencyManager.registerInstance).toHaveBeenCalledWith(AgentConfig, expect.any(AgentConfig)) + + expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toEqual({ + agentContext: tenantAgentContext, + mutex: expect.objectContaining({ + acquire: expect.any(Function), + cancel: expect.any(Function), + isLocked: expect.any(Function), + release: expect.any(Function), + runExclusive: expect.any(Function), + waitForUnlock: expect.any(Function), + }), + sessionCount: 1, + }) + + expect(tenantAgentContext.contextCorrelationId).toBe('tenant1') + }) + + test('rethrows error and releases session if error is throw while getting agent context', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant1', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'test-wallet', + key: 'test-wallet-key', + }, + }, + storageVersion: '0.5', + }) + + // Throw error during wallet initialization + mockFunction(wallet.initialize).mockRejectedValue(new Error('Test error')) + + await expect(tenantSessionCoordinator.getContextForSession(tenantRecord)).rejects.toThrowError('Test error') + + expect(wallet.initialize).toHaveBeenCalledWith({ + ...tenantRecord.config.walletConfig, + storage: { config: { inMemory: true }, type: 'sqlite' }, + }) + expect(tenantSessionMutexMock.acquireSession).toHaveBeenCalledTimes(1) + expect(tenantSessionMutexMock.releaseSession).toHaveBeenCalledTimes(1) + }) + + test('locks and waits for lock to release when initialization is already in progress', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant1', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'test-wallet', + key: 'test-wallet-key', + }, + }, + storageVersion: '0.5', + }) + + // Add timeout to mock the initialization and we can test that the mutex is used. + mockFunction(wallet.initialize).mockReturnValueOnce(new Promise((resolve) => setTimeout(resolve, 100))) + + // Start two context session creations (but don't await). It should set the mutex property on the tenant agent context mapping. + const tenantAgentContext1Promise = tenantSessionCoordinator.getContextForSession(tenantRecord) + const tenantAgentContext2Promise = tenantSessionCoordinator.getContextForSession(tenantRecord) + expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toBeUndefined() + + // Await first session promise, should have 1 session + const tenantAgentContext1 = await tenantAgentContext1Promise + expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toEqual({ + agentContext: tenantAgentContext1, + sessionCount: 1, + mutex: expect.objectContaining({ + acquire: expect.any(Function), + cancel: expect.any(Function), + isLocked: expect.any(Function), + release: expect.any(Function), + runExclusive: expect.any(Function), + waitForUnlock: expect.any(Function), + }), + }) + + // There should be two sessions active now + const tenantAgentContext2 = await tenantAgentContext2Promise + expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toEqual({ + agentContext: tenantAgentContext1, + sessionCount: 2, + mutex: expect.objectContaining({ + acquire: expect.any(Function), + cancel: expect.any(Function), + isLocked: expect.any(Function), + release: expect.any(Function), + runExclusive: expect.any(Function), + waitForUnlock: expect.any(Function), + }), + }) + + // Initialize should only be called once + expect(wallet.initialize).toHaveBeenCalledWith({ + ...tenantRecord.config.walletConfig, + storage: { config: { inMemory: true }, type: 'sqlite' }, + }) + expect(wallet.initialize).toHaveBeenCalledTimes(1) + + expect(tenantAgentContext1).toBe(tenantAgentContext2) + }) + }) + + describe('endAgentContextSessions', () => { + test('Returns early and does not release a session if the agent context correlation id matches the root agent context', async () => { + const rootAgentContextMock = { + contextCorrelationId: 'mock', + dependencyManager: { dispose: jest.fn() }, + } as unknown as AgentContext + await tenantSessionCoordinator.endAgentContextSession(rootAgentContextMock) + + expect(tenantSessionMutexMock.releaseSession).not.toHaveBeenCalled() + }) + + test('throws an error if not agent context session exists for the tenant', async () => { + const tenantAgentContextMock = { contextCorrelationId: 'does-not-exist' } as unknown as AgentContext + expect(tenantSessionCoordinator.endAgentContextSession(tenantAgentContextMock)).rejects.toThrowError( + `Unknown agent context with contextCorrelationId 'does-not-exist'. Cannot end session` + ) + }) + + test('decreases the tenant session count and calls release session', async () => { + const tenant1AgentContext = { contextCorrelationId: 'tenant1' } as unknown as AgentContext + + const tenant1 = { + agentContext: tenant1AgentContext, + mutex: withTimeout(new Mutex(), 0), + sessionCount: 2, + } + tenantSessionCoordinator.tenantAgentContextMapping = { + tenant1, + } + + await tenantSessionCoordinator.endAgentContextSession(tenant1AgentContext) + + // Should have reduced session count by one + expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toEqual({ + agentContext: tenant1AgentContext, + mutex: tenant1.mutex, + sessionCount: 1, + }) + expect(tenantSessionMutexMock.releaseSession).toHaveBeenCalledTimes(1) + }) + + test('closes the agent context and removes the agent context mapping if the number of sessions reaches 0', async () => { + const tenant1AgentContext = { + dependencyManager: { dispose: jest.fn() }, + contextCorrelationId: 'tenant1', + } as unknown as AgentContext + + const tenant1 = { + agentContext: tenant1AgentContext, + mutex: withTimeout(new Mutex(), 0), + sessionCount: 1, + } + tenantSessionCoordinator.tenantAgentContextMapping = { + tenant1, + } + + await tenantSessionCoordinator.endAgentContextSession(tenant1AgentContext) + + // Should have removed tenant1 + expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toBeUndefined() + expect(tenant1AgentContext.dependencyManager.dispose).toHaveBeenCalledTimes(1) + expect(tenantSessionMutexMock.releaseSession).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/tenants/src/context/__tests__/TenantSessionMutex.test.ts b/packages/tenants/src/context/__tests__/TenantSessionMutex.test.ts new file mode 100644 index 0000000000..6430e9b831 --- /dev/null +++ b/packages/tenants/src/context/__tests__/TenantSessionMutex.test.ts @@ -0,0 +1,61 @@ +import testLogger from '../../../../core/tests/logger' +import { TenantSessionMutex } from '../TenantSessionMutex' + +describe('TenantSessionMutex', () => { + test('correctly sets values', () => { + const tenantSessionMutex = new TenantSessionMutex(testLogger, 12, 50) + + expect(tenantSessionMutex.maxSessions).toBe(12) + expect(tenantSessionMutex.currentSessions).toBe(0) + }) + + describe('acquireSession', () => { + test('should immediately acquire the session if maxSessions has not been reached', async () => { + const tenantSessionMutex = new TenantSessionMutex(testLogger, 1, 0) + + expect(tenantSessionMutex.currentSessions).toBe(0) + await expect(tenantSessionMutex.acquireSession()).resolves.toBeUndefined() + expect(tenantSessionMutex.currentSessions).toBe(1) + }) + + test('should throw an error if a session could not be acquired within sessionAcquireTimeout', async () => { + const tenantSessionMutex = new TenantSessionMutex(testLogger, 1, 0) + + expect(tenantSessionMutex.currentSessions).toBe(0) + await tenantSessionMutex.acquireSession() + expect(tenantSessionMutex.currentSessions).toBe(1) + await expect(tenantSessionMutex.acquireSession()).rejects.toThrowError( + 'Failed to acquire an agent context session within 0ms' + ) + expect(tenantSessionMutex.currentSessions).toBe(1) + }) + }) + + describe('releaseSession', () => { + test('should release the session', async () => { + const tenantSessionMutex = new TenantSessionMutex(testLogger, 1, 0) + expect(tenantSessionMutex.currentSessions).toBe(0) + + await tenantSessionMutex.acquireSession() + expect(tenantSessionMutex.currentSessions).toBe(1) + + expect(tenantSessionMutex.releaseSession()).toBeUndefined() + expect(tenantSessionMutex.currentSessions).toBe(0) + }) + + test('resolves an acquire sessions if another sessions is being released', async () => { + const tenantSessionMutex = new TenantSessionMutex(testLogger, 1, 100) + expect(tenantSessionMutex.currentSessions).toBe(0) + + await tenantSessionMutex.acquireSession() + expect(tenantSessionMutex.currentSessions).toBe(1) + + const acquirePromise = tenantSessionMutex.acquireSession() + tenantSessionMutex.releaseSession() + expect(tenantSessionMutex.currentSessions).toBe(0) + + await acquirePromise + expect(tenantSessionMutex.currentSessions).toBe(1) + }) + }) +}) diff --git a/packages/tenants/src/context/types.ts b/packages/tenants/src/context/types.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/tenants/src/index.ts b/packages/tenants/src/index.ts new file mode 100644 index 0000000000..5c3adb8762 --- /dev/null +++ b/packages/tenants/src/index.ts @@ -0,0 +1,5 @@ +export { TenantRecord, TenantRecordProps } from './repository/TenantRecord' +export * from './TenantsModule' +export * from './TenantsApi' +export * from './TenantsApiOptions' +export * from './TenantsModuleConfig' diff --git a/packages/tenants/src/models/TenantConfig.ts b/packages/tenants/src/models/TenantConfig.ts new file mode 100644 index 0000000000..3a6e856c7c --- /dev/null +++ b/packages/tenants/src/models/TenantConfig.ts @@ -0,0 +1,5 @@ +import type { InitConfig, WalletConfig } from '@credo-ts/core' + +export type TenantConfig = Pick & { + walletConfig: Pick +} diff --git a/packages/tenants/src/repository/TenantRecord.ts b/packages/tenants/src/repository/TenantRecord.ts new file mode 100644 index 0000000000..56e866ded4 --- /dev/null +++ b/packages/tenants/src/repository/TenantRecord.ts @@ -0,0 +1,55 @@ +import type { TenantConfig } from '../models/TenantConfig' +import type { RecordTags, TagsBase, VersionString } from '@credo-ts/core' + +import { BaseRecord, utils } from '@credo-ts/core' + +export type TenantRecordTags = RecordTags + +export interface TenantRecordProps { + id?: string + createdAt?: Date + config: TenantConfig + tags?: TagsBase + storageVersion: VersionString +} + +export type DefaultTenantRecordTags = { + label: string + storageVersion: VersionString +} + +export class TenantRecord extends BaseRecord { + public static readonly type = 'TenantRecord' + public readonly type = TenantRecord.type + + public config!: TenantConfig + + /** + * The storage version that is used by this tenant. Can be used to know if the tenant is ready to be used + * with the current version of the application. + * + * @default 0.4 from 0.5 onwards we set the storage version on creation, so if no value + * is stored, it means the storage version is 0.4 (when multi-tenancy was introduced) + */ + public storageVersion: VersionString = '0.4' + + public constructor(props: TenantRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags ?? {} + this.config = props.config + this.storageVersion = props.storageVersion + } + } + + public getTags() { + return { + ...this._tags, + label: this.config.label, + storageVersion: this.storageVersion, + } + } +} diff --git a/packages/tenants/src/repository/TenantRepository.ts b/packages/tenants/src/repository/TenantRepository.ts new file mode 100644 index 0000000000..5141fc897e --- /dev/null +++ b/packages/tenants/src/repository/TenantRepository.ts @@ -0,0 +1,19 @@ +import type { AgentContext } from '@credo-ts/core' + +import { Repository, StorageService, InjectionSymbols, EventEmitter, inject, injectable } from '@credo-ts/core' + +import { TenantRecord } from './TenantRecord' + +@injectable() +export class TenantRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(TenantRecord, storageService, eventEmitter) + } + + public async findByLabel(agentContext: AgentContext, label: string): Promise { + return this.findByQuery(agentContext, { label }) + } +} diff --git a/packages/tenants/src/repository/TenantRoutingRecord.ts b/packages/tenants/src/repository/TenantRoutingRecord.ts new file mode 100644 index 0000000000..36dde915d7 --- /dev/null +++ b/packages/tenants/src/repository/TenantRoutingRecord.ts @@ -0,0 +1,47 @@ +import type { RecordTags, TagsBase } from '@credo-ts/core' + +import { BaseRecord, utils } from '@credo-ts/core' + +export type TenantRoutingRecordTags = RecordTags + +type DefaultTenantRoutingRecordTags = { + tenantId: string + recipientKeyFingerprint: string +} + +export interface TenantRoutingRecordProps { + id?: string + createdAt?: Date + tags?: TagsBase + + tenantId: string + recipientKeyFingerprint: string +} + +export class TenantRoutingRecord extends BaseRecord { + public static readonly type = 'TenantRoutingRecord' + public readonly type = TenantRoutingRecord.type + + public tenantId!: string + public recipientKeyFingerprint!: string + + public constructor(props: TenantRoutingRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags ?? {} + this.tenantId = props.tenantId + this.recipientKeyFingerprint = props.recipientKeyFingerprint + } + } + + public getTags() { + return { + ...this._tags, + tenantId: this.tenantId, + recipientKeyFingerprint: this.recipientKeyFingerprint, + } + } +} diff --git a/packages/tenants/src/repository/TenantRoutingRepository.ts b/packages/tenants/src/repository/TenantRoutingRepository.ts new file mode 100644 index 0000000000..3a6ec1c8b1 --- /dev/null +++ b/packages/tenants/src/repository/TenantRoutingRepository.ts @@ -0,0 +1,21 @@ +import type { AgentContext, Key } from '@credo-ts/core' + +import { Repository, StorageService, InjectionSymbols, EventEmitter, inject, injectable } from '@credo-ts/core' + +import { TenantRoutingRecord } from './TenantRoutingRecord' + +@injectable() +export class TenantRoutingRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(TenantRoutingRecord, storageService, eventEmitter) + } + + public findByRecipientKey(agentContext: AgentContext, key: Key) { + return this.findSingleByQuery(agentContext, { + recipientKeyFingerprint: key.fingerprint, + }) + } +} diff --git a/packages/tenants/src/repository/__tests__/TenantRecord.test.ts b/packages/tenants/src/repository/__tests__/TenantRecord.test.ts new file mode 100644 index 0000000000..6ba6b23344 --- /dev/null +++ b/packages/tenants/src/repository/__tests__/TenantRecord.test.ts @@ -0,0 +1,95 @@ +import { JsonTransformer } from '@credo-ts/core' + +import { TenantRecord } from '../TenantRecord' + +describe('TenantRecord', () => { + test('sets the values passed in the constructor on the record', () => { + const createdAt = new Date() + const tenantRecord = new TenantRecord({ + id: 'tenant-id', + createdAt, + tags: { + some: 'tag', + }, + config: { + label: 'test', + walletConfig: { + id: 'test', + key: 'test', + }, + }, + storageVersion: '0.5', + }) + + expect(tenantRecord.type).toBe('TenantRecord') + expect(tenantRecord.id).toBe('tenant-id') + expect(tenantRecord.createdAt).toBe(createdAt) + expect(tenantRecord.config).toEqual({ + label: 'test', + walletConfig: { + id: 'test', + key: 'test', + }, + }) + expect(tenantRecord.getTags()).toEqual({ + label: 'test', + some: 'tag', + storageVersion: '0.5', + }) + }) + + test('serializes and deserializes', () => { + const createdAt = new Date('2022-02-02') + const tenantRecord = new TenantRecord({ + id: 'tenant-id', + createdAt, + tags: { + some: 'tag', + }, + config: { + label: 'test', + walletConfig: { + id: 'test', + key: 'test', + }, + }, + storageVersion: '0.5', + }) + + const json = tenantRecord.toJSON() + expect(json).toEqual({ + id: 'tenant-id', + createdAt: '2022-02-02T00:00:00.000Z', + metadata: {}, + storageVersion: '0.5', + _tags: { + some: 'tag', + }, + config: { + label: 'test', + walletConfig: { + id: 'test', + key: 'test', + }, + }, + }) + + const instance = JsonTransformer.fromJSON(json, TenantRecord) + + expect(instance.type).toBe('TenantRecord') + expect(instance.id).toBe('tenant-id') + expect(instance.createdAt.getTime()).toBe(createdAt.getTime()) + expect(instance.config).toEqual({ + label: 'test', + walletConfig: { + id: 'test', + key: 'test', + }, + }) + expect(instance.getTags()).toEqual({ + label: 'test', + some: 'tag', + storageVersion: '0.5', + }) + }) +}) diff --git a/packages/tenants/src/repository/__tests__/TenantRoutingRecord.test.ts b/packages/tenants/src/repository/__tests__/TenantRoutingRecord.test.ts new file mode 100644 index 0000000000..960c3b911a --- /dev/null +++ b/packages/tenants/src/repository/__tests__/TenantRoutingRecord.test.ts @@ -0,0 +1,78 @@ +import { JsonTransformer } from '@credo-ts/core' + +import { TenantRoutingRecord } from '../TenantRoutingRecord' + +describe('TenantRoutingRecord', () => { + test('sets the values passed in the constructor on the record', () => { + const createdAt = new Date() + const tenantRoutingRecord = new TenantRoutingRecord({ + id: 'record-id', + createdAt, + tags: { + some: 'tag', + }, + tenantId: 'tenant-id', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + }) + + expect(tenantRoutingRecord.type).toBe('TenantRoutingRecord') + expect(tenantRoutingRecord.id).toBe('record-id') + expect(tenantRoutingRecord.tenantId).toBe('tenant-id') + expect(tenantRoutingRecord.createdAt).toBe(createdAt) + expect(tenantRoutingRecord.recipientKeyFingerprint).toBe('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + expect(tenantRoutingRecord.getTags()).toMatchObject({ + some: 'tag', + }) + }) + + test('returns the default tags', () => { + const tenantRoutingRecord = new TenantRoutingRecord({ + tenantId: 'tenant-id', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + }) + + expect(tenantRoutingRecord.getTags()).toMatchObject({ + tenantId: 'tenant-id', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + }) + }) + + test('serializes and deserializes', () => { + const createdAt = new Date('2022-02-02') + const tenantRoutingRecord = new TenantRoutingRecord({ + id: 'record-id', + createdAt, + tags: { + some: 'tag', + }, + tenantId: 'tenant-id', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + }) + + const json = tenantRoutingRecord.toJSON() + expect(json).toEqual({ + id: 'record-id', + createdAt: '2022-02-02T00:00:00.000Z', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + tenantId: 'tenant-id', + metadata: {}, + _tags: { + some: 'tag', + }, + }) + + const instance = JsonTransformer.fromJSON(json, TenantRoutingRecord) + + expect(instance.type).toBe('TenantRoutingRecord') + expect(instance.id).toBe('record-id') + expect(instance.createdAt.getTime()).toBe(createdAt.getTime()) + expect(instance.recipientKeyFingerprint).toBe('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + expect(instance.tenantId).toBe('tenant-id') + + expect(instance.getTags()).toMatchObject({ + some: 'tag', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + tenantId: 'tenant-id', + }) + }) +}) diff --git a/packages/tenants/src/repository/__tests__/TenantRoutingRepository.test.ts b/packages/tenants/src/repository/__tests__/TenantRoutingRepository.test.ts new file mode 100644 index 0000000000..55052bf4dd --- /dev/null +++ b/packages/tenants/src/repository/__tests__/TenantRoutingRepository.test.ts @@ -0,0 +1,44 @@ +import type { StorageService, EventEmitter } from '@credo-ts/core' + +import { Key } from '@credo-ts/core' + +import { getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { TenantRoutingRecord } from '../TenantRoutingRecord' +import { TenantRoutingRepository } from '../TenantRoutingRepository' + +const storageServiceMock = { + findByQuery: jest.fn(), +} as unknown as StorageService +const eventEmitter = jest.fn() as unknown as EventEmitter +const agentContext = getAgentContext() + +const tenantRoutingRepository = new TenantRoutingRepository(storageServiceMock, eventEmitter) + +describe('TenantRoutingRepository', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('findByRecipientKey', () => { + test('it should correctly transform the key to a fingerprint and return the routing record', async () => { + const key = Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + const tenantRoutingRecord = new TenantRoutingRecord({ + recipientKeyFingerprint: key.fingerprint, + tenantId: 'tenant-id', + }) + + mockFunction(storageServiceMock.findByQuery).mockResolvedValue([tenantRoutingRecord]) + const returnedRecord = await tenantRoutingRepository.findByRecipientKey(agentContext, key) + + expect(storageServiceMock.findByQuery).toHaveBeenCalledWith( + agentContext, + TenantRoutingRecord, + { + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + }, + undefined + ) + expect(returnedRecord).toBe(tenantRoutingRecord) + }) + }) +}) diff --git a/packages/tenants/src/repository/index.ts b/packages/tenants/src/repository/index.ts new file mode 100644 index 0000000000..99ac579c10 --- /dev/null +++ b/packages/tenants/src/repository/index.ts @@ -0,0 +1,4 @@ +export * from './TenantRecord' +export * from './TenantRepository' +export * from './TenantRoutingRecord' +export * from './TenantRoutingRepository' diff --git a/packages/tenants/src/services/TenantRecordService.ts b/packages/tenants/src/services/TenantRecordService.ts new file mode 100644 index 0000000000..40751b2836 --- /dev/null +++ b/packages/tenants/src/services/TenantRecordService.ts @@ -0,0 +1,101 @@ +import type { TenantConfig } from '../models/TenantConfig' +import type { AgentContext, Key, Query, QueryOptions } from '@credo-ts/core' + +import { UpdateAssistant, injectable, utils, KeyDerivationMethod } from '@credo-ts/core' + +import { TenantRepository, TenantRecord, TenantRoutingRepository, TenantRoutingRecord } from '../repository' + +@injectable() +export class TenantRecordService { + private tenantRepository: TenantRepository + private tenantRoutingRepository: TenantRoutingRepository + + public constructor(tenantRepository: TenantRepository, tenantRoutingRepository: TenantRoutingRepository) { + this.tenantRepository = tenantRepository + this.tenantRoutingRepository = tenantRoutingRepository + } + + public async createTenant(agentContext: AgentContext, config: Omit) { + const tenantId = utils.uuid() + + const walletId = `tenant-${tenantId}` + const walletKey = await agentContext.wallet.generateWalletKey() + + const tenantRecord = new TenantRecord({ + id: tenantId, + config: { + ...config, + walletConfig: { + id: walletId, + key: walletKey, + keyDerivationMethod: KeyDerivationMethod.Raw, + }, + }, + storageVersion: UpdateAssistant.frameworkStorageVersion, + }) + + await this.tenantRepository.save(agentContext, tenantRecord) + + return tenantRecord + } + + public async getTenantById(agentContext: AgentContext, tenantId: string) { + return this.tenantRepository.getById(agentContext, tenantId) + } + + public async findTenantsByLabel(agentContext: AgentContext, label: string) { + return this.tenantRepository.findByLabel(agentContext, label) + } + + public async getAllTenants(agentContext: AgentContext) { + return this.tenantRepository.getAll(agentContext) + } + + public async deleteTenantById(agentContext: AgentContext, tenantId: string) { + const tenantRecord = await this.getTenantById(agentContext, tenantId) + + const tenantRoutingRecords = await this.tenantRoutingRepository.findByQuery(agentContext, { + tenantId: tenantRecord.id, + }) + + // Delete all tenant routing records + await Promise.all( + tenantRoutingRecords.map((tenantRoutingRecord) => + this.tenantRoutingRepository.delete(agentContext, tenantRoutingRecord) + ) + ) + + // Delete tenant record + await this.tenantRepository.delete(agentContext, tenantRecord) + } + + public async updateTenant(agentContext: AgentContext, tenantRecord: TenantRecord) { + return this.tenantRepository.update(agentContext, tenantRecord) + } + + public async findTenantsByQuery(agentContext: AgentContext, query: Query, queryOptions?: QueryOptions) { + return this.tenantRepository.findByQuery(agentContext, query, queryOptions) + } + + public async findTenantRoutingRecordByRecipientKey( + agentContext: AgentContext, + recipientKey: Key + ): Promise { + return this.tenantRoutingRepository.findByRecipientKey(agentContext, recipientKey) + } + + public async addTenantRoutingRecord( + agentContext: AgentContext, + tenantId: string, + recipientKey: Key + ): Promise { + const tenantRoutingRecord = new TenantRoutingRecord({ + tenantId, + recipientKeyFingerprint: recipientKey.fingerprint, + }) + + await this.tenantRoutingRepository.save(agentContext, tenantRoutingRecord) + + return tenantRoutingRecord + } +} diff --git a/packages/tenants/src/services/__tests__/TenantService.test.ts b/packages/tenants/src/services/__tests__/TenantService.test.ts new file mode 100644 index 0000000000..f84454dbbc --- /dev/null +++ b/packages/tenants/src/services/__tests__/TenantService.test.ts @@ -0,0 +1,157 @@ +import type { Wallet } from '@credo-ts/core' + +import { Key } from '@credo-ts/core' + +import { getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { TenantRecord, TenantRoutingRecord } from '../../repository' +import { TenantRepository } from '../../repository/TenantRepository' +import { TenantRoutingRepository } from '../../repository/TenantRoutingRepository' +import { TenantRecordService } from '../TenantRecordService' + +jest.mock('../../repository/TenantRepository') +const TenantRepositoryMock = TenantRepository as jest.Mock +jest.mock('../../repository/TenantRoutingRepository') +const TenantRoutingRepositoryMock = TenantRoutingRepository as jest.Mock + +const wallet = { + generateWalletKey: jest.fn(() => Promise.resolve('walletKey')), +} as unknown as Wallet + +const tenantRepository = new TenantRepositoryMock() +const tenantRoutingRepository = new TenantRoutingRepositoryMock() +const agentContext = getAgentContext({ wallet }) + +const tenantRecordService = new TenantRecordService(tenantRepository, tenantRoutingRepository) + +describe('TenantRecordService', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('createTenant', () => { + test('creates a tenant record and stores it in the tenant repository', async () => { + const tenantRecord = await tenantRecordService.createTenant(agentContext, { + label: 'Test Tenant', + connectionImageUrl: 'https://example.com/connection.png', + }) + + expect(tenantRecord).toMatchObject({ + id: expect.any(String), + config: { + label: 'Test Tenant', + connectionImageUrl: 'https://example.com/connection.png', + walletConfig: { + id: expect.any(String), + key: 'walletKey', + }, + }, + }) + + expect(agentContext.wallet.generateWalletKey).toHaveBeenCalled() + expect(tenantRepository.save).toHaveBeenCalledWith(agentContext, tenantRecord) + }) + }) + + describe('getTenantById', () => { + test('returns value from tenant repository get by id', async () => { + const tenantRecord = jest.fn() as unknown as TenantRecord + mockFunction(tenantRepository.getById).mockResolvedValue(tenantRecord) + const returnedTenantRecord = await tenantRecordService.getTenantById(agentContext, 'tenantId') + + expect(returnedTenantRecord).toBe(tenantRecord) + }) + }) + + describe('deleteTenantById', () => { + test('retrieves the tenant record and calls delete on the tenant repository', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant-id', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'tenant-wallet-id', + key: 'tenant-wallet-key', + }, + }, + storageVersion: '0.5', + }) + mockFunction(tenantRepository.getById).mockResolvedValue(tenantRecord) + mockFunction(tenantRoutingRepository.findByQuery).mockResolvedValue([]) + + await tenantRecordService.deleteTenantById(agentContext, 'tenant-id') + + expect(tenantRepository.delete).toHaveBeenCalledWith(agentContext, tenantRecord) + }) + + test('deletes associated tenant routing records', async () => { + const tenantRecord = new TenantRecord({ + id: 'tenant-id', + config: { + label: 'Test Tenant', + walletConfig: { + id: 'tenant-wallet-id', + key: 'tenant-wallet-key', + }, + }, + storageVersion: '0.5', + }) + const tenantRoutingRecords = [ + new TenantRoutingRecord({ + recipientKeyFingerprint: '1', + tenantId: 'tenant-id', + }), + new TenantRoutingRecord({ + recipientKeyFingerprint: '2', + tenantId: 'tenant-id', + }), + ] + + mockFunction(tenantRepository.getById).mockResolvedValue(tenantRecord) + mockFunction(tenantRoutingRepository.findByQuery).mockResolvedValue(tenantRoutingRecords) + + await tenantRecordService.deleteTenantById(agentContext, 'tenant-id') + + expect(tenantRoutingRepository.findByQuery).toHaveBeenCalledWith(agentContext, { + tenantId: 'tenant-id', + }) + + expect(tenantRoutingRepository.delete).toHaveBeenCalledTimes(2) + expect(tenantRoutingRepository.delete).toHaveBeenNthCalledWith(1, agentContext, tenantRoutingRecords[0]) + expect(tenantRoutingRepository.delete).toHaveBeenNthCalledWith(2, agentContext, tenantRoutingRecords[1]) + }) + }) + + describe('findTenantRoutingRecordByRecipientKey', () => { + test('returns value from tenant routing repository findByRecipientKey', async () => { + const tenantRoutingRecord = jest.fn() as unknown as TenantRoutingRecord + mockFunction(tenantRoutingRepository.findByRecipientKey).mockResolvedValue(tenantRoutingRecord) + + const recipientKey = Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + const returnedTenantRoutingRecord = await tenantRecordService.findTenantRoutingRecordByRecipientKey( + agentContext, + recipientKey + ) + + expect(tenantRoutingRepository.findByRecipientKey).toHaveBeenCalledWith(agentContext, recipientKey) + expect(returnedTenantRoutingRecord).toBe(tenantRoutingRecord) + }) + }) + + describe('addTenantRoutingRecord', () => { + test('creates a tenant routing record and stores it in the tenant routing repository', async () => { + const recipientKey = Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + const tenantRoutingRecord = await tenantRecordService.addTenantRoutingRecord( + agentContext, + 'tenant-id', + recipientKey + ) + + expect(tenantRoutingRepository.save).toHaveBeenCalledWith(agentContext, tenantRoutingRecord) + expect(tenantRoutingRecord).toMatchObject({ + id: expect.any(String), + tenantId: 'tenant-id', + recipientKeyFingerprint: 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + }) + }) + }) +}) diff --git a/packages/tenants/src/services/index.ts b/packages/tenants/src/services/index.ts new file mode 100644 index 0000000000..9dde8c8dfc --- /dev/null +++ b/packages/tenants/src/services/index.ts @@ -0,0 +1 @@ +export * from './TenantRecordService' diff --git a/packages/tenants/src/updates/0.4-0.5/__tests__/tenantRecord.test.ts b/packages/tenants/src/updates/0.4-0.5/__tests__/tenantRecord.test.ts new file mode 100644 index 0000000000..39a92639e7 --- /dev/null +++ b/packages/tenants/src/updates/0.4-0.5/__tests__/tenantRecord.test.ts @@ -0,0 +1,71 @@ +import { JsonTransformer, Agent } from '@credo-ts/core' + +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../core/tests' +import { TenantRecord } from '../../../repository' +import { TenantRepository } from '../../../repository/TenantRepository' +import * as testModule from '../tenantRecord' + +const agentConfig = getAgentConfig('Tenants Migration - Tenant Record - 0.4-0.5.0') +const agentContext = getAgentContext() + +TenantRepository +jest.mock('../../../repository/TenantRepository') +const TenantRepositoryMock = TenantRepository as jest.Mock +const tenantRepository = new TenantRepositoryMock() + +jest.mock('../../../../../core/src/agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn(() => tenantRepository), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.4-0.5 | Tenants Migration | Tenant Record', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + describe('migrateTenantRecordToV0_5()', () => { + it('should fetch all records and apply the needed updates ', async () => { + const records: TenantRecord[] = [ + getTenantRecord({ + label: 'Tenant 1', + }), + ] + + mockFunction(tenantRepository.getAll).mockResolvedValue(records) + + await testModule.migrateTenantRecordToV0_5(agent) + + expect(tenantRepository.getAll).toHaveBeenCalledTimes(1) + expect(tenantRepository.update).toHaveBeenCalledTimes(1) + + const [, credentialRecord] = mockFunction(tenantRepository.update).mock.calls[0] + expect(credentialRecord.getTags()).toMatchObject({ + label: 'Tenant 1', + }) + }) + }) +}) + +function getTenantRecord({ id, label }: { id?: string; label: string }) { + return JsonTransformer.fromJSON( + { + id: id ?? 'credential-id', + config: { + label, + }, + }, + TenantRecord + ) +} diff --git a/packages/tenants/src/updates/0.4-0.5/index.ts b/packages/tenants/src/updates/0.4-0.5/index.ts new file mode 100644 index 0000000000..31fbbff97c --- /dev/null +++ b/packages/tenants/src/updates/0.4-0.5/index.ts @@ -0,0 +1,7 @@ +import type { BaseAgent } from '@credo-ts/core' + +import { migrateTenantRecordToV0_5 } from './tenantRecord' + +export async function updateTenantsModuleV0_4ToV0_5(agent: Agent): Promise { + await migrateTenantRecordToV0_5(agent) +} diff --git a/packages/tenants/src/updates/0.4-0.5/tenantRecord.ts b/packages/tenants/src/updates/0.4-0.5/tenantRecord.ts new file mode 100644 index 0000000000..d309700f34 --- /dev/null +++ b/packages/tenants/src/updates/0.4-0.5/tenantRecord.ts @@ -0,0 +1,29 @@ +import type { BaseAgent } from '@credo-ts/core' + +import { TenantRepository } from '../../repository' + +/** + * Migrates the {@link TenantRecord} to 0.5 compatible format. It fetches all tenant records from + * storage and applies the needed updates to the records. After a record has been transformed, it is updated + * in storage and the next record will be transformed. + * + * The following transformations are applied: + * - Re-save record to store new `label` tag + */ +export async function migrateTenantRecordToV0_5(agent: Agent) { + agent.config.logger.info('Migrating tenant records to storage version 0.5') + const tenantRepository = agent.dependencyManager.resolve(TenantRepository) + + agent.config.logger.debug(`Fetching all tenant records from storage`) + const tenantRecords = await tenantRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${tenantRecords.length} tenant records to update.`) + for (const tenantRecord of tenantRecords) { + agent.config.logger.debug(`Migrating tenant record with id ${tenantRecord.id} to storage version 0.5`) + + // NOTE: Record only has change in tags, we need to re-save the record + await tenantRepository.update(agent.context, tenantRecord) + + agent.config.logger.debug(`Successfully migrated tenant record with id ${tenantRecord.id} to storage version 0.5`) + } +} diff --git a/packages/tenants/src/updates/__tests__/0.4.test.ts b/packages/tenants/src/updates/__tests__/0.4.test.ts new file mode 100644 index 0000000000..11af20ab6a --- /dev/null +++ b/packages/tenants/src/updates/__tests__/0.4.test.ts @@ -0,0 +1,92 @@ +import { + DependencyManager, + InjectionSymbols, + Agent, + UpdateAssistant, + utils, + MediatorRoutingRecord, +} from '@credo-ts/core' +import { agentDependencies } from '@credo-ts/node' +import { readFileSync } from 'fs' +import path from 'path' + +import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' +import { RegisteredAskarTestWallet } from '../../../../askar/tests/helpers' +import { TenantsModule } from '../../TenantsModule' + +// Backup date / time is the unique identifier for a backup, needs to be unique for every test +const backupDate = new Date('2023-11-23T22:50:20.522Z') +jest.useFakeTimers().setSystemTime(backupDate) + +describe('UpdateAssistant | Tenants | v0.4 - v0.5', () => { + it(`should correctly update the tenant records`, async () => { + // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. + let uuidCounter = 1 + const uuidSpy = jest.spyOn(utils, 'uuid').mockImplementation(() => `${uuidCounter++}-4e4f-41d9-94c4-f49351b811f1`) + + const tenantRecordsString = readFileSync(path.join(__dirname, '__fixtures__/tenants-no-label-tag-0.4.json'), 'utf8') + + const dependencyManager = new DependencyManager() + const storageService = new InMemoryStorageService() + dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) + + const agent = new Agent( + { + config: { + label: 'Test Agent', + walletConfig: { + id: `Wallet: 0.5 Update Tenants`, + key: `Key: 0.5 Update Tenants`, + }, + }, + dependencies: agentDependencies, + modules: { + // We need to include the TenantsModule to run the updates + tenants: new TenantsModule(), + }, + }, + dependencyManager + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(tenantRecordsString), + creationDate: new Date(), + }, + } + + expect(await updateAssistant.isUpToDate('0.5')).toBe(false) + expect(await updateAssistant.getNeededUpdates('0.5')).toEqual([ + { + fromVersion: '0.4', + toVersion: '0.5', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update({ updateToVersion: '0.5' }) + + expect(await updateAssistant.isUpToDate('0.5')).toBe(true) + expect(await updateAssistant.getNeededUpdates('0.5')).toEqual([]) + + await storageService.deleteById(agent.context, MediatorRoutingRecord, 'MEDIATOR_ROUTING_RECORD') + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() + + await agent.shutdown() + await agent.wallet.delete() + + uuidSpy.mockReset() + }) +}) diff --git a/packages/tenants/src/updates/__tests__/__fixtures__/tenants-no-label-tag-0.4.json b/packages/tenants/src/updates/__tests__/__fixtures__/tenants-no-label-tag-0.4.json new file mode 100644 index 0000000000..e2216003c5 --- /dev/null +++ b/packages/tenants/src/updates/__tests__/__fixtures__/tenants-no-label-tag-0.4.json @@ -0,0 +1,67 @@ +{ + "STORAGE_VERSION_RECORD_ID": { + "value": { + "metadata": {}, + "id": "STORAGE_VERSION_RECORD_ID", + "createdAt": "2023-11-23T22:50:20.522Z", + "storageVersion": "0.4", + "updatedAt": "2023-11-23T22:50:20.522Z" + }, + "id": "STORAGE_VERSION_RECORD_ID", + "type": "StorageVersionRecord", + "tags": {} + }, + "MEDIATOR_ROUTING_RECORD": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "MEDIATOR_ROUTING_RECORD", + "createdAt": "2023-11-23T22:50:20.522Z", + "routingKeys": ["F722LWtW7sZsuzv93PuqfN1xStei1bi5XBRjx4qkAbmJ"], + "updatedAt": "2023-11-23T22:50:20.522Z" + }, + "id": "MEDIATOR_ROUTING_RECORD", + "type": "MediatorRoutingRecord", + "tags": {} + }, + "1-4e4f-41d9-94c4-f49351b811f1": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "createdAt": "2023-11-23T22:50:20.522Z", + "config": { + "label": "Tenant 1", + "walletConfig": { + "id": "tenant-1-4e4f-41d9-94c4-f49351b811f1", + "key": "Bs2YXMZ4mRRYSEzHPGVMuGfAD3qt9xwjgbMFNW4wuVKr", + "keyDerivationMethod": "RAW" + } + }, + "updatedAt": "2023-11-23T22:50:20.522Z" + }, + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "type": "TenantRecord", + "tags": {} + }, + "2-4e4f-41d9-94c4-f49351b811f1": { + "value": { + "_tags": {}, + "metadata": {}, + "id": "2-4e4f-41d9-94c4-f49351b811f1", + "createdAt": "2023-11-23T22:50:20.522Z", + "config": { + "label": "Tenant 2", + "walletConfig": { + "id": "tenant-2-4e4f-41d9-94c4-f49351b811f1", + "key": "5eB1uJwE7c9hkyJ8cba5ziPCZ5GkXNjnhVKh95jBMfou", + "keyDerivationMethod": "RAW" + } + }, + "updatedAt": "2023-11-23T22:50:20.522Z" + }, + "id": "2-4e4f-41d9-94c4-f49351b811f1", + "type": "TenantRecord", + "tags": {} + } +} diff --git a/packages/tenants/src/updates/__tests__/__snapshots__/0.4.test.ts.snap b/packages/tenants/src/updates/__tests__/__snapshots__/0.4.test.ts.snap new file mode 100644 index 0000000000..365a2cafb1 --- /dev/null +++ b/packages/tenants/src/updates/__tests__/__snapshots__/0.4.test.ts.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UpdateAssistant | Tenants | v0.4 - v0.5 should correctly update the tenant records 1`] = ` +{ + "1-4e4f-41d9-94c4-f49351b811f1": { + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "label": "Tenant 1", + "storageVersion": "0.4", + }, + "type": "TenantRecord", + "value": { + "config": { + "label": "Tenant 1", + "walletConfig": { + "id": "tenant-1-4e4f-41d9-94c4-f49351b811f1", + "key": "Bs2YXMZ4mRRYSEzHPGVMuGfAD3qt9xwjgbMFNW4wuVKr", + "keyDerivationMethod": "RAW", + }, + }, + "createdAt": "2023-11-23T22:50:20.522Z", + "id": "1-4e4f-41d9-94c4-f49351b811f1", + "metadata": {}, + "storageVersion": "0.4", + "updatedAt": "2023-11-23T22:50:20.522Z", + }, + }, + "2-4e4f-41d9-94c4-f49351b811f1": { + "id": "2-4e4f-41d9-94c4-f49351b811f1", + "tags": { + "label": "Tenant 2", + "storageVersion": "0.4", + }, + "type": "TenantRecord", + "value": { + "config": { + "label": "Tenant 2", + "walletConfig": { + "id": "tenant-2-4e4f-41d9-94c4-f49351b811f1", + "key": "5eB1uJwE7c9hkyJ8cba5ziPCZ5GkXNjnhVKh95jBMfou", + "keyDerivationMethod": "RAW", + }, + }, + "createdAt": "2023-11-23T22:50:20.522Z", + "id": "2-4e4f-41d9-94c4-f49351b811f1", + "metadata": {}, + "storageVersion": "0.4", + "updatedAt": "2023-11-23T22:50:20.522Z", + }, + }, + "STORAGE_VERSION_RECORD_ID": { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": {}, + "type": "StorageVersionRecord", + "value": { + "createdAt": "2023-11-23T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": {}, + "storageVersion": "0.5", + "updatedAt": "2023-11-23T22:50:20.522Z", + }, + }, +} +`; diff --git a/packages/tenants/tests/setup.ts b/packages/tenants/tests/setup.ts new file mode 100644 index 0000000000..78143033f2 --- /dev/null +++ b/packages/tenants/tests/setup.ts @@ -0,0 +1,3 @@ +import 'reflect-metadata' + +jest.setTimeout(120000) diff --git a/packages/tenants/tests/tenant-sessions.test.ts b/packages/tenants/tests/tenant-sessions.test.ts new file mode 100644 index 0000000000..a434141bea --- /dev/null +++ b/packages/tenants/tests/tenant-sessions.test.ts @@ -0,0 +1,92 @@ +import type { InitConfig } from '@credo-ts/core' + +import { ConnectionsModule, Agent } from '@credo-ts/core' +import { agentDependencies } from '@credo-ts/node' + +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' +import { uuid } from '../../core/src/utils/uuid' +import { testLogger } from '../../core/tests' + +import { TenantsModule } from '@credo-ts/tenants' + +const agentConfig: InitConfig = { + label: 'Tenant Agent 1', + walletConfig: { + id: `tenant sessions e2e agent 1 - ${uuid().slice(0, 4)}`, + key: `tenant sessions e2e agent 1`, + }, + logger: testLogger, + endpoints: ['rxjs:tenant-agent1'], +} + +// Create multi-tenant agent +const agent = new Agent({ + config: agentConfig, + dependencies: agentDependencies, + modules: { + tenants: new TenantsModule({ sessionAcquireTimeout: 10000 }), + inMemory: new InMemoryWalletModule(), + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + }, +}) + +describe('Tenants Sessions E2E', () => { + beforeAll(async () => { + await agent.initialize() + }) + + afterAll(async () => { + await agent.wallet.delete() + await agent.shutdown() + }) + + test('create 100 sessions in parallel for the same tenant and close them', async () => { + const numberOfSessions = 100 + + const tenantRecord = await agent.modules.tenants.createTenant({ + config: { + label: 'Agent 1 Tenant 1', + }, + }) + + const tenantAgentPromises = [] + + for (let session = 0; session < numberOfSessions; session++) { + tenantAgentPromises.push(agent.modules.tenants.getTenantAgent({ tenantId: tenantRecord.id })) + } + + const tenantAgents = await Promise.all(tenantAgentPromises) + + await Promise.all(tenantAgents.map((tenantAgent) => tenantAgent.endSession())) + }) + + test('create 5 sessions each for 20 tenants in parallel and close them', async () => { + const numberOfTenants = 20 + const numberOfSessions = 5 + + const tenantRecordPromises = [] + for (let tenantNo = 0; tenantNo < numberOfTenants; tenantNo++) { + const tenantRecordPromise = agent.modules.tenants.createTenant({ + config: { + label: 'Agent 1 Tenant 1', + }, + }) + + tenantRecordPromises.push(tenantRecordPromise) + } + + const tenantRecords = await Promise.all(tenantRecordPromises) + const tenantAgentPromises = [] + for (const tenantRecord of tenantRecords) { + for (let session = 0; session < numberOfSessions; session++) { + tenantAgentPromises.push(agent.modules.tenants.getTenantAgent({ tenantId: tenantRecord.id })) + } + } + + const tenantAgents = await Promise.all(tenantAgentPromises) + + await Promise.all(tenantAgents.map((tenantAgent) => tenantAgent.endSession())) + }) +}) diff --git a/packages/tenants/tests/tenants-04.db b/packages/tenants/tests/tenants-04.db new file mode 100644 index 0000000000..d2238be9b9 Binary files /dev/null and b/packages/tenants/tests/tenants-04.db differ diff --git a/packages/tenants/tests/tenants-askar-profiles.test.ts b/packages/tenants/tests/tenants-askar-profiles.test.ts new file mode 100644 index 0000000000..93761bc218 --- /dev/null +++ b/packages/tenants/tests/tenants-askar-profiles.test.ts @@ -0,0 +1,120 @@ +import type { InitConfig } from '@credo-ts/core' + +import { Agent } from '@credo-ts/core' +import { agentDependencies } from '@credo-ts/node' + +import { AskarModule, AskarMultiWalletDatabaseScheme, AskarProfileWallet, AskarWallet } from '../../askar/src' +import { askarModuleConfig } from '../../askar/tests/helpers' +import { getAskarWalletConfig, testLogger } from '../../core/tests' + +import { TenantsModule } from '@credo-ts/tenants' + +describe('Tenants Askar database schemes E2E', () => { + test('uses AskarWallet for all wallets and tenants when database schema is DatabasePerWallet', async () => { + const agentConfig: InitConfig = { + label: 'Tenant Agent 1', + walletConfig: getAskarWalletConfig('askar tenants without profiles e2e agent 1', { inMemory: false }), + logger: testLogger, + } + + // Create multi-tenant agent + const agent = new Agent({ + config: agentConfig, + modules: { + tenants: new TenantsModule(), + askar: new AskarModule({ + ariesAskar: askarModuleConfig.ariesAskar, + // Database per wallet + multiWalletDatabaseScheme: AskarMultiWalletDatabaseScheme.DatabasePerWallet, + }), + }, + dependencies: agentDependencies, + }) + + await agent.initialize() + + // main wallet should use AskarWallet + expect(agent.context.wallet).toBeInstanceOf(AskarWallet) + const mainWallet = agent.context.wallet as AskarWallet + + // Create tenant + const tenantRecord = await agent.modules.tenants.createTenant({ + config: { + label: 'Tenant 1', + }, + }) + + // Get tenant agent + const tenantAgent = await agent.modules.tenants.getTenantAgent({ + tenantId: tenantRecord.id, + }) + + expect(tenantAgent.context.wallet).toBeInstanceOf(AskarWallet) + const tenantWallet = tenantAgent.context.wallet as AskarWallet + + // By default, profile is the same as the wallet id + expect(tenantWallet.profile).toEqual(`tenant-${tenantRecord.id}`) + // But the store should be different + expect(tenantWallet.store).not.toBe(mainWallet.store) + + // Insert and end + await tenantAgent.genericRecords.save({ content: { name: 'hello' }, id: 'hello' }) + await tenantAgent.endSession() + + const tenantAgent2 = await agent.modules.tenants.getTenantAgent({ tenantId: tenantRecord.id }) + expect(await tenantAgent2.genericRecords.findById('hello')).not.toBeNull() + + await agent.wallet.delete() + await agent.shutdown() + }) + + test('uses AskarWallet for main agent, and ProfileAskarWallet for tenants', async () => { + const agentConfig: InitConfig = { + label: 'Tenant Agent 1', + walletConfig: getAskarWalletConfig('askar tenants with profiles e2e agent 1'), + logger: testLogger, + } + + // Create multi-tenant agent + const agent = new Agent({ + config: agentConfig, + modules: { + tenants: new TenantsModule(), + askar: new AskarModule({ + ariesAskar: askarModuleConfig.ariesAskar, + // Profile per wallet + multiWalletDatabaseScheme: AskarMultiWalletDatabaseScheme.ProfilePerWallet, + }), + }, + dependencies: agentDependencies, + }) + + await agent.initialize() + + // main wallet should use AskarWallet + expect(agent.context.wallet).toBeInstanceOf(AskarWallet) + const mainWallet = agent.context.wallet as AskarWallet + + // Create tenant + const tenantRecord = await agent.modules.tenants.createTenant({ + config: { + label: 'Tenant 1', + }, + }) + + // Get tenant agent + const tenantAgent = await agent.modules.tenants.getTenantAgent({ + tenantId: tenantRecord.id, + }) + + expect(tenantAgent.context.wallet).toBeInstanceOf(AskarProfileWallet) + const tenantWallet = tenantAgent.context.wallet as AskarProfileWallet + + expect(tenantWallet.profile).toEqual(`tenant-${tenantRecord.id}`) + // When using profile, the wallets should share the same store + expect(tenantWallet.store).toBe(mainWallet.store) + + await agent.wallet.delete() + await agent.shutdown() + }) +}) diff --git a/packages/tenants/tests/tenants-storage-update.test.ts b/packages/tenants/tests/tenants-storage-update.test.ts new file mode 100644 index 0000000000..28eb14293f --- /dev/null +++ b/packages/tenants/tests/tenants-storage-update.test.ts @@ -0,0 +1,244 @@ +import type { InitConfig, FileSystem } from '@credo-ts/core' + +import { + UpdateAssistant, + InjectionSymbols, + ConnectionsModule, + Agent, + CacheModule, + InMemoryLruCache, +} from '@credo-ts/core' +import { agentDependencies } from '@credo-ts/node' +import path from 'path' + +import { AskarModule, AskarMultiWalletDatabaseScheme } from '../../askar/src' +import { ariesAskar } from '../../askar/tests/helpers' +import { testLogger } from '../../core/tests' +import { TenantSessionCoordinator } from '../src/context/TenantSessionCoordinator' + +import { TenantsModule } from '@credo-ts/tenants' + +const agentConfig = { + label: 'Tenant Agent', + walletConfig: { + id: `tenants-agent-04`, + key: `tenants-agent-04`, + }, + logger: testLogger, +} satisfies InitConfig + +const modules = { + tenants: new TenantsModule(), + askar: new AskarModule({ + ariesAskar, + multiWalletDatabaseScheme: AskarMultiWalletDatabaseScheme.ProfilePerWallet, + }), + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 500 }), + }), +} as const + +describe('Tenants Storage Update', () => { + test('auto update storage', async () => { + // Create multi-tenant agents + const agent = new Agent({ + config: { + ...agentConfig, + autoUpdateStorageOnStartup: true, + + // export not supported for askar profile wallet + // so we skip creating a backup + backupBeforeStorageUpdate: false, + }, + modules, + dependencies: agentDependencies, + }) + + // Delete existing wallet at this path + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) + await fileSystem.delete(path.join(fileSystem.dataPath, 'wallet', agentConfig.walletConfig.id)) + + // Import the wallet + await agent.wallet.import(agentConfig.walletConfig, { + key: agentConfig.walletConfig.key, + path: path.join(__dirname, 'tenants-04.db'), + }) + + await agent.initialize() + + // Expect tenant storage version to be still 0.4 + const tenant = await agent.modules.tenants.getTenantById('1d45d3c2-3480-4375-ac6f-47c322f091b0') + expect(tenant.storageVersion).toBe('0.4') + + // Open/close tenant agent so that the storage is updated + await ( + await agent.modules.tenants.getTenantAgent({ tenantId: '1d45d3c2-3480-4375-ac6f-47c322f091b0' }) + ).endSession() + + // Expect tenant storage version to be 0.5 + const updatedTenant = await agent.modules.tenants.getTenantById('1d45d3c2-3480-4375-ac6f-47c322f091b0') + expect(updatedTenant.storageVersion).toBe('0.5') + + await agent.wallet.delete() + await agent.shutdown() + }) + + test('error when trying to open session for tenant when backupBeforeStorageUpdate is not disabled because profile cannot be exported', async () => { + // Create multi-tenant agents + const agent = new Agent({ + config: { ...agentConfig, autoUpdateStorageOnStartup: true, backupBeforeStorageUpdate: true }, + modules, + dependencies: agentDependencies, + }) + + // Delete existing wallet at this path + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) + await fileSystem.delete(path.join(fileSystem.dataPath, 'wallet', agentConfig.walletConfig.id)) + + // Import the wallet + await agent.wallet.import(agentConfig.walletConfig, { + key: agentConfig.walletConfig.key, + path: path.join(__dirname, 'tenants-04.db'), + }) + + // Initialize agent + await agent.initialize() + + // Expect tenant storage version to be still 0.4 + const tenant = await agent.modules.tenants.getTenantById('1d45d3c2-3480-4375-ac6f-47c322f091b0') + expect(tenant.storageVersion).toBe('0.4') + + // Should throw error because not up to date and backupBeforeStorageUpdate is true + await expect( + agent.modules.tenants.getTenantAgent({ tenantId: '1d45d3c2-3480-4375-ac6f-47c322f091b0' }) + ).rejects.toThrow(/the wallet backend does not support exporting/) + + await agent.wallet.delete() + await agent.shutdown() + }) + + test('error when trying to open session for tenant when autoUpdateStorageOnStartup is disabled', async () => { + // Create multi-tenant agents + const agent = new Agent({ + config: agentConfig, + modules, + dependencies: agentDependencies, + }) + + // Delete existing wallet at this path + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) + await fileSystem.delete(path.join(fileSystem.dataPath, 'wallet', agentConfig.walletConfig.id)) + + // Import the wallet + await agent.wallet.import(agentConfig.walletConfig, { + key: agentConfig.walletConfig.key, + path: path.join(__dirname, 'tenants-04.db'), + }) + + // Update root agent (but not tenants) + const updateAssistant = new UpdateAssistant(agent) + await updateAssistant.initialize() + await updateAssistant.update() + + // Initialize agent + await agent.initialize() + + // Expect tenant storage version to be still 0.4 + const tenant = await agent.modules.tenants.getTenantById('1d45d3c2-3480-4375-ac6f-47c322f091b0') + expect(tenant.storageVersion).toBe('0.4') + + // Should throw error because not up to date and autoUpdateStorageOnStartup is not true + await expect( + agent.modules.tenants.getTenantAgent({ tenantId: '1d45d3c2-3480-4375-ac6f-47c322f091b0' }) + ).rejects.toThrow(/Current agent storage for tenant 1d45d3c2-3480-4375-ac6f-47c322f091b0 is not up to date/) + + await agent.wallet.delete() + await agent.shutdown() + }) + + test('update tenant agent manually using update assistant', async () => { + // Create multi-tenant agents + const agent = new Agent({ + config: agentConfig, + modules, + dependencies: agentDependencies, + }) + + // Delete existing wallet at this path + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) + await fileSystem.delete(path.join(fileSystem.dataPath, 'wallet', agentConfig.walletConfig.id)) + + // Import the wallet + await agent.wallet.import(agentConfig.walletConfig, { + key: agentConfig.walletConfig.key, + path: path.join(__dirname, 'tenants-04.db'), + }) + + // Update root agent (but not tenants) + const updateAssistant = new UpdateAssistant(agent) + await updateAssistant.initialize() + await updateAssistant.update() + + // Initialize agent + await agent.initialize() + + // Expect tenant storage version to be still 0.4 + const tenant = await agent.modules.tenants.getTenantById('1d45d3c2-3480-4375-ac6f-47c322f091b0') + expect(tenant.storageVersion).toBe('0.4') + + // Getting tenant should now throw error because not up to date + await expect( + agent.modules.tenants.getTenantAgent({ tenantId: '1d45d3c2-3480-4375-ac6f-47c322f091b0' }) + ).rejects.toThrow(/Current agent storage for tenant 1d45d3c2-3480-4375-ac6f-47c322f091b0 is not up to date/) + + const tenantSessionCoordinator = agent.dependencyManager.resolve(TenantSessionCoordinator) + expect(tenantSessionCoordinator.getSessionCountForTenant(tenant.id)).toBe(0) + + // Update tenant + await agent.modules.tenants.updateTenantStorage({ + tenantId: tenant.id, + updateOptions: { + backupBeforeStorageUpdate: false, + }, + }) + + // Should have closed session after upgrade + expect(tenantSessionCoordinator.getSessionCountForTenant(tenant.id)).toBe(0) + + // Expect tenant storage version to be 0.5 + const updatedTenant = await agent.modules.tenants.getTenantById('1d45d3c2-3480-4375-ac6f-47c322f091b0') + expect(updatedTenant.storageVersion).toBe('0.5') + + // Getting tenant should now work + await expect( + agent.modules.tenants.withTenantAgent({ tenantId: '1d45d3c2-3480-4375-ac6f-47c322f091b0' }, async () => { + /* no-op */ + }) + ).resolves.toBeUndefined() + + const outdatedTenants = await agent.modules.tenants.getTenantsWithOutdatedStorage() + expect(outdatedTenants).toHaveLength(2) + + // Update tenants in parallel + const updatePromises = outdatedTenants.map((tenant) => + agent.modules.tenants.updateTenantStorage({ + tenantId: tenant.id, + updateOptions: { + backupBeforeStorageUpdate: false, + }, + }) + ) + + await Promise.all(updatePromises) + + // Now there should be no outdated tenants + const outdatedTenantsAfterUpdate = await agent.modules.tenants.getTenantsWithOutdatedStorage() + expect(outdatedTenantsAfterUpdate).toHaveLength(0) + + await agent.wallet.delete() + await agent.shutdown() + }) +}) diff --git a/packages/tenants/tests/tenants.test.ts b/packages/tenants/tests/tenants.test.ts new file mode 100644 index 0000000000..a30fb2ce46 --- /dev/null +++ b/packages/tenants/tests/tenants.test.ts @@ -0,0 +1,279 @@ +import type { InitConfig } from '@credo-ts/core' + +import { ConnectionsModule, OutOfBandRecord, Agent, CacheModule, InMemoryLruCache } from '@credo-ts/core' +import { agentDependencies } from '@credo-ts/node' + +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { uuid } from '../../core/src/utils/uuid' +import { testLogger } from '../../core/tests' +import { TenantsModule } from '../src/TenantsModule' + +const agent1Config: InitConfig = { + label: 'Tenant Agent 1', + walletConfig: { + id: `tenants e2e agent 1 - ${uuid().slice(0, 4)}`, + key: `tenants e2e agent 1`, + }, + logger: testLogger, + endpoints: ['rxjs:tenant-agent1'], +} + +const agent2Config: InitConfig = { + label: 'Tenant Agent 2', + walletConfig: { + id: `tenants e2e agent 2 - ${uuid().slice(0, 4)}`, + key: `tenants e2e agent 2`, + }, + logger: testLogger, + endpoints: ['rxjs:tenant-agent2'], +} + +// Create multi-tenant agents +const agent1 = new Agent({ + config: agent1Config, + modules: { + tenants: new TenantsModule(), + inMemory: new InMemoryWalletModule(), + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 500 }), + }), + }, + dependencies: agentDependencies, +}) + +const agent2 = new Agent({ + config: agent2Config, + modules: { + tenants: new TenantsModule(), + inMemory: new InMemoryWalletModule(), + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 500 }), + }), + }, + dependencies: agentDependencies, +}) + +// Register inbound and outbound transports (so we can communicate with ourselves) +const agent1InboundTransport = new SubjectInboundTransport() +const agent2InboundTransport = new SubjectInboundTransport() + +agent1.registerInboundTransport(agent1InboundTransport) +agent2.registerInboundTransport(agent2InboundTransport) + +agent1.registerOutboundTransport( + new SubjectOutboundTransport({ + 'rxjs:tenant-agent1': agent1InboundTransport.ourSubject, + 'rxjs:tenant-agent2': agent2InboundTransport.ourSubject, + }) +) +agent2.registerOutboundTransport( + new SubjectOutboundTransport({ + 'rxjs:tenant-agent1': agent1InboundTransport.ourSubject, + 'rxjs:tenant-agent2': agent2InboundTransport.ourSubject, + }) +) + +describe('Tenants E2E', () => { + beforeAll(async () => { + await agent1.initialize() + await agent2.initialize() + }) + + afterAll(async () => { + await agent1.wallet.delete() + await agent1.shutdown() + await agent2.wallet.delete() + await agent2.shutdown() + }) + + test('create get, find by label, and delete a tenant', async () => { + // Create tenant + let tenantRecord1 = await agent1.modules.tenants.createTenant({ + config: { + label: 'Tenant 1', + }, + }) + + // Retrieve tenant record from storage + tenantRecord1 = await agent1.modules.tenants.getTenantById(tenantRecord1.id) + + const tenantRecordsByLabel = await agent1.modules.tenants.findTenantsByLabel('Tenant 1') + expect(tenantRecordsByLabel.length).toBe(1) + expect(tenantRecordsByLabel[0].id).toBe(tenantRecord1.id) + + // Get tenant agent + const tenantAgent = await agent1.modules.tenants.getTenantAgent({ + tenantId: tenantRecord1.id, + }) + await tenantAgent.endSession() + + // Delete tenant agent + await agent1.modules.tenants.deleteTenantById(tenantRecord1.id) + + // Can not get tenant agent again + await expect(agent1.modules.tenants.getTenantAgent({ tenantId: tenantRecord1.id })).rejects.toThrow( + `TenantRecord: record with id ${tenantRecord1.id} not found.` + ) + }) + + test('withTenantAgent returns value from callback', async () => { + const tenantRecord = await agent1.modules.tenants.createTenant({ + config: { + label: 'Tenant 2', + }, + }) + + const result = await agent1.modules.tenants.withTenantAgent({ tenantId: tenantRecord.id }, async () => { + return { + hello: 'world', + } + }) + + expect(result).toEqual({ hello: 'world' }) + }) + + test('create a connection between two tenants within the same agent', async () => { + // Create tenants + const tenantRecord1 = await agent1.modules.tenants.createTenant({ + config: { + label: 'Tenant 1', + }, + }) + const tenantRecord2 = await agent1.modules.tenants.createTenant({ + config: { + label: 'Tenant 2', + }, + }) + + const tenantAgent1 = await agent1.modules.tenants.getTenantAgent({ + tenantId: tenantRecord1.id, + }) + + const tenantAgent2 = await agent1.modules.tenants.getTenantAgent({ + tenantId: tenantRecord2.id, + }) + + // Create and receive oob invitation in scope of tenants + const outOfBandRecord = await tenantAgent1.oob.createInvitation() + const { connectionRecord: tenant2ConnectionRecord } = await tenantAgent2.oob.receiveInvitation( + outOfBandRecord.outOfBandInvitation + ) + + // Retrieve all oob records for the base and tenant agent, only the + // tenant agent should have a record. + const baseAgentOutOfBandRecords = await agent1.oob.getAll() + const tenantAgent1OutOfBandRecords = await tenantAgent1.oob.getAll() + const tenantAgent2OutOfBandRecords = await tenantAgent2.oob.getAll() + + expect(baseAgentOutOfBandRecords.length).toBe(0) + expect(tenantAgent1OutOfBandRecords.length).toBe(1) + expect(tenantAgent2OutOfBandRecords.length).toBe(1) + + if (!tenant2ConnectionRecord) throw new Error('Receive invitation did not return connection record') + await tenantAgent2.connections.returnWhenIsConnected(tenant2ConnectionRecord.id) + + // Find the connection record for the created oob invitation + const [connectionRecord] = await tenantAgent1.connections.findAllByOutOfBandId(outOfBandRecord.id) + await tenantAgent1.connections.returnWhenIsConnected(connectionRecord.id) + + await tenantAgent1.endSession() + await tenantAgent2.endSession() + + // Delete tenants (will also delete wallets) + await agent1.modules.tenants.deleteTenantById(tenantAgent1.context.contextCorrelationId) + await agent1.modules.tenants.deleteTenantById(tenantAgent2.context.contextCorrelationId) + }) + + test('create a connection between two tenants within different agents', async () => { + // Create tenants + const tenantRecord1 = await agent1.modules.tenants.createTenant({ + config: { + label: 'Agent 1 Tenant 1', + }, + }) + const tenantAgent1 = await agent1.modules.tenants.getTenantAgent({ + tenantId: tenantRecord1.id, + }) + + const tenantRecord2 = await agent2.modules.tenants.createTenant({ + config: { + label: 'Agent 2 Tenant 1', + }, + }) + const tenantAgent2 = await agent2.modules.tenants.getTenantAgent({ + tenantId: tenantRecord2.id, + }) + + // Create and receive oob invitation in scope of tenants + const outOfBandRecord = await tenantAgent1.oob.createInvitation() + const { connectionRecord: tenant2ConnectionRecord } = await tenantAgent2.oob.receiveInvitation( + outOfBandRecord.outOfBandInvitation + ) + + if (!tenant2ConnectionRecord) throw new Error('Receive invitation did not return connection record') + await tenantAgent2.connections.returnWhenIsConnected(tenant2ConnectionRecord.id) + + // Find the connection record for the created oob invitation + const [connectionRecord] = await tenantAgent1.connections.findAllByOutOfBandId(outOfBandRecord.id) + await tenantAgent1.connections.returnWhenIsConnected(connectionRecord.id) + + await tenantAgent1.endSession() + await tenantAgent2.endSession() + + // Delete tenants (will also delete wallets) + await agent1.modules.tenants.deleteTenantById(tenantRecord1.id) + await agent2.modules.tenants.deleteTenantById(tenantRecord2.id) + }) + + test('perform actions within the callback of withTenantAgent', async () => { + const tenantRecord = await agent1.modules.tenants.createTenant({ + config: { + label: 'Agent 1 Tenant 1', + }, + }) + + await agent1.modules.tenants.withTenantAgent({ tenantId: tenantRecord.id }, async (tenantAgent) => { + const outOfBandRecord = await tenantAgent.oob.createInvitation() + + expect(outOfBandRecord).toBeInstanceOf(OutOfBandRecord) + expect(tenantAgent.context.contextCorrelationId).toBe(tenantRecord.id) + expect(tenantAgent.config.label).toBe('Agent 1 Tenant 1') + }) + + await agent1.modules.tenants.deleteTenantById(tenantRecord.id) + }) + + test('fallback middleware for the tenant manager propagated to the tenant', async () => { + expect(agent1.dependencyManager.fallbackMessageHandler).toBeUndefined() + + const fallbackFunction = async () => { + // empty + } + + agent1.dependencyManager.setFallbackMessageHandler(fallbackFunction) + + expect(agent1.dependencyManager.fallbackMessageHandler).toBe(fallbackFunction) + + const tenantRecord = await agent1.modules.tenants.createTenant({ + config: { + label: 'Agent 1 Tenant 1', + }, + }) + + const tenantAgent = await agent1.modules.tenants.getTenantAgent({ + tenantId: tenantRecord.id, + }) + + expect(tenantAgent.dependencyManager.fallbackMessageHandler).toBe(fallbackFunction) + + await tenantAgent.endSession() + }) +}) diff --git a/packages/tenants/tsconfig.build.json b/packages/tenants/tsconfig.build.json new file mode 100644 index 0000000000..9c30e30bd2 --- /dev/null +++ b/packages/tenants/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + + "compilerOptions": { + "outDir": "./build" + }, + + "include": ["src/**/*"] +} diff --git a/packages/tenants/tsconfig.json b/packages/tenants/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/tenants/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000000..2a6ccdd876 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,15577 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@types/node': 18.18.8 + +importers: + + .: + devDependencies: + '@changesets/cli': + specifier: ^2.27.5 + version: 2.27.5 + '@hyperledger/aries-askar-nodejs': + specifier: ^0.2.1 + version: 0.2.1(encoding@0.1.13) + '@jest/types': + specifier: ^29.6.3 + version: 29.6.3 + '@types/bn.js': + specifier: ^5.1.5 + version: 5.1.5 + '@types/cors': + specifier: ^2.8.10 + version: 2.8.17 + '@types/eslint': + specifier: ^8.21.2 + version: 8.56.10 + '@types/express': + specifier: ^4.17.13 + version: 4.17.21 + '@types/jest': + specifier: ^29.5.12 + version: 29.5.12 + '@types/node': + specifier: 18.18.8 + version: 18.18.8 + '@types/uuid': + specifier: ^9.0.1 + version: 9.0.8 + '@types/varint': + specifier: ^6.0.0 + version: 6.0.3 + '@types/ws': + specifier: ^8.5.4 + version: 8.5.10 + '@typescript-eslint/eslint-plugin': + specifier: ^7.14.1 + version: 7.14.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/parser': + specifier: ^7.14.1 + version: 7.14.1(eslint@8.57.0)(typescript@5.5.2) + bn.js: + specifier: ^5.2.1 + version: 5.2.1 + cors: + specifier: ^2.8.5 + version: 2.8.5 + eslint: + specifier: ^8.36.0 + version: 8.57.0 + eslint-config-prettier: + specifier: ^8.3.0 + version: 8.10.0(eslint@8.57.0) + eslint-import-resolver-typescript: + specifier: ^3.5.3 + version: 3.6.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-plugin-import: + specifier: ^2.23.4 + version: 2.29.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-prettier: + specifier: ^4.2.1 + version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) + eslint-plugin-workspaces: + specifier: ^0.8.0 + version: 0.8.0 + express: + specifier: ^4.17.1 + version: 4.19.2 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)) + prettier: + specifier: ^2.3.1 + version: 2.8.8 + rxjs: + specifier: ^7.8.0 + version: 7.8.1 + ts-jest: + specifier: ^29.1.2 + version: 29.1.4(@babel/core@7.24.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(jest@29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)))(typescript@5.5.2) + ts-node: + specifier: ^10.0.0 + version: 10.9.2(@types/node@18.18.8)(typescript@5.5.2) + tsyringe: + specifier: ^4.8.0 + version: 4.8.0 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + ws: + specifier: ^8.13.0 + version: 8.17.0 + + demo: + dependencies: + '@hyperledger/anoncreds-nodejs': + specifier: ^0.2.2 + version: 0.2.2(encoding@0.1.13) + '@hyperledger/aries-askar-nodejs': + specifier: ^0.2.1 + version: 0.2.1(encoding@0.1.13) + '@hyperledger/indy-vdr-nodejs': + specifier: ^0.2.2 + version: 0.2.2(encoding@0.1.13) + inquirer: + specifier: ^8.2.5 + version: 8.2.6 + devDependencies: + '@credo-ts/anoncreds': + specifier: workspace:* + version: link:../packages/anoncreds + '@credo-ts/askar': + specifier: workspace:* + version: link:../packages/askar + '@credo-ts/cheqd': + specifier: workspace:* + version: link:../packages/cheqd + '@credo-ts/core': + specifier: workspace:* + version: link:../packages/core + '@credo-ts/indy-vdr': + specifier: workspace:* + version: link:../packages/indy-vdr + '@credo-ts/node': + specifier: workspace:* + version: link:../packages/node + '@types/figlet': + specifier: ^1.5.4 + version: 1.5.8 + '@types/inquirer': + specifier: ^8.2.6 + version: 8.2.10 + clear: + specifier: ^0.1.0 + version: 0.1.0 + figlet: + specifier: ^1.5.2 + version: 1.7.0 + ts-node: + specifier: ^10.4.0 + version: 10.9.2(@types/node@18.18.8)(typescript@5.5.2) + + demo-openid: + dependencies: + '@hyperledger/anoncreds-nodejs': + specifier: ^0.2.2 + version: 0.2.2(encoding@0.1.13) + '@hyperledger/aries-askar-nodejs': + specifier: ^0.2.1 + version: 0.2.1(encoding@0.1.13) + '@hyperledger/indy-vdr-nodejs': + specifier: ^0.2.2 + version: 0.2.2(encoding@0.1.13) + express: + specifier: ^4.18.1 + version: 4.19.2 + inquirer: + specifier: ^8.2.5 + version: 8.2.6 + devDependencies: + '@credo-ts/askar': + specifier: workspace:* + version: link:../packages/askar + '@credo-ts/core': + specifier: workspace:* + version: link:../packages/core + '@credo-ts/node': + specifier: workspace:* + version: link:../packages/node + '@credo-ts/openid4vc': + specifier: workspace:* + version: link:../packages/openid4vc + '@types/express': + specifier: ^4.17.13 + version: 4.17.21 + '@types/figlet': + specifier: ^1.5.4 + version: 1.5.8 + '@types/inquirer': + specifier: ^8.2.6 + version: 8.2.10 + clear: + specifier: ^0.1.0 + version: 0.1.0 + figlet: + specifier: ^1.5.2 + version: 1.7.0 + ts-node: + specifier: ^10.4.0 + version: 10.9.2(@types/node@18.18.8)(typescript@5.5.2) + + packages/action-menu: + dependencies: + '@credo-ts/core': + specifier: workspace:* + version: link:../core + class-transformer: + specifier: 0.5.1 + version: 0.5.1 + class-validator: + specifier: 0.14.1 + version: 0.14.1 + rxjs: + specifier: ^7.8.0 + version: 7.8.1 + devDependencies: + reflect-metadata: + specifier: ^0.1.13 + version: 0.1.14 + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + packages/anoncreds: + dependencies: + '@astronautlabs/jsonpath': + specifier: ^1.1.2 + version: 1.1.2 + '@credo-ts/core': + specifier: workspace:* + version: link:../core + '@sphereon/pex-models': + specifier: ^2.2.4 + version: 2.2.4 + big-integer: + specifier: ^1.6.51 + version: 1.6.52 + bn.js: + specifier: ^5.2.1 + version: 5.2.1 + class-transformer: + specifier: 0.5.1 + version: 0.5.1 + class-validator: + specifier: 0.14.1 + version: 0.14.1 + reflect-metadata: + specifier: ^0.1.13 + version: 0.1.14 + devDependencies: + '@credo-ts/node': + specifier: workspace:* + version: link:../node + '@hyperledger/anoncreds-nodejs': + specifier: ^0.2.2 + version: 0.2.2(encoding@0.1.13) + '@hyperledger/anoncreds-shared': + specifier: ^0.2.2 + version: 0.2.2 + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + rxjs: + specifier: ^7.8.0 + version: 7.8.1 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + packages/askar: + dependencies: + '@credo-ts/core': + specifier: workspace:* + version: link:../core + bn.js: + specifier: ^5.2.1 + version: 5.2.1 + class-transformer: + specifier: 0.5.1 + version: 0.5.1 + class-validator: + specifier: 0.14.1 + version: 0.14.1 + rxjs: + specifier: ^7.8.0 + version: 7.8.1 + tsyringe: + specifier: ^4.8.0 + version: 4.8.0 + devDependencies: + '@hyperledger/aries-askar-nodejs': + specifier: ^0.2.1 + version: 0.2.1(encoding@0.1.13) + '@hyperledger/aries-askar-shared': + specifier: ^0.2.1 + version: 0.2.1 + '@types/bn.js': + specifier: ^5.1.0 + version: 5.1.5 + '@types/ref-array-di': + specifier: ^1.2.6 + version: 1.2.8 + '@types/ref-struct-di': + specifier: ^1.1.10 + version: 1.1.12 + reflect-metadata: + specifier: ^0.1.13 + version: 0.1.14 + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + packages/bbs-signatures: + dependencies: + '@animo-id/react-native-bbs-signatures': + specifier: ^0.1.0 + version: 0.1.0(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + '@credo-ts/core': + specifier: workspace:* + version: link:../core + '@mattrglobal/bbs-signatures': + specifier: ^1.0.0 + version: 1.3.1(encoding@0.1.13) + '@mattrglobal/bls12381-key-pair': + specifier: ^1.0.0 + version: 1.2.1(encoding@0.1.13) + '@stablelib/random': + specifier: ^1.0.2 + version: 1.0.2 + devDependencies: + '@credo-ts/node': + specifier: workspace:* + version: link:../node + reflect-metadata: + specifier: ^0.1.13 + version: 0.1.14 + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + packages/cheqd: + dependencies: + '@cheqd/sdk': + specifier: ^2.4.4 + version: 2.4.4 + '@cheqd/ts-proto': + specifier: ~2.2.0 + version: 2.2.2 + '@cosmjs/crypto': + specifier: ~0.30.0 + version: 0.30.1 + '@cosmjs/proto-signing': + specifier: ~0.30.0 + version: 0.30.1 + '@cosmjs/stargate': + specifier: ~0.30.0 + version: 0.30.1 + '@credo-ts/anoncreds': + specifier: workspace:* + version: link:../anoncreds + '@credo-ts/core': + specifier: workspace:* + version: link:../core + '@stablelib/ed25519': + specifier: ^1.0.3 + version: 1.0.3 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: 0.14.1 + version: 0.14.1 + rxjs: + specifier: ^7.8.0 + version: 7.8.1 + tsyringe: + specifier: ^4.8.0 + version: 4.8.0 + devDependencies: + rimraf: + specifier: ^4.0.7 + version: 4.4.1 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + packages/core: + dependencies: + '@digitalcredentials/jsonld': + specifier: ^6.0.0 + version: 6.0.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/jsonld-signatures': + specifier: ^9.4.0 + version: 9.4.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/vc': + specifier: ^6.0.1 + version: 6.0.1(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + '@multiformats/base-x': + specifier: ^4.0.1 + version: 4.0.1 + '@sd-jwt/core': + specifier: ^0.7.0 + version: 0.7.1 + '@sd-jwt/decode': + specifier: ^0.7.0 + version: 0.7.1 + '@sd-jwt/jwt-status-list': + specifier: ^0.7.0 + version: 0.7.1 + '@sd-jwt/sd-jwt-vc': + specifier: ^0.7.0 + version: 0.7.1 + '@sd-jwt/types': + specifier: ^0.7.0 + version: 0.7.1 + '@sd-jwt/utils': + specifier: ^0.7.0 + version: 0.7.1 + '@sphereon/pex': + specifier: ^3.3.2 + version: 3.3.3 + '@sphereon/pex-models': + specifier: ^2.2.4 + version: 2.2.4 + '@sphereon/ssi-types': + specifier: ^0.23.0 + version: 0.23.4 + '@stablelib/ed25519': + specifier: ^1.0.2 + version: 1.0.3 + '@stablelib/sha256': + specifier: ^1.0.1 + version: 1.0.1 + '@types/ws': + specifier: ^8.5.4 + version: 8.5.10 + abort-controller: + specifier: ^3.0.0 + version: 3.0.0 + big-integer: + specifier: ^1.6.51 + version: 1.6.52 + borc: + specifier: ^3.0.0 + version: 3.0.0 + buffer: + specifier: ^6.0.3 + version: 6.0.3 + class-transformer: + specifier: 0.5.1 + version: 0.5.1 + class-validator: + specifier: 0.14.1 + version: 0.14.1 + did-resolver: + specifier: ^4.1.0 + version: 4.1.0 + jsonpath: + specifier: ^1.1.1 + version: 1.1.1 + lru_map: + specifier: ^0.4.1 + version: 0.4.1 + luxon: + specifier: ^3.3.0 + version: 3.4.4 + make-error: + specifier: ^1.3.6 + version: 1.3.6 + object-inspect: + specifier: ^1.10.3 + version: 1.13.1 + query-string: + specifier: ^7.0.1 + version: 7.1.3 + reflect-metadata: + specifier: ^0.1.13 + version: 0.1.14 + rxjs: + specifier: ^7.8.0 + version: 7.8.1 + tsyringe: + specifier: ^4.8.0 + version: 4.8.0 + uuid: + specifier: ^9.0.0 + version: 9.0.1 + varint: + specifier: ^6.0.0 + version: 6.0.0 + web-did-resolver: + specifier: ^2.0.21 + version: 2.0.27(encoding@0.1.13) + devDependencies: + '@types/events': + specifier: ^3.0.0 + version: 3.0.3 + '@types/jsonpath': + specifier: ^0.2.4 + version: 0.2.4 + '@types/luxon': + specifier: ^3.2.0 + version: 3.4.2 + '@types/object-inspect': + specifier: ^1.8.0 + version: 1.13.0 + '@types/uuid': + specifier: ^9.0.1 + version: 9.0.8 + '@types/varint': + specifier: ^6.0.0 + version: 6.0.3 + nock: + specifier: ^13.3.0 + version: 13.5.4 + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + tslog: + specifier: ^4.8.2 + version: 4.9.3 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + packages/drpc: + dependencies: + '@credo-ts/core': + specifier: workspace:* + version: link:../core + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: 0.14.1 + version: 0.14.1 + devDependencies: + '@credo-ts/node': + specifier: workspace:* + version: link:../node + reflect-metadata: + specifier: ^0.1.13 + version: 0.1.14 + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + packages/indy-sdk-to-askar-migration: + dependencies: + '@credo-ts/anoncreds': + specifier: workspace:* + version: link:../anoncreds + '@credo-ts/askar': + specifier: workspace:* + version: link:../askar + '@credo-ts/core': + specifier: workspace:* + version: link:../core + '@credo-ts/node': + specifier: workspace:* + version: link:../node + devDependencies: + '@hyperledger/aries-askar-nodejs': + specifier: ^0.2.1 + version: 0.2.1(encoding@0.1.13) + '@hyperledger/aries-askar-shared': + specifier: ^0.2.1 + version: 0.2.1 + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + packages/indy-vdr: + dependencies: + '@credo-ts/anoncreds': + specifier: workspace:* + version: link:../anoncreds + '@credo-ts/core': + specifier: workspace:* + version: link:../core + devDependencies: + '@hyperledger/indy-vdr-nodejs': + specifier: ^0.2.2 + version: 0.2.2(encoding@0.1.13) + '@hyperledger/indy-vdr-shared': + specifier: ^0.2.2 + version: 0.2.2 + '@stablelib/ed25519': + specifier: ^1.0.2 + version: 1.0.3 + '@types/ref-array-di': + specifier: ^1.2.6 + version: 1.2.8 + '@types/ref-struct-di': + specifier: ^1.1.10 + version: 1.1.12 + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + rxjs: + specifier: ^7.8.0 + version: 7.8.1 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + packages/node: + dependencies: + '@2060.io/ffi-napi': + specifier: ^4.0.9 + version: 4.0.9 + '@2060.io/ref-napi': + specifier: ^3.0.6 + version: 3.0.6 + '@credo-ts/core': + specifier: workspace:* + version: link:../core + '@types/express': + specifier: ^4.17.15 + version: 4.17.21 + express: + specifier: ^4.17.1 + version: 4.19.2 + ws: + specifier: ^8.13.0 + version: 8.17.0 + devDependencies: + '@types/node': + specifier: 18.18.8 + version: 18.18.8 + '@types/ws': + specifier: ^8.5.4 + version: 8.5.10 + nock: + specifier: ^13.3.0 + version: 13.5.4 + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + packages/openid4vc: + dependencies: + '@credo-ts/core': + specifier: workspace:* + version: link:../core + '@sphereon/did-auth-siop': + specifier: ^0.6.4 + version: 0.6.4(encoding@0.1.13) + '@sphereon/oid4vci-client': + specifier: ^0.10.3 + version: 0.10.3(encoding@0.1.13)(msrcrypto@1.5.8) + '@sphereon/oid4vci-common': + specifier: ^0.10.3 + version: 0.10.3(encoding@0.1.13)(msrcrypto@1.5.8) + '@sphereon/oid4vci-issuer': + specifier: ^0.10.3 + version: 0.10.3(encoding@0.1.13)(msrcrypto@1.5.8) + '@sphereon/ssi-types': + specifier: ^0.23.0 + version: 0.23.4 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + rxjs: + specifier: ^7.8.0 + version: 7.8.1 + devDependencies: + '@credo-ts/tenants': + specifier: workspace:* + version: link:../tenants + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + express: + specifier: ^4.18.2 + version: 4.19.2 + nock: + specifier: ^13.3.0 + version: 13.5.4 + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + packages/question-answer: + dependencies: + '@credo-ts/core': + specifier: workspace:* + version: link:../core + class-transformer: + specifier: 0.5.1 + version: 0.5.1 + class-validator: + specifier: 0.14.1 + version: 0.14.1 + rxjs: + specifier: ^7.8.0 + version: 7.8.1 + devDependencies: + '@credo-ts/node': + specifier: workspace:* + version: link:../node + reflect-metadata: + specifier: ^0.1.13 + version: 0.1.14 + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + packages/react-native: + dependencies: + '@azure/core-asynciterator-polyfill': + specifier: ^1.0.2 + version: 1.0.2 + '@credo-ts/core': + specifier: workspace:* + version: link:../core + events: + specifier: ^3.3.0 + version: 3.3.0 + devDependencies: + react-native: + specifier: ^0.71.4 + version: 0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1) + react-native-fs: + specifier: ^2.20.0 + version: 2.20.0(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1)) + react-native-get-random-values: + specifier: ^1.8.0 + version: 1.11.0(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1)) + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + packages/tenants: + dependencies: + '@credo-ts/core': + specifier: workspace:* + version: link:../core + async-mutex: + specifier: ^0.4.0 + version: 0.4.1 + devDependencies: + '@credo-ts/node': + specifier: workspace:* + version: link:../node + reflect-metadata: + specifier: ^0.1.13 + version: 0.1.14 + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + typescript: + specifier: ~5.5.2 + version: 5.5.2 + + samples/extension-module: + dependencies: + '@credo-ts/askar': + specifier: workspace:* + version: link:../../packages/askar + '@credo-ts/core': + specifier: workspace:* + version: link:../../packages/core + '@credo-ts/node': + specifier: workspace:* + version: link:../../packages/node + '@hyperledger/aries-askar-nodejs': + specifier: ^0.2.1 + version: 0.2.1(encoding@0.1.13) + class-validator: + specifier: 0.14.1 + version: 0.14.1 + rxjs: + specifier: ^7.8.0 + version: 7.8.1 + devDependencies: + '@types/express': + specifier: ^4.17.13 + version: 4.17.21 + '@types/uuid': + specifier: ^9.0.1 + version: 9.0.8 + '@types/ws': + specifier: ^8.5.4 + version: 8.5.10 + ts-node: + specifier: ^10.4.0 + version: 10.9.2(@types/node@18.18.8)(typescript@5.5.2) + + samples/tails: + dependencies: + '@credo-ts/anoncreds': + specifier: workspace:* + version: link:../../packages/anoncreds + '@credo-ts/core': + specifier: workspace:* + version: link:../../packages/core + '@types/express': + specifier: ^4.17.13 + version: 4.17.21 + '@types/multer': + specifier: ^1.4.7 + version: 1.4.11 + '@types/uuid': + specifier: ^9.0.1 + version: 9.0.8 + '@types/ws': + specifier: ^8.5.4 + version: 8.5.10 + form-data: + specifier: ^4.0.0 + version: 4.0.0 + multer: + specifier: ^1.4.5-lts.1 + version: 1.4.5-lts.1 + devDependencies: + ts-node: + specifier: ^10.4.0 + version: 10.9.2(@types/node@18.18.8)(typescript@5.5.2) + +packages: + + '@2060.io/ffi-napi@4.0.9': + resolution: {integrity: sha512-JfVREbtkJhMXSUpya3JCzDumdjeZDCKv4PemiWK+pts5CYgdoMidxeySVlFeF5pHqbBpox4I0Be7sDwAq4N0VQ==} + engines: {node: '>=18'} + + '@2060.io/ref-napi@3.0.6': + resolution: {integrity: sha512-8VAIXLdKL85E85jRYpPcZqATBL6fGnC/XjBGNeSgRSMJtrAMSmfRksqIq5AmuZkA2eeJXMWCiN6UQOUdozcymg==} + engines: {node: '>= 18.0'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@animo-id/react-native-bbs-signatures@0.1.0': + resolution: {integrity: sha512-7qvsiWhGfUev8ngE8YzF6ON9PtCID5LiYVYM4EC5eyj80gCdhx3R46CI7K1qbqIlGsoTYQ/Xx5Ubo5Ji9eaUEA==} + peerDependencies: + react: '>= 16' + react-native: '>= 0.66.0' + + '@astronautlabs/jsonpath@1.1.2': + resolution: {integrity: sha512-FqL/muoreH7iltYC1EB5Tvox5E8NSOOPGkgns4G+qxRKl6k5dxEVljUjB5NcKESzkqwnUqWjSZkL61XGYOuV+A==} + + '@azure/core-asynciterator-polyfill@1.0.2': + resolution: {integrity: sha512-3rkP4LnnlWawl0LZptJOdXNrT/fHp2eQMadoasa6afspXdpGrtPZuAQc2PD0cpgyuoXtUWyC3tv7xfntjGS5Dw==} + engines: {node: '>=12.0.0'} + + '@babel/code-frame@7.10.4': + resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==} + + '@babel/code-frame@7.24.7': + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.24.7': + resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.24.7': + resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.24.7': + resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.24.7': + resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': + resolution: {integrity: sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.24.7': + resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.24.7': + resolution: {integrity: sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.24.7': + resolution: {integrity: sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.2': + resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-environment-visitor@7.24.7': + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-function-name@7.24.7': + resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-hoist-variables@7.24.7': + resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.24.7': + resolution: {integrity: sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.24.7': + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.24.7': + resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.24.7': + resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.24.7': + resolution: {integrity: sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.24.7': + resolution: {integrity: sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.24.7': + resolution: {integrity: sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-simple-access@7.24.7': + resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-skip-transparent-expression-wrappers@7.24.7': + resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-split-export-declaration@7.24.7': + resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.24.7': + resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.24.7': + resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.24.7': + resolution: {integrity: sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.24.7': + resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.24.7': + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.24.7': + resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7': + resolution: {integrity: sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.7': + resolution: {integrity: sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7': + resolution: {integrity: sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.7': + resolution: {integrity: sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-async-generator-functions@7.20.7': + resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-class-properties@7.18.6': + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-decorators@7.24.7': + resolution: {integrity: sha512-RL9GR0pUG5Kc8BUWLNDm2T5OpYwSX15r98I0IkgmRQTXuELq/OynH8xtMTMvTJFjXbMWFVTKtYkTaYQsuAwQlQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-export-default-from@7.24.7': + resolution: {integrity: sha512-CcmFwUJ3tKhLjPdt4NP+SHMshebytF8ZTYOv5ZDpkzq2sin80Wb5vJrGt8fhPrORQCfoSa0LAxC/DW+GAC5+Hw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-export-namespace-from@7.18.9': + resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-logical-assignment-operators@7.20.7': + resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6': + resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-numeric-separator@7.18.6': + resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-object-rest-spread@7.20.7': + resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-optional-catch-binding@7.18.6': + resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-optional-chaining@7.21.0': + resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.24.7': + resolution: {integrity: sha512-Ui4uLJJrRV1lb38zg1yYTmRKmiZLiftDEvZN2iq3kd9kUFU+PttmzTbAFC2ucRk/XJmtek6G23gPsuZbhrT8fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-dynamic-import@7.8.3': + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-export-default-from@7.24.7': + resolution: {integrity: sha512-bTPz4/635WQ9WhwsyPdxUJDVpsi/X9BMmy/8Rf/UAlOO4jSql4CxUCjWI5PiM+jG+c4LVPTScoTw80geFj9+Bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-export-namespace-from@7.8.3': + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-flow@7.24.7': + resolution: {integrity: sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.24.7': + resolution: {integrity: sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.24.7': + resolution: {integrity: sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.24.7': + resolution: {integrity: sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.24.7': + resolution: {integrity: sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.24.7': + resolution: {integrity: sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.24.7': + resolution: {integrity: sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.24.7': + resolution: {integrity: sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.24.7': + resolution: {integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.24.7': + resolution: {integrity: sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.24.7': + resolution: {integrity: sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.24.7': + resolution: {integrity: sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.24.7': + resolution: {integrity: sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.24.7': + resolution: {integrity: sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.24.7': + resolution: {integrity: sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.24.7': + resolution: {integrity: sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.24.7': + resolution: {integrity: sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dynamic-import@7.24.7': + resolution: {integrity: sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.24.7': + resolution: {integrity: sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.24.7': + resolution: {integrity: sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-flow-strip-types@7.24.7': + resolution: {integrity: sha512-cjRKJ7FobOH2eakx7Ja+KpJRj8+y+/SiB3ooYm/n2UJfxu0oEaOoxOinitkJcPqv9KxS0kxTGPUaR7L2XcXDXA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.24.7': + resolution: {integrity: sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.24.7': + resolution: {integrity: sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.24.7': + resolution: {integrity: sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.24.7': + resolution: {integrity: sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.24.7': + resolution: {integrity: sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.24.7': + resolution: {integrity: sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.24.7': + resolution: {integrity: sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.24.7': + resolution: {integrity: sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.24.7': + resolution: {integrity: sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.24.7': + resolution: {integrity: sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.24.7': + resolution: {integrity: sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.24.7': + resolution: {integrity: sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.24.7': + resolution: {integrity: sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.24.7': + resolution: {integrity: sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.24.7': + resolution: {integrity: sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.24.7': + resolution: {integrity: sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.24.7': + resolution: {integrity: sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.24.7': + resolution: {integrity: sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.24.7': + resolution: {integrity: sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.24.7': + resolution: {integrity: sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.24.7': + resolution: {integrity: sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.24.7': + resolution: {integrity: sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-display-name@7.24.7': + resolution: {integrity: sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-development@7.24.7': + resolution: {integrity: sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.24.7': + resolution: {integrity: sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.24.7': + resolution: {integrity: sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.24.7': + resolution: {integrity: sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-pure-annotations@7.24.7': + resolution: {integrity: sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.24.7': + resolution: {integrity: sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-reserved-words@7.24.7': + resolution: {integrity: sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-runtime@7.24.7': + resolution: {integrity: sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.24.7': + resolution: {integrity: sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.24.7': + resolution: {integrity: sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.24.7': + resolution: {integrity: sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.24.7': + resolution: {integrity: sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.24.7': + resolution: {integrity: sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.24.7': + resolution: {integrity: sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.24.7': + resolution: {integrity: sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.24.7': + resolution: {integrity: sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.24.7': + resolution: {integrity: sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.24.7': + resolution: {integrity: sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.24.7': + resolution: {integrity: sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-flow@7.24.7': + resolution: {integrity: sha512-NL3Lo0NorCU607zU3NwRyJbpaB6E3t0xtd3LfAQKDfkeX4/ggcDXvkmkW42QWT5owUeW/jAe4hn+2qvkV1IbfQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-react@7.24.7': + resolution: {integrity: sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.24.7': + resolution: {integrity: sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/register@7.24.6': + resolution: {integrity: sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/regjsgen@0.8.0': + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + + '@babel/runtime@7.24.7': + resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.24.7': + resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.24.7': + resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.24.7': + resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@changesets/apply-release-plan@7.0.3': + resolution: {integrity: sha512-klL6LCdmfbEe9oyfLxnidIf/stFXmrbFO/3gT5LU5pcyoZytzJe4gWpTBx3BPmyNPl16dZ1xrkcW7b98e3tYkA==} + + '@changesets/assemble-release-plan@6.0.2': + resolution: {integrity: sha512-n9/Tdq+ze+iUtjmq0mZO3pEhJTKkku9hUxtUadW30jlN7kONqJG3O6ALeXrmc6gsi/nvoCuKjqEJ68Hk8RbMTQ==} + + '@changesets/changelog-git@0.2.0': + resolution: {integrity: sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ==} + + '@changesets/cli@2.27.5': + resolution: {integrity: sha512-UVppOvzCjjylBenFcwcZNG5IaZ8jsIaEVraV/pbXgukYNb0Oqa0d8UWb0LkYzA1Bf1HmUrOfccFcRLheRuA7pA==} + hasBin: true + + '@changesets/config@3.0.1': + resolution: {integrity: sha512-nCr8pOemUjvGJ8aUu8TYVjqnUL+++bFOQHBVmtNbLvKzIDkN/uiP/Z4RKmr7NNaiujIURHySDEGFPftR4GbTUA==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.0': + resolution: {integrity: sha512-QOt6pQq9RVXKGHPVvyKimJDYJumx7p4DO5MO9AhRJYgAPgv0emhNqAqqysSVKHBm4sxKlGN4S1zXOIb5yCFuhQ==} + + '@changesets/get-release-plan@4.0.2': + resolution: {integrity: sha512-rOalz7nMuMV2vyeP7KBeAhqEB7FM2GFPO5RQSoOoUKKH9L6wW3QyPA2K+/rG9kBrWl2HckPVES73/AuwPvbH3w==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.0': + resolution: {integrity: sha512-vvhnZDHe2eiBNRFHEgMiGd2CT+164dfYyrJDhwwxTVD/OW0FUD6G7+4DIx1dNwkwjHyzisxGAU96q0sVNBns0w==} + + '@changesets/logger@0.1.0': + resolution: {integrity: sha512-pBrJm4CQm9VqFVwWnSqKEfsS2ESnwqwH+xR7jETxIErZcfd1u2zBSqrHbRHR7xjhSgep9x2PSKFKY//FAshA3g==} + + '@changesets/parse@0.4.0': + resolution: {integrity: sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw==} + + '@changesets/pre@2.0.0': + resolution: {integrity: sha512-HLTNYX/A4jZxc+Sq8D1AMBsv+1qD6rmmJtjsCJa/9MSRybdxh0mjbTvE6JYZQ/ZiQ0mMlDOlGPXTm9KLTU3jyw==} + + '@changesets/read@0.6.0': + resolution: {integrity: sha512-ZypqX8+/im1Fm98K4YcZtmLKgjs1kDQ5zHpc2U1qdtNBmZZfo/IBiG162RoP0CUF05tvp2y4IspH11PLnPxuuw==} + + '@changesets/should-skip-package@0.1.0': + resolution: {integrity: sha512-FxG6Mhjw7yFStlSM7Z0Gmg3RiyQ98d/9VpQAZ3Fzr59dCOM9G6ZdYbjiSAt0XtFr9JR5U2tBaJWPjrkGGc618g==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.0.0': + resolution: {integrity: sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ==} + + '@changesets/write@0.3.1': + resolution: {integrity: sha512-SyGtMXzH3qFqlHKcvFY2eX+6b0NGiFcNav8AFsYwy5l8hejOeoeTDemu5Yjmke2V5jpzY+pBvM0vCCQ3gdZpfw==} + + '@cheqd/sdk@2.4.4': + resolution: {integrity: sha512-ratcHNuKUZH6pmRvyLeiEFODhrlawfiDssaSzANscOTjeDMJzHK0YvEiSXswZAHcsB/DWbGlR+9gKhbLyD5G7w==} + engines: {node: '>=18.0.0'} + + '@cheqd/ts-proto@2.2.2': + resolution: {integrity: sha512-32XCz1tD/T8r9Pw6IWH+XDttnGEguN0/1dWoUnTZ6uIPAA65YYSz2Ba9ZJ69a7YipYzX9C1CRddVZ3u229dfYg==} + + '@confio/ics23@0.6.8': + resolution: {integrity: sha512-wB6uo+3A50m0sW/EWcU64xpV/8wShZ6bMTa7pF8eYsTrSkQA7oLUIJcs/wb8g4y2Oyq701BaGiO6n/ak5WXO1w==} + + '@cosmjs/amino@0.30.1': + resolution: {integrity: sha512-yNHnzmvAlkETDYIpeCTdVqgvrdt1qgkOXwuRVi8s27UKI5hfqyE9fJ/fuunXE6ZZPnKkjIecDznmuUOMrMvw4w==} + + '@cosmjs/crypto@0.30.1': + resolution: {integrity: sha512-rAljUlake3MSXs9xAm87mu34GfBLN0h/1uPPV6jEwClWjNkAMotzjC0ab9MARy5FFAvYHL3lWb57bhkbt2GtzQ==} + + '@cosmjs/encoding@0.30.1': + resolution: {integrity: sha512-rXmrTbgqwihORwJ3xYhIgQFfMSrwLu1s43RIK9I8EBudPx3KmnmyAKzMOVsRDo9edLFNuZ9GIvysUCwQfq3WlQ==} + + '@cosmjs/json-rpc@0.30.1': + resolution: {integrity: sha512-pitfC/2YN9t+kXZCbNuyrZ6M8abnCC2n62m+JtU9vQUfaEtVsgy+1Fk4TRQ175+pIWSdBMFi2wT8FWVEE4RhxQ==} + + '@cosmjs/math@0.30.1': + resolution: {integrity: sha512-yaoeI23pin9ZiPHIisa6qqLngfnBR/25tSaWpkTm8Cy10MX70UF5oN4+/t1heLaM6SSmRrhk3psRkV4+7mH51Q==} + + '@cosmjs/proto-signing@0.30.1': + resolution: {integrity: sha512-tXh8pPYXV4aiJVhTKHGyeZekjj+K9s2KKojMB93Gcob2DxUjfKapFYBMJSgfKPuWUPEmyr8Q9km2hplI38ILgQ==} + + '@cosmjs/socket@0.30.1': + resolution: {integrity: sha512-r6MpDL+9N+qOS/D5VaxnPaMJ3flwQ36G+vPvYJsXArj93BjgyFB7BwWwXCQDzZ+23cfChPUfhbINOenr8N2Kow==} + + '@cosmjs/stargate@0.30.1': + resolution: {integrity: sha512-RdbYKZCGOH8gWebO7r6WvNnQMxHrNXInY/gPHPzMjbQF6UatA6fNM2G2tdgS5j5u7FTqlCI10stNXrknaNdzog==} + + '@cosmjs/stream@0.30.1': + resolution: {integrity: sha512-Fg0pWz1zXQdoxQZpdHRMGvUH5RqS6tPv+j9Eh7Q953UjMlrwZVo0YFLC8OTf/HKVf10E4i0u6aM8D69Q6cNkgQ==} + + '@cosmjs/tendermint-rpc@0.30.1': + resolution: {integrity: sha512-Z3nCwhXSbPZJ++v85zHObeUggrEHVfm1u18ZRwXxFE9ZMl5mXTybnwYhczuYOl7KRskgwlB+rID0WYACxj4wdQ==} + + '@cosmjs/utils@0.30.1': + resolution: {integrity: sha512-KvvX58MGMWh7xA+N+deCfunkA/ZNDvFLw4YbOmX3f/XBIkqrVY7qlotfy2aNb1kgp6h4B6Yc8YawJPDTfvWX7g==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@digitalbazaar/bitstring@3.1.0': + resolution: {integrity: sha512-Cii+Sl++qaexOvv3vchhgZFfSmtHPNIPzGegaq4ffPnflVXFu+V2qrJ17aL2+gfLxrlC/zazZFuAltyKTPq7eg==} + engines: {node: '>=16'} + + '@digitalbazaar/http-client@3.4.1': + resolution: {integrity: sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==} + engines: {node: '>=14.0'} + + '@digitalbazaar/security-context@1.0.1': + resolution: {integrity: sha512-0WZa6tPiTZZF8leBtQgYAfXQePFQp2z5ivpCEN/iZguYYZ0TB9qRmWtan5XH6mNFuusHtMcyIzAcReyE6rZPhA==} + + '@digitalbazaar/vc-status-list-context@3.1.1': + resolution: {integrity: sha512-cMVtd+EV+4KN2kUG4/vsV74JVsGE6dcpod6zRoFB/AJA2W/sZbJqR44KL3G6P262+GcAECNhtnSsKsTnQ6y8+w==} + + '@digitalbazaar/vc-status-list@7.1.0': + resolution: {integrity: sha512-p5uxKJlX13N8TcTuv9qFDeej+6bndU+Rh1Cez2MT+bXQE6Jpn5t336FBSHmcECB4yUfZQpkmV/LOcYU4lW8Ojw==} + engines: {node: '>=16'} + + '@digitalbazaar/vc@5.0.0': + resolution: {integrity: sha512-XmLM7Ag5W+XidGnFuxFIyUFSMnHnWEMJlHei602GG94+WzFJ6Ik8txzPQL8T18egSoiTsd1VekymbIlSimhuaQ==} + engines: {node: '>=14'} + + '@digitalcredentials/base58-universal@1.0.1': + resolution: {integrity: sha512-1xKdJnfITMvrF/sCgwBx2C4p7qcNAARyIvrAOZGqIHmBaT/hAenpC8bf44qVY+UIMuCYP23kqpIfJQebQDThDQ==} + engines: {node: '>=12'} + + '@digitalcredentials/base64url-universal@2.0.6': + resolution: {integrity: sha512-QJyK6xS8BYNnkKLhEAgQc6Tb9DMe+GkHnBAWJKITCxVRXJAFLhJnr+FsJnCThS3x2Y0UiiDAXoWjwMqtUrp4Kg==} + engines: {node: '>=14'} + + '@digitalcredentials/bitstring@2.0.1': + resolution: {integrity: sha512-9priXvsEJGI4LYHPwLqf5jv9HtQGlG0MgeuY8Q4NHN+xWz5rYMylh1TYTVThKa3XI6xF2pR2oEfKZD21eWXveQ==} + engines: {node: '>=14'} + + '@digitalcredentials/ed25519-signature-2020@3.0.2': + resolution: {integrity: sha512-R8IrR21Dh+75CYriQov3nVHKaOVusbxfk9gyi6eCAwLHKn6fllUt+2LQfuUrL7Ts/sGIJqQcev7YvkX9GvyYRA==} + engines: {node: '>=14'} + + '@digitalcredentials/ed25519-verification-key-2020@3.2.2': + resolution: {integrity: sha512-ZfxNFZlA379MZpf+gV2tUYyiZ15eGVgjtCQLWlyu3frWxsumUgv++o0OJlMnrDsWGwzFMRrsXcosd5+752rLOA==} + engines: {node: '>=14'} + + '@digitalcredentials/http-client@1.2.2': + resolution: {integrity: sha512-YOwaE+vUDSwiDhZT0BbXSWVg+bvp1HA1eg/gEc8OCwCOj9Bn9FRQdu8P9Y/fnYqyFCioDwwTRzGxgJLl50baEg==} + engines: {node: '>=12.0.0'} + + '@digitalcredentials/jsonld-signatures@9.4.0': + resolution: {integrity: sha512-DnR+HDTm7qpcDd0wcD1w6GdlAwfHjQSgu+ahion8REkCkkMRywF+CLunU7t8AZpFB2Gr/+N8naUtiEBNje1Oew==} + engines: {node: '>=18'} + + '@digitalcredentials/jsonld@5.2.2': + resolution: {integrity: sha512-hz7YR3kv6+8UUdgMyTGl1o8NjVKKwnMry/Rh/rWeAvwL+NqgoUHorWzI3rM+PW+MPFyDC0ieXStClt9n9D9SGA==} + engines: {node: '>=12'} + + '@digitalcredentials/jsonld@6.0.0': + resolution: {integrity: sha512-5tTakj0/GsqAJi8beQFVMQ97wUJZnuxViW9xRuAATL6eOBIefGBwHkVryAgEq2I4J/xKgb/nEyw1ZXX0G8wQJQ==} + engines: {node: '>=12'} + + '@digitalcredentials/open-badges-context@2.1.0': + resolution: {integrity: sha512-VK7X5u6OoBFxkyIFplNqUPVbo+8vFSAEoam8tSozpj05KPfcGw41Tp5p9fqMnY38oPfwtZR2yDNSctj/slrE0A==} + + '@digitalcredentials/rdf-canonize@1.0.0': + resolution: {integrity: sha512-z8St0Ex2doecsExCFK1uI4gJC+a5EqYYu1xpRH1pKmqSS9l/nxfuVxexNFyaeEum4dUdg1EetIC2rTwLIFhPRA==} + engines: {node: '>=12'} + + '@digitalcredentials/vc-status-list@5.0.2': + resolution: {integrity: sha512-PI0N7SM0tXpaNLelbCNsMAi34AjOeuhUzMSYTkHdeqRPX7oT2F3ukyOssgr4koEqDxw9shHtxHu3fSJzrzcPMQ==} + engines: {node: '>=14'} + + '@digitalcredentials/vc@4.2.0': + resolution: {integrity: sha512-8Rxpn77JghJN7noBQdcMuzm/tB8vhDwPoFepr3oGd5w+CyJxOk2RnBlgIGlAAGA+mALFWECPv1rANfXno+hdjA==} + engines: {node: '>=12'} + + '@digitalcredentials/vc@6.0.1': + resolution: {integrity: sha512-TZgLoi00Jc9uv3b6jStH+G8+bCqpHIqFw9DYODz+fVjNh197ksvcYqSndUDHa2oi0HCcK+soI8j4ba3Sa4Pl4w==} + engines: {node: '>=12'} + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.10.1': + resolution: {integrity: sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.0': + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@expo/bunyan@4.0.0': + resolution: {integrity: sha512-Ydf4LidRB/EBI+YrB+cVLqIseiRfjUI/AeHBgjGMtq3GroraDu81OV7zqophRgupngoL3iS3JUMDMnxO7g39qA==} + engines: {'0': node >=0.10.0} + + '@expo/cli@0.18.19': + resolution: {integrity: sha512-8Rj18cTofpLl+7D++auMVS71KungldHbrArR44fpE8loMVAvYZA+U932lmd0K2lOYBASPhm7SVP9wzls//ESFQ==} + hasBin: true + + '@expo/code-signing-certificates@0.0.5': + resolution: {integrity: sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw==} + + '@expo/config-plugins@8.0.5': + resolution: {integrity: sha512-VGseKX1dYvaf2qHUDGzIQwSOJrO5fomH0gE5cKSQyi6wn+Q6rcV2Dj2E5aga+9aKNPL6FxZ0dqRFC3t2sbhaSA==} + + '@expo/config-types@51.0.1': + resolution: {integrity: sha512-5JuzUFobFImrUgnq93LeucP44ZMxq8WMXmCtIUf3ZC3mJSwjvvHJBMO2fS/sIlmgvvQk9eq4VnX06/7tgDFMSg==} + + '@expo/config@9.0.1': + resolution: {integrity: sha512-0tjaXBstTbXmD4z+UMFBkh2SZFwilizSQhW6DlaTMnPG5ezuw93zSFEWAuEC3YzkpVtNQTmYzxAYjxwh6seOGg==} + + '@expo/devcert@1.1.2': + resolution: {integrity: sha512-FyWghLu7rUaZEZSTLt/XNRukm0c9GFfwP0iFaswoDWpV6alvVg+zRAfCLdIVQEz1SVcQ3zo1hMZFDrnKGvkCuQ==} + + '@expo/env@0.3.0': + resolution: {integrity: sha512-OtB9XVHWaXidLbHvrVDeeXa09yvTl3+IQN884sO6PhIi2/StXfgSH/9zC7IvzrDB8kW3EBJ1PPLuCUJ2hxAT7Q==} + + '@expo/image-utils@0.5.1': + resolution: {integrity: sha512-U/GsFfFox88lXULmFJ9Shfl2aQGcwoKPF7fawSCLixIKtMCpsI+1r0h+5i0nQnmt9tHuzXZDL8+Dg1z6OhkI9A==} + + '@expo/json-file@8.3.3': + resolution: {integrity: sha512-eZ5dld9AD0PrVRiIWpRkm5aIoWBw3kAyd8VkuWEy92sEthBKDDDHAnK2a0dw0Eil6j7rK7lS/Qaq/Zzngv2h5A==} + + '@expo/metro-config@0.18.7': + resolution: {integrity: sha512-MzAyFP0fvoyj9IUc6SPnpy6/HLT23j/p5J+yWjGug2ddOpSuKNDHOOqlwWZbJp5KfZCEIVWNHeUoE+TaC/yhaQ==} + + '@expo/osascript@2.1.3': + resolution: {integrity: sha512-aOEkhPzDsaAfolSswObGiYW0Pf0ROfR9J2NBRLQACdQ6uJlyAMiPF45DVEVknAU9juKh0y8ZyvC9LXqLEJYohA==} + engines: {node: '>=12'} + + '@expo/package-manager@1.5.2': + resolution: {integrity: sha512-IuA9XtGBilce0q8cyxtWINqbzMB1Fia0Yrug/O53HNuRSwQguV/iqjV68bsa4z8mYerePhcFgtvISWLAlNEbUA==} + + '@expo/plist@0.1.3': + resolution: {integrity: sha512-GW/7hVlAylYg1tUrEASclw1MMk9FP4ZwyFAY/SUTJIhPDQHtfOlXREyWV3hhrHdX/K+pS73GNgdfT6E/e+kBbg==} + + '@expo/prebuild-config@7.0.6': + resolution: {integrity: sha512-Hts+iGBaG6OQ+N8IEMMgwQElzJeSTb7iUJ26xADEHkaexsucAK+V52dM8M4ceicvbZR9q8M+ebJEGj0MCNA3dQ==} + peerDependencies: + expo-modules-autolinking: '>=0.8.1' + + '@expo/rudder-sdk-node@1.1.1': + resolution: {integrity: sha512-uy/hS/awclDJ1S88w9UGpc6Nm9XnNUjzOAAib1A3PVAnGQIwebg8DpFqOthFBTlZxeuV/BKbZ5jmTbtNZkp1WQ==} + engines: {node: '>=12'} + + '@expo/sdk-runtime-versions@1.0.0': + resolution: {integrity: sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==} + + '@expo/spawn-async@1.7.2': + resolution: {integrity: sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==} + engines: {node: '>=12'} + + '@expo/vector-icons@14.0.2': + resolution: {integrity: sha512-70LpmXQu4xa8cMxjp1fydgRPsalefnHaXLzIwaHMEzcZhnyjw2acZz8azRrZOslPVAWlxItOa2Dd7WtD/kI+CA==} + + '@expo/xcpretty@4.3.1': + resolution: {integrity: sha512-sqXgo1SCv+j4VtYEwl/bukuOIBrVgx6euIoCat3Iyx5oeoXwEA2USCoeL0IPubflMxncA2INkqJ/Wr3NGrSgzw==} + hasBin: true + + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + + '@humanwhocodes/config-array@0.11.14': + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@hyperledger/anoncreds-nodejs@0.2.2': + resolution: {integrity: sha512-qRMSSyERwjAVCPlHjCAY3OJno4DNIJ0uLi+g6ek7HrFVich3X6Kzr0ng/MSiDKmTBXyGiip1zDIBABA8y3yNGg==} + + '@hyperledger/anoncreds-shared@0.2.2': + resolution: {integrity: sha512-dfYpqbAkqtHJkRkuGmWdJruHfLegLUIbu/dSAWnz5dMRKd8ad8rEnQkwRnockQZ/pc7QDr8kxfG6bT2YDGZeMw==} + + '@hyperledger/aries-askar-nodejs@0.2.1': + resolution: {integrity: sha512-RSBa+onshUSIJlVyGBzndZtcw2KPb8mgnYIio9z0RquKgGitufc0ymNiL2kLKWNjk2gET20jAUHijhlE4ssk5A==} + engines: {node: '>= 18'} + + '@hyperledger/aries-askar-shared@0.2.1': + resolution: {integrity: sha512-7d8tiqq27dxFl7+0Cf2I40IzzDoRU9aEolyPyvfdLGbco6NAtWB4CV8AzgY11EZ7/ou4RirJxfP9hBjgYBo1Ag==} + + '@hyperledger/indy-vdr-nodejs@0.2.2': + resolution: {integrity: sha512-mc0iKuHCtKuOV0sMnGOTVWnQrpfBMS+1tIRyob+CvGCvwC2llGo3Hu5AvgPcR9nqCo/wJ0LoKBo66dYYb0uZbw==} + engines: {node: '>= 18'} + + '@hyperledger/indy-vdr-shared@0.2.2': + resolution: {integrity: sha512-9425MHU3K+/ahccCRjOIX3Z/51gqxvp3Nmyujyqlx9cd7PWG2Rianx7iNWecFBkdAEqS0DfHsb6YqqH39YZp/A==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@isaacs/ttlcache@1.4.1': + resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/create-cache-key-function@29.7.0': + resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@26.6.2': + resolution: {integrity: sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==} + engines: {node: '>= 10.14.2'} + + '@jest/types@27.5.1': + resolution: {integrity: sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.4.15': + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + + '@mattrglobal/bbs-signatures@1.3.1': + resolution: {integrity: sha512-syZGkapPpktD2el4lPTCQRw/LSia6/NwBS83hzCKu4dTlaJRO636qo5NCiiQb+iBYWyZQQEzN0jdRik8N9EUGA==} + engines: {node: '>=14'} + + '@mattrglobal/bls12381-key-pair@1.2.1': + resolution: {integrity: sha512-Xh63NP1iSGBLW10N5uRpDyoPo2LtNHHh/TRGVJEHRgo+07yxgl8tS06Q2zO9gN9+b+GU5COKvR3lACwrvn+MYw==} + engines: {node: '>=14.0.0'} + + '@mattrglobal/node-bbs-signatures@0.18.1': + resolution: {integrity: sha512-s9ccL/1TTvCP1N//4QR84j/d5D/stx/AI1kPcRgiE4O3KrxyF7ZdL9ca8fmFuN6yh9LAbn/OiGRnOXgvn38Dgg==} + engines: {node: '>=14', yarn: 1.x} + + '@multiformats/base-x@4.0.1': + resolution: {integrity: sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==} + + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/fs@3.1.1': + resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@peculiar/asn1-schema@2.3.8': + resolution: {integrity: sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==} + + '@peculiar/json-schema@1.1.12': + resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} + engines: {node: '>=8.0.0'} + + '@peculiar/webcrypto@1.5.0': + resolution: {integrity: sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==} + engines: {node: '>=10.12.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@react-native-community/cli-clean@10.1.1': + resolution: {integrity: sha512-iNsrjzjIRv9yb5y309SWJ8NDHdwYtnCpmxZouQDyOljUdC9MwdZ4ChbtA4rwQyAwgOVfS9F/j56ML3Cslmvrxg==} + + '@react-native-community/cli-config@10.1.1': + resolution: {integrity: sha512-p4mHrjC+s/ayiNVG6T35GdEGdP6TuyBUg5plVGRJfTl8WT6LBfLYLk+fz/iETrEZ/YkhQIsQcEUQC47MqLNHog==} + + '@react-native-community/cli-debugger-ui@10.0.0': + resolution: {integrity: sha512-8UKLcvpSNxnUTRy8CkCl27GGLqZunQ9ncGYhSrWyKrU9SWBJJGeZwi2k2KaoJi5FvF2+cD0t8z8cU6lsq2ZZmA==} + + '@react-native-community/cli-doctor@10.2.7': + resolution: {integrity: sha512-MejE7m+63DxfKwFSvyZGfq+72jX0RSP9SdSmDbW0Bjz2NIEE3BsE8rNay+ByFbdSLsapRPvaZv2Jof+dK2Y/yg==} + + '@react-native-community/cli-hermes@10.2.7': + resolution: {integrity: sha512-MULfkgeLx1fietx10pLFLmlfRh0dcfX/HABXB5Tm0BzQDXy7ofFIJ/UxH+IF55NwPKXl6aEpTbPwbgsyJxqPiA==} + + '@react-native-community/cli-platform-android@10.2.0': + resolution: {integrity: sha512-CBenYwGxwFdObZTn1lgxWtMGA5ms2G/ALQhkS+XTAD7KHDrCxFF9yT/fnAjFZKM6vX/1TqGI1RflruXih3kAhw==} + + '@react-native-community/cli-platform-ios@10.2.5': + resolution: {integrity: sha512-hq+FZZuSBK9z82GLQfzdNDl8vbFx5UlwCLFCuTtNCROgBoapFtVZQKRP2QBftYNrQZ0dLAb01gkwxagHsQCFyg==} + + '@react-native-community/cli-plugin-metro@10.2.3': + resolution: {integrity: sha512-jHi2oDuTePmW4NEyVT8JEGNlIYcnFXCSV2ZMp4rnDrUk4TzzyvS3IMvDlESEmG8Kry8rvP0KSUx/hTpy37Sbkw==} + + '@react-native-community/cli-server-api@10.1.1': + resolution: {integrity: sha512-NZDo/wh4zlm8as31UEBno2bui8+ufzsZV+KN7QjEJWEM0levzBtxaD+4je0OpfhRIIkhaRm2gl/vVf7OYAzg4g==} + + '@react-native-community/cli-tools@10.1.1': + resolution: {integrity: sha512-+FlwOnZBV+ailEzXjcD8afY2ogFEBeHOw/8+XXzMgPaquU2Zly9B+8W089tnnohO3yfiQiZqkQlElP423MY74g==} + + '@react-native-community/cli-types@10.0.0': + resolution: {integrity: sha512-31oUM6/rFBZQfSmDQsT1DX/5fjqfxg7sf2u8kTPJK7rXVya5SRpAMaCXsPAG0omsmJxXt+J9HxUi3Ic+5Ux5Iw==} + + '@react-native-community/cli@10.2.7': + resolution: {integrity: sha512-31GrAP5PjHosXV5bkHWVnYGjAeka2gkTTsPqasJAki5RI1njB1a2WAkYFV0sn+gqc4RU1s96RELBBfT+EGzhAQ==} + engines: {node: '>=14'} + hasBin: true + + '@react-native/assets@1.0.0': + resolution: {integrity: sha512-KrwSpS1tKI70wuKl68DwJZYEvXktDHdZMG0k2AXD/rJVSlB23/X2CB2cutVR0HwNMJIal9HOUOBB2rVfa6UGtQ==} + + '@react-native/babel-plugin-codegen@0.74.84': + resolution: {integrity: sha512-UR4uiii5szIJA84mSC6GJOfYKDq7/ThyetOQT62+BBcyGeHVtHlNLNRzgaMeLqIQaT8Fq4pccMI+7QqLOMXzdw==} + engines: {node: '>=18'} + + '@react-native/babel-preset@0.74.84': + resolution: {integrity: sha512-WUfu6Y4aGuVdocQZvx33BJiQWFH6kRCHYbZfBn2psgFrSRLgQWEQrDCxqPFObNAVSayM0rNhp2FvI5K/Eyeqlg==} + engines: {node: '>=18'} + peerDependencies: + '@babel/core': '*' + + '@react-native/codegen@0.74.84': + resolution: {integrity: sha512-0hXlnu9i0o8v+gXKQi+x6T471L85kCDwW4WrJiYAeOheWrQdNNW6rC3g8+LL7HXAf7QcHGU/8/d57iYfdVK2BQ==} + engines: {node: '>=18'} + peerDependencies: + '@babel/preset-env': ^7.1.6 + + '@react-native/debugger-frontend@0.74.84': + resolution: {integrity: sha512-YUEA03UNFbiYzHpYxlcS2D9+3eNT5YLGkl5yRg3nOSN6KbCc/OttGnNZme+tuSOJwjMN/vcvtDKYkTqjJw8U0A==} + engines: {node: '>=18'} + + '@react-native/dev-middleware@0.74.84': + resolution: {integrity: sha512-veYw/WmyrAOQHUiIeULzn2duJQnXDPiKq2jZ/lcmDo6jsLirpp+Q73lx09TYgy/oVoPRuV0nfmU3x9B6EV/7qQ==} + engines: {node: '>=18'} + + '@react-native/normalize-color@2.1.0': + resolution: {integrity: sha512-Z1jQI2NpdFJCVgpY+8Dq/Bt3d+YUi1928Q+/CZm/oh66fzM0RUl54vvuXlPJKybH4pdCZey1eDTPaLHkMPNgWA==} + + '@react-native/normalize-colors@0.74.84': + resolution: {integrity: sha512-Y5W6x8cC5RuakUcTVUFNAIhUZ/tYpuqHZlRBoAuakrTwVuoNHXfQki8lj1KsYU7rW6e3VWgdEx33AfOQpdNp6A==} + + '@react-native/polyfills@2.0.0': + resolution: {integrity: sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ==} + + '@rnx-kit/chromium-edge-launcher@1.0.0': + resolution: {integrity: sha512-lzD84av1ZQhYUS+jsGqJiCMaJO2dn9u+RTT9n9q6D3SaKVwWqv+7AoRKqBu19bkwyE+iFRl1ymr40QS90jVFYg==} + engines: {node: '>=14.15'} + + '@sd-jwt/core@0.7.1': + resolution: {integrity: sha512-7u7cNeYNYcNNgzDj+mSeHrloY/C44XsewdKzViMp+8jpQSi/TEeudM9CkR5wxx1KulvnGojHZfMygK8Arxey6g==} + engines: {node: '>=18'} + + '@sd-jwt/decode@0.6.1': + resolution: {integrity: sha512-QgTIoYd5zyKKLgXB4xEYJTrvumVwtsj5Dog0v0L9UH9ZvHekDaeexS247X7A4iSdzTvmZzUpGskgABOa4D8NmQ==} + engines: {node: '>=16'} + + '@sd-jwt/decode@0.7.1': + resolution: {integrity: sha512-jPNjwb9S0PqNULLLl3qR0NPpK0UePpzjB57QJEjEeY9Bdws5N5uANvyr7bF/MG496B+XZE1AugvnBtk4SQguVA==} + engines: {node: '>=18'} + + '@sd-jwt/jwt-status-list@0.7.1': + resolution: {integrity: sha512-HeLluuKrixoAkaHO7buFjPpRuFIjICNGgvT5f4mH06bwrzj7uZ5VNNUWPK9Nb1jq8vHnMpIhpbnSSAmoaVWPEA==} + engines: {node: '>=18'} + + '@sd-jwt/present@0.6.1': + resolution: {integrity: sha512-QRD3TUDLj4PqQNZ70bBxh8FLLrOE9mY8V9qiZrJSsaDOLFs2p1CtZG+v9ig62fxFYJZMf4bWKwYjz+qqGAtxCg==} + engines: {node: '>=16'} + + '@sd-jwt/present@0.7.1': + resolution: {integrity: sha512-X8ADyHq2DUYRy0snd0KXe9G9vOY8MwsP/1YsmgScEFUXfJM6LFhVNiBGS5uzUr6BkFYz6sFZ6WAHrdhg459J5A==} + engines: {node: '>=18'} + + '@sd-jwt/sd-jwt-vc@0.7.1': + resolution: {integrity: sha512-iwAFoxQJbRAzYlahai3YCUqGzHZea69fJI3ct38iJG7IVKxsgBRj6SdACyS1opDNdZSst7McBl4aWyokzGgRvA==} + engines: {node: '>=18'} + + '@sd-jwt/types@0.6.1': + resolution: {integrity: sha512-LKpABZJGT77jNhOLvAHIkNNmGqXzyfwBT+6r+DN9zNzMx1CzuNR0qXk1GMUbast9iCfPkGbnEpUv/jHTBvlIvg==} + engines: {node: '>=16'} + + '@sd-jwt/types@0.7.1': + resolution: {integrity: sha512-rPXS+kWiDDznWUuRkvAeXTWOhYn2tb5dZLI3deepsXmofjhTGqMP89qNNNBqhnA99kJx9gxnUj/jpQgUm0MjmQ==} + engines: {node: '>=18'} + + '@sd-jwt/utils@0.6.1': + resolution: {integrity: sha512-1NHZ//+GecGQJb+gSdDicnrHG0DvACUk9jTnXA5yLZhlRjgkjyfJLNsCZesYeCyVp/SiyvIC9B+JwoY4kI0TwQ==} + engines: {node: '>=16'} + + '@sd-jwt/utils@0.7.1': + resolution: {integrity: sha512-Dx9QxhkBvHD7J52zir2+FNnXlPX55ON0Xc/VFKrBFxC1yHAU6/+pyLXRJMIQLampxqYlreIN9xo7gSipWcY1uQ==} + engines: {node: '>=18'} + + '@segment/loosely-validate-event@2.0.0': + resolution: {integrity: sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==} + + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@sovpro/delimited-stream@1.1.0': + resolution: {integrity: sha512-kQpk267uxB19X3X2T1mvNMjyvIEonpNSHrMlK5ZaBU6aZxw7wPbpgKJOjHN3+/GPVpXgAV9soVT2oyHpLkLtyw==} + engines: {node: '>= 8'} + + '@sphereon/did-auth-siop@0.6.4': + resolution: {integrity: sha512-0hw/lypy7kHpChJc/206XFd1XVhfUEIg2RIuw2u0RE3POqMeuOL5DWiPHh3e7Oo0nzG9gdgJC8Yffv69d9QIrg==} + engines: {node: '>=18'} + + '@sphereon/did-uni-client@0.6.3': + resolution: {integrity: sha512-g7LD7ofbE36slHN7Bhr5dwUrj6t0BuZeXBYJMaVY/pOeL1vJxW1cZHbZqu0NSfOmzyBg4nsYVlgTjyi/Aua2ew==} + + '@sphereon/oid4vci-client@0.10.3': + resolution: {integrity: sha512-PkIZrwTMrHlgwcDNimWDQaAgi+9ptkV79g/sQJJAe4g8NCt3WyXtsV9l88CdzxDGVGDtzsnYqPXkimxP4eSScw==} + engines: {node: '>=18'} + + '@sphereon/oid4vci-common@0.10.3': + resolution: {integrity: sha512-VsUnDKkKm2yQ3lzAt2CY6vL06mZDK9dhwFT6T92aq03ncbUcS6gelwccdsXEMEfi5r4baFemiFM1O5v+mPjuEA==} + engines: {node: '>=18'} + peerDependencies: + msrcrypto: ^1.5.8 + peerDependenciesMeta: + msrcrypto: + optional: true + + '@sphereon/oid4vci-issuer@0.10.3': + resolution: {integrity: sha512-qhm8ypkXuYsaG5XmXIFwL9DUJQ0TJScNjvg5w7beAm+zjz0sOkwIjXdS7S+29LfWj0BkYiRZp1d3mj8H/rmdUw==} + engines: {node: '>=18'} + peerDependencies: + awesome-qr: ^2.1.5-rc.0 + peerDependenciesMeta: + awesome-qr: + optional: true + + '@sphereon/pex-models@2.2.4': + resolution: {integrity: sha512-pGlp+wplneE1+Lk3U48/2htYKTbONMeG5/x7vhO6AnPUOsnOXeJdftPrBYWVSzz/JH5GJptAc6+pAyYE1zMu4Q==} + + '@sphereon/pex@3.3.3': + resolution: {integrity: sha512-CXwdEcMTUh2z/5AriBn3OuShEG06l2tgiIr7qDJthnkez8DQ3sZo2vr4NEQWKKAL+DeAWAI4FryQGO4KuK7yfg==} + engines: {node: '>=18'} + + '@sphereon/ssi-types@0.22.0': + resolution: {integrity: sha512-YPJAZlKmzNALXK8ohP3ETxj1oVzL4+M9ljj3fD5xrbacvYax1JPCVKc8BWSubGcQckKHPbgbpcS7LYEeghyT9Q==} + + '@sphereon/ssi-types@0.23.4': + resolution: {integrity: sha512-1lM2yfOEhpcYYBxm/12KYY4n3ZSahVf5rFqGdterQkMJMthwr20HqTjw3+VK5p7IVf+86DyBoZJyS4V9tSsoCA==} + + '@sphereon/ssi-types@0.9.0': + resolution: {integrity: sha512-umCr/syNcmvMMbQ+i/r/mwjI1Qw2aFPp9AwBTvTo1ailAVaaJjJGPkkVz1K9/2NZATNdDiQ3A8yGzdVJoKh9pA==} + + '@sphereon/wellknown-dids-client@0.1.3': + resolution: {integrity: sha512-TAT24L3RoXD8ocrkTcsz7HuJmgjNjdoV6IXP1p3DdaI/GqkynytXE3J1+F7vUFMRYwY5nW2RaXSgDQhrFJemaA==} + + '@stablelib/aead@1.0.1': + resolution: {integrity: sha512-q39ik6sxGHewqtO0nP4BuSe3db5G1fEJE8ukvngS2gLkBXyy6E7pLubhbYgnkDFv6V8cWaxcE4Xn0t6LWcJkyg==} + + '@stablelib/binary@1.0.1': + resolution: {integrity: sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==} + + '@stablelib/bytes@1.0.1': + resolution: {integrity: sha512-Kre4Y4kdwuqL8BR2E9hV/R5sOrUj6NanZaZis0V6lX5yzqC3hBuVSDXUIBqQv/sCpmuWRiHLwqiT1pqqjuBXoQ==} + + '@stablelib/chacha20poly1305@1.0.1': + resolution: {integrity: sha512-MmViqnqHd1ymwjOQfghRKw2R/jMIGT3wySN7cthjXCBdO+qErNPUBnRzqNpnvIwg7JBCg3LdeCZZO4de/yEhVA==} + + '@stablelib/chacha@1.0.1': + resolution: {integrity: sha512-Pmlrswzr0pBzDofdFuVe1q7KdsHKhhU24e8gkEwnTGOmlC7PADzLVxGdn2PoNVBBabdg0l/IfLKg6sHAbTQugg==} + + '@stablelib/constant-time@1.0.1': + resolution: {integrity: sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg==} + + '@stablelib/ed25519@1.0.3': + resolution: {integrity: sha512-puIMWaX9QlRsbhxfDc5i+mNPMY+0TmQEskunY1rZEBPi1acBCVQAhnsk/1Hk50DGPtVsZtAWQg4NHGlVaO9Hqg==} + + '@stablelib/hash@1.0.1': + resolution: {integrity: sha512-eTPJc/stDkdtOcrNMZ6mcMK1e6yBbqRBaNW55XA1jU8w/7QdnCF0CmMmOD1m7VSkBR44PWrMHU2l6r8YEQHMgg==} + + '@stablelib/int@1.0.1': + resolution: {integrity: sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==} + + '@stablelib/keyagreement@1.0.1': + resolution: {integrity: sha512-VKL6xBwgJnI6l1jKrBAfn265cspaWBPAPEc62VBQrWHLqVgNRE09gQ/AnOEyKUWrrqfD+xSQ3u42gJjLDdMDQg==} + + '@stablelib/poly1305@1.0.1': + resolution: {integrity: sha512-1HlG3oTSuQDOhSnLwJRKeTRSAdFNVB/1djy2ZbS35rBSJ/PFqx9cf9qatinWghC2UbfOYD8AcrtbUQl8WoxabA==} + + '@stablelib/random@1.0.0': + resolution: {integrity: sha512-G9vwwKrNCGMI/uHL6XeWe2Nk4BuxkYyWZagGaDU9wrsuV+9hUwNI1lok2WVo8uJDa2zx7ahNwN7Ij983hOUFEw==} + + '@stablelib/random@1.0.2': + resolution: {integrity: sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==} + + '@stablelib/sha256@1.0.1': + resolution: {integrity: sha512-GIIH3e6KH+91FqGV42Kcj71Uefd/QEe7Dy42sBTeqppXV95ggCcxLTk39bEr+lZfJmp+ghsR07J++ORkRELsBQ==} + + '@stablelib/sha512@1.0.1': + resolution: {integrity: sha512-13gl/iawHV9zvDKciLo1fQ8Bgn2Pvf7OV6amaRVKiq3pjQ3UmEpXxWiAfV8tYjUpeZroBxtyrwtdooQT/i3hzw==} + + '@stablelib/wipe@1.0.1': + resolution: {integrity: sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==} + + '@stablelib/x25519@1.0.3': + resolution: {integrity: sha512-KnTbKmUhPhHavzobclVJQG5kuivH+qDLpe84iRqX3CLrKp881cF160JvXJ+hjn1aMyCwYOKeIZefIH/P5cJoRw==} + + '@stablelib/xchacha20@1.0.1': + resolution: {integrity: sha512-1YkiZnFF4veUwBVhDnDYwo6EHeKzQK4FnLiO7ezCl/zu64uG0bCCAUROJaBkaLH+5BEsO3W7BTXTguMbSLlWSw==} + + '@stablelib/xchacha20poly1305@1.0.1': + resolution: {integrity: sha512-B1Abj0sMJ8h3HNmGnJ7vHBrAvxuNka6cJJoZ1ILN7iuacXp7sUYcgOVEOTLWj+rtQMpspY9tXSCRLPmN1mQNWg==} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + + '@types/bn.js@5.1.5': + resolution: {integrity: sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==} + + '@types/body-parser@1.19.5': + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cors@2.8.17': + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + + '@types/eslint@8.56.10': + resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} + + '@types/estree@1.0.5': + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + + '@types/events@3.0.3': + resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==} + + '@types/express-serve-static-core@4.19.3': + resolution: {integrity: sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==} + + '@types/express@4.17.21': + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + + '@types/figlet@1.5.8': + resolution: {integrity: sha512-G22AUvy4Tl95XLE7jmUM8s8mKcoz+Hr+Xm9W90gJsppJq9f9tHvOGkrpn4gRX0q/cLtBdNkWtWCKDg2UDZoZvQ==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + + '@types/inquirer@8.2.10': + resolution: {integrity: sha512-IdD5NmHyVjWM8SHWo/kPBgtzXatwPkfwzyP3fN1jF2g9BWt5WO+8hL2F4o2GKIYsU40PpqeevuUWvkS/roXJkA==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.12': + resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/jsonpath@0.2.4': + resolution: {integrity: sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==} + + '@types/long@4.0.2': + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/minimist@1.2.5': + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + + '@types/multer@1.4.11': + resolution: {integrity: sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==} + + '@types/node-forge@1.3.11': + resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} + + '@types/node@18.18.8': + resolution: {integrity: sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/object-inspect@1.13.0': + resolution: {integrity: sha512-lwGTVESDDV+XsQ1pH4UifpJ1f7OtXzQ6QBOX2Afq2bM/T3oOt8hF6exJMjjIjtEWeAN2YAo25J7HxWh97CCz9w==} + + '@types/qs@6.9.15': + resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/ref-array-di@1.2.8': + resolution: {integrity: sha512-+re5xrhRXDUR3sicMvN9N3C+6mklq5kd7FkN3ciRWio3BAvUDh2OEUTTG+619r10dqc6de25LIDtgpHtXCKGbA==} + + '@types/ref-napi@3.0.12': + resolution: {integrity: sha512-UZPKghRaLlWx2lPAphpdtYe62TbGBaPeqUM6gF1vI6FPRIu/Tff/WMAzpJRFU3jJIiD8HiXpVt2RjcFHtA6YRg==} + + '@types/ref-struct-di@1.1.12': + resolution: {integrity: sha512-R2RNkGIROGoJTbXYTXrsXybnsQD4iAy26ih/G6HCeCB9luWFQKkr537XGz0uGJ1kH8y8RMkdbQmD/wBulrOPHw==} + + '@types/semver@7.5.8': + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + + '@types/send@0.17.4': + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + + '@types/serve-static@1.15.7': + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/through@0.0.33': + resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} + + '@types/uuid@9.0.8': + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + + '@types/validator@13.11.10': + resolution: {integrity: sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg==} + + '@types/varint@6.0.3': + resolution: {integrity: sha512-DHukoGWdJ2aYkveZJTB2rN2lp6m7APzVsoJQ7j/qy1fQxyamJTPD5xQzCMoJ2Qtgn0mE3wWeNOpbTyBFvF+dyA==} + + '@types/ws@8.5.10': + resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@15.0.19': + resolution: {integrity: sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==} + + '@types/yargs@16.0.9': + resolution: {integrity: sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==} + + '@types/yargs@17.0.32': + resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + + '@typescript-eslint/eslint-plugin@7.14.1': + resolution: {integrity: sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@7.14.1': + resolution: {integrity: sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@7.14.1': + resolution: {integrity: sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/type-utils@7.14.1': + resolution: {integrity: sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@7.14.1': + resolution: {integrity: sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/typescript-estree@7.14.1': + resolution: {integrity: sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@7.14.1': + resolution: {integrity: sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/visitor-keys@7.14.1': + resolution: {integrity: sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + '@unimodules/core@7.1.2': + resolution: {integrity: sha512-lY+e2TAFuebD3vshHMIRqru3X4+k7Xkba4Wa7QsDBd+ex4c4N2dHAO61E2SrGD9+TRBD8w/o7mzK6ljbqRnbyg==} + deprecated: 'replaced by the ''expo'' package, learn more: https://blog.expo.dev/whats-new-in-expo-modules-infrastructure-7a7cdda81ebc' + + '@unimodules/react-native-adapter@6.3.9': + resolution: {integrity: sha512-i9/9Si4AQ8awls+YGAKkByFbeAsOPgUNeLoYeh2SQ3ddjxJ5ZJDtq/I74clDnpDcn8zS9pYlcDJ9fgVJa39Glw==} + deprecated: 'replaced by the ''expo'' package, learn more: https://blog.expo.dev/whats-new-in-expo-modules-infrastructure-7a7cdda81ebc' + + '@urql/core@2.3.6': + resolution: {integrity: sha512-PUxhtBh7/8167HJK6WqBv6Z0piuiaZHQGYbhwpNL9aIQmLROPEdaUYkY4wh45wPQXcTpnd11l0q3Pw+TI11pdw==} + peerDependencies: + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@urql/exchange-retry@0.3.0': + resolution: {integrity: sha512-hHqer2mcdVC0eYnVNbWyi28AlGOPb2vjH3lP3/Bc8Lc8BjhMsDwFMm7WhoP5C1+cfbr/QJ6Er3H/L08wznXxfg==} + peerDependencies: + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 + + '@xmldom/xmldom@0.7.13': + resolution: {integrity: sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==} + engines: {node: '>=10.0.0'} + + '@xmldom/xmldom@0.8.10': + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + absolute-path@0.0.0: + resolution: {integrity: sha512-HQiug4c+/s3WOvEnDRxXVmNtSG5s2gJM9r19BTcqjp7BWcE48PB+Y2G6jE65kqI0LpsQeMZygt/b60Gi4KxGyA==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.3: + resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} + engines: {node: '>=0.4.0'} + + acorn@8.12.0: + resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.16.0: + resolution: {integrity: sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==} + + anser@1.4.10: + resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-fragments@0.2.1: + resolution: {integrity: sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + appdirsjs@1.2.7: + resolution: {integrity: sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + application-config-path@0.1.1: + resolution: {integrity: sha512-zy9cHePtMP0YhwG+CfHm0bgwdnga2X3gZexpdCwEj//dpb+TKajtiC8REEUJUSq6Ab4f9cgNy2l8ObXzCXFkEw==} + + aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-back@3.1.0: + resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} + engines: {node: '>=6'} + + array-back@4.0.2: + resolution: {integrity: sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==} + engines: {node: '>=8'} + + array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + + array-index@1.0.0: + resolution: {integrity: sha512-jesyNbBkLQgGZMSwA1FanaFjalb1mZUGxGeUEkSDidzgrbjBGhvizJkaItdhkt8eIHFOJC7nDsrXk+BaehTdRw==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlastindex@1.2.5: + resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + + arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asmcrypto.js@0.22.0: + resolution: {integrity: sha512-usgMoyXjMbx/ZPdzTSXExhMPur2FTdz/Vo5PVx2gIaBcdAAJNOFlsdgqveM8Cff7W0v+xrf9BwjOV26JSAF9qA==} + + asn1js@3.0.5: + resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} + engines: {node: '>=12.0.0'} + + ast-types@0.15.2: + resolution: {integrity: sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==} + engines: {node: '>=4'} + + astral-regex@1.0.0: + resolution: {integrity: sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==} + engines: {node: '>=4'} + + async-limiter@1.0.1: + resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} + + async-mutex@0.4.1: + resolution: {integrity: sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==} + + async@3.2.5: + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios@0.21.4: + resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} + + b64-lite@1.4.0: + resolution: {integrity: sha512-aHe97M7DXt+dkpa8fHlCcm1CnskAHrJqEfMI0KN7dwqlzml/aUe1AGt6lk51HzrSfVD67xOso84sOpr+0wIe2w==} + + b64u-lite@1.1.0: + resolution: {integrity: sha512-929qWGDVCRph7gQVTC6koHqQIpF4vtVaSbwLltFQo44B1bYUquALswZdBKFfrJCPEnsCOvWkJsPdQYZ/Ukhw8A==} + + babel-core@7.0.0-bridge.0: + resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-plugin-polyfill-corejs2@0.4.11: + resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.10.4: + resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.2: + resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-react-native-web@0.19.12: + resolution: {integrity: sha512-eYZ4+P6jNcB37lObWIg0pUbi7+3PKoU1Oie2j0C8UF3cXyXoR74tO2NBjI/FORb2LJyItJZEAmjU5pSaJYEL1w==} + + babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: + resolution: {integrity: sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==} + + babel-plugin-transform-flow-enums@0.0.2: + resolution: {integrity: sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==} + + babel-preset-current-node-syntax@1.0.1: + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-preset-expo@11.0.10: + resolution: {integrity: sha512-YBg40Om31gw9IPlRw5v8elzgtPUtNEh4GSibBi5MsmmYddGg4VPjWtCZIFJChN543qRmbGb/fa/kejvLX567hQ==} + + babel-preset-fbjs@3.4.0: + resolution: {integrity: sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base-64@0.1.0: + resolution: {integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==} + + base-x@3.0.9: + resolution: {integrity: sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64url-universal@1.1.0: + resolution: {integrity: sha512-WyftvZqye29YQ10ZnuiBeEj0lk8SN8xHU9hOznkLc85wS1cLTp6RpzlMrHxMPD9nH7S55gsBqMqgGyz93rqmkA==} + engines: {node: '>=8.3.0'} + + base64url-universal@2.0.0: + resolution: {integrity: sha512-6Hpg7EBf3t148C3+fMzjf+CHnADVDafWzlJUXAqqqbm4MKNXbsoPdOkWeRTjNlkYG7TpyjIpRO1Gk0SnsFD1rw==} + engines: {node: '>=14'} + + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + + bech32@1.1.4: + resolution: {integrity: sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==} + + bech32@2.0.0: + resolution: {integrity: sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==} + + better-opn@3.0.2: + resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} + engines: {node: '>=12.0.0'} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + + bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + + bn.js@5.2.1: + resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + + body-parser@1.20.2: + resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + borc@3.0.0: + resolution: {integrity: sha512-ec4JmVC46kE0+layfnwM3l15O70MlFiEbmQHY/vpqIKiUtPVntv4BY4NVnz3N4vb21edV3mY97XVckFvYHWF9g==} + engines: {node: '>=4'} + hasBin: true + + bplist-creator@0.0.7: + resolution: {integrity: sha512-xp/tcaV3T5PCiaY04mXga7o/TE+t95gqeLmADeBI1CvZtdWTbgBt3uLpvh4UWtenKeBhCV6oVxGk38yZr2uYEA==} + + bplist-creator@0.1.0: + resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} + + bplist-parser@0.3.1: + resolution: {integrity: sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==} + engines: {node: '>= 5.10.0'} + + bplist-parser@0.3.2: + resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} + engines: {node: '>= 5.10.0'} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + breakword@1.0.6: + resolution: {integrity: sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==} + + brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + + browserslist@4.23.1: + resolution: {integrity: sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bs58@4.0.1: + resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-alloc-unsafe@1.1.0: + resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} + + buffer-alloc@1.2.0: + resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==} + + buffer-fill@1.0.0: + resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + builtins@1.0.3: + resolution: {integrity: sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.0.0: + resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + engines: {node: '>= 0.8'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cacache@18.0.3: + resolution: {integrity: sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg==} + engines: {node: ^16.14.0 || >=18.0.0} + + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + + caller-callsite@2.0.0: + resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} + engines: {node: '>=4'} + + caller-path@2.0.0: + resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==} + engines: {node: '>=4'} + + callsites@2.0.0: + resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==} + engines: {node: '>=4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001634: + resolution: {integrity: sha512-fbBYXQ9q3+yp1q1gBk86tOFs4pyn/yxFm5ZNP18OXJDfA3txImOY9PhfxVggZ4vRHDqoU8NrKU81eN0OtzOgRA==} + + canonicalize@1.0.8: + resolution: {integrity: sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==} + + canonicalize@2.0.0: + resolution: {integrity: sha512-ulDEYPv7asdKvqahuAY35c1selLdzDwHqugK92hfkzvlDCwXRRelDkR+Er33md/PtnpqHemgkuDPanZ4fiYZ8w==} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.0: + resolution: {integrity: sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==} + engines: {node: '>=10'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chrome-launcher@0.15.2: + resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} + engines: {node: '>=12.13.0'} + hasBin: true + + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.3.1: + resolution: {integrity: sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==} + + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.14.1: + resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + clear@0.1.0: + resolution: {integrity: sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw==} + + cli-cursor@2.1.0: + resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} + engines: {node: '>=4'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + command-exists@1.2.9: + resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} + + command-line-args@5.2.1: + resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} + engines: {node: '>=4.0.0'} + + command-line-commands@3.0.2: + resolution: {integrity: sha512-ac6PdCtdR6q7S3HN+JiVLIWGHY30PRYIEl2qPo+FuEuzwAUk0UYyimrngrg7FvF/mCr4Jgoqv5ZnHZgads50rw==} + engines: {node: '>=8'} + + command-line-usage@6.1.3: + resolution: {integrity: sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==} + engines: {node: '>=8.0.0'} + + commander@2.13.0: + resolution: {integrity: sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + compare-versions@3.6.0: + resolution: {integrity: sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==} + + component-type@1.2.2: + resolution: {integrity: sha512-99VUHREHiN5cLeHm3YLq312p6v+HUEcwtLCAtelvUDI6+SH5g5Cr85oNR2S1o6ywzL0ykMbuwLzM2ANocjEOIA==} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.7.4: + resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} + engines: {node: '>= 0.8.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + + connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + core-js-compat@3.37.1: + resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cosmiconfig@5.2.1: + resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==} + engines: {node: '>=4'} + + cosmjs-types@0.7.2: + resolution: {integrity: sha512-vf2uLyktjr/XVAgEq0DjMxeAWh1yYREe7AMHDKd7EiHVqxBPCaBS+qEEQUkXbR9ndnckqr1sUG8BQhazh4X5lA==} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + credentials-context@2.0.0: + resolution: {integrity: sha512-/mFKax6FK26KjgV2KW2D4YqKgoJ5DVJpNt87X2Jc9IxT2HBMy7nEIlc+n7pEi+YFFe721XqrvZPd+jbyyBjsvQ==} + + cross-fetch@3.1.8: + resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} + + cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + + cross-spawn@5.1.0: + resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} + + cross-spawn@6.0.5: + resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} + engines: {node: '>=4.8'} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + + crypto-ld@6.0.0: + resolution: {integrity: sha512-XWL1LslqggNoaCI/m3I7HcvaSt9b2tYzdrXO+jHLUj9G1BvRfvV7ZTFDVY5nifYuIGAPdAGu7unPxLRustw3VA==} + engines: {node: '>=8.3.0'} + + crypto-random-string@1.0.0: + resolution: {integrity: sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==} + engines: {node: '>=4'} + + crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + + csv-generate@3.4.3: + resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} + + csv-parse@4.16.3: + resolution: {integrity: sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==} + + csv-stringify@5.6.5: + resolution: {integrity: sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==} + + csv@5.5.3: + resolution: {integrity: sha512-QTaY0XjjhTQOdguARF0lGKm5/mEq9PD9/VhZZegHDIBq2tQwgNpHc3dneD4mGo2iJs+fTKv5Bp0fZ+BRuY3Z0g==} + engines: {node: '>= 0.1.90'} + + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + + dag-map@1.0.2: + resolution: {integrity: sha512-+LSAiGFwQ9dRnRdOeaj7g47ZFJcOUPukAP8J3A3fuZ1g9Y44BG+P1sgApjLXTQPOzC4+7S9Wr8kXsfpINM4jpw==} + + data-uri-to-buffer@3.0.1: + resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} + engines: {node: '>= 6'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + + dayjs@1.11.11: + resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@3.3.0: + resolution: {integrity: sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==} + engines: {node: '>=0.10.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-gateway@4.2.0: + resolution: {integrity: sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==} + engines: {node: '>=6'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + del@6.1.1: + resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} + engines: {node: '>=10'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + denodeify@1.2.1: + resolution: {integrity: sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + deprecated-react-native-prop-types@3.0.2: + resolution: {integrity: sha512-JoZY5iNM+oJlN2Ldpq0KSi0h3Nig4hlNJj5nWzWp8eL3uikMCvHwjSGPitwkEw0arL5JFra5nuGJQpXRbEjApg==} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + did-jwt@6.11.6: + resolution: {integrity: sha512-OfbWknRxJuUqH6Lk0x+H1FsuelGugLbBDEwsoJnicFOntIG/A4y19fn0a8RLxaQbWQ5gXg0yDq5E2huSBiiXzw==} + + did-resolver@4.1.0: + resolution: {integrity: sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA==} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dotenv-expand@11.0.6: + resolution: {integrity: sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==} + engines: {node: '>=12'} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ed25519-signature-2018-context@1.1.0: + resolution: {integrity: sha512-ppDWYMNwwp9bploq0fS4l048vHIq41nWsAbPq6H4mNVx9G/GxW3fwg4Ln0mqctP13MoEpREK7Biz8TbVVdYXqA==} + + ed25519-signature-2020-context@1.1.0: + resolution: {integrity: sha512-dBGSmoUIK6h2vadDctrDnhhTO01PR2hJk0mRNEfrRDPCjaIwrfy4J+eziEQ9Q1m8By4f/CSRgKM1h53ydKfdNg==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.4.803: + resolution: {integrity: sha512-61H9mLzGOCLLVsnLiRzCbc63uldP0AniRYPV3hbGVtONA1pI7qSGILdbofR7A8TMbOypDocEAjH/e+9k1QIe3g==} + + elliptic@6.5.5: + resolution: {integrity: sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + enhanced-resolve@5.17.0: + resolution: {integrity: sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==} + engines: {node: '>=10.13.0'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + env-editor@0.4.2: + resolution: {integrity: sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==} + engines: {node: '>=8'} + + envinfo@7.13.0: + resolution: {integrity: sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==} + engines: {node: '>=4'} + hasBin: true + + eol@0.9.1: + resolution: {integrity: sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + errorhandler@1.5.1: + resolution: {integrity: sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==} + engines: {node: '>= 0.8'} + + es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + + es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@1.14.3: + resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} + engines: {node: '>=4.0'} + hasBin: true + + eslint-config-prettier@8.10.0: + resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.6.1: + resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + + eslint-module-utils@2.8.1: + resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.29.1: + resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-prettier@4.2.1: + resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + eslint: '>=7.28.0' + eslint-config-prettier: '*' + prettier: '>=2.0.0' + peerDependenciesMeta: + eslint-config-prettier: + optional: true + + eslint-plugin-workspaces@0.8.0: + resolution: {integrity: sha512-8BhKZaGFpl0xAVo7KHaWffaBvvroaOeLuqLkVsMNZvMaN6ZHKYx7QZoaXC/Y299tG3wvN6v7hu27VBHmyg4q4g==} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@1.2.2: + resolution: {integrity: sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==} + engines: {node: '>=0.4.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + exec-async@2.2.0: + resolution: {integrity: sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==} + + execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + expo-asset@10.0.9: + resolution: {integrity: sha512-KX7LPtVf9eeMidUvYZafXZldrVdzfjZNKKFAjFvDy2twg7sTa2R0L4VdCXp32eGLWZyk+i/rpOUSbyD1YFyJnA==} + peerDependencies: + expo: '*' + + expo-constants@16.0.2: + resolution: {integrity: sha512-9tNY3OVO0jfiMzl7ngb6IOyR5VFzNoN5OOazUWoeGfmMqVB5kltTemRvKraK9JRbBKIw+SOYLEmF0sEqgFZ6OQ==} + peerDependencies: + expo: '*' + + expo-file-system@17.0.1: + resolution: {integrity: sha512-dYpnZJqTGj6HCYJyXAgpFkQWsiCH3HY1ek2cFZVHFoEc5tLz9gmdEgTF6nFHurvmvfmXqxi7a5CXyVm0aFYJBw==} + peerDependencies: + expo: '*' + + expo-font@12.0.7: + resolution: {integrity: sha512-rbSdpjtT/A3M+u9xchR9tdD+5VGSxptUis7ngX5zfAVp3O5atOcPNSA82Jeo15HkrQE+w/upfFBOvi56lsGdsQ==} + peerDependencies: + expo: '*' + + expo-keep-awake@13.0.2: + resolution: {integrity: sha512-kKiwkVg/bY0AJ5q1Pxnm/GvpeB6hbNJhcFsoOWDh2NlpibhCLaHL826KHUM+WsnJRbVRxJ+K9vbPRHEMvFpVyw==} + peerDependencies: + expo: '*' + + expo-modules-autolinking@0.0.3: + resolution: {integrity: sha512-azkCRYj/DxbK4udDuDxA9beYzQTwpJ5a9QA0bBgha2jHtWdFGF4ZZWSY+zNA5mtU3KqzYt8jWHfoqgSvKyu1Aw==} + hasBin: true + + expo-modules-autolinking@1.11.1: + resolution: {integrity: sha512-2dy3lTz76adOl7QUvbreMCrXyzUiF8lygI7iFJLjgIQIVH+43KnFWE5zBumpPbkiaq0f0uaFpN9U0RGQbnKiMw==} + hasBin: true + + expo-modules-core@1.12.15: + resolution: {integrity: sha512-VjDPIgUyhCZzf692NF4p2iFTsKAQMcU3jc0pg33eNvN/kdrJqkeucqCDuuwoNxg0vIBKtoqAJDuPnWiemldsTg==} + + expo-random@14.0.1: + resolution: {integrity: sha512-gX2mtR9o+WelX21YizXUCD/y+a4ZL+RDthDmFkHxaYbdzjSYTn8u/igoje/l3WEO+/RYspmqUFa8w/ckNbt6Vg==} + peerDependencies: + expo: '*' + + expo@51.0.14: + resolution: {integrity: sha512-99BAMSYBH1aq1TIEJqM03kRpsZjN8OqZXDqYHRq9/PXT67axRUOvRjwMMLprnCmqkAVM7m7FpiECNWN4U0gvLQ==} + hasBin: true + + express@4.19.2: + resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} + engines: {node: '>= 0.10.0'} + + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + fast-base64-decode@1.0.0: + resolution: {integrity: sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-text-encoding@1.0.6: + resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==} + + fast-xml-parser@4.4.0: + resolution: {integrity: sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==} + hasBin: true + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fbemitter@3.0.0: + resolution: {integrity: sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==} + + fbjs-css-vars@1.0.2: + resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} + + fbjs@3.0.5: + resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + + fetch-blob@2.1.2: + resolution: {integrity: sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==} + engines: {node: ^10.17.0 || >=12.3.0} + peerDependencies: + domexception: '*' + peerDependenciesMeta: + domexception: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + fetch-retry@4.1.1: + resolution: {integrity: sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA==} + + figlet@1.7.0: + resolution: {integrity: sha512-gO8l3wvqo0V7wEFLXPbkX83b7MVjRrk1oRLfYlZXol8nEpb/ON9pcKLI4qpBv5YtOTfrINtqb7b40iYY2FTWFg==} + engines: {node: '>= 0.4.0'} + hasBin: true + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-type@16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + + finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + + finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + + find-cache-dir@2.1.0: + resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} + engines: {node: '>=6'} + + find-replace@3.0.0: + resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} + engines: {node: '>=4.0.0'} + + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-workspaces@0.1.0: + resolution: {integrity: sha512-DmHumOdSCtwY6qW6Syx3a/W6ZGYLhGiwqWCiPOsld4sxP9yeRh3LraKeu+G3l5ilgt8jOUAgjDHT4MOFZ8dQ3Q==} + + find-yarn-workspace-root2@1.2.16: + resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} + + find-yarn-workspace-root@2.0.0: + resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} + + fix-esm@1.0.1: + resolution: {integrity: sha512-EZtb7wPXZS54GaGxaWxMlhd1DUDCnAg5srlYdu/1ZVeW+7wwR3Tp59nu52dXByFs3MBRq+SByx1wDOJpRvLEXw==} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + flow-parser@0.185.2: + resolution: {integrity: sha512-2hJ5ACYeJCzNtiVULov6pljKOLygy0zddoqSI1fFetM+XRPpRshFdGEijtqlamA1XwyZ+7rhryI6FQFzvtLWUQ==} + engines: {node: '>=0.4.0'} + + follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + fontfaceobserver@2.3.0: + resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==} + + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + + foreground-child@3.2.1: + resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} + engines: {node: '>=14'} + + form-data@3.0.1: + resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} + engines: {node: '>= 6'} + + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + freeport-async@2.0.0: + resolution: {integrity: sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==} + engines: {node: '>=8'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.0.0: + resolution: {integrity: sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==} + engines: {node: '>=10'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-port@3.2.0: + resolution: {integrity: sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==} + engines: {node: '>=4'} + + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + + get-symbol-from-current-process-h@1.0.2: + resolution: {integrity: sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw==} + + get-tsconfig@4.7.5: + resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==} + + get-uv-event-loop-napi-h@1.0.6: + resolution: {integrity: sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg==} + + getenv@1.0.0: + resolution: {integrity: sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==} + engines: {node: '>=6'} + + git-config@0.0.7: + resolution: {integrity: sha512-LidZlYZXWzVjS+M3TEwhtYBaYwLeOZrXci1tBgqp/vDdZTBMl02atvwb6G35L64ibscYoPnxfbwwUS+VZAISLA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.1: + resolution: {integrity: sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==} + engines: {node: '>=16 || 14 >=14.18'} + hasBin: true + + glob@6.0.4: + resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + grapheme-splitter@1.0.4: + resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + graphql-tag@2.12.6: + resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + graphql@15.8.0: + resolution: {integrity: sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==} + engines: {node: '>= 10.x'} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + + has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hermes-estree@0.19.1: + resolution: {integrity: sha512-daLGV3Q2MKk8w4evNMKwS8zBE/rcpA800nu1Q5kM08IKijoSnPe9Uo1iIxzPKRkn95IxxsgBMPeYHt3VG4ej2g==} + + hermes-estree@0.8.0: + resolution: {integrity: sha512-W6JDAOLZ5pMPMjEiQGLCXSSV7pIBEgRR5zGkxgmzGSXHOxqV5dC/M1Zevqpbm9TZDE5tu358qZf8Vkzmsc+u7Q==} + + hermes-parser@0.19.1: + resolution: {integrity: sha512-Vp+bXzxYJWrpEuJ/vXxUsLnt0+y4q9zyi4zUlkLqD8FKv4LjIfOvP69R/9Lty3dCyKh0E2BU7Eypqr63/rKT/A==} + + hermes-parser@0.8.0: + resolution: {integrity: sha512-yZKalg1fTYG5eOiToLUaw69rQfZq/fi+/NtEXRU7N87K/XobNRhRWorh80oSge2lWUiZfTgUvRJH+XgZWrhoqA==} + + hermes-profile-transformer@0.0.6: + resolution: {integrity: sha512-cnN7bQUm65UWOy6cbGcCcZ3rpwW8Q/j4OP5aWRhEry4Z2t2aR1cjrbp0BS+KiBN0smvP1caBgAuxutvyvJILzQ==} + engines: {node: '>=8'} + + hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + hosted-git-info@3.0.8: + resolution: {integrity: sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==} + engines: {node: '>=10'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + human-id@1.0.2: + resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + + image-size@0.6.3: + resolution: {integrity: sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA==} + engines: {node: '>=4.0'} + hasBin: true + + import-fresh@2.0.0: + resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} + engines: {node: '>=4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + iniparser@1.0.5: + resolution: {integrity: sha512-i40MWqgTU6h/70NtMsDVVDLjDYWwcIR1yIEVDPfxZIJno9z9L4s83p/V7vAu2i48Vj0gpByrkGFub7ko9XvPrw==} + + inquirer@7.3.3: + resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} + engines: {node: '>=8.0.0'} + + inquirer@8.2.6: + resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} + engines: {node: '>=12.0.0'} + + internal-ip@4.3.0: + resolution: {integrity: sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==} + engines: {node: '>=6'} + + internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ip-regex@2.1.0: + resolution: {integrity: sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==} + engines: {node: '>=4'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + + is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + + is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + + is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + + is-directory@0.3.1: + resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==} + engines: {node: '>=0.10.0'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@1.0.0: + resolution: {integrity: sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-glob@2.0.1: + resolution: {integrity: sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-invalid-path@0.1.0: + resolution: {integrity: sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==} + engines: {node: '>=0.10.0'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-cwd@2.2.0: + resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} + engines: {node: '>=6'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-valid-path@0.1.1: + resolution: {integrity: sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==} + engines: {node: '>=0.10.0'} + + is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + is-wsl@1.1.0: + resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==} + engines: {node: '>=4'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iso-url@1.2.1: + resolution: {integrity: sha512-9JPDgCN4B7QPkLtYAAOrEuAWvP9rWvR5offAr0/SeF046wIkglqH3VXgYYP6NcsKslH80UIVgmPqNe3j7tG2ng==} + engines: {node: '>=12'} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + isomorphic-webcrypto@2.3.8: + resolution: {integrity: sha512-XddQSI0WYlSCjxtm1AI8kWQOulf7hAN3k3DclF1sxDJZqOe0pcsOt675zvWW91cZH9hYs3nlA3Ev8QK5i80SxQ==} + + isomorphic-ws@4.0.1: + resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} + peerDependencies: + ws: '*' + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.2: + resolution: {integrity: sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + jackspeak@3.4.0: + resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==} + engines: {node: '>=14'} + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': 18.18.8 + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@26.3.0: + resolution: {integrity: sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==} + engines: {node: '>= 10.14.2'} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@27.5.1: + resolution: {integrity: sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-serializer@27.5.1: + resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@27.5.1: + resolution: {integrity: sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@26.6.2: + resolution: {integrity: sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==} + engines: {node: '>= 10.14.2'} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jimp-compact@0.16.1: + resolution: {integrity: sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==} + + joi@17.13.1: + resolution: {integrity: sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg==} + + join-component@1.1.0: + resolution: {integrity: sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==} + + js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + + js-sha3@0.8.0: + resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsc-android@250231.0.0: + resolution: {integrity: sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw==} + + jsc-safe-url@0.2.4: + resolution: {integrity: sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==} + + jscodeshift@0.14.0: + resolution: {integrity: sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA==} + hasBin: true + peerDependencies: + '@babel/preset-env': ^7.1.6 + + jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-deref-sync@0.13.0: + resolution: {integrity: sha512-YBOEogm5w9Op337yb6pAT6ZXDqlxAsQCanM3grid8lMWNxRJO/zWEJi3ZzqDL8boWfwhTFym5EFrNgWwpqcBRg==} + engines: {node: '>=6.0.0'} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json-text-sequence@0.3.0: + resolution: {integrity: sha512-7khKIYPKwXQem4lWXfpIN/FEnhztCeRPSxH4qm3fVlqulwujrRDD54xAwDDn/qVKpFtV550+QAkcWJcufzqQuA==} + engines: {node: '>=10.18.0'} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonld-signatures@11.2.1: + resolution: {integrity: sha512-RNaHTEeRrX0jWeidPCwxMq/E/Ze94zFyEZz/v267ObbCHQlXhPO7GtkY6N5PSHQfQhZPXa8NlMBg5LiDF4dNbA==} + engines: {node: '>=14'} + + jsonld@8.3.2: + resolution: {integrity: sha512-MwBbq95szLwt8eVQ1Bcfwmgju/Y5P2GdtlHE2ncyfuYjIdEhluUVyj1eudacf1mOkWIoS9GpDBTECqhmq7EOaA==} + engines: {node: '>=14'} + + jsonpath@1.1.1: + resolution: {integrity: sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==} + + jwt-decode@3.1.2: + resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + ky-universal@0.11.0: + resolution: {integrity: sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw==} + engines: {node: '>=14.16'} + peerDependencies: + ky: '>=0.31.4' + web-streams-polyfill: '>=3.2.1' + peerDependenciesMeta: + web-streams-polyfill: + optional: true + + ky-universal@0.8.2: + resolution: {integrity: sha512-xe0JaOH9QeYxdyGLnzUOVGK4Z6FGvDVzcXFTdrYA1f33MZdEa45sUDaMBy98xQMcsd2XIBrTXRrRYnegcSdgVQ==} + engines: {node: '>=10.17'} + peerDependencies: + ky: '>=0.17.0' + web-streams-polyfill: '>=2.0.0' + peerDependenciesMeta: + web-streams-polyfill: + optional: true + + ky@0.25.1: + resolution: {integrity: sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA==} + engines: {node: '>=10'} + + ky@0.33.3: + resolution: {integrity: sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==} + engines: {node: '>=14.16'} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + libphonenumber-js@1.11.3: + resolution: {integrity: sha512-RU0CTsLCu2v6VEzdP+W6UU2n5+jEpMDRkGxUeBgsAJgre3vKgm17eApISH9OQY4G0jZYJVIc8qXmz6CJFueAFg==} + + libsodium-wrappers@0.7.13: + resolution: {integrity: sha512-kasvDsEi/r1fMzKouIDv7B8I6vNmknXwGiYodErGuESoFTohGSKZplFtVxZqHaoQ217AynyIFgnOVRitpHs0Qw==} + + libsodium@0.7.13: + resolution: {integrity: sha512-mK8ju0fnrKXXfleL53vtp9xiPq5hKM0zbDQtcxQIsSmxNgSxqCj6R7Hl9PkrNe2j29T4yoDaF7DJLK9/i5iWUw==} + + lighthouse-logger@1.4.2: + resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} + + lightningcss-darwin-arm64@1.19.0: + resolution: {integrity: sha512-wIJmFtYX0rXHsXHSr4+sC5clwblEMji7HHQ4Ub1/CznVRxtCFha6JIt5JZaNf8vQrfdZnBxLLC6R8pC818jXqg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.19.0: + resolution: {integrity: sha512-Lif1wD6P4poaw9c/4Uh2z+gmrWhw/HtXFoeZ3bEsv6Ia4tt8rOJBdkfVaUJ6VXmpKHALve+iTyP2+50xY1wKPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-linux-arm-gnueabihf@1.19.0: + resolution: {integrity: sha512-P15VXY5682mTXaiDtbnLYQflc8BYb774j2R84FgDLJTN6Qp0ZjWEFyN1SPqyfTj2B2TFjRHRUvQSSZ7qN4Weig==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.19.0: + resolution: {integrity: sha512-zwXRjWqpev8wqO0sv0M1aM1PpjHz6RVIsBcxKszIG83Befuh4yNysjgHVplF9RTU7eozGe3Ts7r6we1+Qkqsww==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.19.0: + resolution: {integrity: sha512-vSCKO7SDnZaFN9zEloKSZM5/kC5gbzUjoJQ43BvUpyTFUX7ACs/mDfl2Eq6fdz2+uWhUh7vf92c4EaaP4udEtA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.19.0: + resolution: {integrity: sha512-0AFQKvVzXf9byrXUq9z0anMGLdZJS+XSDqidyijI5njIwj6MdbvX2UZK/c4FfNmeRa2N/8ngTffoIuOUit5eIQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.19.0: + resolution: {integrity: sha512-SJoM8CLPt6ECCgSuWe+g0qo8dqQYVcPiW2s19dxkmSI5+Uu1GIRzyKA0b7QqmEXolA+oSJhQqCmJpzjY4CuZAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-x64-msvc@1.19.0: + resolution: {integrity: sha512-C+VuUTeSUOAaBZZOPT7Etn/agx/MatzJzGRkeV+zEABmPuntv1zihncsi+AyGmjkkzq3wVedEy7h0/4S84mUtg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.19.0: + resolution: {integrity: sha512-yV5UR7og+Og7lQC+70DA7a8ta1uiOPnWPJfxa0wnxylev5qfo4P+4iMpzWAdYWOca4jdNQZii+bDL/l+4hUXIA==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-yaml-file@0.2.0: + resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} + engines: {node: '>=6'} + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@2.2.0: + resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} + engines: {node: '>=4'} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + logkitty@0.7.1: + resolution: {integrity: sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==} + hasBin: true + + long@4.0.0: + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + + long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.2.2: + resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} + engines: {node: 14 || >=16.14} + + lru-cache@4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru_map@0.4.1: + resolution: {integrity: sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==} + + luxon@3.4.4: + resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} + engines: {node: '>=12'} + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + make-promises-safe@5.1.0: + resolution: {integrity: sha512-AfdZ49rtyhQR/6cqVKGoH7y4ql7XkS5HJI1lZm0/5N6CQosy1eYbBJ/qbhkKHzo17UH7M918Bysf6XB9f3kS1g==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + + marky@1.2.5: + resolution: {integrity: sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==} + + md5-file@3.2.3: + resolution: {integrity: sha512-3Tkp1piAHaworfcCgH0jKbTvj1jWWFgbvh2cXaNCgHwyTCBxxvD1Y04rmfpvdPm1P4oXMOpm6+2H7sr7v9v8Fw==} + engines: {node: '>=0.10'} + hasBin: true + + md5@2.2.1: + resolution: {integrity: sha512-PlGG4z5mBANDGCKsYQe0CaUYHdZYZt8ZPZLmEt+Urf0W4GlpTX4HescwHU+dc9+Z/G/vZKYZYFrwgm9VxK6QOQ==} + + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + + md5hex@1.0.0: + resolution: {integrity: sha512-c2YOUbp33+6thdCUi34xIyOU/a7bvGKj/3DB1iaPMTuPHf/Q2d5s4sn1FaCOO43XkXggnb08y5W2PU8UNYNLKQ==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + + memory-cache@0.2.0: + resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==} + + meow@6.1.1: + resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} + engines: {node: '>=8'} + + merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + metro-babel-transformer@0.73.10: + resolution: {integrity: sha512-Yv2myTSnpzt/lTyurLvqYbBkytvUJcLHN8XD3t7W6rGiLTQPzmf1zypHQLphvcAXtCWBOXFtH7KLOSi2/qMg+A==} + + metro-cache-key@0.73.10: + resolution: {integrity: sha512-JMVDl/EREDiUW//cIcUzRjKSwE2AFxVWk47cFBer+KA4ohXIG2CQPEquT56hOw1Y1s6gKNxxs1OlAOEsubrFjw==} + + metro-cache@0.73.10: + resolution: {integrity: sha512-wPGlQZpdVlM404m7MxJqJ+hTReDr5epvfPbt2LerUAHY9RN99w61FeeAe25BMZBwgUgDtAsfGlJ51MBHg8MAqw==} + + metro-config@0.73.10: + resolution: {integrity: sha512-wIlybd1Z9I8K2KcStTiJxTB7OK529dxFgogNpKCTU/3DxkgAASqSkgXnZP6kVyqjh5EOWAKFe5U6IPic7kXDdQ==} + + metro-core@0.73.10: + resolution: {integrity: sha512-5uYkajIxKyL6W45iz/ftNnYPe1l92CvF2QJeon1CHsMXkEiOJxEjo41l+iSnO/YodBGrmMCyupSO4wOQGUc0lw==} + + metro-file-map@0.73.10: + resolution: {integrity: sha512-XOMWAybeaXyD6zmVZPnoCCL2oO3rp4ta76oUlqWP0skBzhFxVtkE/UtDwApEMUY361JeBBago647gnKiARs+1g==} + + metro-hermes-compiler@0.73.10: + resolution: {integrity: sha512-rTRWEzkVrwtQLiYkOXhSdsKkIObnL+Jqo+IXHI7VEK2aSLWRAbtGNqECBs44kbOUypDYTFFE+WLtoqvUWqYkWg==} + + metro-inspector-proxy@0.73.10: + resolution: {integrity: sha512-CEEvocYc5xCCZBtGSIggMCiRiXTrnBbh8pmjKQqm9TtJZALeOGyt5pXUaEkKGnhrXETrexsg6yIbsQHhEvVfvQ==} + hasBin: true + + metro-minify-terser@0.73.10: + resolution: {integrity: sha512-uG7TSKQ/i0p9kM1qXrwbmY3v+6BrMItsOcEXcSP8Z+68bb+t9HeVK0T/hIfUu1v1PEnonhkhfzVsaP8QyTd5lQ==} + + metro-minify-uglify@0.73.10: + resolution: {integrity: sha512-eocnSeJKnLz/UoYntVFhCJffED7SLSgbCHgNvI6ju6hFb6EFHGJT9OLbkJWeXaWBWD3Zw5mYLS8GGqGn/CHZPA==} + + metro-react-native-babel-preset@0.73.10: + resolution: {integrity: sha512-1/dnH4EHwFb2RKEKx34vVDpUS3urt2WEeR8FYim+ogqALg4sTpG7yeQPxWpbgKATezt4rNfqAANpIyH19MS4BQ==} + peerDependencies: + '@babel/core': '*' + + metro-react-native-babel-transformer@0.73.10: + resolution: {integrity: sha512-4G/upwqKdmKEjmsNa92/NEgsOxUWOygBVs+FXWfXWKgybrmcjh3NoqdRYrROo9ZRA/sB9Y/ZXKVkWOGKHtGzgg==} + peerDependencies: + '@babel/core': '*' + + metro-resolver@0.73.10: + resolution: {integrity: sha512-HeXbs+0wjakaaVQ5BI7eT7uqxlZTc9rnyw6cdBWWMgUWB++KpoI0Ge7Hi6eQAOoVAzXC3m26mPFYLejpzTWjng==} + + metro-runtime@0.73.10: + resolution: {integrity: sha512-EpVKm4eN0Fgx2PEWpJ5NiMArV8zVoOin866jIIvzFLpmkZz1UEqgjf2JAfUJnjgv3fjSV3JqeGG2vZCaGQBTow==} + + metro-source-map@0.73.10: + resolution: {integrity: sha512-NAGv14701p/YaFZ76KzyPkacBw/QlEJF1f8elfs23N1tC33YyKLDKvPAzFJiYqjdcFvuuuDCA8JCXd2TgLxNPw==} + + metro-symbolicate@0.73.10: + resolution: {integrity: sha512-PmCe3TOe1c/NVwMlB+B17me951kfkB3Wve5RqJn+ErPAj93od1nxicp6OJe7JT4QBRnpUP8p9tw2sHKqceIzkA==} + engines: {node: '>=8.3'} + hasBin: true + + metro-transform-plugins@0.73.10: + resolution: {integrity: sha512-D4AgD3Vsrac+4YksaPmxs/0ocT67bvwTkFSIgWWeDvWwIG0U1iHzTS9f8Bvb4PITnXryDoFtjI6OWF7uOpGxpA==} + + metro-transform-worker@0.73.10: + resolution: {integrity: sha512-IySvVubudFxahxOljWtP0QIMMpgUrCP0bW16cz2Enof0PdumwmR7uU3dTbNq6S+XTzuMHR+076aIe4VhPAWsIQ==} + + metro@0.73.10: + resolution: {integrity: sha512-J2gBhNHFtc/Z48ysF0B/bfTwUwaRDLjNv7egfhQCc+934dpXcjJG2KZFeuybF+CvA9vo4QUi56G2U+RSAJ5tsA==} + hasBin: true + + micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@2.0.1: + resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mixme@0.5.10: + resolution: {integrity: sha512-5H76ANWinB1H3twpJ6JY8uvAtpmFvHNArpilJAjXRKXSDDLPIMoZArw5SH0q9z+lLs8IrMw7Q2VWpWimFKFT1Q==} + engines: {node: '>= 8.0.0'} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msrcrypto@1.5.8: + resolution: {integrity: sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q==} + + multer@1.4.5-lts.1: + resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==} + engines: {node: '>= 6.0.0'} + + multiformats@12.1.3: + resolution: {integrity: sha512-eajQ/ZH7qXZQR2AgtfpmSMizQzmyYVmCql7pdhldPuYQi4atACekbJaQplk6dWyIi10jCaFnd6pqvcEFXjbaJw==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} + + multiformats@9.9.0: + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + mv@2.1.1: + resolution: {integrity: sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==} + engines: {node: '>=0.8.0'} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + ncp@2.0.0: + resolution: {integrity: sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==} + hasBin: true + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + neon-cli@0.10.1: + resolution: {integrity: sha512-kOd9ELaYETe1J1nBEOYD7koAZVj6xR9TGwOPccAsWmwL5amkaXXXwXHCUHkBAWujlgSZY5f2pT+pFGkzoHExYQ==} + engines: {node: '>=8'} + hasBin: true + + nested-error-stacks@2.0.1: + resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==} + + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + + nocache@3.0.4: + resolution: {integrity: sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==} + engines: {node: '>=12.0.0'} + + nock@13.5.4: + resolution: {integrity: sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==} + engines: {node: '>= 10.13'} + + node-addon-api@3.2.1: + resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} + + node-cache@5.1.2: + resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} + engines: {node: '>= 8.0.0'} + + node-dir@0.1.17: + resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} + engines: {node: '>= 0.10.5'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-fetch@3.0.0-beta.9: + resolution: {integrity: sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==} + engines: {node: ^10.17 || >=12.3} + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + + node-gyp-build@4.8.1: + resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + + node-stream-zip@1.15.0: + resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==} + engines: {node: '>=0.12.0'} + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-package-arg@7.0.0: + resolution: {integrity: sha512-xXxr8y5U0kl8dVkz2oK7yZjPBvqM2fwaO5l3Yg13p03v8+E3qQcD0JNhHzjL1vyGgxcKkD0cco+NLR72iuPk3g==} + + npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + + nullthrows@1.1.1: + resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + + ob1@0.73.10: + resolution: {integrity: sha512-aO6EYC+QRRCkZxVJhCWhLKgVjhNuD6Gu1riGjxrIm89CqLsmKgxzYDDEsktmKsoDeRdWGQM5EdMzXDl5xcVfsw==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@2.0.1: + resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} + engines: {node: '>=4'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@6.4.0: + resolution: {integrity: sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==} + engines: {node: '>=8'} + + open@7.4.2: + resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} + engines: {node: '>=8'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optionator@0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@3.4.0: + resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} + engines: {node: '>=6'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + os-homedir@1.0.2: + resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} + engines: {node: '>=0.10.0'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + osenv@0.1.5: + resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==} + deprecated: This package is no longer supported. + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-png@2.1.0: + resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==} + engines: {node: '>=10'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + password-prompt@1.1.3: + resolution: {integrity: sha512-HkrjG2aJlvF0t2BMH0e2LB/EHf3Lcq3fNMzy4GYHcQblAvOl+QQji1Lx7WRBMqpVK8p+KR7bCg7oqAMXtdgqyw==} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + peek-readable@4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@3.0.1: + resolution: {integrity: sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==} + engines: {node: '>=10'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + pkg-dir@3.0.0: + resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} + engines: {node: '>=6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + pngjs@3.4.0: + resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} + engines: {node: '>=4.0.0'} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} + engines: {node: ^10 || ^12 || >=14} + + preferred-pm@3.1.3: + resolution: {integrity: sha512-MkXsENfftWSRpzCzImcp4FRsCc3y1opwB73CfCNWyzMqArju2CrlMHlqB7VexKiPEOjGMbttv1r9fSCn5S610w==} + engines: {node: '>=10'} + + prelude-ls@1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + pretty-format@26.6.2: + resolution: {integrity: sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==} + engines: {node: '>= 10'} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + + promise@8.3.0: + resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + propagate@2.0.1: + resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} + engines: {node: '>= 8'} + + protobufjs@6.11.4: + resolution: {integrity: sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==} + hasBin: true + + protobufjs@7.3.2: + resolution: {integrity: sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pseudomap@1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + + pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + pvtsutils@1.3.5: + resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==} + + pvutils@1.1.3: + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} + + qrcode-terminal@0.11.0: + resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==} + hasBin: true + + qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + + qs@6.12.1: + resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==} + engines: {node: '>=0.6'} + + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + rdf-canonize@3.4.0: + resolution: {integrity: sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==} + engines: {node: '>=12'} + + react-devtools-core@4.28.5: + resolution: {integrity: sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==} + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-native-codegen@0.71.6: + resolution: {integrity: sha512-e5pR4VldIhEaFctfSAEgxbng0uG4gjBQxAHes3EKLdosH/Av90pQfSe9IDVdFIngvNPzt8Y14pNjrtqov/yNIg==} + + react-native-fs@2.20.0: + resolution: {integrity: sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ==} + peerDependencies: + react-native: '*' + react-native-windows: '*' + peerDependenciesMeta: + react-native-windows: + optional: true + + react-native-get-random-values@1.11.0: + resolution: {integrity: sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ==} + peerDependencies: + react-native: '>=0.56' + + react-native-gradle-plugin@0.71.19: + resolution: {integrity: sha512-1dVk9NwhoyKHCSxcrM6vY6cxmojeATsBobDicX0ZKr7DgUF2cBQRTKsimQFvzH8XhOVXyH8p4HyDSZNIFI8OlQ==} + + react-native-securerandom@0.1.1: + resolution: {integrity: sha512-CozcCx0lpBLevxiXEb86kwLRalBCHNjiGPlw3P7Fi27U6ZLdfjOCNRHD1LtBKcvPvI3TvkBXB3GOtLvqaYJLGw==} + peerDependencies: + react-native: '*' + + react-native@0.71.19: + resolution: {integrity: sha512-E6Rz4lpe4NC9ZR6zq9AWiB0MAoVeidr+aPu/pmwk5ezvVL/wbQ1Gdj70v7fKMC8Nz5wYFO1S0XUNPUKYaPhfeg==} + engines: {node: '>=14'} + hasBin: true + peerDependencies: + react: 18.2.0 + + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + + react-refresh@0.4.3: + resolution: {integrity: sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA==} + engines: {node: '>=0.10.0'} + + react-shallow-renderer@16.15.0: + resolution: {integrity: sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-web-to-node-stream@3.0.2: + resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==} + engines: {node: '>=8'} + + readline@1.3.0: + resolution: {integrity: sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==} + + readonly-date@1.0.0: + resolution: {integrity: sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ==} + + recast@0.21.5: + resolution: {integrity: sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==} + engines: {node: '>= 4'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + reduce-flatten@2.0.0: + resolution: {integrity: sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==} + engines: {node: '>=6'} + + ref-array-di@1.2.2: + resolution: {integrity: sha512-jhCmhqWa7kvCVrWhR/d7RemkppqPUdxEil1CtTtm7FkZV8LcHHCK3Or9GinUiFP5WY3k0djUkMvhBhx49Jb2iA==} + + ref-struct-di@1.1.1: + resolution: {integrity: sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g==} + + reflect-metadata@0.1.14: + resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} + + regenerate-unicode-properties@10.1.1: + resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + + regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + + regexpu-core@5.3.2: + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} + engines: {node: '>=4'} + + regjsparser@0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + + remove-trailing-slash@0.1.1: + resolution: {integrity: sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + requireg@0.2.2: + resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==} + engines: {node: '>= 4.0.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@3.0.0: + resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} + engines: {node: '>=4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + resolve@1.7.1: + resolution: {integrity: sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==} + + restore-cursor@2.0.0: + resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} + engines: {node: '>=4'} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfc4648@1.5.2: + resolution: {integrity: sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg==} + + rimraf@2.2.8: + resolution: {integrity: sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@2.4.5: + resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@4.4.1: + resolution: {integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==} + engines: {node: '>=14'} + hasBin: true + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-json-stringify@1.2.0: + resolution: {integrity: sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==} + + safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + selfsigned@2.4.1: + resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} + engines: {node: '>=10'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.2: + resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + engines: {node: '>=10'} + hasBin: true + + send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + + serialize-error@2.1.0: + resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} + engines: {node: '>=0.10.0'} + + serialize-error@8.1.0: + resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==} + engines: {node: '>=10'} + + serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + + shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-plist@1.3.1: + resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@2.1.0: + resolution: {integrity: sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==} + engines: {node: '>=6'} + + slugify@1.6.6: + resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} + engines: {node: '>=8.0.0'} + + smartwrap@2.0.2: + resolution: {integrity: sha512-vCsKNQxb7PnCNd2wY1WClWifAc2lwqsG8OaswpJkVJsvMGcnEntdTCDajZCkk93Ay1U3t/9puJmb525Rg5MZBA==} + engines: {node: '>=6'} + hasBin: true + + source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + spawndamnit@2.0.0: + resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.18: + resolution: {integrity: sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==} + + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + ssri@10.0.6: + resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + stacktrace-parser@0.1.10: + resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==} + engines: {node: '>=6'} + + static-eval@2.0.2: + resolution: {integrity: sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + str2buf@1.3.0: + resolution: {integrity: sha512-xIBmHIUHYZDP4HyoXGHYNVmxlXLXDrtFHYT0eV6IOdEj3VO9ccaF1Ejl9Oq8iFjITllpT8FhaXb4KsNmw+3EuA==} + + stream-buffers@2.2.0: + resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} + engines: {node: '>= 0.10.0'} + + stream-transform@2.1.3: + resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string.prototype.matchall@4.0.11: + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} + engines: {node: '>= 0.4'} + + string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + + strtok3@6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + + structured-headers@0.4.1: + resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} + + sucrase@3.34.0: + resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} + engines: {node: '>=8'} + hasBin: true + + sudo-prompt@8.2.5: + resolution: {integrity: sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==} + + sudo-prompt@9.1.1: + resolution: {integrity: sha512-es33J1g2HjMpyAhz8lOR+ICmXXAqTuKbuXuUWLhOLew20oN9oUCgCJx615U/v7aioZg7IX5lIh9x34vwneu4pA==} + + sudo-prompt@9.2.1: + resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-hyperlinks@2.3.0: + resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-observable@2.0.3: + resolution: {integrity: sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==} + engines: {node: '>=0.10'} + + table-layout@1.0.2: + resolution: {integrity: sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==} + engines: {node: '>=8.0.0'} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + temp-dir@1.0.0: + resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} + engines: {node: '>=4'} + + temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + + temp@0.8.3: + resolution: {integrity: sha512-jtnWJs6B1cZlHs9wPG7BrowKxZw/rf6+UpGAkr8AaYmiTyTO7zQlLoST8zx/8TcUPnZmeBoB+H8ARuHZaSijVw==} + engines: {'0': node >=0.8.0} + + temp@0.8.4: + resolution: {integrity: sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==} + engines: {node: '>=6.0.0'} + + tempy@0.3.0: + resolution: {integrity: sha512-WrH/pui8YCwmeiAoxV+lpRH9HpRtgBhSR2ViBPgpGb/wnYDzp21R4MN45fsCGvLROvY67o3byhJRYRONJyImVQ==} + engines: {node: '>=8'} + + tempy@0.7.1: + resolution: {integrity: sha512-vXPxwOyaNVi9nyczO16mxmHGpl6ASC5/TVhRRHpqeYHvKQm58EaWNvZXxAhR0lYYnBOQFjXjhzeLsaXdjxLjRg==} + engines: {node: '>=10'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + terminal-link@2.1.1: + resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} + engines: {node: '>=8'} + + terser@5.31.1: + resolution: {integrity: sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + throat@5.0.0: + resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + traverse@0.6.9: + resolution: {integrity: sha512-7bBrcF+/LQzSgFmT0X5YclVqQxtv7TDJ1f8Wj7ibBu/U6BMLeOpUxuZjV7rMc44UtKxlnMFigdhFAIszSX1DMg==} + engines: {node: '>= 0.4'} + + trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + + ts-api-utils@1.3.0: + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-jest@29.1.4: + resolution: {integrity: sha512-YiHwDhSvCiItoAgsKtoLFCuakDzDsJ1DLDnSouTaTmdOcOwIkSzbLXduaQ6M5DRVhuZC/NYaaZ/mtHbWMv/S6Q==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': 18.18.8 + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + ts-typed-json@0.3.2: + resolution: {integrity: sha512-Tdu3BWzaer7R5RvBIJcg9r8HrTZgpJmsX+1meXMJzYypbkj8NK2oJN0yvm4Dp/Iv6tzFa/L5jKRmEVTga6K3nA==} + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + + tslog@4.9.3: + resolution: {integrity: sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==} + engines: {node: '>=16'} + + tsyringe@4.8.0: + resolution: {integrity: sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==} + engines: {node: '>= 6.0.0'} + + tty-table@4.2.3: + resolution: {integrity: sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==} + engines: {node: '>=8.0.0'} + hasBin: true + + type-check@0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + type-fest@0.16.0: + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} + engines: {node: '>=10'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.3.1: + resolution: {integrity: sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==} + engines: {node: '>=6'} + + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + type-fest@0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + + typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + + typedarray.prototype.slice@1.0.3: + resolution: {integrity: sha512-8WbVAQAUlENo1q3c3zZYuy5k9VzBQvp8AX9WOtbvyWlLM1v5JaSRmjubLjzHF4JFtptjH/5c/i95yaElvcjC0A==} + engines: {node: '>= 0.4'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript@5.5.2: + resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} + engines: {node: '>=14.17'} + hasBin: true + + typical@4.0.0: + resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} + engines: {node: '>=8'} + + typical@5.2.0: + resolution: {integrity: sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==} + engines: {node: '>=8'} + + ua-parser-js@1.0.38: + resolution: {integrity: sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==} + + uglify-es@3.3.9: + resolution: {integrity: sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==} + engines: {node: '>=0.8.0'} + deprecated: support for ECMAScript is superseded by `uglify-js` as of v3.13.0 + hasBin: true + + uglify-js@3.18.0: + resolution: {integrity: sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==} + engines: {node: '>=0.8.0'} + hasBin: true + + uint8arrays@3.1.1: + resolution: {integrity: sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==} + + unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + + underscore@1.12.1: + resolution: {integrity: sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} + engines: {node: '>=14.0'} + + unicode-canonical-property-names-ecmascript@2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + unique-filename@3.0.0: + resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + unique-slug@4.0.0: + resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + unique-string@1.0.0: + resolution: {integrity: sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==} + engines: {node: '>=4'} + + unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@1.0.0: + resolution: {integrity: sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==} + engines: {node: '>= 10.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.0.16: + resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-join@4.0.0: + resolution: {integrity: sha512-EGXjXJZhIHiQMK2pQukuFcL303nskqIRzWvPvV5O8miOfwoUb9G+a/Cld60kUyeaybEI94wvVClT10DtfeAExA==} + + use-sync-external-store@1.2.2: + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + utf8@3.0.0: + resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@7.0.3: + resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-to-istanbul@9.2.0: + resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} + engines: {node: '>=10.12.0'} + + valid-url@1.0.9: + resolution: {integrity: sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + validate-npm-package-name@3.0.0: + resolution: {integrity: sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==} + + validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + + varint@6.0.0: + resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vlq@1.0.1: + resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-did-resolver@2.0.27: + resolution: {integrity: sha512-YxQlNdeYBXLhVpMW62+TPlc6sSOiWyBYq7DNvY6FXmXOD9g0zLeShpq2uCKFFQV/WlSrBi/yebK/W5lMTDxMUQ==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webcrypto-core@1.8.0: + resolution: {integrity: sha512-kR1UQNH8MD42CYuLzvibfakG5Ew5seG85dMMoAM/1LqvckxaF6pUiidLuraIu4V+YCIFabYecUZAW0TuxAoaqw==} + + webcrypto-shim@0.1.7: + resolution: {integrity: sha512-JAvAQR5mRNRxZW2jKigWMjCMkjSdmP5cColRP1U/pTg69VgHXEi1orv5vVpJ55Zc5MIaPc1aaurzd9pjv2bveg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@5.0.0: + resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} + engines: {node: '>=8'} + + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + + whatwg-url-without-unicode@8.0.0-3: + resolution: {integrity: sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==} + engines: {node: '>=10'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-pm@2.0.0: + resolution: {integrity: sha512-Lhs9Pmyph0p5n5Z3mVnN0yWcbQYUAD7rbQUiMsQxOJ3T57k7RFe35SUwWMf7dsbDZks1uOmw4AecB/JMDj3v/w==} + engines: {node: '>=8.15'} + + which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + wonka@4.0.15: + resolution: {integrity: sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg==} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wordwrapjs@4.0.1: + resolution: {integrity: sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==} + engines: {node: '>=8.0.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@2.4.3: + resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + ws@6.2.2: + resolution: {integrity: sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@7.5.9: + resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.17.0: + resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xcode@3.0.1: + resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==} + engines: {node: '>=10.0.0'} + + xml2js@0.6.0: + resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xmlbuilder@14.0.0: + resolution: {integrity: sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg==} + engines: {node: '>=8.0'} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + xstream@11.14.0: + resolution: {integrity: sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml@2.4.5: + resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@2060.io/ffi-napi@4.0.9': + dependencies: + '@2060.io/ref-napi': 3.0.6 + debug: 4.3.5 + get-uv-event-loop-napi-h: 1.0.6 + node-addon-api: 3.2.1 + node-gyp-build: 4.8.1 + ref-struct-di: 1.1.1 + transitivePeerDependencies: + - supports-color + + '@2060.io/ref-napi@3.0.6': + dependencies: + debug: 4.3.5 + get-symbol-from-current-process-h: 1.0.2 + node-addon-api: 3.2.1 + node-gyp-build: 4.8.1 + transitivePeerDependencies: + - supports-color + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@animo-id/react-native-bbs-signatures@0.1.0(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-native: 0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1) + + '@astronautlabs/jsonpath@1.1.2': + dependencies: + static-eval: 2.0.2 + + '@azure/core-asynciterator-polyfill@1.0.2': {} + + '@babel/code-frame@7.10.4': + dependencies: + '@babel/highlight': 7.24.7 + optional: true + + '@babel/code-frame@7.24.7': + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 + + '@babel/compat-data@7.24.7': {} + + '@babel/core@7.24.7': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helpers': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + convert-source-map: 2.0.0 + debug: 4.3.5 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.24.7': + dependencies: + '@babel/types': 7.24.7 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + + '@babel/helper-annotate-as-pure@7.24.7': + dependencies: + '@babel/types': 7.24.7 + + '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-compilation-targets@7.24.7': + dependencies: + '@babel/compat-data': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + browserslist: 4.23.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-member-expression-to-functions': 7.24.7 + '@babel/helper-optimise-call-expression': 7.24.7 + '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + regexpu-core: 5.3.2 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + debug: 4.3.5 + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-environment-visitor@7.24.7': + dependencies: + '@babel/types': 7.24.7 + + '@babel/helper-function-name@7.24.7': + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 + + '@babel/helper-hoist-variables@7.24.7': + dependencies: + '@babel/types': 7.24.7 + + '@babel/helper-member-expression-to-functions@7.24.7': + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.24.7': + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.24.7': + dependencies: + '@babel/types': 7.24.7 + + '@babel/helper-plugin-utils@7.24.7': {} + + '@babel/helper-remap-async-to-generator@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-wrap-function': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-member-expression-to-functions': 7.24.7 + '@babel/helper-optimise-call-expression': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-simple-access@7.24.7': + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.24.7': + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-split-export-declaration@7.24.7': + dependencies: + '@babel/types': 7.24.7 + + '@babel/helper-string-parser@7.24.7': {} + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/helper-validator-option@7.24.7': {} + + '@babel/helper-wrap-function@7.24.7': + dependencies: + '@babel/helper-function-name': 7.24.7 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.24.7': + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 + + '@babel/highlight@7.24.7': + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + + '@babel/parser@7.24.7': + dependencies: + '@babel/types': 7.24.7 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-decorators@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-decorators': 7.24.7(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + optional: true + + '@babel/plugin-proposal-export-default-from@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-export-default-from': 7.24.7(@babel/core@7.24.7) + + '@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.7) + + '@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7) + optional: true + + '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) + + '@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7) + optional: true + + '@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.24.7)': + dependencies: + '@babel/compat-data': 7.24.7 + '@babel/core': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7) + + '@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) + + '@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-decorators@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + optional: true + + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-export-default-from@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-flow@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-import-assertions@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-import-attributes@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-arrow-functions@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-async-generator-functions@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-block-scoping@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-class-properties@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.7) + '@babel/helper-split-export-declaration': 7.24.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/template': 7.24.7 + + '@babel/plugin-transform-destructuring@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-dotall-regex@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-duplicate-keys@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-dynamic-import@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.7) + + '@babel/plugin-transform-exponentiation-operator@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-export-namespace-from@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.7) + + '@babel/plugin-transform-flow-strip-types@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.24.7) + + '@babel/plugin-transform-for-of@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-json-strings@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.7) + + '@babel/plugin-transform-literals@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-logical-assignment-operators@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7) + + '@babel/plugin-transform-member-expression-literals@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-modules-amd@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-new-target@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-nullish-coalescing-operator@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) + + '@babel/plugin-transform-numeric-separator@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7) + + '@babel/plugin-transform-object-rest-spread@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7) + + '@babel/plugin-transform-object-super@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) + + '@babel/plugin-transform-optional-chaining@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-private-methods@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-react-display-name@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-react-jsx-development@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/plugin-transform-react-jsx': 7.24.7(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + optional: true + + '@babel/plugin-transform-react-jsx-self@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-react-jsx-source@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-react-jsx@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-pure-annotations@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + optional: true + + '@babel/plugin-transform-regenerator@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + regenerator-transform: 0.15.2 + + '@babel/plugin-transform-reserved-words@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-runtime@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.7) + babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.7) + babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.7) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-shorthand-properties@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-spread@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-template-literals@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-typeof-symbol@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-typescript@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-unicode-property-regex@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-unicode-regex@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-unicode-sets-regex@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/preset-env@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/compat-data': 7.24.7 + '@babel/core': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.7) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.7) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.7) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-import-assertions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-import-attributes': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.7) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.7) + '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-async-generator-functions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-async-to-generator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-block-scoped-functions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-block-scoping': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-class-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-class-static-block': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-classes': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-destructuring': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-dotall-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-duplicate-keys': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-dynamic-import': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-exponentiation-operator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-export-namespace-from': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-for-of': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-function-name': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-json-strings': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-logical-assignment-operators': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-member-expression-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-modules-amd': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-modules-systemjs': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-modules-umd': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-new-target': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-numeric-separator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-object-rest-spread': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-object-super': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-optional-catch-binding': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-private-methods': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-private-property-in-object': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-property-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-regenerator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-reserved-words': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-sticky-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-typeof-symbol': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-unicode-escapes': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-unicode-property-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-unicode-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-unicode-sets-regex': 7.24.7(@babel/core@7.24.7) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.7) + babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.7) + babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.7) + babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.7) + core-js-compat: 3.37.1 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-flow@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + '@babel/plugin-transform-flow-strip-types': 7.24.7(@babel/core@7.24.7) + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/types': 7.24.7 + esutils: 2.0.3 + + '@babel/preset-react@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + '@babel/plugin-transform-react-display-name': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx-development': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-pure-annotations': 7.24.7(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + optional: true + + '@babel/preset-typescript@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-typescript': 7.24.7(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + '@babel/register@7.24.6(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + clone-deep: 4.0.1 + find-cache-dir: 2.1.0 + make-dir: 2.1.0 + pirates: 4.0.6 + source-map-support: 0.5.21 + + '@babel/regjsgen@0.8.0': {} + + '@babel/runtime@7.24.7': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.24.7': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + + '@babel/traverse@7.24.7': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + debug: 4.3.5 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.24.7': + dependencies: + '@babel/helper-string-parser': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + + '@bcoe/v8-coverage@0.2.3': {} + + '@changesets/apply-release-plan@7.0.3': + dependencies: + '@babel/runtime': 7.24.7 + '@changesets/config': 3.0.1 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.0 + '@changesets/should-skip-package': 0.1.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.6.2 + + '@changesets/assemble-release-plan@6.0.2': + dependencies: + '@babel/runtime': 7.24.7 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.0 + '@changesets/should-skip-package': 0.1.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.6.2 + + '@changesets/changelog-git@0.2.0': + dependencies: + '@changesets/types': 6.0.0 + + '@changesets/cli@2.27.5': + dependencies: + '@babel/runtime': 7.24.7 + '@changesets/apply-release-plan': 7.0.3 + '@changesets/assemble-release-plan': 6.0.2 + '@changesets/changelog-git': 0.2.0 + '@changesets/config': 3.0.1 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.0 + '@changesets/get-release-plan': 4.0.2 + '@changesets/git': 3.0.0 + '@changesets/logger': 0.1.0 + '@changesets/pre': 2.0.0 + '@changesets/read': 0.6.0 + '@changesets/should-skip-package': 0.1.0 + '@changesets/types': 6.0.0 + '@changesets/write': 0.3.1 + '@manypkg/get-packages': 1.1.3 + '@types/semver': 7.5.8 + ansi-colors: 4.1.3 + chalk: 2.4.2 + ci-info: 3.9.0 + enquirer: 2.4.1 + external-editor: 3.1.0 + fs-extra: 7.0.1 + human-id: 1.0.2 + meow: 6.1.1 + outdent: 0.5.0 + p-limit: 2.3.0 + preferred-pm: 3.1.3 + resolve-from: 5.0.0 + semver: 7.6.2 + spawndamnit: 2.0.0 + term-size: 2.2.1 + tty-table: 4.2.3 + + '@changesets/config@3.0.1': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.0 + '@changesets/logger': 0.1.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.7 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.0': + dependencies: + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + chalk: 2.4.2 + fs-extra: 7.0.1 + semver: 7.6.2 + + '@changesets/get-release-plan@4.0.2': + dependencies: + '@babel/runtime': 7.24.7 + '@changesets/assemble-release-plan': 6.0.2 + '@changesets/config': 3.0.1 + '@changesets/pre': 2.0.0 + '@changesets/read': 0.6.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.0': + dependencies: + '@babel/runtime': 7.24.7 + '@changesets/errors': 0.2.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.7 + spawndamnit: 2.0.0 + + '@changesets/logger@0.1.0': + dependencies: + chalk: 2.4.2 + + '@changesets/parse@0.4.0': + dependencies: + '@changesets/types': 6.0.0 + js-yaml: 3.14.1 + + '@changesets/pre@2.0.0': + dependencies: + '@babel/runtime': 7.24.7 + '@changesets/errors': 0.2.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.0': + dependencies: + '@babel/runtime': 7.24.7 + '@changesets/git': 3.0.0 + '@changesets/logger': 0.1.0 + '@changesets/parse': 0.4.0 + '@changesets/types': 6.0.0 + chalk: 2.4.2 + fs-extra: 7.0.1 + p-filter: 2.1.0 + + '@changesets/should-skip-package@0.1.0': + dependencies: + '@babel/runtime': 7.24.7 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.0.0': {} + + '@changesets/write@0.3.1': + dependencies: + '@babel/runtime': 7.24.7 + '@changesets/types': 6.0.0 + fs-extra: 7.0.1 + human-id: 1.0.2 + prettier: 2.8.8 + + '@cheqd/sdk@2.4.4': + dependencies: + '@cheqd/ts-proto': 2.2.2 + '@cosmjs/amino': 0.30.1 + '@cosmjs/crypto': 0.30.1 + '@cosmjs/encoding': 0.30.1 + '@cosmjs/math': 0.30.1 + '@cosmjs/proto-signing': 0.30.1 + '@cosmjs/stargate': 0.30.1 + '@cosmjs/tendermint-rpc': 0.30.1 + '@cosmjs/utils': 0.30.1 + '@stablelib/ed25519': 1.0.3 + cosmjs-types: 0.7.2 + did-jwt: 6.11.6 + did-resolver: 4.1.0 + file-type: 16.5.4 + long: 4.0.0 + multiformats: 9.9.0 + uuid: 9.0.1 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + '@cheqd/ts-proto@2.2.2': + dependencies: + long: 5.2.3 + protobufjs: 7.3.2 + + '@confio/ics23@0.6.8': + dependencies: + '@noble/hashes': 1.4.0 + protobufjs: 6.11.4 + + '@cosmjs/amino@0.30.1': + dependencies: + '@cosmjs/crypto': 0.30.1 + '@cosmjs/encoding': 0.30.1 + '@cosmjs/math': 0.30.1 + '@cosmjs/utils': 0.30.1 + + '@cosmjs/crypto@0.30.1': + dependencies: + '@cosmjs/encoding': 0.30.1 + '@cosmjs/math': 0.30.1 + '@cosmjs/utils': 0.30.1 + '@noble/hashes': 1.4.0 + bn.js: 5.2.1 + elliptic: 6.5.5 + libsodium-wrappers: 0.7.13 + + '@cosmjs/encoding@0.30.1': + dependencies: + base64-js: 1.5.1 + bech32: 1.1.4 + readonly-date: 1.0.0 + + '@cosmjs/json-rpc@0.30.1': + dependencies: + '@cosmjs/stream': 0.30.1 + xstream: 11.14.0 + + '@cosmjs/math@0.30.1': + dependencies: + bn.js: 5.2.1 + + '@cosmjs/proto-signing@0.30.1': + dependencies: + '@cosmjs/amino': 0.30.1 + '@cosmjs/crypto': 0.30.1 + '@cosmjs/encoding': 0.30.1 + '@cosmjs/math': 0.30.1 + '@cosmjs/utils': 0.30.1 + cosmjs-types: 0.7.2 + long: 4.0.0 + + '@cosmjs/socket@0.30.1': + dependencies: + '@cosmjs/stream': 0.30.1 + isomorphic-ws: 4.0.1(ws@7.5.9) + ws: 7.5.9 + xstream: 11.14.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@cosmjs/stargate@0.30.1': + dependencies: + '@confio/ics23': 0.6.8 + '@cosmjs/amino': 0.30.1 + '@cosmjs/encoding': 0.30.1 + '@cosmjs/math': 0.30.1 + '@cosmjs/proto-signing': 0.30.1 + '@cosmjs/stream': 0.30.1 + '@cosmjs/tendermint-rpc': 0.30.1 + '@cosmjs/utils': 0.30.1 + cosmjs-types: 0.7.2 + long: 4.0.0 + protobufjs: 6.11.4 + xstream: 11.14.0 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + '@cosmjs/stream@0.30.1': + dependencies: + xstream: 11.14.0 + + '@cosmjs/tendermint-rpc@0.30.1': + dependencies: + '@cosmjs/crypto': 0.30.1 + '@cosmjs/encoding': 0.30.1 + '@cosmjs/json-rpc': 0.30.1 + '@cosmjs/math': 0.30.1 + '@cosmjs/socket': 0.30.1 + '@cosmjs/stream': 0.30.1 + '@cosmjs/utils': 0.30.1 + axios: 0.21.4 + readonly-date: 1.0.0 + xstream: 11.14.0 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + '@cosmjs/utils@0.30.1': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@digitalbazaar/bitstring@3.1.0': + dependencies: + base64url-universal: 2.0.0 + pako: 2.1.0 + + '@digitalbazaar/http-client@3.4.1(web-streams-polyfill@3.3.3)': + dependencies: + ky: 0.33.3 + ky-universal: 0.11.0(ky@0.33.3)(web-streams-polyfill@3.3.3) + undici: 5.28.4 + transitivePeerDependencies: + - web-streams-polyfill + + '@digitalbazaar/security-context@1.0.1': {} + + '@digitalbazaar/vc-status-list-context@3.1.1': {} + + '@digitalbazaar/vc-status-list@7.1.0(web-streams-polyfill@3.3.3)': + dependencies: + '@digitalbazaar/bitstring': 3.1.0 + '@digitalbazaar/vc': 5.0.0(web-streams-polyfill@3.3.3) + '@digitalbazaar/vc-status-list-context': 3.1.1 + credentials-context: 2.0.0 + transitivePeerDependencies: + - web-streams-polyfill + + '@digitalbazaar/vc@5.0.0(web-streams-polyfill@3.3.3)': + dependencies: + credentials-context: 2.0.0 + jsonld: 8.3.2(web-streams-polyfill@3.3.3) + jsonld-signatures: 11.2.1(web-streams-polyfill@3.3.3) + transitivePeerDependencies: + - web-streams-polyfill + + '@digitalcredentials/base58-universal@1.0.1': {} + + '@digitalcredentials/base64url-universal@2.0.6': + dependencies: + base64url: 3.0.1 + + '@digitalcredentials/bitstring@2.0.1': + dependencies: + '@digitalcredentials/base64url-universal': 2.0.6 + pako: 2.1.0 + + '@digitalcredentials/ed25519-signature-2020@3.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3)': + dependencies: + '@digitalcredentials/base58-universal': 1.0.1 + '@digitalcredentials/ed25519-verification-key-2020': 3.2.2 + '@digitalcredentials/jsonld-signatures': 9.4.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + ed25519-signature-2018-context: 1.1.0 + ed25519-signature-2020-context: 1.1.0 + transitivePeerDependencies: + - domexception + - expo + - react-native + - web-streams-polyfill + + '@digitalcredentials/ed25519-verification-key-2020@3.2.2': + dependencies: + '@digitalcredentials/base58-universal': 1.0.1 + '@stablelib/ed25519': 1.0.3 + base64url-universal: 1.1.0 + crypto-ld: 6.0.0 + + '@digitalcredentials/http-client@1.2.2(web-streams-polyfill@3.3.3)': + dependencies: + ky: 0.25.1 + ky-universal: 0.8.2(ky@0.25.1)(web-streams-polyfill@3.3.3) + transitivePeerDependencies: + - domexception + - web-streams-polyfill + + '@digitalcredentials/jsonld-signatures@9.4.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3)': + dependencies: + '@digitalbazaar/security-context': 1.0.1 + '@digitalcredentials/jsonld': 6.0.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + fast-text-encoding: 1.0.6 + isomorphic-webcrypto: 2.3.8(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1)) + serialize-error: 8.1.0 + transitivePeerDependencies: + - domexception + - expo + - react-native + - web-streams-polyfill + + '@digitalcredentials/jsonld@5.2.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3)': + dependencies: + '@digitalcredentials/http-client': 1.2.2(web-streams-polyfill@3.3.3) + '@digitalcredentials/rdf-canonize': 1.0.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1)) + canonicalize: 1.0.8 + lru-cache: 6.0.0 + transitivePeerDependencies: + - domexception + - expo + - react-native + - web-streams-polyfill + + '@digitalcredentials/jsonld@6.0.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3)': + dependencies: + '@digitalcredentials/http-client': 1.2.2(web-streams-polyfill@3.3.3) + '@digitalcredentials/rdf-canonize': 1.0.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1)) + canonicalize: 1.0.8 + lru-cache: 6.0.0 + transitivePeerDependencies: + - domexception + - expo + - react-native + - web-streams-polyfill + + '@digitalcredentials/open-badges-context@2.1.0': {} + + '@digitalcredentials/rdf-canonize@1.0.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))': + dependencies: + fast-text-encoding: 1.0.6 + isomorphic-webcrypto: 2.3.8(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1)) + transitivePeerDependencies: + - expo + - react-native + + '@digitalcredentials/vc-status-list@5.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3)': + dependencies: + '@digitalbazaar/vc-status-list-context': 3.1.1 + '@digitalcredentials/bitstring': 2.0.1 + '@digitalcredentials/vc': 4.2.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + credentials-context: 2.0.0 + transitivePeerDependencies: + - domexception + - expo + - react-native + - web-streams-polyfill + + '@digitalcredentials/vc@4.2.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3)': + dependencies: + '@digitalcredentials/jsonld': 5.2.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/jsonld-signatures': 9.4.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + credentials-context: 2.0.0 + transitivePeerDependencies: + - domexception + - expo + - react-native + - web-streams-polyfill + + '@digitalcredentials/vc@6.0.1(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3)': + dependencies: + '@digitalbazaar/vc-status-list': 7.1.0(web-streams-polyfill@3.3.3) + '@digitalcredentials/ed25519-signature-2020': 3.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/jsonld': 6.0.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/jsonld-signatures': 9.4.0(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/open-badges-context': 2.1.0 + '@digitalcredentials/vc-status-list': 5.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1))(web-streams-polyfill@3.3.3) + credentials-context: 2.0.0 + fix-esm: 1.0.1 + transitivePeerDependencies: + - domexception + - expo + - react-native + - supports-color + - web-streams-polyfill + + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.10.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.3.5 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.0': {} + + '@expo/bunyan@4.0.0': + dependencies: + uuid: 8.3.2 + optionalDependencies: + mv: 2.1.1 + safe-json-stringify: 1.2.0 + optional: true + + '@expo/cli@0.18.19(encoding@0.1.13)(expo-modules-autolinking@1.11.1)': + dependencies: + '@babel/runtime': 7.24.7 + '@expo/code-signing-certificates': 0.0.5 + '@expo/config': 9.0.1 + '@expo/config-plugins': 8.0.5 + '@expo/devcert': 1.1.2 + '@expo/env': 0.3.0 + '@expo/image-utils': 0.5.1(encoding@0.1.13) + '@expo/json-file': 8.3.3 + '@expo/metro-config': 0.18.7 + '@expo/osascript': 2.1.3 + '@expo/package-manager': 1.5.2 + '@expo/plist': 0.1.3 + '@expo/prebuild-config': 7.0.6(encoding@0.1.13)(expo-modules-autolinking@1.11.1) + '@expo/rudder-sdk-node': 1.1.1(encoding@0.1.13) + '@expo/spawn-async': 1.7.2 + '@expo/xcpretty': 4.3.1 + '@react-native/dev-middleware': 0.74.84(encoding@0.1.13) + '@urql/core': 2.3.6(graphql@15.8.0) + '@urql/exchange-retry': 0.3.0(graphql@15.8.0) + accepts: 1.3.8 + arg: 5.0.2 + better-opn: 3.0.2 + bplist-creator: 0.0.7 + bplist-parser: 0.3.2 + cacache: 18.0.3 + chalk: 4.1.2 + ci-info: 3.9.0 + connect: 3.7.0 + debug: 4.3.5 + env-editor: 0.4.2 + fast-glob: 3.3.2 + find-yarn-workspace-root: 2.0.0 + form-data: 3.0.1 + freeport-async: 2.0.0 + fs-extra: 8.1.0 + getenv: 1.0.0 + glob: 7.2.3 + graphql: 15.8.0 + graphql-tag: 2.12.6(graphql@15.8.0) + https-proxy-agent: 5.0.1 + internal-ip: 4.3.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + js-yaml: 3.14.1 + json-schema-deref-sync: 0.13.0 + lodash.debounce: 4.0.8 + md5hex: 1.0.0 + minimatch: 3.1.2 + node-fetch: 2.7.0(encoding@0.1.13) + node-forge: 1.3.1 + npm-package-arg: 7.0.0 + open: 8.4.2 + ora: 3.4.0 + picomatch: 3.0.1 + pretty-bytes: 5.6.0 + progress: 2.0.3 + prompts: 2.4.2 + qrcode-terminal: 0.11.0 + require-from-string: 2.0.2 + requireg: 0.2.2 + resolve: 1.22.8 + resolve-from: 5.0.0 + resolve.exports: 2.0.2 + semver: 7.6.2 + send: 0.18.0 + slugify: 1.6.6 + source-map-support: 0.5.21 + stacktrace-parser: 0.1.10 + structured-headers: 0.4.1 + tar: 6.2.1 + temp-dir: 2.0.0 + tempy: 0.7.1 + terminal-link: 2.1.1 + text-table: 0.2.0 + url-join: 4.0.0 + wrap-ansi: 7.0.0 + ws: 8.17.0 + transitivePeerDependencies: + - bufferutil + - encoding + - expo-modules-autolinking + - supports-color + - utf-8-validate + optional: true + + '@expo/code-signing-certificates@0.0.5': + dependencies: + node-forge: 1.3.1 + nullthrows: 1.1.1 + optional: true + + '@expo/config-plugins@8.0.5': + dependencies: + '@expo/config-types': 51.0.1 + '@expo/json-file': 8.3.3 + '@expo/plist': 0.1.3 + '@expo/sdk-runtime-versions': 1.0.0 + chalk: 4.1.2 + debug: 4.3.5 + find-up: 5.0.0 + getenv: 1.0.0 + glob: 7.1.6 + resolve-from: 5.0.0 + semver: 7.6.2 + slash: 3.0.0 + slugify: 1.6.6 + xcode: 3.0.1 + xml2js: 0.6.0 + transitivePeerDependencies: + - supports-color + optional: true + + '@expo/config-types@51.0.1': + optional: true + + '@expo/config@9.0.1': + dependencies: + '@babel/code-frame': 7.10.4 + '@expo/config-plugins': 8.0.5 + '@expo/config-types': 51.0.1 + '@expo/json-file': 8.3.3 + getenv: 1.0.0 + glob: 7.1.6 + require-from-string: 2.0.2 + resolve-from: 5.0.0 + semver: 7.6.2 + slugify: 1.6.6 + sucrase: 3.34.0 + transitivePeerDependencies: + - supports-color + optional: true + + '@expo/devcert@1.1.2': + dependencies: + application-config-path: 0.1.1 + command-exists: 1.2.9 + debug: 3.2.7 + eol: 0.9.1 + get-port: 3.2.0 + glob: 7.2.3 + lodash: 4.17.21 + mkdirp: 0.5.6 + password-prompt: 1.1.3 + rimraf: 2.7.1 + sudo-prompt: 8.2.5 + tmp: 0.0.33 + tslib: 2.6.3 + transitivePeerDependencies: + - supports-color + optional: true + + '@expo/env@0.3.0': + dependencies: + chalk: 4.1.2 + debug: 4.3.5 + dotenv: 16.4.5 + dotenv-expand: 11.0.6 + getenv: 1.0.0 + transitivePeerDependencies: + - supports-color + optional: true + + '@expo/image-utils@0.5.1(encoding@0.1.13)': + dependencies: + '@expo/spawn-async': 1.7.2 + chalk: 4.1.2 + fs-extra: 9.0.0 + getenv: 1.0.0 + jimp-compact: 0.16.1 + node-fetch: 2.7.0(encoding@0.1.13) + parse-png: 2.1.0 + resolve-from: 5.0.0 + semver: 7.6.2 + tempy: 0.3.0 + transitivePeerDependencies: + - encoding + optional: true + + '@expo/json-file@8.3.3': + dependencies: + '@babel/code-frame': 7.10.4 + json5: 2.2.3 + write-file-atomic: 2.4.3 + optional: true + + '@expo/metro-config@0.18.7': + dependencies: + '@babel/core': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + '@expo/config': 9.0.1 + '@expo/env': 0.3.0 + '@expo/json-file': 8.3.3 + '@expo/spawn-async': 1.7.2 + chalk: 4.1.2 + debug: 4.3.5 + find-yarn-workspace-root: 2.0.0 + fs-extra: 9.1.0 + getenv: 1.0.0 + glob: 7.2.3 + jsc-safe-url: 0.2.4 + lightningcss: 1.19.0 + postcss: 8.4.38 + resolve-from: 5.0.0 + transitivePeerDependencies: + - supports-color + optional: true + + '@expo/osascript@2.1.3': + dependencies: + '@expo/spawn-async': 1.7.2 + exec-async: 2.2.0 + optional: true + + '@expo/package-manager@1.5.2': + dependencies: + '@expo/json-file': 8.3.3 + '@expo/spawn-async': 1.7.2 + ansi-regex: 5.0.1 + chalk: 4.1.2 + find-up: 5.0.0 + find-yarn-workspace-root: 2.0.0 + js-yaml: 3.14.1 + micromatch: 4.0.7 + npm-package-arg: 7.0.0 + ora: 3.4.0 + split: 1.0.1 + sudo-prompt: 9.1.1 + optional: true + + '@expo/plist@0.1.3': + dependencies: + '@xmldom/xmldom': 0.7.13 + base64-js: 1.5.1 + xmlbuilder: 14.0.0 + optional: true + + '@expo/prebuild-config@7.0.6(encoding@0.1.13)(expo-modules-autolinking@1.11.1)': + dependencies: + '@expo/config': 9.0.1 + '@expo/config-plugins': 8.0.5 + '@expo/config-types': 51.0.1 + '@expo/image-utils': 0.5.1(encoding@0.1.13) + '@expo/json-file': 8.3.3 + '@react-native/normalize-colors': 0.74.84 + debug: 4.3.5 + expo-modules-autolinking: 1.11.1 + fs-extra: 9.1.0 + resolve-from: 5.0.0 + semver: 7.6.2 + xml2js: 0.6.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@expo/rudder-sdk-node@1.1.1(encoding@0.1.13)': + dependencies: + '@expo/bunyan': 4.0.0 + '@segment/loosely-validate-event': 2.0.0 + fetch-retry: 4.1.1 + md5: 2.3.0 + node-fetch: 2.7.0(encoding@0.1.13) + remove-trailing-slash: 0.1.1 + uuid: 8.3.2 + transitivePeerDependencies: + - encoding + optional: true + + '@expo/sdk-runtime-versions@1.0.0': + optional: true + + '@expo/spawn-async@1.7.2': + dependencies: + cross-spawn: 7.0.3 + optional: true + + '@expo/vector-icons@14.0.2': + dependencies: + prop-types: 15.8.1 + optional: true + + '@expo/xcpretty@4.3.1': + dependencies: + '@babel/code-frame': 7.10.4 + chalk: 4.1.2 + find-up: 5.0.0 + js-yaml: 4.1.0 + optional: true + + '@fastify/busboy@2.1.1': {} + + '@graphql-typed-document-node/core@3.2.0(graphql@15.8.0)': + dependencies: + graphql: 15.8.0 + optional: true + + '@hapi/hoek@9.3.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + + '@humanwhocodes/config-array@0.11.14': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.5 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@hyperledger/anoncreds-nodejs@0.2.2(encoding@0.1.13)': + dependencies: + '@2060.io/ffi-napi': 4.0.9 + '@2060.io/ref-napi': 3.0.6 + '@hyperledger/anoncreds-shared': 0.2.2 + '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) + ref-array-di: 1.2.2 + ref-struct-di: 1.1.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@hyperledger/anoncreds-shared@0.2.2': {} + + '@hyperledger/aries-askar-nodejs@0.2.1(encoding@0.1.13)': + dependencies: + '@2060.io/ffi-napi': 4.0.9 + '@2060.io/ref-napi': 3.0.6 + '@hyperledger/aries-askar-shared': 0.2.1 + '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) + node-cache: 5.1.2 + ref-array-di: 1.2.2 + ref-struct-di: 1.1.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@hyperledger/aries-askar-shared@0.2.1': + dependencies: + buffer: 6.0.3 + + '@hyperledger/indy-vdr-nodejs@0.2.2(encoding@0.1.13)': + dependencies: + '@2060.io/ffi-napi': 4.0.9 + '@2060.io/ref-napi': 3.0.6 + '@hyperledger/indy-vdr-shared': 0.2.2 + '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) + ref-array-di: 1.2.2 + ref-struct-di: 1.1.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@hyperledger/indy-vdr-shared@0.2.2': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + optional: true + + '@isaacs/ttlcache@1.4.1': + optional: true + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.18.8 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.18.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.7 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/create-cache-key-function@29.7.0': + dependencies: + '@jest/types': 29.6.3 + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.18.8 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 18.18.8 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + '@types/node': 18.18.8 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.2.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.24.7 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.7 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@26.6.2': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 18.18.8 + '@types/yargs': 15.0.19 + chalk: 4.1.2 + + '@jest/types@27.5.1': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 18.18.8 + '@types/yargs': 16.0.9 + chalk: 4.1.2 + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 18.18.8 + '@types/yargs': 17.0.32 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.4.15': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.24.7 + '@types/node': 18.18.8 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.24.7 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)': + dependencies: + detect-libc: 2.0.3 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0(encoding@0.1.13) + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.6.2 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@mattrglobal/bbs-signatures@1.3.1(encoding@0.1.13)': + dependencies: + '@stablelib/random': 1.0.0 + optionalDependencies: + '@mattrglobal/node-bbs-signatures': 0.18.1(encoding@0.1.13) + transitivePeerDependencies: + - encoding + - supports-color + + '@mattrglobal/bls12381-key-pair@1.2.1(encoding@0.1.13)': + dependencies: + '@mattrglobal/bbs-signatures': 1.3.1(encoding@0.1.13) + bs58: 4.0.1 + rfc4648: 1.5.2 + transitivePeerDependencies: + - encoding + - supports-color + + '@mattrglobal/node-bbs-signatures@0.18.1(encoding@0.1.13)': + dependencies: + '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) + neon-cli: 0.10.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@multiformats/base-x@4.0.1': {} + + '@noble/hashes@1.4.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@npmcli/fs@3.1.1': + dependencies: + semver: 7.6.2 + optional: true + + '@peculiar/asn1-schema@2.3.8': + dependencies: + asn1js: 3.0.5 + pvtsutils: 1.3.5 + tslib: 2.6.3 + + '@peculiar/json-schema@1.1.12': + dependencies: + tslib: 2.6.3 + + '@peculiar/webcrypto@1.5.0': + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/json-schema': 1.1.12 + pvtsutils: 1.3.5 + tslib: 2.6.3 + webcrypto-core: 1.8.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@react-native-community/cli-clean@10.1.1(encoding@0.1.13)': + dependencies: + '@react-native-community/cli-tools': 10.1.1(encoding@0.1.13) + chalk: 4.1.2 + execa: 1.0.0 + prompts: 2.4.2 + transitivePeerDependencies: + - encoding + + '@react-native-community/cli-config@10.1.1(encoding@0.1.13)': + dependencies: + '@react-native-community/cli-tools': 10.1.1(encoding@0.1.13) + chalk: 4.1.2 + cosmiconfig: 5.2.1 + deepmerge: 3.3.0 + glob: 7.2.3 + joi: 17.13.1 + transitivePeerDependencies: + - encoding + + '@react-native-community/cli-debugger-ui@10.0.0': + dependencies: + serve-static: 1.15.0 + transitivePeerDependencies: + - supports-color + + '@react-native-community/cli-doctor@10.2.7(encoding@0.1.13)': + dependencies: + '@react-native-community/cli-config': 10.1.1(encoding@0.1.13) + '@react-native-community/cli-platform-ios': 10.2.5(encoding@0.1.13) + '@react-native-community/cli-tools': 10.1.1(encoding@0.1.13) + chalk: 4.1.2 + command-exists: 1.2.9 + envinfo: 7.13.0 + execa: 1.0.0 + hermes-profile-transformer: 0.0.6 + node-stream-zip: 1.15.0 + ora: 5.4.1 + prompts: 2.4.2 + semver: 6.3.1 + strip-ansi: 5.2.0 + sudo-prompt: 9.2.1 + wcwidth: 1.0.1 + transitivePeerDependencies: + - encoding + + '@react-native-community/cli-hermes@10.2.7(encoding@0.1.13)': + dependencies: + '@react-native-community/cli-platform-android': 10.2.0(encoding@0.1.13) + '@react-native-community/cli-tools': 10.1.1(encoding@0.1.13) + chalk: 4.1.2 + hermes-profile-transformer: 0.0.6 + transitivePeerDependencies: + - encoding + + '@react-native-community/cli-platform-android@10.2.0(encoding@0.1.13)': + dependencies: + '@react-native-community/cli-tools': 10.1.1(encoding@0.1.13) + chalk: 4.1.2 + execa: 1.0.0 + glob: 7.2.3 + logkitty: 0.7.1 + transitivePeerDependencies: + - encoding + + '@react-native-community/cli-platform-ios@10.2.5(encoding@0.1.13)': + dependencies: + '@react-native-community/cli-tools': 10.1.1(encoding@0.1.13) + chalk: 4.1.2 + execa: 1.0.0 + fast-xml-parser: 4.4.0 + glob: 7.2.3 + ora: 5.4.1 + transitivePeerDependencies: + - encoding + + '@react-native-community/cli-plugin-metro@10.2.3(@babel/core@7.24.7)(encoding@0.1.13)': + dependencies: + '@react-native-community/cli-server-api': 10.1.1(encoding@0.1.13) + '@react-native-community/cli-tools': 10.1.1(encoding@0.1.13) + chalk: 4.1.2 + execa: 1.0.0 + metro: 0.73.10(encoding@0.1.13) + metro-config: 0.73.10(encoding@0.1.13) + metro-core: 0.73.10 + metro-react-native-babel-transformer: 0.73.10(@babel/core@7.24.7) + metro-resolver: 0.73.10 + metro-runtime: 0.73.10 + readline: 1.3.0 + transitivePeerDependencies: + - '@babel/core' + - bufferutil + - encoding + - supports-color + - utf-8-validate + + '@react-native-community/cli-server-api@10.1.1(encoding@0.1.13)': + dependencies: + '@react-native-community/cli-debugger-ui': 10.0.0 + '@react-native-community/cli-tools': 10.1.1(encoding@0.1.13) + compression: 1.7.4 + connect: 3.7.0 + errorhandler: 1.5.1 + nocache: 3.0.4 + pretty-format: 26.6.2 + serve-static: 1.15.0 + ws: 7.5.9 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + '@react-native-community/cli-tools@10.1.1(encoding@0.1.13)': + dependencies: + appdirsjs: 1.2.7 + chalk: 4.1.2 + find-up: 5.0.0 + mime: 2.6.0 + node-fetch: 2.7.0(encoding@0.1.13) + open: 6.4.0 + ora: 5.4.1 + semver: 6.3.1 + shell-quote: 1.8.1 + transitivePeerDependencies: + - encoding + + '@react-native-community/cli-types@10.0.0': + dependencies: + joi: 17.13.1 + + '@react-native-community/cli@10.2.7(@babel/core@7.24.7)(encoding@0.1.13)': + dependencies: + '@react-native-community/cli-clean': 10.1.1(encoding@0.1.13) + '@react-native-community/cli-config': 10.1.1(encoding@0.1.13) + '@react-native-community/cli-debugger-ui': 10.0.0 + '@react-native-community/cli-doctor': 10.2.7(encoding@0.1.13) + '@react-native-community/cli-hermes': 10.2.7(encoding@0.1.13) + '@react-native-community/cli-plugin-metro': 10.2.3(@babel/core@7.24.7)(encoding@0.1.13) + '@react-native-community/cli-server-api': 10.1.1(encoding@0.1.13) + '@react-native-community/cli-tools': 10.1.1(encoding@0.1.13) + '@react-native-community/cli-types': 10.0.0 + chalk: 4.1.2 + commander: 9.5.0 + execa: 1.0.0 + find-up: 4.1.0 + fs-extra: 8.1.0 + graceful-fs: 4.2.11 + prompts: 2.4.2 + semver: 6.3.1 + transitivePeerDependencies: + - '@babel/core' + - bufferutil + - encoding + - supports-color + - utf-8-validate + + '@react-native/assets@1.0.0': {} + + '@react-native/babel-plugin-codegen@0.74.84(@babel/preset-env@7.24.7(@babel/core@7.24.7))': + dependencies: + '@react-native/codegen': 0.74.84(@babel/preset-env@7.24.7(@babel/core@7.24.7)) + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + optional: true + + '@react-native/babel-preset@0.74.84(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))': + dependencies: + '@babel/core': 7.24.7 + '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.24.7) + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.24.7) + '@babel/plugin-proposal-export-default-from': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-proposal-logical-assignment-operators': 7.20.7(@babel/core@7.24.7) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.24.7) + '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.24.7) + '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.24.7) + '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.24.7) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.24.7) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-export-default-from': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-async-to-generator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-block-scoping': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-classes': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-destructuring': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-flow-strip-types': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-function-name': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-private-methods': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-private-property-in-object': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-display-name': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-runtime': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-sticky-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-typescript': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-unicode-regex': 7.24.7(@babel/core@7.24.7) + '@babel/template': 7.24.7 + '@react-native/babel-plugin-codegen': 0.74.84(@babel/preset-env@7.24.7(@babel/core@7.24.7)) + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.24.7) + react-refresh: 0.14.2 + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + optional: true + + '@react-native/codegen@0.74.84(@babel/preset-env@7.24.7(@babel/core@7.24.7))': + dependencies: + '@babel/parser': 7.24.7 + '@babel/preset-env': 7.24.7(@babel/core@7.24.7) + glob: 7.2.3 + hermes-parser: 0.19.1 + invariant: 2.2.4 + jscodeshift: 0.14.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) + mkdirp: 0.5.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@react-native/debugger-frontend@0.74.84': + optional: true + + '@react-native/dev-middleware@0.74.84(encoding@0.1.13)': + dependencies: + '@isaacs/ttlcache': 1.4.1 + '@react-native/debugger-frontend': 0.74.84 + '@rnx-kit/chromium-edge-launcher': 1.0.0 + chrome-launcher: 0.15.2 + connect: 3.7.0 + debug: 2.6.9 + node-fetch: 2.7.0(encoding@0.1.13) + nullthrows: 1.1.1 + open: 7.4.2 + selfsigned: 2.4.1 + serve-static: 1.15.0 + temp-dir: 2.0.0 + ws: 6.2.2 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + optional: true + + '@react-native/normalize-color@2.1.0': {} + + '@react-native/normalize-colors@0.74.84': + optional: true + + '@react-native/polyfills@2.0.0': {} + + '@rnx-kit/chromium-edge-launcher@1.0.0': + dependencies: + '@types/node': 18.18.8 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 1.4.2 + mkdirp: 1.0.4 + rimraf: 3.0.2 + transitivePeerDependencies: + - supports-color + optional: true + + '@sd-jwt/core@0.7.1': + dependencies: + '@sd-jwt/decode': 0.7.1 + '@sd-jwt/present': 0.7.1 + '@sd-jwt/types': 0.7.1 + '@sd-jwt/utils': 0.7.1 + + '@sd-jwt/decode@0.6.1': + dependencies: + '@sd-jwt/types': 0.6.1 + '@sd-jwt/utils': 0.6.1 + + '@sd-jwt/decode@0.7.1': + dependencies: + '@sd-jwt/types': 0.7.1 + '@sd-jwt/utils': 0.7.1 + + '@sd-jwt/jwt-status-list@0.7.1': + dependencies: + '@sd-jwt/types': 0.7.1 + base64url: 3.0.1 + pako: 2.1.0 + + '@sd-jwt/present@0.6.1': + dependencies: + '@sd-jwt/decode': 0.6.1 + '@sd-jwt/types': 0.6.1 + '@sd-jwt/utils': 0.6.1 + + '@sd-jwt/present@0.7.1': + dependencies: + '@sd-jwt/decode': 0.7.1 + '@sd-jwt/types': 0.7.1 + '@sd-jwt/utils': 0.7.1 + + '@sd-jwt/sd-jwt-vc@0.7.1': + dependencies: + '@sd-jwt/core': 0.7.1 + '@sd-jwt/jwt-status-list': 0.7.1 + '@sd-jwt/utils': 0.7.1 + + '@sd-jwt/types@0.6.1': {} + + '@sd-jwt/types@0.7.1': {} + + '@sd-jwt/utils@0.6.1': + dependencies: + '@sd-jwt/types': 0.6.1 + js-base64: 3.7.7 + + '@sd-jwt/utils@0.7.1': + dependencies: + '@sd-jwt/types': 0.7.1 + js-base64: 3.7.7 + + '@segment/loosely-validate-event@2.0.0': + dependencies: + component-type: 1.2.2 + join-component: 1.1.0 + optional: true + + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + + '@sideway/formula@3.0.1': {} + + '@sideway/pinpoint@2.0.0': {} + + '@sinclair/typebox@0.27.8': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@sovpro/delimited-stream@1.1.0': {} + + '@sphereon/did-auth-siop@0.6.4(encoding@0.1.13)': + dependencies: + '@astronautlabs/jsonpath': 1.1.2 + '@sphereon/did-uni-client': 0.6.3(encoding@0.1.13) + '@sphereon/pex': 3.3.3 + '@sphereon/pex-models': 2.2.4 + '@sphereon/ssi-types': 0.22.0 + '@sphereon/wellknown-dids-client': 0.1.3(encoding@0.1.13) + cross-fetch: 4.0.0(encoding@0.1.13) + did-jwt: 6.11.6 + did-resolver: 4.1.0 + events: 3.3.0 + language-tags: 1.0.9 + multiformats: 12.1.3 + qs: 6.12.1 + sha.js: 2.4.11 + uint8arrays: 3.1.1 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + + '@sphereon/did-uni-client@0.6.3(encoding@0.1.13)': + dependencies: + cross-fetch: 3.1.8(encoding@0.1.13) + did-resolver: 4.1.0 + transitivePeerDependencies: + - encoding + + '@sphereon/oid4vci-client@0.10.3(encoding@0.1.13)(msrcrypto@1.5.8)': + dependencies: + '@sphereon/oid4vci-common': 0.10.3(encoding@0.1.13)(msrcrypto@1.5.8) + '@sphereon/ssi-types': 0.23.4 + cross-fetch: 3.1.8(encoding@0.1.13) + debug: 4.3.5 + transitivePeerDependencies: + - encoding + - msrcrypto + - supports-color + + '@sphereon/oid4vci-common@0.10.3(encoding@0.1.13)(msrcrypto@1.5.8)': + dependencies: + '@sphereon/ssi-types': 0.23.4 + cross-fetch: 3.1.8(encoding@0.1.13) + jwt-decode: 3.1.2 + sha.js: 2.4.11 + uint8arrays: 3.1.1 + optionalDependencies: + msrcrypto: 1.5.8 + transitivePeerDependencies: + - encoding + + '@sphereon/oid4vci-issuer@0.10.3(encoding@0.1.13)(msrcrypto@1.5.8)': + dependencies: + '@sphereon/oid4vci-common': 0.10.3(encoding@0.1.13)(msrcrypto@1.5.8) + '@sphereon/ssi-types': 0.23.4 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - msrcrypto + + '@sphereon/pex-models@2.2.4': {} + + '@sphereon/pex@3.3.3': + dependencies: + '@astronautlabs/jsonpath': 1.1.2 + '@sd-jwt/decode': 0.6.1 + '@sd-jwt/present': 0.6.1 + '@sd-jwt/types': 0.6.1 + '@sphereon/pex-models': 2.2.4 + '@sphereon/ssi-types': 0.22.0 + ajv: 8.16.0 + ajv-formats: 2.1.1(ajv@8.16.0) + jwt-decode: 3.1.2 + nanoid: 3.3.7 + string.prototype.matchall: 4.0.11 + uint8arrays: 3.1.1 + + '@sphereon/ssi-types@0.22.0': + dependencies: + '@sd-jwt/decode': 0.6.1 + jwt-decode: 3.1.2 + + '@sphereon/ssi-types@0.23.4': + dependencies: + '@sd-jwt/decode': 0.6.1 + jwt-decode: 3.1.2 + + '@sphereon/ssi-types@0.9.0': + dependencies: + jwt-decode: 3.1.2 + + '@sphereon/wellknown-dids-client@0.1.3(encoding@0.1.13)': + dependencies: + '@sphereon/ssi-types': 0.9.0 + cross-fetch: 3.1.8(encoding@0.1.13) + jwt-decode: 3.1.2 + transitivePeerDependencies: + - encoding + + '@stablelib/aead@1.0.1': {} + + '@stablelib/binary@1.0.1': + dependencies: + '@stablelib/int': 1.0.1 + + '@stablelib/bytes@1.0.1': {} + + '@stablelib/chacha20poly1305@1.0.1': + dependencies: + '@stablelib/aead': 1.0.1 + '@stablelib/binary': 1.0.1 + '@stablelib/chacha': 1.0.1 + '@stablelib/constant-time': 1.0.1 + '@stablelib/poly1305': 1.0.1 + '@stablelib/wipe': 1.0.1 + + '@stablelib/chacha@1.0.1': + dependencies: + '@stablelib/binary': 1.0.1 + '@stablelib/wipe': 1.0.1 + + '@stablelib/constant-time@1.0.1': {} + + '@stablelib/ed25519@1.0.3': + dependencies: + '@stablelib/random': 1.0.2 + '@stablelib/sha512': 1.0.1 + '@stablelib/wipe': 1.0.1 + + '@stablelib/hash@1.0.1': {} + + '@stablelib/int@1.0.1': {} + + '@stablelib/keyagreement@1.0.1': + dependencies: + '@stablelib/bytes': 1.0.1 + + '@stablelib/poly1305@1.0.1': + dependencies: + '@stablelib/constant-time': 1.0.1 + '@stablelib/wipe': 1.0.1 + + '@stablelib/random@1.0.0': + dependencies: + '@stablelib/binary': 1.0.1 + '@stablelib/wipe': 1.0.1 + + '@stablelib/random@1.0.2': + dependencies: + '@stablelib/binary': 1.0.1 + '@stablelib/wipe': 1.0.1 + + '@stablelib/sha256@1.0.1': + dependencies: + '@stablelib/binary': 1.0.1 + '@stablelib/hash': 1.0.1 + '@stablelib/wipe': 1.0.1 + + '@stablelib/sha512@1.0.1': + dependencies: + '@stablelib/binary': 1.0.1 + '@stablelib/hash': 1.0.1 + '@stablelib/wipe': 1.0.1 + + '@stablelib/wipe@1.0.1': {} + + '@stablelib/x25519@1.0.3': + dependencies: + '@stablelib/keyagreement': 1.0.1 + '@stablelib/random': 1.0.2 + '@stablelib/wipe': 1.0.1 + + '@stablelib/xchacha20@1.0.1': + dependencies: + '@stablelib/binary': 1.0.1 + '@stablelib/chacha': 1.0.1 + '@stablelib/wipe': 1.0.1 + + '@stablelib/xchacha20poly1305@1.0.1': + dependencies: + '@stablelib/aead': 1.0.1 + '@stablelib/chacha20poly1305': 1.0.1 + '@stablelib/constant-time': 1.0.1 + '@stablelib/wipe': 1.0.1 + '@stablelib/xchacha20': 1.0.1 + + '@tokenizer/token@0.3.0': {} + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.24.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.24.7 + + '@types/bn.js@5.1.5': + dependencies: + '@types/node': 18.18.8 + + '@types/body-parser@1.19.5': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 18.18.8 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 18.18.8 + + '@types/cors@2.8.17': + dependencies: + '@types/node': 18.18.8 + + '@types/eslint@8.56.10': + dependencies: + '@types/estree': 1.0.5 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.5': {} + + '@types/events@3.0.3': {} + + '@types/express-serve-static-core@4.19.3': + dependencies: + '@types/node': 18.18.8 + '@types/qs': 6.9.15 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express@4.17.21': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.19.3 + '@types/qs': 6.9.15 + '@types/serve-static': 1.15.7 + + '@types/figlet@1.5.8': {} + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 18.18.8 + + '@types/http-errors@2.0.4': {} + + '@types/inquirer@8.2.10': + dependencies: + '@types/through': 0.0.33 + rxjs: 7.8.1 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.12': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/jsonpath@0.2.4': {} + + '@types/long@4.0.2': {} + + '@types/luxon@3.4.2': {} + + '@types/mime@1.3.5': {} + + '@types/minimist@1.2.5': {} + + '@types/multer@1.4.11': + dependencies: + '@types/express': 4.17.21 + + '@types/node-forge@1.3.11': + dependencies: + '@types/node': 18.18.8 + optional: true + + '@types/node@18.18.8': + dependencies: + undici-types: 5.26.5 + + '@types/normalize-package-data@2.4.4': {} + + '@types/object-inspect@1.13.0': {} + + '@types/qs@6.9.15': {} + + '@types/range-parser@1.2.7': {} + + '@types/ref-array-di@1.2.8': + dependencies: + '@types/ref-napi': 3.0.12 + + '@types/ref-napi@3.0.12': + dependencies: + '@types/node': 18.18.8 + + '@types/ref-struct-di@1.1.12': + dependencies: + '@types/ref-napi': 3.0.12 + + '@types/semver@7.5.8': {} + + '@types/send@0.17.4': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 18.18.8 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 18.18.8 + '@types/send': 0.17.4 + + '@types/stack-utils@2.0.3': {} + + '@types/through@0.0.33': + dependencies: + '@types/node': 18.18.8 + + '@types/uuid@9.0.8': {} + + '@types/validator@13.11.10': {} + + '@types/varint@6.0.3': + dependencies: + '@types/node': 18.18.8 + + '@types/ws@8.5.10': + dependencies: + '@types/node': 18.18.8 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@15.0.19': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@types/yargs@16.0.9': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@types/yargs@17.0.32': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@7.14.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint@8.57.0)(typescript@5.5.2)': + dependencies: + '@eslint-community/regexpp': 4.10.1 + '@typescript-eslint/parser': 7.14.1(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/scope-manager': 7.14.1 + '@typescript-eslint/type-utils': 7.14.1(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/utils': 7.14.1(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/visitor-keys': 7.14.1 + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.5.2) + optionalDependencies: + typescript: 5.5.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2)': + dependencies: + '@typescript-eslint/scope-manager': 7.14.1 + '@typescript-eslint/types': 7.14.1 + '@typescript-eslint/typescript-estree': 7.14.1(typescript@5.5.2) + '@typescript-eslint/visitor-keys': 7.14.1 + debug: 4.3.5 + eslint: 8.57.0 + optionalDependencies: + typescript: 5.5.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@7.14.1': + dependencies: + '@typescript-eslint/types': 7.14.1 + '@typescript-eslint/visitor-keys': 7.14.1 + + '@typescript-eslint/type-utils@7.14.1(eslint@8.57.0)(typescript@5.5.2)': + dependencies: + '@typescript-eslint/typescript-estree': 7.14.1(typescript@5.5.2) + '@typescript-eslint/utils': 7.14.1(eslint@8.57.0)(typescript@5.5.2) + debug: 4.3.5 + eslint: 8.57.0 + ts-api-utils: 1.3.0(typescript@5.5.2) + optionalDependencies: + typescript: 5.5.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@7.14.1': {} + + '@typescript-eslint/typescript-estree@7.14.1(typescript@5.5.2)': + dependencies: + '@typescript-eslint/types': 7.14.1 + '@typescript-eslint/visitor-keys': 7.14.1 + debug: 4.3.5 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.4 + semver: 7.6.2 + ts-api-utils: 1.3.0(typescript@5.5.2) + optionalDependencies: + typescript: 5.5.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@7.14.1(eslint@8.57.0)(typescript@5.5.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@typescript-eslint/scope-manager': 7.14.1 + '@typescript-eslint/types': 7.14.1 + '@typescript-eslint/typescript-estree': 7.14.1(typescript@5.5.2) + eslint: 8.57.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@7.14.1': + dependencies: + '@typescript-eslint/types': 7.14.1 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.2.0': {} + + '@unimodules/core@7.1.2': + dependencies: + compare-versions: 3.6.0 + optional: true + + '@unimodules/react-native-adapter@6.3.9': + dependencies: + expo-modules-autolinking: 0.0.3 + invariant: 2.2.4 + optional: true + + '@urql/core@2.3.6(graphql@15.8.0)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@15.8.0) + graphql: 15.8.0 + wonka: 4.0.15 + optional: true + + '@urql/exchange-retry@0.3.0(graphql@15.8.0)': + dependencies: + '@urql/core': 2.3.6(graphql@15.8.0) + graphql: 15.8.0 + wonka: 4.0.15 + optional: true + + '@xmldom/xmldom@0.7.13': + optional: true + + '@xmldom/xmldom@0.8.10': + optional: true + + abbrev@1.1.1: {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + absolute-path@0.0.0: {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-jsx@5.3.2(acorn@8.12.0): + dependencies: + acorn: 8.12.0 + + acorn-walk@8.3.3: + dependencies: + acorn: 8.12.0 + + acorn@8.12.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + optional: true + + ajv-formats@2.1.1(ajv@8.16.0): + optionalDependencies: + ajv: 8.16.0 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.16.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + anser@1.4.10: {} + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-fragments@0.2.1: + dependencies: + colorette: 1.4.0 + slice-ansi: 2.1.0 + strip-ansi: 5.2.0 + + ansi-regex@4.1.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.0.1: + optional: true + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.1: + optional: true + + any-promise@1.3.0: + optional: true + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + appdirsjs@1.2.7: {} + + append-field@1.0.0: {} + + application-config-path@0.1.1: + optional: true + + aproba@2.0.0: {} + + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + + arg@4.1.3: {} + + arg@5.0.2: + optional: true + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-back@3.1.0: + optional: true + + array-back@4.0.2: + optional: true + + array-buffer-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + + array-flatten@1.1.1: {} + + array-includes@3.1.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + is-string: 1.0.7 + + array-index@1.0.0: + dependencies: + debug: 2.6.9 + es6-symbol: 3.1.4 + transitivePeerDependencies: + - supports-color + + array-union@2.1.0: {} + + array.prototype.findlastindex@1.2.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + + array.prototype.flat@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-shim-unscopables: 1.0.2 + + array.prototype.flatmap@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-shim-unscopables: 1.0.2 + + arraybuffer.prototype.slice@1.0.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + + arrify@1.0.1: {} + + asap@2.0.6: {} + + asmcrypto.js@0.22.0: {} + + asn1js@3.0.5: + dependencies: + pvtsutils: 1.3.5 + pvutils: 1.1.3 + tslib: 2.6.3 + + ast-types@0.15.2: + dependencies: + tslib: 2.6.3 + + astral-regex@1.0.0: {} + + async-limiter@1.0.1: {} + + async-mutex@0.4.1: + dependencies: + tslib: 2.6.3 + + async@3.2.5: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: + optional: true + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + axios@0.21.4: + dependencies: + follow-redirects: 1.15.6 + transitivePeerDependencies: + - debug + + b64-lite@1.4.0: + dependencies: + base-64: 0.1.0 + + b64u-lite@1.1.0: + dependencies: + b64-lite: 1.4.0 + + babel-core@7.0.0-bridge.0(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + + babel-jest@29.7.0(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.24.7) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.24.7 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.6 + + babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.7): + dependencies: + '@babel/compat-data': 7.24.7 + '@babel/core': 7.24.7 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.7) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.7) + core-js-compat: 3.37.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + babel-plugin-react-native-web@0.19.12: + optional: true + + babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: {} + + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.24.7): + dependencies: + '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.24.7) + transitivePeerDependencies: + - '@babel/core' + optional: true + + babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.7) + + babel-preset-expo@11.0.10(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)): + dependencies: + '@babel/plugin-proposal-decorators': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-export-namespace-from': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-object-rest-spread': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7) + '@babel/preset-react': 7.24.7(@babel/core@7.24.7) + '@babel/preset-typescript': 7.24.7(@babel/core@7.24.7) + '@react-native/babel-preset': 0.74.84(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)) + babel-plugin-react-native-web: 0.19.12 + react-refresh: 0.14.2 + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - supports-color + optional: true + + babel-preset-fbjs@3.4.0(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.24.7) + '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.24.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.7) + '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-block-scoped-functions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-block-scoping': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-classes': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-destructuring': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-flow-strip-types': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-for-of': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-function-name': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-member-expression-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-object-super': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-property-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-display-name': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.24.7) + babel-plugin-syntax-trailing-function-commas: 7.0.0-beta.0 + transitivePeerDependencies: + - supports-color + + babel-preset-jest@29.6.3(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.7) + + balanced-match@1.0.2: {} + + base-64@0.1.0: {} + + base-x@3.0.9: + dependencies: + safe-buffer: 5.2.1 + + base64-js@1.5.1: {} + + base64url-universal@1.1.0: + dependencies: + base64url: 3.0.1 + + base64url-universal@2.0.0: + dependencies: + base64url: 3.0.1 + + base64url@3.0.1: {} + + bech32@1.1.4: {} + + bech32@2.0.0: {} + + better-opn@3.0.2: + dependencies: + open: 8.4.2 + optional: true + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + big-integer@1.6.52: {} + + bignumber.js@9.1.2: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bn.js@4.12.0: {} + + bn.js@5.2.1: {} + + body-parser@1.20.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + borc@3.0.0: + dependencies: + bignumber.js: 9.1.2 + buffer: 6.0.3 + commander: 2.20.3 + ieee754: 1.2.1 + iso-url: 1.2.1 + json-text-sequence: 0.3.0 + readable-stream: 3.6.2 + + bplist-creator@0.0.7: + dependencies: + stream-buffers: 2.2.0 + optional: true + + bplist-creator@0.1.0: + dependencies: + stream-buffers: 2.2.0 + optional: true + + bplist-parser@0.3.1: + dependencies: + big-integer: 1.6.52 + optional: true + + bplist-parser@0.3.2: + dependencies: + big-integer: 1.6.52 + optional: true + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + breakword@1.0.6: + dependencies: + wcwidth: 1.0.1 + + brorand@1.1.0: {} + + browserslist@4.23.1: + dependencies: + caniuse-lite: 1.0.30001634 + electron-to-chromium: 1.4.803 + node-releases: 2.0.14 + update-browserslist-db: 1.0.16(browserslist@4.23.1) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bs58@4.0.1: + dependencies: + base-x: 3.0.9 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-alloc-unsafe@1.1.0: + optional: true + + buffer-alloc@1.2.0: + dependencies: + buffer-alloc-unsafe: 1.1.0 + buffer-fill: 1.0.0 + optional: true + + buffer-fill@1.0.0: + optional: true + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builtins@1.0.3: + optional: true + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.0.0: {} + + bytes@3.1.2: {} + + cacache@18.0.3: + dependencies: + '@npmcli/fs': 3.1.1 + fs-minipass: 3.0.3 + glob: 10.4.1 + lru-cache: 10.2.2 + minipass: 7.1.2 + minipass-collect: 2.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + p-map: 4.0.0 + ssri: 10.0.6 + tar: 6.2.1 + unique-filename: 3.0.0 + optional: true + + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + caller-callsite@2.0.0: + dependencies: + callsites: 2.0.0 + + caller-path@2.0.0: + dependencies: + caller-callsite: 2.0.0 + + callsites@2.0.0: {} + + callsites@3.1.0: {} + + camelcase-keys@6.2.2: + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001634: {} + + canonicalize@1.0.8: {} + + canonicalize@2.0.0: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + chardet@0.7.0: {} + + charenc@0.0.2: + optional: true + + chownr@2.0.0: {} + + chrome-launcher@0.15.2: + dependencies: + '@types/node': 18.18.8 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 1.4.2 + transitivePeerDependencies: + - supports-color + optional: true + + ci-info@2.0.0: {} + + ci-info@3.9.0: {} + + cjs-module-lexer@1.3.1: {} + + class-transformer@0.5.1: {} + + class-validator@0.14.1: + dependencies: + '@types/validator': 13.11.10 + libphonenumber-js: 1.11.3 + validator: 13.12.0 + + clean-stack@2.2.0: + optional: true + + clear@0.1.0: {} + + cli-cursor@2.1.0: + dependencies: + restore-cursor: 2.0.0 + optional: true + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-width@3.0.0: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-deep@4.0.1: + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + + clone@1.0.4: {} + + clone@2.1.2: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.2: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-support@1.1.3: {} + + colorette@1.4.0: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + command-exists@1.2.9: {} + + command-line-args@5.2.1: + dependencies: + array-back: 3.1.0 + find-replace: 3.0.0 + lodash.camelcase: 4.3.0 + typical: 4.0.0 + optional: true + + command-line-commands@3.0.2: + dependencies: + array-back: 4.0.2 + optional: true + + command-line-usage@6.1.3: + dependencies: + array-back: 4.0.2 + chalk: 2.4.2 + table-layout: 1.0.2 + typical: 5.2.0 + optional: true + + commander@2.13.0: {} + + commander@2.20.3: {} + + commander@4.1.1: + optional: true + + commander@7.2.0: + optional: true + + commander@9.5.0: {} + + commondir@1.0.1: {} + + compare-versions@3.6.0: + optional: true + + component-type@1.2.2: + optional: true + + compressible@2.0.18: + dependencies: + mime-db: 1.52.0 + + compression@1.7.4: + dependencies: + accepts: 1.3.8 + bytes: 3.0.0 + compressible: 2.0.18 + debug: 2.6.9 + on-headers: 1.0.2 + safe-buffer: 5.1.2 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + concat-map@0.0.1: {} + + concat-stream@1.6.2: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + + connect@3.7.0: + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + + console-control-strings@1.1.0: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.0.6: {} + + cookie@0.6.0: {} + + core-js-compat@3.37.1: + dependencies: + browserslist: 4.23.1 + + core-util-is@1.0.3: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@5.2.1: + dependencies: + import-fresh: 2.0.0 + is-directory: 0.3.1 + js-yaml: 3.14.1 + parse-json: 4.0.0 + + cosmjs-types@0.7.2: + dependencies: + long: 4.0.0 + protobufjs: 6.11.4 + + create-jest@29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + create-require@1.1.1: {} + + credentials-context@2.0.0: {} + + cross-fetch@3.1.8(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + cross-fetch@4.0.0(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + cross-spawn@5.1.0: + dependencies: + lru-cache: 4.1.5 + shebang-command: 1.2.0 + which: 1.3.1 + + cross-spawn@6.0.5: + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypt@0.0.2: + optional: true + + crypto-ld@6.0.0: {} + + crypto-random-string@1.0.0: + optional: true + + crypto-random-string@2.0.0: + optional: true + + csv-generate@3.4.3: {} + + csv-parse@4.16.3: {} + + csv-stringify@5.6.5: {} + + csv@5.5.3: + dependencies: + csv-generate: 3.4.3 + csv-parse: 4.16.3 + csv-stringify: 5.6.5 + stream-transform: 2.1.3 + + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + + dag-map@1.0.2: + optional: true + + data-uri-to-buffer@3.0.1: {} + + data-uri-to-buffer@4.0.1: {} + + data-view-buffer@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-offset@1.0.0: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + dayjs@1.11.11: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.3.5: + dependencies: + ms: 2.1.2 + + decamelize-keys@1.1.1: + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + + decamelize@1.2.0: {} + + decode-uri-component@0.2.2: {} + + dedent@1.5.3: {} + + deep-extend@0.6.0: + optional: true + + deep-is@0.1.4: {} + + deepmerge@3.3.0: {} + + deepmerge@4.3.1: {} + + default-gateway@4.2.0: + dependencies: + execa: 1.0.0 + ip-regex: 2.1.0 + optional: true + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + define-lazy-prop@2.0.0: + optional: true + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + del@6.1.1: + dependencies: + globby: 11.1.0 + graceful-fs: 4.2.11 + is-glob: 4.0.3 + is-path-cwd: 2.2.0 + is-path-inside: 3.0.3 + p-map: 4.0.0 + rimraf: 3.0.2 + slash: 3.0.0 + optional: true + + delayed-stream@1.0.0: {} + + delegates@1.0.0: {} + + denodeify@1.2.1: {} + + depd@2.0.0: {} + + deprecated-react-native-prop-types@3.0.2: + dependencies: + '@react-native/normalize-color': 2.1.0 + invariant: 2.2.4 + prop-types: 15.8.1 + + destroy@1.2.0: {} + + detect-indent@6.1.0: {} + + detect-libc@1.0.3: + optional: true + + detect-libc@2.0.3: {} + + detect-newline@3.1.0: {} + + did-jwt@6.11.6: + dependencies: + '@stablelib/ed25519': 1.0.3 + '@stablelib/random': 1.0.2 + '@stablelib/sha256': 1.0.1 + '@stablelib/x25519': 1.0.3 + '@stablelib/xchacha20poly1305': 1.0.1 + bech32: 2.0.0 + canonicalize: 2.0.0 + did-resolver: 4.1.0 + elliptic: 6.5.5 + js-sha3: 0.8.0 + multiformats: 9.9.0 + uint8arrays: 3.1.1 + + did-resolver@4.1.0: {} + + diff-sequences@29.6.3: {} + + diff@4.0.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dotenv-expand@11.0.6: + dependencies: + dotenv: 16.4.5 + optional: true + + dotenv@16.4.5: + optional: true + + eastasianwidth@0.2.0: + optional: true + + ed25519-signature-2018-context@1.1.0: {} + + ed25519-signature-2020-context@1.1.0: {} + + ee-first@1.1.1: {} + + electron-to-chromium@1.4.803: {} + + elliptic@6.5.5: + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: + optional: true + + encodeurl@1.0.2: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.17.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + env-editor@0.4.2: + optional: true + + envinfo@7.13.0: {} + + eol@0.9.1: + optional: true + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + + errorhandler@1.5.1: + dependencies: + accepts: 1.3.8 + escape-html: 1.0.3 + + es-abstract@1.23.3: + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + + es-errors@1.3.0: {} + + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.0.2: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.2.1: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + + escalade@3.1.2: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + escodegen@1.14.3: + dependencies: + esprima: 4.0.1 + estraverse: 4.3.0 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-config-prettier@8.10.0(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.13.1 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0): + dependencies: + debug: 4.3.5 + enhanced-resolve: 5.17.0 + eslint: 8.57.0 + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + fast-glob: 3.3.2 + get-tsconfig: 4.7.5 + is-core-module: 2.13.1 + is-glob: 4.0.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 7.14.1(eslint@8.57.0)(typescript@5.5.2) + eslint: 8.57.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + dependencies: + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + hasown: 2.0.2 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.0 + semver: 6.3.1 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 7.14.1(eslint@8.57.0)(typescript@5.5.2) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8): + dependencies: + eslint: 8.57.0 + prettier: 2.8.8 + prettier-linter-helpers: 1.0.0 + optionalDependencies: + eslint-config-prettier: 8.10.0(eslint@8.57.0) + + eslint-plugin-workspaces@0.8.0: + dependencies: + find-workspaces: 0.1.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.10.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.5 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + + espree@9.6.1: + dependencies: + acorn: 8.12.0 + acorn-jsx: 5.3.2(acorn@8.12.0) + eslint-visitor-keys: 3.4.3 + + esprima@1.2.2: {} + + esprima@4.0.1: {} + + esquery@1.5.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + + event-target-shim@5.0.1: {} + + events@3.3.0: {} + + exec-async@2.2.0: + optional: true + + execa@1.0.0: + dependencies: + cross-spawn: 6.0.5 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit@0.1.2: {} + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + expo-asset@10.0.9(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)): + dependencies: + expo: 51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13) + expo-constants: 16.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)) + invariant: 2.2.4 + md5-file: 3.2.3 + transitivePeerDependencies: + - supports-color + optional: true + + expo-constants@16.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)): + dependencies: + '@expo/config': 9.0.1 + '@expo/env': 0.3.0 + expo: 51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13) + transitivePeerDependencies: + - supports-color + optional: true + + expo-file-system@17.0.1(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)): + dependencies: + expo: 51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13) + optional: true + + expo-font@12.0.7(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)): + dependencies: + expo: 51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13) + fontfaceobserver: 2.3.0 + optional: true + + expo-keep-awake@13.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)): + dependencies: + expo: 51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13) + optional: true + + expo-modules-autolinking@0.0.3: + dependencies: + chalk: 4.1.2 + commander: 7.2.0 + fast-glob: 3.3.2 + find-up: 5.0.0 + fs-extra: 9.1.0 + optional: true + + expo-modules-autolinking@1.11.1: + dependencies: + chalk: 4.1.2 + commander: 7.2.0 + fast-glob: 3.3.2 + find-up: 5.0.0 + fs-extra: 9.1.0 + optional: true + + expo-modules-core@1.12.15: + dependencies: + invariant: 2.2.4 + optional: true + + expo-random@14.0.1(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)): + dependencies: + base64-js: 1.5.1 + expo: 51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13) + optional: true + + expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13): + dependencies: + '@babel/runtime': 7.24.7 + '@expo/cli': 0.18.19(encoding@0.1.13)(expo-modules-autolinking@1.11.1) + '@expo/config': 9.0.1 + '@expo/config-plugins': 8.0.5 + '@expo/metro-config': 0.18.7 + '@expo/vector-icons': 14.0.2 + babel-preset-expo: 11.0.10(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)) + expo-asset: 10.0.9(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)) + expo-file-system: 17.0.1(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)) + expo-font: 12.0.7(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)) + expo-keep-awake: 13.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)) + expo-modules-autolinking: 1.11.1 + expo-modules-core: 1.12.15 + fbemitter: 3.0.0(encoding@0.1.13) + whatwg-url-without-unicode: 8.0.0-3 + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - bufferutil + - encoding + - supports-color + - utf-8-validate + optional: true + + express@4.19.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.2 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.6.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + ext@1.7.0: + dependencies: + type: 2.7.3 + + extendable-error@0.1.7: {} + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + fast-base64-decode@1.0.0: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.7 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-text-encoding@1.0.6: {} + + fast-xml-parser@4.4.0: + dependencies: + strnum: 1.0.5 + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fbemitter@3.0.0(encoding@0.1.13): + dependencies: + fbjs: 3.0.5(encoding@0.1.13) + transitivePeerDependencies: + - encoding + optional: true + + fbjs-css-vars@1.0.2: + optional: true + + fbjs@3.0.5(encoding@0.1.13): + dependencies: + cross-fetch: 3.1.8(encoding@0.1.13) + fbjs-css-vars: 1.0.2 + loose-envify: 1.4.0 + object-assign: 4.1.1 + promise: 7.3.1 + setimmediate: 1.0.5 + ua-parser-js: 1.0.38 + transitivePeerDependencies: + - encoding + optional: true + + fetch-blob@2.1.2: {} + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + fetch-retry@4.1.1: + optional: true + + figlet@1.7.0: {} + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + file-type@16.5.4: + dependencies: + readable-web-to-node-stream: 3.0.2 + strtok3: 6.3.0 + token-types: 4.2.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + filter-obj@1.1.0: {} + + finalhandler@1.1.2: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@1.2.0: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-cache-dir@2.1.0: + dependencies: + commondir: 1.0.1 + make-dir: 2.1.0 + pkg-dir: 3.0.0 + + find-replace@3.0.0: + dependencies: + array-back: 3.1.0 + optional: true + + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-workspaces@0.1.0: + dependencies: + fast-glob: 3.3.2 + type-fest: 3.13.1 + yaml: 2.4.5 + + find-yarn-workspace-root2@1.2.16: + dependencies: + micromatch: 4.0.7 + pkg-dir: 4.2.0 + + find-yarn-workspace-root@2.0.0: + dependencies: + micromatch: 4.0.7 + optional: true + + fix-esm@1.0.1: + dependencies: + '@babel/core': 7.24.7 + '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.24.7) + '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.1: {} + + flow-parser@0.185.2: {} + + follow-redirects@1.15.6: {} + + fontfaceobserver@2.3.0: + optional: true + + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.2.1: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + optional: true + + form-data@3.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + optional: true + + form-data@4.0.0: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + freeport-async@2.0.0: + optional: true + + fresh@0.5.2: {} + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.0.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 1.0.0 + optional: true + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + optional: true + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.2 + optional: true + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + functions-have-names: 1.2.3 + + functions-have-names@1.2.3: {} + + gauge@3.0.2: + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + get-package-type@0.1.0: {} + + get-port@3.2.0: + optional: true + + get-stream@4.1.0: + dependencies: + pump: 3.0.0 + + get-stream@6.0.1: {} + + get-symbol-description@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + + get-symbol-from-current-process-h@1.0.2: {} + + get-tsconfig@4.7.5: + dependencies: + resolve-pkg-maps: 1.0.0 + + get-uv-event-loop-napi-h@1.0.6: + dependencies: + get-symbol-from-current-process-h: 1.0.2 + + getenv@1.0.0: + optional: true + + git-config@0.0.7: + dependencies: + iniparser: 1.0.5 + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.1: + dependencies: + foreground-child: 3.2.1 + jackspeak: 3.4.0 + minimatch: 9.0.4 + minipass: 7.1.2 + path-scurry: 1.11.1 + optional: true + + glob@6.0.4: + dependencies: + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + optional: true + + glob@7.1.6: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + optional: true + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@9.3.5: + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.4 + minipass: 4.2.8 + path-scurry: 1.11.1 + + globals@11.12.0: {} + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.0.1 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.0.1: + dependencies: + get-intrinsic: 1.2.4 + + graceful-fs@4.2.11: {} + + grapheme-splitter@1.0.4: {} + + graphemer@1.4.0: {} + + graphql-tag@2.12.6(graphql@15.8.0): + dependencies: + graphql: 15.8.0 + tslib: 2.6.3 + optional: true + + graphql@15.8.0: + optional: true + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.18.0 + optional: true + + hard-rejection@2.1.0: {} + + has-bigints@1.0.2: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + + has-proto@1.0.3: {} + + has-symbols@1.0.3: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + + has-unicode@2.0.1: {} + + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.19.1: + optional: true + + hermes-estree@0.8.0: {} + + hermes-parser@0.19.1: + dependencies: + hermes-estree: 0.19.1 + optional: true + + hermes-parser@0.8.0: + dependencies: + hermes-estree: 0.8.0 + + hermes-profile-transformer@0.0.6: + dependencies: + source-map: 0.7.4 + + hmac-drbg@1.0.1: + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + hosted-git-info@2.8.9: {} + + hosted-git-info@3.0.8: + dependencies: + lru-cache: 6.0.0 + optional: true + + html-escaper@2.0.2: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + + human-id@1.0.2: {} + + human-signals@2.1.0: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + ieee754@1.2.1: {} + + ignore@5.3.1: {} + + image-size@0.6.3: {} + + import-fresh@2.0.0: + dependencies: + caller-path: 2.0.0 + resolve-from: 3.0.0 + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.1.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: + optional: true + + iniparser@1.0.5: + optional: true + + inquirer@7.3.3: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + optional: true + + inquirer@8.2.6: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + + internal-ip@4.3.0: + dependencies: + default-gateway: 4.2.0 + ipaddr.js: 1.9.1 + optional: true + + internal-slot@1.0.7: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ip-regex@2.1.0: + optional: true + + ipaddr.js@1.9.1: {} + + is-array-buffer@3.0.4: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + + is-arrayish@0.2.1: {} + + is-bigint@1.0.4: + dependencies: + has-bigints: 1.0.2 + + is-boolean-object@1.1.2: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-buffer@1.1.6: + optional: true + + is-callable@1.2.7: {} + + is-core-module@2.13.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.1: + dependencies: + is-typed-array: 1.1.13 + + is-date-object@1.0.5: + dependencies: + has-tostringtag: 1.0.2 + + is-directory@0.3.1: {} + + is-docker@2.2.1: + optional: true + + is-extglob@1.0.0: + optional: true + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@2.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-glob@2.0.1: + dependencies: + is-extglob: 1.0.0 + optional: true + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-invalid-path@0.1.0: + dependencies: + is-glob: 2.0.1 + optional: true + + is-negative-zero@2.0.3: {} + + is-number-object@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-path-cwd@2.2.0: + optional: true + + is-path-inside@3.0.3: {} + + is-plain-obj@1.1.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-regex@1.1.4: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-shared-array-buffer@1.0.3: + dependencies: + call-bind: 1.0.7 + + is-stream@1.1.0: {} + + is-stream@2.0.1: {} + + is-string@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-symbol@1.0.4: + dependencies: + has-symbols: 1.0.3 + + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + + is-unicode-supported@0.1.0: {} + + is-valid-path@0.1.1: + dependencies: + is-invalid-path: 0.1.0 + optional: true + + is-weakref@1.0.2: + dependencies: + call-bind: 1.0.7 + + is-windows@1.0.2: {} + + is-wsl@1.1.0: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + optional: true + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iso-url@1.2.1: {} + + isobject@3.0.1: {} + + isomorphic-webcrypto@2.3.8(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13))(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1)): + dependencies: + '@peculiar/webcrypto': 1.5.0 + asmcrypto.js: 0.22.0 + b64-lite: 1.4.0 + b64u-lite: 1.1.0 + msrcrypto: 1.5.8 + str2buf: 1.3.0 + webcrypto-shim: 0.1.7 + optionalDependencies: + '@unimodules/core': 7.1.2 + '@unimodules/react-native-adapter': 6.3.9 + expo-random: 14.0.1(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)) + react-native-securerandom: 0.1.1(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1)) + transitivePeerDependencies: + - expo + - react-native + + isomorphic-ws@4.0.1(ws@7.5.9): + dependencies: + ws: 7.5.9 + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.24.7 + '@babel/parser': 7.24.7 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.2: + dependencies: + '@babel/core': 7.24.7 + '@babel/parser': 7.24.7 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.3.5 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.0: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + optional: true + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.18.8 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.3 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)): + dependencies: + '@babel/core': 7.24.7 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.24.7) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.7 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 18.18.8 + ts-node: 10.9.2(@types/node@18.18.8)(typescript@5.5.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.0 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.18.8 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@26.3.0: {} + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 18.18.8 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.7 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.24.7 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.7 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.18.8 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@27.5.1: {} + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 + resolve.exports: 2.0.2 + slash: 3.0.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.18.8 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.18.8 + chalk: 4.1.2 + cjs-module-lexer: 1.3.1 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-serializer@27.5.1: + dependencies: + '@types/node': 18.18.8 + graceful-fs: 4.2.11 + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.24.7) + '@babel/types': 7.24.7 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.7) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + + jest-util@27.5.1: + dependencies: + '@jest/types': 27.5.1 + '@types/node': 18.18.8 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.18.8 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-validate@26.6.2: + dependencies: + '@jest/types': 26.6.2 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 26.3.0 + leven: 3.1.0 + pretty-format: 26.6.2 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.18.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@27.5.1: + dependencies: + '@types/node': 18.18.8 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@29.7.0: + dependencies: + '@types/node': 18.18.8 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)) + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jimp-compact@0.16.1: + optional: true + + joi@17.13.1: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + + join-component@1.1.0: + optional: true + + js-base64@3.7.7: {} + + js-sha3@0.8.0: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsc-android@250231.0.0: {} + + jsc-safe-url@0.2.4: {} + + jscodeshift@0.14.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)): + dependencies: + '@babel/core': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.24.7) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.24.7) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.24.7) + '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) + '@babel/preset-env': 7.24.7(@babel/core@7.24.7) + '@babel/preset-flow': 7.24.7(@babel/core@7.24.7) + '@babel/preset-typescript': 7.24.7(@babel/core@7.24.7) + '@babel/register': 7.24.6(@babel/core@7.24.7) + babel-core: 7.0.0-bridge.0(@babel/core@7.24.7) + chalk: 4.1.2 + flow-parser: 0.185.2 + graceful-fs: 4.2.11 + micromatch: 4.0.7 + neo-async: 2.6.2 + node-dir: 0.1.17 + recast: 0.21.5 + temp: 0.8.4 + write-file-atomic: 2.4.3 + transitivePeerDependencies: + - supports-color + + jsesc@0.5.0: {} + + jsesc@2.5.2: {} + + json-buffer@3.0.1: {} + + json-parse-better-errors@1.0.2: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-deref-sync@0.13.0: + dependencies: + clone: 2.1.2 + dag-map: 1.0.2 + is-valid-path: 0.1.1 + lodash: 4.17.21 + md5: 2.2.1 + memory-cache: 0.2.0 + traverse: 0.6.9 + valid-url: 1.0.9 + optional: true + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stringify-safe@5.0.1: {} + + json-text-sequence@0.3.0: + dependencies: + '@sovpro/delimited-stream': 1.1.0 + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + optional: true + + jsonld-signatures@11.2.1(web-streams-polyfill@3.3.3): + dependencies: + '@digitalbazaar/security-context': 1.0.1 + jsonld: 8.3.2(web-streams-polyfill@3.3.3) + serialize-error: 8.1.0 + transitivePeerDependencies: + - web-streams-polyfill + + jsonld@8.3.2(web-streams-polyfill@3.3.3): + dependencies: + '@digitalbazaar/http-client': 3.4.1(web-streams-polyfill@3.3.3) + canonicalize: 1.0.8 + lru-cache: 6.0.0 + rdf-canonize: 3.4.0 + transitivePeerDependencies: + - web-streams-polyfill + + jsonpath@1.1.1: + dependencies: + esprima: 1.2.2 + static-eval: 2.0.2 + underscore: 1.12.1 + + jwt-decode@3.1.2: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kind-of@6.0.3: {} + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + ky-universal@0.11.0(ky@0.33.3)(web-streams-polyfill@3.3.3): + dependencies: + abort-controller: 3.0.0 + ky: 0.33.3 + node-fetch: 3.3.2 + optionalDependencies: + web-streams-polyfill: 3.3.3 + + ky-universal@0.8.2(ky@0.25.1)(web-streams-polyfill@3.3.3): + dependencies: + abort-controller: 3.0.0 + ky: 0.25.1 + node-fetch: 3.0.0-beta.9 + optionalDependencies: + web-streams-polyfill: 3.3.3 + transitivePeerDependencies: + - domexception + + ky@0.25.1: {} + + ky@0.33.3: {} + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + leven@3.1.0: {} + + levn@0.3.0: + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + libphonenumber-js@1.11.3: {} + + libsodium-wrappers@0.7.13: + dependencies: + libsodium: 0.7.13 + + libsodium@0.7.13: {} + + lighthouse-logger@1.4.2: + dependencies: + debug: 2.6.9 + marky: 1.2.5 + transitivePeerDependencies: + - supports-color + optional: true + + lightningcss-darwin-arm64@1.19.0: + optional: true + + lightningcss-darwin-x64@1.19.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.19.0: + optional: true + + lightningcss-linux-arm64-gnu@1.19.0: + optional: true + + lightningcss-linux-arm64-musl@1.19.0: + optional: true + + lightningcss-linux-x64-gnu@1.19.0: + optional: true + + lightningcss-linux-x64-musl@1.19.0: + optional: true + + lightningcss-win32-x64-msvc@1.19.0: + optional: true + + lightningcss@1.19.0: + dependencies: + detect-libc: 1.0.3 + optionalDependencies: + lightningcss-darwin-arm64: 1.19.0 + lightningcss-darwin-x64: 1.19.0 + lightningcss-linux-arm-gnueabihf: 1.19.0 + lightningcss-linux-arm64-gnu: 1.19.0 + lightningcss-linux-arm64-musl: 1.19.0 + lightningcss-linux-x64-gnu: 1.19.0 + lightningcss-linux-x64-musl: 1.19.0 + lightningcss-win32-x64-msvc: 1.19.0 + optional: true + + lines-and-columns@1.2.4: {} + + load-yaml-file@0.2.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + + locate-path@3.0.0: + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.camelcase@4.3.0: + optional: true + + lodash.debounce@4.0.8: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.startcase@4.4.0: {} + + lodash.throttle@4.1.1: {} + + lodash@4.17.21: {} + + log-symbols@2.2.0: + dependencies: + chalk: 2.4.2 + optional: true + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + logkitty@0.7.1: + dependencies: + ansi-fragments: 0.2.1 + dayjs: 1.11.11 + yargs: 15.4.1 + + long@4.0.0: {} + + long@5.2.3: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.2.2: {} + + lru-cache@4.1.5: + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru_map@0.4.1: {} + + luxon@3.4.4: {} + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-dir@4.0.0: + dependencies: + semver: 7.6.2 + + make-error@1.3.6: {} + + make-promises-safe@5.1.0: + optional: true + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + map-obj@1.0.1: {} + + map-obj@4.3.0: {} + + marky@1.2.5: + optional: true + + md5-file@3.2.3: + dependencies: + buffer-alloc: 1.2.0 + optional: true + + md5@2.2.1: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + optional: true + + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + optional: true + + md5hex@1.0.0: + optional: true + + media-typer@0.3.0: {} + + memoize-one@5.2.1: {} + + memory-cache@0.2.0: + optional: true + + meow@6.1.1: + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 6.2.2 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 2.5.0 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.13.1 + yargs-parser: 18.1.3 + + merge-descriptors@1.0.1: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + metro-babel-transformer@0.73.10: + dependencies: + '@babel/core': 7.24.7 + hermes-parser: 0.8.0 + metro-source-map: 0.73.10 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-cache-key@0.73.10: {} + + metro-cache@0.73.10: + dependencies: + metro-core: 0.73.10 + rimraf: 3.0.2 + + metro-config@0.73.10(encoding@0.1.13): + dependencies: + cosmiconfig: 5.2.1 + jest-validate: 26.6.2 + metro: 0.73.10(encoding@0.1.13) + metro-cache: 0.73.10 + metro-core: 0.73.10 + metro-runtime: 0.73.10 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + metro-core@0.73.10: + dependencies: + lodash.throttle: 4.1.1 + metro-resolver: 0.73.10 + + metro-file-map@0.73.10: + dependencies: + abort-controller: 3.0.0 + anymatch: 3.1.3 + debug: 2.6.9 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + invariant: 2.2.4 + jest-regex-util: 27.5.1 + jest-serializer: 27.5.1 + jest-util: 27.5.1 + jest-worker: 27.5.1 + micromatch: 4.0.7 + nullthrows: 1.1.1 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - supports-color + + metro-hermes-compiler@0.73.10: {} + + metro-inspector-proxy@0.73.10: + dependencies: + connect: 3.7.0 + debug: 2.6.9 + ws: 7.5.9 + yargs: 17.7.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro-minify-terser@0.73.10: + dependencies: + terser: 5.31.1 + + metro-minify-uglify@0.73.10: + dependencies: + uglify-es: 3.3.9 + + metro-react-native-babel-preset@0.73.10(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.24.7) + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.24.7) + '@babel/plugin-proposal-export-default-from': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.24.7) + '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.24.7) + '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.24.7) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.24.7) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-export-default-from': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-async-to-generator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-block-scoping': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-classes': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-destructuring': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-flow-strip-types': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-function-name': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-display-name': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-runtime': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-sticky-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-typescript': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-unicode-regex': 7.24.7(@babel/core@7.24.7) + '@babel/template': 7.24.7 + react-refresh: 0.4.3 + transitivePeerDependencies: + - supports-color + + metro-react-native-babel-transformer@0.73.10(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + babel-preset-fbjs: 3.4.0(@babel/core@7.24.7) + hermes-parser: 0.8.0 + metro-babel-transformer: 0.73.10 + metro-react-native-babel-preset: 0.73.10(@babel/core@7.24.7) + metro-source-map: 0.73.10 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-resolver@0.73.10: + dependencies: + absolute-path: 0.0.0 + + metro-runtime@0.73.10: + dependencies: + '@babel/runtime': 7.24.7 + react-refresh: 0.4.3 + + metro-source-map@0.73.10: + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + invariant: 2.2.4 + metro-symbolicate: 0.73.10 + nullthrows: 1.1.1 + ob1: 0.73.10 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-symbolicate@0.73.10: + dependencies: + invariant: 2.2.4 + metro-source-map: 0.73.10 + nullthrows: 1.1.1 + source-map: 0.5.7 + through2: 2.0.5 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-transform-plugins@0.73.10: + dependencies: + '@babel/core': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.7 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-transform-worker@0.73.10(encoding@0.1.13): + dependencies: + '@babel/core': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + babel-preset-fbjs: 3.4.0(@babel/core@7.24.7) + metro: 0.73.10(encoding@0.1.13) + metro-babel-transformer: 0.73.10 + metro-cache: 0.73.10 + metro-cache-key: 0.73.10 + metro-hermes-compiler: 0.73.10 + metro-source-map: 0.73.10 + metro-transform-plugins: 0.73.10 + nullthrows: 1.1.1 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + metro@0.73.10(encoding@0.1.13): + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/core': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + absolute-path: 0.0.0 + accepts: 1.3.8 + async: 3.2.5 + chalk: 4.1.2 + ci-info: 2.0.0 + connect: 3.7.0 + debug: 2.6.9 + denodeify: 1.2.1 + error-stack-parser: 2.1.4 + graceful-fs: 4.2.11 + hermes-parser: 0.8.0 + image-size: 0.6.3 + invariant: 2.2.4 + jest-worker: 27.5.1 + jsc-safe-url: 0.2.4 + lodash.throttle: 4.1.1 + metro-babel-transformer: 0.73.10 + metro-cache: 0.73.10 + metro-cache-key: 0.73.10 + metro-config: 0.73.10(encoding@0.1.13) + metro-core: 0.73.10 + metro-file-map: 0.73.10 + metro-hermes-compiler: 0.73.10 + metro-inspector-proxy: 0.73.10 + metro-minify-terser: 0.73.10 + metro-minify-uglify: 0.73.10 + metro-react-native-babel-preset: 0.73.10(@babel/core@7.24.7) + metro-resolver: 0.73.10 + metro-runtime: 0.73.10 + metro-source-map: 0.73.10 + metro-symbolicate: 0.73.10 + metro-transform-plugins: 0.73.10 + metro-transform-worker: 0.73.10(encoding@0.1.13) + mime-types: 2.1.35 + node-fetch: 2.7.0(encoding@0.1.13) + nullthrows: 1.1.1 + rimraf: 3.0.2 + serialize-error: 2.1.0 + source-map: 0.5.7 + strip-ansi: 6.0.1 + temp: 0.8.3 + throat: 5.0.0 + ws: 7.5.9 + yargs: 17.7.2 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + micromatch@4.0.7: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mime@2.6.0: {} + + mimic-fn@1.2.0: + optional: true + + mimic-fn@2.1.0: {} + + min-indent@1.0.1: {} + + minimalistic-assert@1.0.1: {} + + minimalistic-crypto-utils@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@8.0.4: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.4: + dependencies: + brace-expansion: 2.0.1 + + minimist-options@4.1.0: + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + + minimist@1.2.8: {} + + minipass-collect@2.0.1: + dependencies: + minipass: 7.1.2 + optional: true + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + optional: true + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@4.2.8: {} + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mixme@0.5.10: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mkdirp@1.0.4: {} + + ms@2.0.0: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + msrcrypto@1.5.8: {} + + multer@1.4.5-lts.1: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 1.6.2 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + + multiformats@12.1.3: {} + + multiformats@9.9.0: {} + + mute-stream@0.0.8: {} + + mv@2.1.1: + dependencies: + mkdirp: 0.5.6 + ncp: 2.0.0 + rimraf: 2.4.5 + optional: true + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + optional: true + + nanoid@3.3.7: {} + + natural-compare@1.4.0: {} + + ncp@2.0.0: + optional: true + + negotiator@0.6.3: {} + + neo-async@2.6.2: {} + + neon-cli@0.10.1: + dependencies: + chalk: 4.1.2 + command-line-args: 5.2.1 + command-line-commands: 3.0.2 + command-line-usage: 6.1.3 + git-config: 0.0.7 + handlebars: 4.7.8 + inquirer: 7.3.3 + make-promises-safe: 5.1.0 + rimraf: 3.0.2 + semver: 7.6.2 + toml: 3.0.0 + ts-typed-json: 0.3.2 + validate-npm-package-license: 3.0.4 + validate-npm-package-name: 3.0.0 + optional: true + + nested-error-stacks@2.0.1: + optional: true + + next-tick@1.1.0: {} + + nice-try@1.0.5: {} + + nocache@3.0.4: {} + + nock@13.5.4: + dependencies: + debug: 4.3.5 + json-stringify-safe: 5.0.1 + propagate: 2.0.1 + transitivePeerDependencies: + - supports-color + + node-addon-api@3.2.1: {} + + node-cache@5.1.2: + dependencies: + clone: 2.1.2 + + node-dir@0.1.17: + dependencies: + minimatch: 3.1.2 + + node-domexception@1.0.0: {} + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-fetch@3.0.0-beta.9: + dependencies: + data-uri-to-buffer: 3.0.1 + fetch-blob: 2.1.2 + transitivePeerDependencies: + - domexception + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-forge@1.3.1: + optional: true + + node-gyp-build@4.8.1: {} + + node-int64@0.4.0: {} + + node-releases@2.0.14: {} + + node-stream-zip@1.15.0: {} + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.8 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + + normalize-path@3.0.0: {} + + npm-package-arg@7.0.0: + dependencies: + hosted-git-info: 3.0.8 + osenv: 0.1.5 + semver: 5.7.2 + validate-npm-package-name: 3.0.0 + optional: true + + npm-run-path@2.0.2: + dependencies: + path-key: 2.0.1 + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + + nullthrows@1.1.1: {} + + ob1@0.73.10: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.1: {} + + object-keys@1.1.1: {} + + object.assign@4.1.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + + object.values@1.2.0: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.0.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@2.0.1: + dependencies: + mimic-fn: 1.2.0 + optional: true + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@6.4.0: + dependencies: + is-wsl: 1.1.0 + + open@7.4.2: + dependencies: + is-docker: 2.2.1 + is-wsl: 2.2.0 + optional: true + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + optional: true + + optionator@0.8.3: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.5 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@3.4.0: + dependencies: + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-spinners: 2.9.2 + log-symbols: 2.2.0 + strip-ansi: 5.2.0 + wcwidth: 1.0.1 + optional: true + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + os-homedir@1.0.2: + optional: true + + os-tmpdir@1.0.2: {} + + osenv@0.1.5: + dependencies: + os-homedir: 1.0.2 + os-tmpdir: 1.0.2 + optional: true + + outdent@0.5.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-finally@1.0.0: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@2.1.0: {} + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + optional: true + + p-try@2.2.0: {} + + pako@2.1.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@4.0.0: + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.24.7 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-png@2.1.0: + dependencies: + pngjs: 3.4.0 + optional: true + + parseurl@1.3.3: {} + + password-prompt@1.1.3: + dependencies: + ansi-escapes: 4.3.2 + cross-spawn: 7.0.3 + optional: true + + path-exists@3.0.0: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@2.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.2.2 + minipass: 7.1.2 + + path-to-regexp@0.1.7: {} + + path-type@4.0.0: {} + + peek-readable@4.1.0: {} + + picocolors@1.0.1: {} + + picomatch@2.3.1: {} + + picomatch@3.0.1: + optional: true + + pify@4.0.1: {} + + pirates@4.0.6: {} + + pkg-dir@3.0.0: + dependencies: + find-up: 3.0.0 + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.10 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + optional: true + + pngjs@3.4.0: + optional: true + + possible-typed-array-names@1.0.0: {} + + postcss@8.4.38: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + optional: true + + preferred-pm@3.1.3: + dependencies: + find-up: 5.0.0 + find-yarn-workspace-root2: 1.2.16 + path-exists: 4.0.0 + which-pm: 2.0.0 + + prelude-ls@1.1.2: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@2.8.8: {} + + pretty-bytes@5.6.0: + optional: true + + pretty-format@26.6.2: + dependencies: + '@jest/types': 26.6.2 + ansi-regex: 5.0.1 + ansi-styles: 4.3.0 + react-is: 17.0.2 + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + process-nextick-args@2.0.1: {} + + progress@2.0.3: + optional: true + + promise@7.3.1: + dependencies: + asap: 2.0.6 + optional: true + + promise@8.3.0: + dependencies: + asap: 2.0.6 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + propagate@2.0.1: {} + + protobufjs@6.11.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/long': 4.0.2 + '@types/node': 18.18.8 + long: 4.0.0 + + protobufjs@7.3.2: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 18.18.8 + long: 5.2.3 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pseudomap@1.0.2: {} + + pump@3.0.0: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + pvtsutils@1.3.5: + dependencies: + tslib: 2.6.3 + + pvutils@1.1.3: {} + + qrcode-terminal@0.11.0: + optional: true + + qs@6.11.0: + dependencies: + side-channel: 1.0.6 + + qs@6.12.1: + dependencies: + side-channel: 1.0.6 + + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + + queue-microtask@1.2.3: {} + + quick-lru@4.0.1: {} + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + optional: true + + rdf-canonize@3.4.0: + dependencies: + setimmediate: 1.0.5 + + react-devtools-core@4.28.5: + dependencies: + shell-quote: 1.8.1 + ws: 7.5.9 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react-native-codegen@0.71.6(@babel/preset-env@7.24.7(@babel/core@7.24.7)): + dependencies: + '@babel/parser': 7.24.7 + flow-parser: 0.185.2 + jscodeshift: 0.14.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + + react-native-fs@2.20.0(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1)): + dependencies: + base-64: 0.1.0 + react-native: 0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1) + utf8: 3.0.0 + + react-native-get-random-values@1.11.0(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1)): + dependencies: + fast-base64-decode: 1.0.0 + react-native: 0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1) + + react-native-gradle-plugin@0.71.19: {} + + react-native-securerandom@0.1.1(react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1)): + dependencies: + base64-js: 1.5.1 + react-native: 0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1) + optional: true + + react-native@0.71.19(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(encoding@0.1.13)(react@18.3.1): + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@react-native-community/cli': 10.2.7(@babel/core@7.24.7)(encoding@0.1.13) + '@react-native-community/cli-platform-android': 10.2.0(encoding@0.1.13) + '@react-native-community/cli-platform-ios': 10.2.5(encoding@0.1.13) + '@react-native/assets': 1.0.0 + '@react-native/normalize-color': 2.1.0 + '@react-native/polyfills': 2.0.0 + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + base64-js: 1.5.1 + deprecated-react-native-prop-types: 3.0.2 + event-target-shim: 5.0.1 + invariant: 2.2.4 + jest-environment-node: 29.7.0 + jsc-android: 250231.0.0 + memoize-one: 5.2.1 + metro-react-native-babel-transformer: 0.73.10(@babel/core@7.24.7) + metro-runtime: 0.73.10 + metro-source-map: 0.73.10 + mkdirp: 0.5.6 + nullthrows: 1.1.1 + pretty-format: 26.6.2 + promise: 8.3.0 + react: 18.3.1 + react-devtools-core: 4.28.5 + react-native-codegen: 0.71.6(@babel/preset-env@7.24.7(@babel/core@7.24.7)) + react-native-gradle-plugin: 0.71.19 + react-refresh: 0.4.3 + react-shallow-renderer: 16.15.0(react@18.3.1) + regenerator-runtime: 0.13.11 + scheduler: 0.23.2 + stacktrace-parser: 0.1.10 + use-sync-external-store: 1.2.2(react@18.3.1) + whatwg-fetch: 3.6.20 + ws: 6.2.2 + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - bufferutil + - encoding + - supports-color + - utf-8-validate + + react-refresh@0.14.2: + optional: true + + react-refresh@0.4.3: {} + + react-shallow-renderer@16.15.0(react@18.3.1): + dependencies: + object-assign: 4.1.1 + react: 18.3.1 + react-is: 18.3.1 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-pkg-up@7.0.1: + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + + read-pkg@5.2.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-web-to-node-stream@3.0.2: + dependencies: + readable-stream: 3.6.2 + + readline@1.3.0: {} + + readonly-date@1.0.0: {} + + recast@0.21.5: + dependencies: + ast-types: 0.15.2 + esprima: 4.0.1 + source-map: 0.6.1 + tslib: 2.6.3 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + reduce-flatten@2.0.0: + optional: true + + ref-array-di@1.2.2: + dependencies: + array-index: 1.0.0 + debug: 3.2.7 + transitivePeerDependencies: + - supports-color + + ref-struct-di@1.1.1: + dependencies: + debug: 3.2.7 + transitivePeerDependencies: + - supports-color + + reflect-metadata@0.1.14: {} + + regenerate-unicode-properties@10.1.1: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regenerator-runtime@0.13.11: {} + + regenerator-runtime@0.14.1: {} + + regenerator-transform@0.15.2: + dependencies: + '@babel/runtime': 7.24.7 + + regexp.prototype.flags@1.5.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + + regexpu-core@5.3.2: + dependencies: + '@babel/regjsgen': 0.8.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.1 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + + regjsparser@0.9.1: + dependencies: + jsesc: 0.5.0 + + remove-trailing-slash@0.1.1: + optional: true + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-main-filename@2.0.0: {} + + requireg@0.2.2: + dependencies: + nested-error-stacks: 2.0.1 + rc: 1.2.8 + resolve: 1.7.1 + optional: true + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@3.0.0: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve.exports@2.0.2: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@1.7.1: + dependencies: + path-parse: 1.0.7 + optional: true + + restore-cursor@2.0.0: + dependencies: + onetime: 2.0.1 + signal-exit: 3.0.7 + optional: true + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + reusify@1.0.4: {} + + rfc4648@1.5.2: {} + + rimraf@2.2.8: {} + + rimraf@2.4.5: + dependencies: + glob: 6.0.4 + optional: true + + rimraf@2.6.3: + dependencies: + glob: 7.2.3 + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + optional: true + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rimraf@4.4.1: + dependencies: + glob: 9.3.5 + + run-async@2.4.1: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@6.6.7: + dependencies: + tslib: 1.14.1 + optional: true + + rxjs@7.8.1: + dependencies: + tslib: 2.6.3 + + safe-array-concat@1.1.2: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-json-stringify@1.2.0: + optional: true + + safe-regex-test@1.0.3: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + + safer-buffer@2.1.2: {} + + sax@1.4.1: + optional: true + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + selfsigned@2.4.1: + dependencies: + '@types/node-forge': 1.3.11 + node-forge: 1.3.1 + optional: true + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.6.2: {} + + send@0.18.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serialize-error@2.1.0: {} + + serialize-error@8.1.0: + dependencies: + type-fest: 0.20.2 + + serve-static@1.15.0: + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + setimmediate@1.0.5: {} + + setprototypeof@1.2.0: {} + + sha.js@2.4.11: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + + shallow-clone@3.0.1: + dependencies: + kind-of: 6.0.3 + + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@1.0.0: {} + + shebang-regex@3.0.0: {} + + shell-quote@1.8.1: {} + + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.1 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: + optional: true + + simple-plist@1.3.1: + dependencies: + bplist-creator: 0.1.0 + bplist-parser: 0.3.1 + plist: 3.1.0 + optional: true + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + slice-ansi@2.1.0: + dependencies: + ansi-styles: 3.2.1 + astral-regex: 1.0.0 + is-fullwidth-code-point: 2.0.0 + + slugify@1.6.6: + optional: true + + smartwrap@2.0.2: + dependencies: + array.prototype.flat: 1.3.2 + breakword: 1.0.6 + grapheme-splitter: 1.0.4 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + yargs: 15.4.1 + + source-map-js@1.2.0: + optional: true + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + spawndamnit@2.0.0: + dependencies: + cross-spawn: 5.1.0 + signal-exit: 3.0.7 + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.18 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.18 + + spdx-license-ids@3.0.18: {} + + split-on-first@1.1.0: {} + + split@1.0.1: + dependencies: + through: 2.3.8 + optional: true + + sprintf-js@1.0.3: {} + + ssri@10.0.6: + dependencies: + minipass: 7.1.2 + optional: true + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + stackframe@1.3.4: {} + + stacktrace-parser@0.1.10: + dependencies: + type-fest: 0.7.1 + + static-eval@2.0.2: + dependencies: + escodegen: 1.14.3 + + statuses@1.5.0: {} + + statuses@2.0.1: {} + + str2buf@1.3.0: {} + + stream-buffers@2.2.0: + optional: true + + stream-transform@2.1.3: + dependencies: + mixme: 0.5.10 + + streamsearch@1.1.0: {} + + strict-uri-encode@2.0.0: {} + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + optional: true + + string.prototype.matchall@4.0.11: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.2 + set-function-name: 2.0.2 + side-channel: 1.0.6 + + string.prototype.trim@1.2.9: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + + string.prototype.trimend@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.0.1 + optional: true + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-eof@1.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@2.0.1: + optional: true + + strip-json-comments@3.1.1: {} + + strnum@1.0.5: {} + + strtok3@6.3.0: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 4.1.0 + + structured-headers@0.4.1: + optional: true + + sucrase@3.34.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + optional: true + + sudo-prompt@8.2.5: + optional: true + + sudo-prompt@9.1.1: + optional: true + + sudo-prompt@9.2.1: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@2.3.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + optional: true + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-observable@2.0.3: {} + + table-layout@1.0.2: + dependencies: + array-back: 4.0.2 + deep-extend: 0.6.0 + typical: 5.2.0 + wordwrapjs: 4.0.1 + optional: true + + tapable@2.2.1: {} + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + temp-dir@1.0.0: + optional: true + + temp-dir@2.0.0: + optional: true + + temp@0.8.3: + dependencies: + os-tmpdir: 1.0.2 + rimraf: 2.2.8 + + temp@0.8.4: + dependencies: + rimraf: 2.6.3 + + tempy@0.3.0: + dependencies: + temp-dir: 1.0.0 + type-fest: 0.3.1 + unique-string: 1.0.0 + optional: true + + tempy@0.7.1: + dependencies: + del: 6.1.1 + is-stream: 2.0.1 + temp-dir: 2.0.0 + type-fest: 0.16.0 + unique-string: 2.0.0 + optional: true + + term-size@2.2.1: {} + + terminal-link@2.1.1: + dependencies: + ansi-escapes: 4.3.2 + supports-hyperlinks: 2.3.0 + optional: true + + terser@5.31.1: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.12.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + optional: true + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + optional: true + + throat@5.0.0: {} + + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + + through@2.3.8: {} + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + tmpl@1.0.5: {} + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + token-types@4.2.1: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + toml@3.0.0: + optional: true + + tr46@0.0.3: {} + + traverse@0.6.9: + dependencies: + gopd: 1.0.1 + typedarray.prototype.slice: 1.0.3 + which-typed-array: 1.1.15 + optional: true + + trim-newlines@3.0.1: {} + + ts-api-utils@1.3.0(typescript@5.5.2): + dependencies: + typescript: 5.5.2 + + ts-interface-checker@0.1.13: + optional: true + + ts-jest@29.1.4(@babel/core@7.24.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(jest@29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)))(typescript@5.5.2): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2)) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.2 + typescript: 5.5.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.24.7 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.24.7) + + ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.18.8 + acorn: 8.12.0 + acorn-walk: 8.3.3 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.5.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + ts-typed-json@0.3.2: + optional: true + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@1.14.1: {} + + tslib@2.6.3: {} + + tslog@4.9.3: {} + + tsyringe@4.8.0: + dependencies: + tslib: 1.14.1 + + tty-table@4.2.3: + dependencies: + chalk: 4.1.2 + csv: 5.5.3 + kleur: 4.1.5 + smartwrap: 2.0.2 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + yargs: 17.7.2 + + type-check@0.3.2: + dependencies: + prelude-ls: 1.1.2 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.13.1: {} + + type-fest@0.16.0: + optional: true + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@0.3.1: + optional: true + + type-fest@0.6.0: {} + + type-fest@0.7.1: {} + + type-fest@0.8.1: {} + + type-fest@3.13.1: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type@2.7.3: {} + + typed-array-buffer@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + + typed-array-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-byte-offset@1.0.2: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-length@1.0.6: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + + typedarray.prototype.slice@1.0.3: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + typed-array-buffer: 1.0.2 + typed-array-byte-offset: 1.0.2 + optional: true + + typedarray@0.0.6: {} + + typescript@5.5.2: {} + + typical@4.0.0: + optional: true + + typical@5.2.0: + optional: true + + ua-parser-js@1.0.38: + optional: true + + uglify-es@3.3.9: + dependencies: + commander: 2.13.0 + source-map: 0.6.1 + + uglify-js@3.18.0: + optional: true + + uint8arrays@3.1.1: + dependencies: + multiformats: 9.9.0 + + unbox-primitive@1.0.2: + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + + underscore@1.12.1: {} + + undici-types@5.26.5: {} + + undici@5.28.4: + dependencies: + '@fastify/busboy': 2.1.1 + + unicode-canonical-property-names-ecmascript@2.0.0: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + + unicode-match-property-value-ecmascript@2.1.0: {} + + unicode-property-aliases-ecmascript@2.1.0: {} + + unique-filename@3.0.0: + dependencies: + unique-slug: 4.0.0 + optional: true + + unique-slug@4.0.0: + dependencies: + imurmurhash: 0.1.4 + optional: true + + unique-string@1.0.0: + dependencies: + crypto-random-string: 1.0.0 + optional: true + + unique-string@2.0.0: + dependencies: + crypto-random-string: 2.0.0 + optional: true + + universalify@0.1.2: {} + + universalify@1.0.0: + optional: true + + universalify@2.0.1: + optional: true + + unpipe@1.0.0: {} + + update-browserslist-db@1.0.16(browserslist@4.23.1): + dependencies: + browserslist: 4.23.1 + escalade: 3.1.2 + picocolors: 1.0.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-join@4.0.0: + optional: true + + use-sync-external-store@1.2.2(react@18.3.1): + dependencies: + react: 18.3.1 + + utf8@3.0.0: {} + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + uuid@7.0.3: + optional: true + + uuid@8.3.2: + optional: true + + uuid@9.0.1: {} + + v8-compile-cache-lib@3.0.1: {} + + v8-to-istanbul@9.2.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + valid-url@1.0.9: + optional: true + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + validate-npm-package-name@3.0.0: + dependencies: + builtins: 1.0.3 + optional: true + + validator@13.12.0: {} + + varint@6.0.0: {} + + vary@1.1.2: {} + + vlq@1.0.1: {} + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-did-resolver@2.0.27(encoding@0.1.13): + dependencies: + cross-fetch: 4.0.0(encoding@0.1.13) + did-resolver: 4.1.0 + transitivePeerDependencies: + - encoding + + web-streams-polyfill@3.3.3: {} + + webcrypto-core@1.8.0: + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/json-schema': 1.1.12 + asn1js: 3.0.5 + pvtsutils: 1.3.5 + tslib: 2.6.3 + + webcrypto-shim@0.1.7: {} + + webidl-conversions@3.0.1: {} + + webidl-conversions@5.0.0: + optional: true + + whatwg-fetch@3.6.20: {} + + whatwg-url-without-unicode@8.0.0-3: + dependencies: + buffer: 5.7.1 + punycode: 2.3.1 + webidl-conversions: 5.0.0 + optional: true + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.0.2: + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + + which-module@2.0.1: {} + + which-pm@2.0.0: + dependencies: + load-yaml-file: 0.2.0 + path-exists: 4.0.0 + + which-typed-array@1.1.15: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + + wonka@4.0.15: + optional: true + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: + optional: true + + wordwrapjs@4.0.1: + dependencies: + reduce-flatten: 2.0.0 + typical: 5.2.0 + optional: true + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + optional: true + + wrappy@1.0.2: {} + + write-file-atomic@2.4.3: + dependencies: + graceful-fs: 4.2.11 + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + ws@6.2.2: + dependencies: + async-limiter: 1.0.1 + + ws@7.5.9: {} + + ws@8.17.0: {} + + xcode@3.0.1: + dependencies: + simple-plist: 1.3.1 + uuid: 7.0.3 + optional: true + + xml2js@0.6.0: + dependencies: + sax: 1.4.1 + xmlbuilder: 11.0.1 + optional: true + + xmlbuilder@11.0.1: + optional: true + + xmlbuilder@14.0.0: + optional: true + + xmlbuilder@15.1.1: + optional: true + + xstream@11.14.0: + dependencies: + globalthis: 1.0.4 + symbol-observable: 2.0.3 + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yallist@2.1.2: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml@2.4.5: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@21.1.1: {} + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000000..027958c415 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +packages: + - 'packages/*' + - 'demo' + - 'demo-openid' + - 'samples/*' diff --git a/samples/extension-module/README.md b/samples/extension-module/README.md new file mode 100644 index 0000000000..6ea2ee157b --- /dev/null +++ b/samples/extension-module/README.md @@ -0,0 +1,93 @@ +

Extension module example

+ +This example shows how an extension module can be written and injected to an Credo `Agent` instance. Its structure is similar to the one of regular modules, although is not strictly needed to follow it to achieve this goal. + +An extension module could be used for different purposes, such as storing data in an Identity Wallet, supporting custom protocols over Didcomm or implementing new [Aries RFCs](https://github.com/hyperledger/aries-rfcs/tree/main/features) without the need of embed them right into Credo's Core package. Injected modules can access to other core modules and services and trigger events, so in practice they work much in the same way as if they were included statically. + +> **Note** the custom module API is in heavy development and can have regular breaking changes. This is an experimental feature, so use it at your own risk. Over time we will provide a stable API for extension modules. + +## Dummy module + +This example consists of a module that implements a very simple request-response protocol called Dummy. In order to do so and be able to be injected into an Credo instance, some steps were followed: + +- Define Dummy protocol message classes (inherited from `AgentMessage`) +- Create handlers for those messages (inherited from `MessageHandler`) +- Define records (inherited from `BaseRecord`) and a singleton repository (inherited from `Repository`) for state persistance +- Define events (inherited from `BaseEvent`) +- Create a singleton service class that manages records and repository, and also trigger events using Agent's `EventEmitter` +- Create a singleton api class that registers handlers in Agent's `Dispatcher` and provides a simple API to do requests and responses, with the aid of service classes and Agent's `MessageSender` +- Create a module class that registers all the above on the dependency manager so it can be be injected from the `Agent` instance, and also register the features (such as protocols) the module adds to the Agent. + +## Usage + +In order to use this module, you first need to register `DummyModule` on the `Agent` instance. This can be done by adding an entry for it in `AgentOptions`'s modules property: + +```ts +import { DummyModule } from './dummy' + +// Register the module with it's dependencies +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + dummy: new DummyModule({ + /* module config */ + }), + /* other custom modules */ + }, +}) + +await agent.initialize() +``` + +Then, Dummy module API methods can be called from `agent.modules.dummy` namespace, and events listeners can be created: + +```ts +agent.events.on(DummyEventTypes.StateChanged, async (event: DummyStateChangedEvent) => { + if (event.payload.dummyRecord.state === DummyState.RequestReceived) { + await agent.modules.dummy.respond(event.payload.dummyRecord) + } +}) + +const record = await agent.modules.dummy.request(connection) +``` + +## Run demo + +This repository includes a demonstration of a requester and a responder controller using this module to exchange Dummy protocol messages. For environment set up, make sure you followed the [Credo Prerequisites](https://credo.js.org/guides/getting-started/prerequisites). + +These are the steps for running it: + +Clone the Credo git repository: + +```sh +git clone https://github.com/openwallet-foundation/credo-ts.git +``` + +Open two different terminals and go to the extension-module directory: + +```sh +cd credo-ts/samples/extension-module +``` + +Install the project in one of the terminals: + +```sh +pnpm install +``` + +In that terminal run the responder: + +```sh +pnpm responder +``` + +Wait for it to finish the startup process (i.e. logger showing 'Responder listening to port ...') and run requester in another terminal: + +```sh +pnpm requester +``` + +If everything goes right, requester will connect to responder and, as soon as connection protocol is finished, it will send a Dummy request. Responder will answer with a Dummy response and requester will happily exit. diff --git a/samples/extension-module/dummy/DummyApi.ts b/samples/extension-module/dummy/DummyApi.ts new file mode 100644 index 0000000000..5083a5b66e --- /dev/null +++ b/samples/extension-module/dummy/DummyApi.ts @@ -0,0 +1,99 @@ +import type { DummyRecord } from './repository/DummyRecord' +import type { Query, QueryOptions } from '@credo-ts/core' + +import { getOutboundMessageContext, AgentContext, ConnectionService, injectable, MessageSender } from '@credo-ts/core' + +import { DummyRequestHandler, DummyResponseHandler } from './handlers' +import { DummyState } from './repository' +import { DummyService } from './services' + +@injectable() +export class DummyApi { + private messageSender: MessageSender + private dummyService: DummyService + private connectionService: ConnectionService + private agentContext: AgentContext + + public constructor( + messageSender: MessageSender, + dummyService: DummyService, + connectionService: ConnectionService, + agentContext: AgentContext + ) { + this.messageSender = messageSender + this.dummyService = dummyService + this.connectionService = connectionService + this.agentContext = agentContext + + this.agentContext.dependencyManager.registerMessageHandlers([ + new DummyRequestHandler(this.dummyService), + new DummyResponseHandler(this.dummyService), + ]) + } + + /** + * Send a Dummy Request + * + * @param connection record of the target responder (must be active) + * @returns created Dummy Record + */ + public async request(connectionId: string) { + const connection = await this.connectionService.getById(this.agentContext, connectionId) + const { record, message } = await this.dummyService.createRequest(this.agentContext, connection) + + await this.messageSender.sendMessage( + await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: record, + connectionRecord: connection, + }) + ) + + await this.dummyService.updateState(this.agentContext, record, DummyState.RequestSent) + + return record + } + + /** + * Respond a Dummy Request + * + * @param record Dummy record + * @returns Updated dummy record + */ + public async respond(dummyId: string) { + const record = await this.dummyService.getById(this.agentContext, dummyId) + const connection = await this.connectionService.getById(this.agentContext, record.connectionId) + + const message = await this.dummyService.createResponse(this.agentContext, record) + + await this.messageSender.sendMessage( + await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: record, + connectionRecord: connection, + }) + ) + + await this.dummyService.updateState(this.agentContext, record, DummyState.ResponseSent) + + return record + } + + /** + * Retrieve all dummy records + * + * @returns List containing all records + */ + public getAll(): Promise { + return this.dummyService.getAll(this.agentContext) + } + + /** + * Retrieve all dummy records + * + * @returns List containing all records + */ + public findAllByQuery(query: Query, queryOptions?: QueryOptions): Promise { + return this.dummyService.findAllByQuery(this.agentContext, query, queryOptions) + } +} diff --git a/samples/extension-module/dummy/DummyModule.ts b/samples/extension-module/dummy/DummyModule.ts new file mode 100644 index 0000000000..f7669486c9 --- /dev/null +++ b/samples/extension-module/dummy/DummyModule.ts @@ -0,0 +1,35 @@ +import type { DummyModuleConfigOptions } from './DummyModuleConfig' +import type { DependencyManager, FeatureRegistry, Module } from '@credo-ts/core' + +import { Protocol } from '@credo-ts/core' + +import { DummyApi } from './DummyApi' +import { DummyModuleConfig } from './DummyModuleConfig' +import { DummyRepository } from './repository' +import { DummyService } from './services' + +export class DummyModule implements Module { + public readonly config: DummyModuleConfig + + public readonly api = DummyApi + + public constructor(config?: DummyModuleConfigOptions) { + this.config = new DummyModuleConfig(config) + } + + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Config + dependencyManager.registerInstance(DummyModuleConfig, this.config) + + dependencyManager.registerSingleton(DummyRepository) + dependencyManager.registerSingleton(DummyService) + + // Features + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/dummy/1.0', + roles: ['requester', 'responder'], + }) + ) + } +} diff --git a/samples/extension-module/dummy/DummyModuleConfig.ts b/samples/extension-module/dummy/DummyModuleConfig.ts new file mode 100644 index 0000000000..ef742f4b3f --- /dev/null +++ b/samples/extension-module/dummy/DummyModuleConfig.ts @@ -0,0 +1,25 @@ +/** + * DummyModuleConfigOptions defines the interface for the options of the DummyModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface DummyModuleConfigOptions { + /** + * Whether to automatically accept request messages. + * + * @default false + */ + autoAcceptRequests?: boolean +} + +export class DummyModuleConfig { + private options: DummyModuleConfigOptions + + public constructor(options?: DummyModuleConfigOptions) { + this.options = options ?? {} + } + + /** See {@link DummyModuleConfigOptions.autoAcceptRequests} */ + public get autoAcceptRequests() { + return this.options.autoAcceptRequests ?? false + } +} diff --git a/samples/extension-module/dummy/handlers/DummyRequestHandler.ts b/samples/extension-module/dummy/handlers/DummyRequestHandler.ts new file mode 100644 index 0000000000..928c070af0 --- /dev/null +++ b/samples/extension-module/dummy/handlers/DummyRequestHandler.ts @@ -0,0 +1,27 @@ +import type { DummyService } from '../services' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { getOutboundMessageContext } from '@credo-ts/core' + +import { DummyRequestMessage } from '../messages' + +export class DummyRequestHandler implements MessageHandler { + public supportedMessages = [DummyRequestMessage] + private dummyService: DummyService + + public constructor(dummyService: DummyService) { + this.dummyService = dummyService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + const connectionRecord = inboundMessage.assertReadyConnection() + const responseMessage = await this.dummyService.processRequest(inboundMessage) + + if (responseMessage) { + return getOutboundMessageContext(inboundMessage.agentContext, { + connectionRecord, + message: responseMessage, + }) + } + } +} diff --git a/samples/extension-module/dummy/handlers/DummyResponseHandler.ts b/samples/extension-module/dummy/handlers/DummyResponseHandler.ts new file mode 100644 index 0000000000..194a57f639 --- /dev/null +++ b/samples/extension-module/dummy/handlers/DummyResponseHandler.ts @@ -0,0 +1,19 @@ +import type { DummyService } from '../services' +import type { MessageHandler, MessageHandlerInboundMessage } from '@credo-ts/core' + +import { DummyResponseMessage } from '../messages' + +export class DummyResponseHandler implements MessageHandler { + public supportedMessages = [DummyResponseMessage] + private dummyService: DummyService + + public constructor(dummyService: DummyService) { + this.dummyService = dummyService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + inboundMessage.assertReadyConnection() + + await this.dummyService.processResponse(inboundMessage) + } +} diff --git a/samples/extension-module/dummy/handlers/index.ts b/samples/extension-module/dummy/handlers/index.ts new file mode 100644 index 0000000000..1aacc16089 --- /dev/null +++ b/samples/extension-module/dummy/handlers/index.ts @@ -0,0 +1,2 @@ +export * from './DummyRequestHandler' +export * from './DummyResponseHandler' diff --git a/samples/extension-module/dummy/index.ts b/samples/extension-module/dummy/index.ts new file mode 100644 index 0000000000..f2014dc391 --- /dev/null +++ b/samples/extension-module/dummy/index.ts @@ -0,0 +1,7 @@ +export * from './DummyApi' +export * from './handlers' +export * from './messages' +export * from './services' +export * from './repository' +export * from './DummyModule' +export * from './DummyModuleConfig' diff --git a/samples/extension-module/dummy/messages/DummyRequestMessage.ts b/samples/extension-module/dummy/messages/DummyRequestMessage.ts new file mode 100644 index 0000000000..871c8de61d --- /dev/null +++ b/samples/extension-module/dummy/messages/DummyRequestMessage.ts @@ -0,0 +1,21 @@ +import { AgentMessage, IsValidMessageType, parseMessageType, ReturnRouteTypes } from '@credo-ts/core' + +export interface DummyRequestMessageOptions { + id?: string +} + +export class DummyRequestMessage extends AgentMessage { + public constructor(options: DummyRequestMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + } + + this.setReturnRouting(ReturnRouteTypes.all) + } + + @IsValidMessageType(DummyRequestMessage.type) + public readonly type = DummyRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/dummy/1.0/request') +} diff --git a/samples/extension-module/dummy/messages/DummyResponseMessage.ts b/samples/extension-module/dummy/messages/DummyResponseMessage.ts new file mode 100644 index 0000000000..ce4e32ebbe --- /dev/null +++ b/samples/extension-module/dummy/messages/DummyResponseMessage.ts @@ -0,0 +1,23 @@ +import { AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core' + +export interface DummyResponseMessageOptions { + id?: string + threadId: string +} + +export class DummyResponseMessage extends AgentMessage { + public constructor(options: DummyResponseMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.setThread({ + threadId: options.threadId, + }) + } + } + + @IsValidMessageType(DummyResponseMessage.type) + public readonly type = DummyResponseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/dummy/1.0/response') +} diff --git a/samples/extension-module/dummy/messages/index.ts b/samples/extension-module/dummy/messages/index.ts new file mode 100644 index 0000000000..7b11bafe4f --- /dev/null +++ b/samples/extension-module/dummy/messages/index.ts @@ -0,0 +1,2 @@ +export * from './DummyRequestMessage' +export * from './DummyResponseMessage' diff --git a/samples/extension-module/dummy/repository/DummyRecord.ts b/samples/extension-module/dummy/repository/DummyRecord.ts new file mode 100644 index 0000000000..4bd52cc4c3 --- /dev/null +++ b/samples/extension-module/dummy/repository/DummyRecord.ts @@ -0,0 +1,51 @@ +import type { DummyState } from './DummyState' + +import { BaseRecord } from '@credo-ts/core' +import { v4 as uuid } from 'uuid' + +export interface DummyStorageProps { + id?: string + createdAt?: Date + connectionId: string + threadId: string + state: DummyState +} + +export class DummyRecord extends BaseRecord implements DummyStorageProps { + public connectionId!: string + public threadId!: string + public state!: DummyState + + public static readonly type = 'DummyRecord' + public readonly type = DummyRecord.type + + public constructor(props: DummyStorageProps) { + super() + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.state = props.state + this.connectionId = props.connectionId + this.threadId = props.threadId + } + } + + public getTags() { + return { + ...this._tags, + threadId: this.threadId, + connectionId: this.connectionId, + state: this.state, + } + } + + public assertState(expectedStates: DummyState | DummyState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new Error(`Dummy record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.`) + } + } +} diff --git a/samples/extension-module/dummy/repository/DummyRepository.ts b/samples/extension-module/dummy/repository/DummyRepository.ts new file mode 100644 index 0000000000..142f05380f --- /dev/null +++ b/samples/extension-module/dummy/repository/DummyRepository.ts @@ -0,0 +1,13 @@ +import { Repository, StorageService, InjectionSymbols, EventEmitter, inject, injectable } from '@credo-ts/core' + +import { DummyRecord } from './DummyRecord' + +@injectable() +export class DummyRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(DummyRecord, storageService, eventEmitter) + } +} diff --git a/samples/extension-module/dummy/repository/DummyState.ts b/samples/extension-module/dummy/repository/DummyState.ts new file mode 100644 index 0000000000..c5f8f411b1 --- /dev/null +++ b/samples/extension-module/dummy/repository/DummyState.ts @@ -0,0 +1,7 @@ +export enum DummyState { + Init = 'init', + RequestSent = 'request-sent', + RequestReceived = 'request-received', + ResponseSent = 'response-sent', + ResponseReceived = 'response-received', +} diff --git a/samples/extension-module/dummy/repository/index.ts b/samples/extension-module/dummy/repository/index.ts new file mode 100644 index 0000000000..38d0353bd5 --- /dev/null +++ b/samples/extension-module/dummy/repository/index.ts @@ -0,0 +1,3 @@ +export * from './DummyRecord' +export * from './DummyRepository' +export * from './DummyState' diff --git a/samples/extension-module/dummy/services/DummyEvents.ts b/samples/extension-module/dummy/services/DummyEvents.ts new file mode 100644 index 0000000000..9a584b0213 --- /dev/null +++ b/samples/extension-module/dummy/services/DummyEvents.ts @@ -0,0 +1,15 @@ +import type { DummyRecord } from '../repository/DummyRecord' +import type { DummyState } from '../repository/DummyState' +import type { BaseEvent } from '@credo-ts/core' + +export enum DummyEventTypes { + StateChanged = 'DummyStateChanged', +} + +export interface DummyStateChangedEvent extends BaseEvent { + type: DummyEventTypes.StateChanged + payload: { + dummyRecord: DummyRecord + previousState: DummyState | null + } +} diff --git a/samples/extension-module/dummy/services/DummyService.ts b/samples/extension-module/dummy/services/DummyService.ts new file mode 100644 index 0000000000..98e05502c2 --- /dev/null +++ b/samples/extension-module/dummy/services/DummyService.ts @@ -0,0 +1,203 @@ +import type { DummyStateChangedEvent } from './DummyEvents' +import type { Query, QueryOptions, AgentContext, ConnectionRecord, InboundMessageContext } from '@credo-ts/core' + +import { injectable, EventEmitter } from '@credo-ts/core' + +import { DummyModuleConfig } from '../DummyModuleConfig' +import { DummyRequestMessage, DummyResponseMessage } from '../messages' +import { DummyRecord } from '../repository/DummyRecord' +import { DummyRepository } from '../repository/DummyRepository' +import { DummyState } from '../repository/DummyState' + +import { DummyEventTypes } from './DummyEvents' + +@injectable() +export class DummyService { + private dummyRepository: DummyRepository + private eventEmitter: EventEmitter + private dummyModuleConfig: DummyModuleConfig + + public constructor( + dummyModuleConfig: DummyModuleConfig, + dummyRepository: DummyRepository, + eventEmitter: EventEmitter + ) { + this.dummyModuleConfig = dummyModuleConfig + this.dummyRepository = dummyRepository + this.eventEmitter = eventEmitter + } + + /** + * Create a {@link DummyRequestMessage}. + * + * @param connectionRecord The connection for which to create the dummy request + * @returns Object containing dummy request message and associated dummy record + * + */ + public async createRequest(agentContext: AgentContext, connectionRecord: ConnectionRecord) { + // Create message + const message = new DummyRequestMessage({}) + + // Create record + const record = new DummyRecord({ + connectionId: connectionRecord.id, + threadId: message.threadId, + state: DummyState.Init, + }) + + await this.dummyRepository.save(agentContext, record) + + this.emitStateChangedEvent(agentContext, record, null) + + return { record, message } + } + + /** + * Create a dummy response message for the specified dummy record. + * + * @param record the dummy record for which to create a dummy response + * @returns outbound message containing dummy response + */ + public async createResponse(agentContext: AgentContext, record: DummyRecord) { + const responseMessage = new DummyResponseMessage({ + threadId: record.threadId, + }) + + return responseMessage + } + + /** + * Process a received {@link DummyRequestMessage}. + * + * @param messageContext The message context containing a dummy request message + * @returns dummy record associated with the dummy request message + * + */ + public async processRequest(messageContext: InboundMessageContext) { + const connectionRecord = messageContext.assertReadyConnection() + + // Create record + const record = new DummyRecord({ + connectionId: connectionRecord.id, + threadId: messageContext.message.threadId, + state: DummyState.RequestReceived, + }) + + await this.dummyRepository.save(messageContext.agentContext, record) + + this.emitStateChangedEvent(messageContext.agentContext, record, null) + + if (this.dummyModuleConfig.autoAcceptRequests) { + return await this.createResponse(messageContext.agentContext, record) + } + } + + /** + * Process a received {@link DummyResponseMessage}. + * + * @param messageContext The message context containing a dummy response message + * @returns dummy record associated with the dummy response message + * + */ + public async processResponse(messageContext: InboundMessageContext) { + const { message } = messageContext + + const connection = messageContext.assertReadyConnection() + + // Dummy record already exists + const record = await this.findByThreadAndConnectionId(messageContext.agentContext, message.threadId, connection.id) + + if (record) { + // Check current state + record.assertState(DummyState.RequestSent) + + await this.updateState(messageContext.agentContext, record, DummyState.ResponseReceived) + } else { + throw new Error(`Dummy record not found with threadId ${message.threadId}`) + } + + return record + } + + /** + * Retrieve all dummy records + * + * @returns List containing all dummy records + */ + public getAll(agentContext: AgentContext): Promise { + return this.dummyRepository.getAll(agentContext) + } + + /** + * Retrieve dummy records by query + * + * @returns List containing all dummy records matching query + */ + public findAllByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ): Promise { + return this.dummyRepository.findByQuery(agentContext, query, queryOptions) + } + + /** + * Retrieve a dummy record by id + * + * @param dummyRecordId The dummy record id + * @throws {RecordNotFoundError} If no record is found + * @return The dummy record + * + */ + public getById(agentContext: AgentContext, dummyRecordId: string): Promise { + return this.dummyRepository.getById(agentContext, dummyRecordId) + } + + /** + * Retrieve a dummy record by connection id and thread id + * + * @param connectionId The connection id + * @param threadId The thread id + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * @returns The dummy record + */ + public async findByThreadAndConnectionId( + agentContext: AgentContext, + threadId: string, + connectionId?: string + ): Promise { + return this.dummyRepository.findSingleByQuery(agentContext, { threadId, connectionId }) + } + + /** + * Update the record to a new state and emit an state changed event. Also updates the record + * in storage. + * + * @param dummyRecord The record to update the state for + * @param newState The state to update to + * + */ + public async updateState(agentContext: AgentContext, dummyRecord: DummyRecord, newState: DummyState) { + const previousState = dummyRecord.state + dummyRecord.state = newState + await this.dummyRepository.update(agentContext, dummyRecord) + + this.emitStateChangedEvent(agentContext, dummyRecord, previousState) + } + + private emitStateChangedEvent( + agentContext: AgentContext, + dummyRecord: DummyRecord, + previousState: DummyState | null + ) { + this.eventEmitter.emit(agentContext, { + type: DummyEventTypes.StateChanged, + payload: { + // we need to clone the dummy record to avoid mutating records after they're emitted in an event + dummyRecord: dummyRecord.clone(), + previousState: previousState, + }, + }) + } +} diff --git a/samples/extension-module/dummy/services/index.ts b/samples/extension-module/dummy/services/index.ts new file mode 100644 index 0000000000..05bcbc5d0a --- /dev/null +++ b/samples/extension-module/dummy/services/index.ts @@ -0,0 +1,2 @@ +export * from './DummyService' +export * from './DummyEvents' diff --git a/samples/extension-module/jest.config.ts b/samples/extension-module/jest.config.ts new file mode 100644 index 0000000000..93c0197296 --- /dev/null +++ b/samples/extension-module/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/samples/extension-module/package.json b/samples/extension-module/package.json new file mode 100644 index 0000000000..259750d1f2 --- /dev/null +++ b/samples/extension-module/package.json @@ -0,0 +1,29 @@ +{ + "name": "credo-extension-module-sample", + "version": "1.0.0", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "samples/extension-module/" + }, + "license": "Apache-2.0", + "scripts": { + "requester": "ts-node requester.ts", + "responder": "ts-node responder.ts" + }, + "devDependencies": { + "ts-node": "^10.4.0", + "@types/express": "^4.17.13", + "@types/uuid": "^9.0.1", + "@types/ws": "^8.5.4" + }, + "dependencies": { + "@credo-ts/core": "workspace:*", + "@credo-ts/node": "workspace:*", + "@credo-ts/askar": "workspace:*", + "class-validator": "0.14.1", + "rxjs": "^7.8.0", + "@hyperledger/aries-askar-nodejs": "^0.2.1" + } +} diff --git a/samples/extension-module/requester.ts b/samples/extension-module/requester.ts new file mode 100644 index 0000000000..a2e4d29b9f --- /dev/null +++ b/samples/extension-module/requester.ts @@ -0,0 +1,82 @@ +import type { DummyRecord, DummyStateChangedEvent } from './dummy' + +import { + HttpOutboundTransport, + Agent, + CredoError, + ConsoleLogger, + LogLevel, + WsOutboundTransport, + ConnectionsModule, +} from '@credo-ts/core' +import { agentDependencies } from '@credo-ts/node' +import { filter, first, firstValueFrom, map, ReplaySubject, timeout } from 'rxjs' + +import { DummyEventTypes, DummyState, DummyModule } from './dummy' + +const run = async () => { + // Create transports + const port = process.env.RESPONDER_PORT ? Number(process.env.RESPONDER_PORT) : 3002 + const wsOutboundTransport = new WsOutboundTransport() + const httpOutboundTransport = new HttpOutboundTransport() + + // Setup the agent + const agent = new Agent({ + config: { + label: 'Dummy-powered agent - requester', + walletConfig: { + id: 'requester', + key: 'requester', + }, + logger: new ConsoleLogger(LogLevel.info), + }, + modules: { + dummy: new DummyModule(), + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + }, + dependencies: agentDependencies, + }) + + // Register transports + agent.registerOutboundTransport(wsOutboundTransport) + agent.registerOutboundTransport(httpOutboundTransport) + + // Now agent will handle messages and events from Dummy protocol + + //Initialize the agent + await agent.initialize() + + // Connect to responder using its invitation endpoint + const invitationUrl = await (await agentDependencies.fetch(`http://localhost:${port}/invitation`)).text() + const { connectionRecord } = await agent.oob.receiveInvitationFromUrl(invitationUrl) + if (!connectionRecord) { + throw new CredoError('Connection record for out-of-band invitation was not created.') + } + await agent.connections.returnWhenIsConnected(connectionRecord.id) + + // Create observable for Response Received event + const observable = agent.events.observable(DummyEventTypes.StateChanged) + const subject = new ReplaySubject(1) + + observable + .pipe( + filter((event: DummyStateChangedEvent) => event.payload.dummyRecord.state === DummyState.ResponseReceived), + map((e) => e.payload.dummyRecord), + first(), + timeout(5000) + ) + .subscribe(subject) + + // Send a dummy request and wait for response + const record = await agent.modules.dummy.request(connectionRecord.id) + agent.config.logger.info(`Request sent for Dummy Record: ${record.id}`) + + const dummyRecord = await firstValueFrom(subject) + agent.config.logger.info(`Response received for Dummy Record: ${dummyRecord.id}`) + + await agent.shutdown() +} + +void run() diff --git a/samples/extension-module/responder.ts b/samples/extension-module/responder.ts new file mode 100644 index 0000000000..f1b3e655d5 --- /dev/null +++ b/samples/extension-module/responder.ts @@ -0,0 +1,75 @@ +import type { DummyStateChangedEvent } from './dummy' +import type { Socket } from 'net' + +import { Agent, ConnectionsModule, ConsoleLogger, LogLevel } from '@credo-ts/core' +import { agentDependencies, HttpInboundTransport, WsInboundTransport } from '@credo-ts/node' +import express from 'express' +import { Server } from 'ws' + +import { DummyModule, DummyEventTypes, DummyState } from './dummy' + +const run = async () => { + // Create transports + const port = process.env.RESPONDER_PORT ? Number(process.env.RESPONDER_PORT) : 3002 + const autoAcceptRequests = true + const app = express() + const socketServer = new Server({ noServer: true }) + + const httpInboundTransport = new HttpInboundTransport({ app, port }) + const wsInboundTransport = new WsInboundTransport({ server: socketServer }) + + // Setup the agent + const agent = new Agent({ + config: { + label: 'Dummy-powered agent - responder', + endpoints: [`http://localhost:${port}`], + walletConfig: { + id: 'responder', + key: 'responder', + }, + logger: new ConsoleLogger(LogLevel.debug), + }, + modules: { + dummy: new DummyModule({ autoAcceptRequests }), + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + }, + dependencies: agentDependencies, + }) + + // Register transports + agent.registerInboundTransport(httpInboundTransport) + agent.registerInboundTransport(wsInboundTransport) + + // Allow to create invitation, no other way to ask for invitation yet + app.get('/invitation', async (req, res) => { + const { outOfBandInvitation } = await agent.oob.createInvitation() + res.send(outOfBandInvitation.toUrl({ domain: `http://localhost:${port}/invitation` })) + }) + + // Now agent will handle messages and events from Dummy protocol + + //Initialize the agent + await agent.initialize() + + httpInboundTransport.server?.on('upgrade', (request, socket, head) => { + socketServer.handleUpgrade(request, socket as Socket, head, (socket) => { + socketServer.emit('connection', socket, request) + }) + }) + + // If autoAcceptRequests is enabled, the handler will automatically respond + // (no need to subscribe to event and manually accept) + if (!autoAcceptRequests) { + agent.events.on(DummyEventTypes.StateChanged, async (event: DummyStateChangedEvent) => { + if (event.payload.dummyRecord.state === DummyState.RequestReceived) { + await agent.modules.dummy.respond(event.payload.dummyRecord.id) + } + }) + } + + agent.config.logger.info(`Responder listening to port ${port}`) +} + +void run() diff --git a/samples/extension-module/tests/dummy.test.ts b/samples/extension-module/tests/dummy.test.ts new file mode 100644 index 0000000000..50458e9044 --- /dev/null +++ b/samples/extension-module/tests/dummy.test.ts @@ -0,0 +1,102 @@ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' +import type { ConnectionRecord } from '@credo-ts/core' + +import { AskarModule } from '@credo-ts/askar' +import { Agent } from '@credo-ts/core' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import { Subject } from 'rxjs' + +import { getAgentOptions, makeConnection } from '../../../packages/core/tests/helpers' +import testLogger from '../../../packages/core/tests/logger' +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { DummyModule } from '../dummy/DummyModule' +import { DummyState } from '../dummy/repository' + +import { waitForDummyRecord } from './helpers' + +const modules = { + dummy: new DummyModule(), + askar: new AskarModule({ + ariesAskar, + }), +} + +const bobAgentOptions = getAgentOptions( + 'Bob Dummy', + { + endpoints: ['rxjs:bob'], + }, + modules +) + +const aliceAgentOptions = getAgentOptions( + 'Alice Dummy', + { + endpoints: ['rxjs:alice'], + }, + modules +) + +describe('Dummy extension module test', () => { + let bobAgent: Agent + let aliceAgent: Agent + let aliceConnection: ConnectionRecord + + beforeEach(async () => { + const bobMessages = new Subject() + const aliceMessages = new Subject() + const subjectMap = { + 'rxjs:bob': bobMessages, + 'rxjs:alice': aliceMessages, + } + + bobAgent = new Agent(bobAgentOptions) + bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages)) + bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await bobAgent.initialize() + + aliceAgent = new Agent(aliceAgentOptions) + + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + ;[aliceConnection] = await makeConnection(aliceAgent, bobAgent) + }) + + afterEach(async () => { + await bobAgent.shutdown() + await bobAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice sends a request and Bob answers', async () => { + testLogger.test('Alice sends request to Bob') + let aliceDummyRecord = await aliceAgent.modules.dummy.request(aliceConnection.id) + + testLogger.test('Bob waits for request from Alice') + const bobDummyRecord = await waitForDummyRecord(bobAgent, { + threadId: aliceDummyRecord.threadId, + state: DummyState.RequestReceived, + }) + + testLogger.test('Bob sends response to Alice') + await bobAgent.modules.dummy.respond(bobDummyRecord.id) + + testLogger.test('Alice waits until Bob responds') + aliceDummyRecord = await waitForDummyRecord(aliceAgent, { + threadId: aliceDummyRecord.threadId, + state: DummyState.ResponseReceived, + }) + + const retrievedRecord = (await aliceAgent.modules.dummy.getAll()).find((item) => item.id === aliceDummyRecord.id) + expect(retrievedRecord).toMatchObject( + expect.objectContaining({ + id: aliceDummyRecord.id, + threadId: aliceDummyRecord.threadId, + state: DummyState.ResponseReceived, + }) + ) + }) +}) diff --git a/samples/extension-module/tests/helpers.ts b/samples/extension-module/tests/helpers.ts new file mode 100644 index 0000000000..8372fbbea3 --- /dev/null +++ b/samples/extension-module/tests/helpers.ts @@ -0,0 +1,57 @@ +import type { DummyState } from '../dummy/repository' +import type { DummyStateChangedEvent } from '../dummy/services' +import type { Agent } from '@credo-ts/core' +import type { Observable } from 'rxjs' + +import { catchError, filter, firstValueFrom, map, ReplaySubject, timeout } from 'rxjs' + +import { DummyEventTypes } from '../dummy/services' + +export async function waitForDummyRecord( + agent: Agent, + options: { + threadId?: string + state?: DummyState + previousState?: DummyState | null + timeoutMs?: number + } +) { + const observable = agent.events.observable(DummyEventTypes.StateChanged) + + return waitForDummyRecordSubject(observable, options) +} + +export function waitForDummyRecordSubject( + subject: ReplaySubject | Observable, + { + threadId, + state, + previousState, + timeoutMs = 10000, + }: { + threadId?: string + state?: DummyState + previousState?: DummyState | null + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( + observable.pipe( + filter((e) => previousState === undefined || e.payload.previousState === previousState), + filter((e) => threadId === undefined || e.payload.dummyRecord.threadId === threadId), + filter((e) => state === undefined || e.payload.dummyRecord.state === state), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `DummyStateChangedEvent event not emitted within specified timeout: { + previousState: ${previousState}, + threadId: ${threadId}, + state: ${state} + }` + ) + }), + map((e) => e.payload.dummyRecord) + ) + ) +} diff --git a/samples/extension-module/tests/setup.ts b/samples/extension-module/tests/setup.ts new file mode 100644 index 0000000000..34e38c9705 --- /dev/null +++ b/samples/extension-module/tests/setup.ts @@ -0,0 +1 @@ +jest.setTimeout(120000) diff --git a/samples/extension-module/tsconfig.json b/samples/extension-module/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/samples/extension-module/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/samples/mediator.ts b/samples/mediator.ts new file mode 100644 index 0000000000..9e5c967099 --- /dev/null +++ b/samples/mediator.ts @@ -0,0 +1,110 @@ +/** + * This file contains a sample mediator. The mediator supports both + * HTTP and WebSockets for communication and will automatically accept + * incoming mediation requests. + * + * You can get an invitation by going to '/invitation', which by default is + * http://localhost:3001/invitation + * + * To connect to the mediator from another agent, you can set the + * 'mediatorConnectionsInvite' parameter in the agent config to the + * url that is returned by the '/invitation/ endpoint. This will connect + * to the mediator, request mediation and set the mediator as default. + */ + +import type { InitConfig } from '@credo-ts/core' +import type { Socket } from 'net' + +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import express from 'express' +import { Server } from 'ws' + +import { TestLogger } from '../packages/core/tests/logger' + +import { AskarModule } from '@credo-ts/askar' +import { + ConnectionsModule, + MediatorModule, + HttpOutboundTransport, + Agent, + ConnectionInvitationMessage, + LogLevel, + WsOutboundTransport, +} from '@credo-ts/core' +import { HttpInboundTransport, agentDependencies, WsInboundTransport } from '@credo-ts/node' + +const port = process.env.AGENT_PORT ? Number(process.env.AGENT_PORT) : 3001 + +// We create our own instance of express here. This is not required +// but allows use to use the same server (and port) for both WebSockets and HTTP +const app = express() +const socketServer = new Server({ noServer: true }) + +const endpoints = process.env.AGENT_ENDPOINTS?.split(',') ?? [`http://localhost:${port}`, `ws://localhost:${port}`] + +const logger = new TestLogger(LogLevel.info) + +const agentConfig: InitConfig = { + endpoints, + label: process.env.AGENT_LABEL || 'Credo Mediator', + walletConfig: { + id: process.env.WALLET_NAME || 'Credo', + key: process.env.WALLET_KEY || 'Credo', + }, + + logger, +} + +// Set up agent +const agent = new Agent({ + config: agentConfig, + dependencies: agentDependencies, + modules: { + askar: new AskarModule({ ariesAskar }), + mediator: new MediatorModule({ + autoAcceptMediationRequests: true, + }), + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + }, +}) +const config = agent.config + +// Create all transports +const httpInboundTransport = new HttpInboundTransport({ app, port }) +const httpOutboundTransport = new HttpOutboundTransport() +const wsInboundTransport = new WsInboundTransport({ server: socketServer }) +const wsOutboundTransport = new WsOutboundTransport() + +// Register all Transports +agent.registerInboundTransport(httpInboundTransport) +agent.registerOutboundTransport(httpOutboundTransport) +agent.registerInboundTransport(wsInboundTransport) +agent.registerOutboundTransport(wsOutboundTransport) + +// Allow to create invitation, no other way to ask for invitation yet +httpInboundTransport.app.get('/invitation', async (req, res) => { + if (typeof req.query.c_i === 'string') { + const invitation = ConnectionInvitationMessage.fromUrl(req.url) + res.send(invitation.toJSON()) + } else { + const { outOfBandInvitation } = await agent.oob.createInvitation() + const httpEndpoint = config.endpoints.find((e) => e.startsWith('http')) + res.send(outOfBandInvitation.toUrl({ domain: httpEndpoint + '/invitation' })) + } +}) + +const run = async () => { + await agent.initialize() + + // When an 'upgrade' to WS is made on our http server, we forward the + // request to the WS server + httpInboundTransport.server?.on('upgrade', (request, socket, head) => { + socketServer.handleUpgrade(request, socket as Socket, head, (socket) => { + socketServer.emit('connection', socket, request) + }) + }) +} + +void run() diff --git a/samples/tails/.gitignore b/samples/tails/.gitignore new file mode 100644 index 0000000000..6c4291916e --- /dev/null +++ b/samples/tails/.gitignore @@ -0,0 +1 @@ +tails \ No newline at end of file diff --git a/samples/tails/FullTailsFileService.ts b/samples/tails/FullTailsFileService.ts new file mode 100644 index 0000000000..9edc2a9f18 --- /dev/null +++ b/samples/tails/FullTailsFileService.ts @@ -0,0 +1,41 @@ +import type { AnonCredsRevocationRegistryDefinition } from '@credo-ts/anoncreds' +import type { AgentContext } from '@credo-ts/core' + +import { BasicTailsFileService } from '@credo-ts/anoncreds' +import { utils } from '@credo-ts/core' +import FormData from 'form-data' +import fs from 'fs' + +export class FullTailsFileService extends BasicTailsFileService { + private tailsServerBaseUrl?: string + public constructor(options?: { tailsDirectoryPath?: string; tailsServerBaseUrl?: string }) { + super(options) + this.tailsServerBaseUrl = options?.tailsServerBaseUrl + } + + public async uploadTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ) { + const revocationRegistryDefinition = options.revocationRegistryDefinition + const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation + + const tailsFileId = utils.uuid() + const data = new FormData() + const readStream = fs.createReadStream(localTailsFilePath) + data.append('file', readStream) + const response = await agentContext.config.agentDependencies.fetch( + `${this.tailsServerBaseUrl}/${encodeURIComponent(tailsFileId)}`, + { + method: 'PUT', + body: data, + } + ) + if (response.status !== 200) { + throw new Error('Cannot upload tails file') + } + return { tailsFileUrl: `${this.tailsServerBaseUrl}/${encodeURIComponent(tailsFileId)}` } + } +} diff --git a/samples/tails/README.md b/samples/tails/README.md new file mode 100644 index 0000000000..838d207160 --- /dev/null +++ b/samples/tails/README.md @@ -0,0 +1,5 @@ +

Sample tails file server

+ +This is a very simple server that can be used to host AnonCreds tails files. It is intended to be used only for development purposes. + +It offers a single endpoint at the root that takes an URI-encoded `tailsFileId` as URL path and allows to upload (using PUT method and a through a multi-part encoded form) or retrieve a tails file (using GET method). diff --git a/samples/tails/package.json b/samples/tails/package.json new file mode 100644 index 0000000000..c23828be26 --- /dev/null +++ b/samples/tails/package.json @@ -0,0 +1,27 @@ +{ + "name": "test-tails-file-server", + "version": "1.0.0", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/credo-ts", + "directory": "samples/tails/" + }, + "license": "Apache-2.0", + "scripts": { + "start": "ts-node server.ts" + }, + "devDependencies": { + "ts-node": "^10.4.0" + }, + "dependencies": { + "@credo-ts/anoncreds": "workspace:*", + "@credo-ts/core": "workspace:*", + "@types/express": "^4.17.13", + "@types/multer": "^1.4.7", + "@types/uuid": "^9.0.1", + "@types/ws": "^8.5.4", + "form-data": "^4.0.0", + "multer": "^1.4.5-lts.1" + } +} diff --git a/samples/tails/server.ts b/samples/tails/server.ts new file mode 100644 index 0000000000..4406586958 --- /dev/null +++ b/samples/tails/server.ts @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ConsoleLogger, LogLevel } from '@credo-ts/core' +import { createHash } from 'crypto' +import express from 'express' +import fs from 'fs' +import multer, { diskStorage } from 'multer' + +const port = process.env.AGENT_PORT ? Number(process.env.AGENT_PORT) : 3001 +const app = express() + +const baseFilePath = './tails' +const indexFilePath = `./${baseFilePath}/index.json` + +if (!fs.existsSync(baseFilePath)) { + fs.mkdirSync(baseFilePath, { recursive: true }) +} +const tailsIndex = ( + fs.existsSync(indexFilePath) ? JSON.parse(fs.readFileSync(indexFilePath, { encoding: 'utf-8' })) : {} +) as Record + +const logger = new ConsoleLogger(LogLevel.debug) + +function fileHash(filePath: string, algorithm = 'sha256') { + return new Promise((resolve, reject) => { + const shasum = createHash(algorithm) + try { + const s = fs.createReadStream(filePath) + s.on('data', function (data) { + shasum.update(data) + }) + // making digest + s.on('end', function () { + const hash = shasum.digest('hex') + return resolve(hash) + }) + } catch (error) { + return reject('error in calculation') + } + }) +} + +const fileStorage = diskStorage({ + filename: (req: any, file: { originalname: string }, cb: (arg0: null, arg1: string) => void) => { + cb(null, file.originalname + '-' + new Date().toISOString()) + }, +}) + +// Allow to create invitation, no other way to ask for invitation yet +app.get('/:tailsFileId', async (req, res) => { + logger.debug(`requested file`) + + const tailsFileId = req.params.tailsFileId + if (!tailsFileId) { + res.status(409).end() + return + } + + const fileName = tailsIndex[tailsFileId] + + if (!fileName) { + logger.debug(`no entry found for tailsFileId: ${tailsFileId}`) + res.status(404).end() + return + } + + const path = `${baseFilePath}/${fileName}` + try { + logger.debug(`reading file: ${path}`) + + if (!fs.existsSync(path)) { + logger.debug(`file not found: ${path}`) + res.status(404).end() + return + } + + const file = fs.createReadStream(path) + res.setHeader('Content-Disposition', `attachment: filename="${fileName}"`) + file.pipe(res) + } catch (error) { + logger.debug(`error reading file: ${path}`) + res.status(500).end() + } +}) + +app.put('/:tailsFileId', multer({ storage: fileStorage }).single('file'), async (req, res) => { + logger.info(`tails file upload: ${req.params.tailsFileId}`) + + const file = req.file + + if (!file) { + logger.info(`No file found: ${JSON.stringify(req.headers)}`) + return res.status(400).send('No files were uploaded.') + } + + const tailsFileId = req.params.tailsFileId + if (!tailsFileId) { + // Clean up temporary file + fs.rmSync(file.path) + return res.status(409).send('Missing tailsFileId') + } + + const item = tailsIndex[tailsFileId] + + if (item) { + logger.debug(`there is already an entry for: ${tailsFileId}`) + res.status(409).end() + return + } + + const hash = await fileHash(file.path) + const destinationPath = `${baseFilePath}/${hash}` + + if (fs.existsSync(destinationPath)) { + logger.warn('tails file already exists') + } else { + fs.copyFileSync(file.path, destinationPath) + fs.rmSync(file.path) + } + + // Store filename in index + tailsIndex[tailsFileId] = hash + fs.writeFileSync(indexFilePath, JSON.stringify(tailsIndex)) + + res.status(200).end() +}) + +const run = async () => { + app.listen(port) + logger.info(`server started at port ${port}`) +} + +void run() diff --git a/samples/tails/tsconfig.json b/samples/tails/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/samples/tails/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/scripts/ngrok-wait.sh b/scripts/ngrok-wait.sh deleted file mode 100755 index 849e6ab4a1..0000000000 --- a/scripts/ngrok-wait.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -NGROK_NAME=${NGROK_NAME:-ngrok} - -echo "ngrok end point [$NGROK_NAME]" - -ENDPOINT=null -while [ -z "$ENDPOINT" ] || [ "$ENDPOINT" = "null" ]; do - echo "Fetching end point from ngrok service" - ENDPOINT=$(curl -s $NGROK_NAME:4040/api/tunnels/command_line | grep -o "https:\/\/.*\.ngrok\.io") - - if [ -z "$ENDPOINT" ] || [ "$ENDPOINT" = "null" ]; then - echo "ngrok not ready, sleeping 5 seconds...." - sleep 5 - fi -done - -echo "fetched end point [$ENDPOINT]" - -export AGENT_ENDPOINT=$ENDPOINT -exec "$@" \ No newline at end of file diff --git a/scripts/run-mediator.sh b/scripts/run-mediator.sh deleted file mode 100755 index 7ffd39003e..0000000000 --- a/scripts/run-mediator.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -AGENT="$1" -YARN_COMMAND=yarn - - -if [[ "$AGENT" = "mediator01" ]] || [[ "$AGENT" = "alice" ]]; then - AGENT_ENDPOINT="${AGENT_ENDPOINT:-http://localhost:3001}" - AGENT_HOST=http://localhost - AGENT_PORT=3001 - AGENT_LABEL=RoutingMediator01 - WALLET_NAME=mediator01 - WALLET_KEY=0000000000000000000000000Mediator01 - PUBLIC_DID=DtWRdd6C5dN5vpcN6XRAvu - PUBLIC_DID_SEED=00000000000000000000000Forward01 -elif [[ "$AGENT" = "mediator02" ]] || [[ "$AGENT" = "bob" ]]; then - AGENT_ENDPOINT="${AGENT_ENDPOINT:-http://localhost:3002}" - AGENT_HOST=http://localhost - AGENT_PORT=3002 - AGENT_LABEL=RoutingMediator02 - WALLET_NAME=mediator02 - WALLET_KEY=0000000000000000000000000Mediator02 - PUBLIC_DID=SHbU5SEwdmkQkVQ1sMwSEv - PUBLIC_DID_SEED=00000000000000000000000Forward02 -else - echo "Please specify which agent you want to run. Choose from 'alice' or 'bob'." - exit 1 -fi - -if [ "$2" = "server" ]; then - YARN_COMMAND=.yarn/bin/yarn -fi - -# Docker image already compiles. Not needed to do again -if [ "$RUN_MODE" != "docker" ]; then - ${YARN_COMMAND} prod:build -fi - -AGENT_ENDPOINT=${AGENT_ENDPOINT} AGENT_HOST=${AGENT_HOST} AGENT_PORT=${AGENT_PORT} AGENT_LABEL=${AGENT_LABEL} WALLET_NAME=${WALLET_NAME} WALLET_KEY=${WALLET_KEY} PUBLIC_DID=${PUBLIC_DID} PUBLIC_DID_SEED=${PUBLIC_DID_SEED} ${YARN_COMMAND} prod:start diff --git a/src/lib/__tests__/agents.test.ts b/src/lib/__tests__/agents.test.ts deleted file mode 100644 index 9630dfe610..0000000000 --- a/src/lib/__tests__/agents.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Subject } from 'rxjs'; -import { Agent } from '..'; -import { - toBeConnectedWith, - SubjectInboundTransporter, - SubjectOutboundTransporter, - waitForBasicMessage, -} from './helpers'; -import { InitConfig } from '../types'; -import indy from 'indy-sdk'; -import { ConnectionRecord } from '../modules/connections'; -import testLogger from './logger'; - -expect.extend({ toBeConnectedWith }); - -const aliceConfig: InitConfig = { - label: 'Alice', - walletConfig: { id: 'alice' }, - walletCredentials: { key: '00000000000000000000000000000Test01' }, - autoAcceptConnections: true, - logger: testLogger, - indy, -}; - -const bobConfig: InitConfig = { - label: 'Bob', - walletConfig: { id: 'bob' }, - walletCredentials: { key: '00000000000000000000000000000Test02' }, - autoAcceptConnections: true, - logger: testLogger, - indy, -}; - -describe('agents', () => { - let aliceAgent: Agent; - let bobAgent: Agent; - let aliceConnection: ConnectionRecord; - let bobConnection: ConnectionRecord; - - afterAll(async () => { - await aliceAgent.closeAndDeleteWallet(); - await bobAgent.closeAndDeleteWallet(); - }); - - test('make a connection between agents', async () => { - const aliceMessages = new Subject(); - const bobMessages = new Subject(); - - const aliceAgentInbound = new SubjectInboundTransporter(aliceMessages); - const aliceAgentOutbound = new SubjectOutboundTransporter(bobMessages); - const bobAgentInbound = new SubjectInboundTransporter(bobMessages); - const bobAgentOutbound = new SubjectOutboundTransporter(aliceMessages); - - aliceAgent = new Agent(aliceConfig, aliceAgentInbound, aliceAgentOutbound); - await aliceAgent.init(); - - bobAgent = new Agent(bobConfig, bobAgentInbound, bobAgentOutbound); - await bobAgent.init(); - - const aliceConnectionAtAliceBob = await aliceAgent.connections.createConnection(); - const bobConnectionAtBobAlice = await bobAgent.connections.receiveInvitation(aliceConnectionAtAliceBob.invitation); - - aliceConnection = await aliceAgent.connections.returnWhenIsConnected(aliceConnectionAtAliceBob.connectionRecord.id); - bobConnection = await bobAgent.connections.returnWhenIsConnected(bobConnectionAtBobAlice.id); - - expect(aliceConnection).toBeConnectedWith(bobConnection); - expect(bobConnection).toBeConnectedWith(aliceConnection); - }); - - test('send a message to connection', async () => { - const message = 'hello, world'; - await aliceAgent.basicMessages.sendMessage(aliceConnection, message); - - const basicMessage = await waitForBasicMessage(bobAgent, { - content: message, - }); - - expect(basicMessage.content).toBe(message); - }); -}); diff --git a/src/lib/__tests__/credentials.test.ts b/src/lib/__tests__/credentials.test.ts deleted file mode 100644 index adbfcea8c1..0000000000 --- a/src/lib/__tests__/credentials.test.ts +++ /dev/null @@ -1,330 +0,0 @@ -/* eslint-disable no-console */ -import indy from 'indy-sdk'; -import type { CredDefId } from 'indy-sdk'; -import { Subject } from 'rxjs'; -import { Agent, ConnectionRecord } from '..'; -import { - ensurePublicDidIsOnLedger, - makeConnection, - registerDefinition, - registerSchema, - SubjectInboundTransporter, - SubjectOutboundTransporter, - waitForCredentialRecord, - genesisPath, -} from './helpers'; -import { - CredentialRecord, - CredentialState, - CredentialPreview, - CredentialPreviewAttribute, -} from '../modules/credentials'; -import { InitConfig } from '../types'; - -import testLogger from './logger'; - -const faberConfig: InitConfig = { - label: 'Faber', - walletConfig: { id: 'credentials-test-faber' }, - walletCredentials: { key: '00000000000000000000000000000Test01' }, - publicDidSeed: process.env.TEST_AGENT_PUBLIC_DID_SEED, - autoAcceptConnections: true, - genesisPath, - poolName: 'credentials-test-faber-pool', - indy, - logger: testLogger, -}; - -const aliceConfig: InitConfig = { - label: 'Alice', - walletConfig: { id: 'credentials-test-alice' }, - walletCredentials: { key: '00000000000000000000000000000Test01' }, - autoAcceptConnections: true, - genesisPath, - poolName: 'credentials-test-alice-pool', - indy, - logger: testLogger, -}; - -const credentialPreview = new CredentialPreview({ - attributes: [ - new CredentialPreviewAttribute({ - name: 'name', - mimeType: 'text/plain', - value: 'John', - }), - new CredentialPreviewAttribute({ - name: 'age', - mimeType: 'text/plain', - value: '99', - }), - ], -}); - -describe('credentials', () => { - let faberAgent: Agent; - let aliceAgent: Agent; - let credDefId: CredDefId; - let faberConnection: ConnectionRecord; - let aliceConnection: ConnectionRecord; - let faberCredentialRecord: CredentialRecord; - let aliceCredentialRecord: CredentialRecord; - - beforeAll(async () => { - const faberMessages = new Subject(); - const aliceMessages = new Subject(); - - const faberAgentInbound = new SubjectInboundTransporter(faberMessages); - const faberAgentOutbound = new SubjectOutboundTransporter(aliceMessages); - const aliceAgentInbound = new SubjectInboundTransporter(aliceMessages); - const aliceAgentOutbound = new SubjectOutboundTransporter(faberMessages); - - faberAgent = new Agent(faberConfig, faberAgentInbound, faberAgentOutbound); - aliceAgent = new Agent(aliceConfig, aliceAgentInbound, aliceAgentOutbound); - await faberAgent.init(); - await aliceAgent.init(); - - const schemaTemplate = { - name: `test-schema-${Date.now()}`, - attributes: ['name', 'age'], - version: '1.0', - }; - const [, ledgerSchema] = await registerSchema(faberAgent, schemaTemplate); - - const definitionTemplate = { - schema: ledgerSchema, - tag: 'TAG', - signatureType: 'CL', - config: { supportRevocation: false }, - }; - const [ledgerCredDefId] = await registerDefinition(faberAgent, definitionTemplate); - credDefId = ledgerCredDefId; - - const publicDid = faberAgent.publicDid?.did; - await ensurePublicDidIsOnLedger(faberAgent, publicDid!); - const { agentAConnection, agentBConnection } = await makeConnection(faberAgent, aliceAgent); - faberConnection = agentAConnection; - aliceConnection = agentBConnection; - }); - - afterAll(async () => { - await faberAgent.closeAndDeleteWallet(); - await aliceAgent.closeAndDeleteWallet(); - }); - - test('Alice starts with credential proposal to Faber', async () => { - testLogger.test('Alice sends credential proposal to Faber'); - let aliceCredentialRecord = await aliceAgent.credentials.proposeCredential(aliceConnection.id, { - credentialProposal: credentialPreview, - credentialDefinitionId: credDefId, - }); - - testLogger.test('Faber waits for credential proposal from Alice'); - let faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: aliceCredentialRecord.tags.threadId, - state: CredentialState.ProposalReceived, - }); - - testLogger.test('Faber sends credential offer to Alice'); - faberCredentialRecord = await faberAgent.credentials.acceptProposal(faberCredentialRecord.id, { - comment: 'some comment about credential', - }); - - testLogger.test('Alice waits for credential offer from Faber'); - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.tags.threadId, - state: CredentialState.OfferReceived, - }); - - // FIXME: expect below expects json, so we do a refetch because the current - // returns class instance - aliceCredentialRecord = await aliceAgent.credentials.getById(aliceCredentialRecord.id); - - expect(aliceCredentialRecord).toMatchObject({ - createdAt: expect.any(Number), - id: expect.any(String), - offerMessage: { - '@id': expect.any(String), - '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/offer-credential', - comment: 'some comment about credential', - credential_preview: { - '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/credential-preview', - attributes: [ - { - name: 'name', - 'mime-type': 'text/plain', - value: 'John', - }, - { - name: 'age', - 'mime-type': 'text/plain', - value: '99', - }, - ], - }, - 'offers~attach': expect.any(Array), - }, - tags: { threadId: faberCredentialRecord.tags.threadId }, - type: CredentialRecord.name, - state: CredentialState.OfferReceived, - }); - - testLogger.test('Alice sends credential request to Faber'); - aliceCredentialRecord = await aliceAgent.credentials.acceptOffer(aliceCredentialRecord.id); - - testLogger.test('Faber waits for credential request from Alice'); - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: aliceCredentialRecord.tags.threadId, - state: CredentialState.RequestReceived, - }); - - testLogger.test('Faber sends credential to Alice'); - faberCredentialRecord = await faberAgent.credentials.acceptRequest(faberCredentialRecord.id); - - testLogger.test('Alice waits for credential from Faber'); - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.tags.threadId, - state: CredentialState.CredentialReceived, - }); - - testLogger.test('Alice sends credential ack to Faber'); - aliceCredentialRecord = await aliceAgent.credentials.acceptCredential(aliceCredentialRecord.id); - - testLogger.test('Faber waits for credential ack from Alice'); - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: faberCredentialRecord.tags.threadId, - state: CredentialState.Done, - }); - - expect(aliceCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Number), - tags: { - threadId: expect.any(String), - }, - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - requestMetadata: expect.any(Object), - credentialId: expect.any(String), - state: CredentialState.Done, - }); - - expect(faberCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Number), - tags: { - threadId: expect.any(String), - }, - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - requestMetadata: undefined, - credentialId: undefined, - state: CredentialState.Done, - }); - }); - - test('Faber starts with credential offer to Alice', async () => { - testLogger.test('Faber sends credential offer to Alice'); - faberCredentialRecord = await faberAgent.credentials.offerCredential(faberConnection.id, { - preview: credentialPreview, - credentialDefinitionId: credDefId, - comment: 'some comment about credential', - }); - - testLogger.test('Alice waits for credential offer from Faber'); - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.tags.threadId, - state: CredentialState.OfferReceived, - }); - - // FIXME: expect below expects json, so we do a refetch because the current - // returns class instance - aliceCredentialRecord = await aliceAgent.credentials.getById(aliceCredentialRecord.id); - - expect(aliceCredentialRecord).toMatchObject({ - createdAt: expect.any(Number), - id: expect.any(String), - offerMessage: { - '@id': expect.any(String), - '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/offer-credential', - comment: 'some comment about credential', - credential_preview: { - '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/credential-preview', - attributes: [ - { - name: 'name', - 'mime-type': 'text/plain', - value: 'John', - }, - { - name: 'age', - 'mime-type': 'text/plain', - value: '99', - }, - ], - }, - 'offers~attach': expect.any(Array), - }, - tags: { threadId: faberCredentialRecord.tags.threadId }, - type: CredentialRecord.name, - state: CredentialState.OfferReceived, - }); - - testLogger.test('Alice sends credential request to Faber'); - aliceCredentialRecord = await aliceAgent.credentials.acceptOffer(aliceCredentialRecord.id); - - testLogger.test('Faber waits for credential request from Alice'); - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: aliceCredentialRecord.tags.threadId, - state: CredentialState.RequestReceived, - }); - - testLogger.test('Faber sends credential to Alice'); - faberCredentialRecord = await faberAgent.credentials.acceptRequest(faberCredentialRecord.id); - - testLogger.test('Alice waits for credential from Faber'); - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.tags.threadId, - state: CredentialState.CredentialReceived, - }); - - testLogger.test('Alice sends credential ack to Faber'); - aliceCredentialRecord = await aliceAgent.credentials.acceptCredential(aliceCredentialRecord.id); - - testLogger.test('Faber waits for credential ack from Alice'); - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: faberCredentialRecord.tags.threadId, - state: CredentialState.Done, - }); - - expect(aliceCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Number), - tags: { - threadId: expect.any(String), - }, - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - requestMetadata: expect.any(Object), - credentialId: expect.any(String), - state: CredentialState.Done, - }); - - expect(faberCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Number), - tags: { - threadId: expect.any(String), - }, - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - requestMetadata: undefined, - credentialId: undefined, - state: CredentialState.Done, - }); - }); -}); diff --git a/src/lib/__tests__/helpers.ts b/src/lib/__tests__/helpers.ts deleted file mode 100644 index 9902d6a7be..0000000000 --- a/src/lib/__tests__/helpers.ts +++ /dev/null @@ -1,223 +0,0 @@ -/* eslint-disable no-console */ -import type { SchemaId, Schema, CredDefId, CredDef, Did } from 'indy-sdk'; -import path from 'path'; -import { Subject } from 'rxjs'; -import { Agent, InboundTransporter, OutboundTransporter } from '..'; -import { OutboundPackage, WireMessage } from '../types'; -import { ConnectionRecord } from '../modules/connections'; -import { ProofRecord, ProofState, ProofEventType, ProofStateChangedEvent } from '../modules/proofs'; -import { SchemaTemplate, CredDefTemplate } from '../modules/ledger'; -import { - CredentialRecord, - CredentialOfferTemplate, - CredentialEventType, - CredentialStateChangedEvent, - CredentialState, -} from '../modules/credentials'; -import { BasicMessage, BasicMessageEventType, BasicMessageReceivedEvent } from '../modules/basic-messages'; -import testLogger from './logger'; - -export const genesisPath = process.env.GENESIS_TXN_PATH - ? path.resolve(process.env.GENESIS_TXN_PATH) - : path.join(__dirname, '../../../network/genesis/local-genesis.txn'); - -export const sleep = (ms: number) => new Promise(res => setTimeout(res, ms)); - -// Custom matchers which can be used to extend Jest matchers via extend, e. g. `expect.extend({ toBeConnectedWith })`. - -export function toBeConnectedWith(received: ConnectionRecord, connection: ConnectionRecord) { - const pass = received.theirDid === connection.did && received.theirKey === connection.verkey; - if (pass) { - return { - message: () => - `expected connection ${received.did}, ${received.verkey} not to be connected to with ${connection.did}, ${connection.verkey}`, - pass: true, - }; - } else { - return { - message: () => - `expected connection ${received.did}, ${received.verkey} to be connected to with ${connection.did}, ${connection.verkey}`, - pass: false, - }; - } -} - -export async function waitForProofRecord( - agent: Agent, - { threadId, state, previousState }: { threadId?: string; state?: ProofState; previousState?: ProofState | null } -): Promise { - return new Promise(resolve => { - const listener = (event: ProofStateChangedEvent) => { - const previousStateMatches = previousState === undefined || event.previousState === previousState; - const threadIdMatches = threadId === undefined || event.proofRecord.tags.threadId === threadId; - const stateMatches = state === undefined || event.proofRecord.state === state; - - if (previousStateMatches && threadIdMatches && stateMatches) { - agent.proofs.events.removeListener(ProofEventType.StateChanged, listener); - - resolve(event.proofRecord); - } - }; - - agent.proofs.events.addListener(ProofEventType.StateChanged, listener); - }); -} - -export async function waitForCredentialRecord( - agent: Agent, - { - threadId, - state, - previousState, - }: { threadId?: string; state?: CredentialState; previousState?: CredentialState | null } -): Promise { - return new Promise(resolve => { - const listener = (event: CredentialStateChangedEvent) => { - const previousStateMatches = previousState === undefined || event.previousState === previousState; - const threadIdMatches = threadId === undefined || event.credentialRecord.tags.threadId === threadId; - const stateMatches = state === undefined || event.credentialRecord.state === state; - - if (previousStateMatches && threadIdMatches && stateMatches) { - agent.credentials.events.removeListener(CredentialEventType.StateChanged, listener); - - resolve(event.credentialRecord); - } - }; - - agent.credentials.events.addListener(CredentialEventType.StateChanged, listener); - }); -} - -export async function waitForBasicMessage( - agent: Agent, - { verkey, content }: { verkey?: string; content?: string } -): Promise { - return new Promise(resolve => { - const listener = (event: BasicMessageReceivedEvent) => { - const verkeyMatches = verkey === undefined || event.verkey === verkey; - const contentMatches = content === undefined || event.message.content === content; - - if (verkeyMatches && contentMatches) { - agent.basicMessages.events.removeListener(BasicMessageEventType.MessageReceived, listener); - - resolve(event.message); - } - }; - - agent.basicMessages.events.addListener(BasicMessageEventType.MessageReceived, listener); - }); -} - -export class SubjectInboundTransporter implements InboundTransporter { - private subject: Subject; - - public constructor(subject: Subject) { - this.subject = subject; - } - - public start(agent: Agent) { - this.subscribe(agent, this.subject); - } - - private subscribe(agent: Agent, subject: Subject) { - subject.subscribe({ - next: (message: WireMessage) => agent.receiveMessage(message), - }); - } -} - -export class SubjectOutboundTransporter implements OutboundTransporter { - private subject: Subject; - - public constructor(subject: Subject) { - this.subject = subject; - } - - public async sendMessage(outboundPackage: OutboundPackage) { - testLogger.test(`Sending outbound message to connection ${outboundPackage.connection.id}`); - const { payload } = outboundPackage; - this.subject.next(payload); - } -} - -export async function makeConnection(agentA: Agent, agentB: Agent) { - // eslint-disable-next-line prefer-const - let { invitation, connectionRecord: agentAConnection } = await agentA.connections.createConnection(); - let agentBConnection = await agentB.connections.receiveInvitation(invitation); - - agentAConnection = await agentA.connections.returnWhenIsConnected(agentAConnection.id); - agentBConnection = await agentB.connections.returnWhenIsConnected(agentBConnection.id); - - return { - agentAConnection, - agentBConnection, - }; -} - -export async function registerSchema(agent: Agent, schemaTemplate: SchemaTemplate): Promise<[SchemaId, Schema]> { - const [schemaId, ledgerSchema] = await agent.ledger.registerSchema(schemaTemplate); - testLogger.test(`created schema with id ${schemaId}`, ledgerSchema); - return [schemaId, ledgerSchema]; -} - -export async function registerDefinition( - agent: Agent, - definitionTemplate: CredDefTemplate -): Promise<[CredDefId, CredDef]> { - const [credDefId, ledgerCredDef] = await agent.ledger.registerCredentialDefinition(definitionTemplate); - testLogger.test(`created credential definition with id ${credDefId}`, ledgerCredDef); - return [credDefId, ledgerCredDef]; -} - -export async function ensurePublicDidIsOnLedger(agent: Agent, publicDid: Did) { - try { - testLogger.test(`Ensure test DID ${publicDid} is written to ledger`); - await agent.ledger.getPublicDid(publicDid); - } catch (error) { - // Unfortunately, this won't prevent from the test suite running because of Jest runner runs all tests - // regardless of thrown errors. We're more explicit about the problem with this error handling. - throw new Error(`Test DID ${publicDid} is not written on ledger or ledger is not available.`); - } -} - -export async function issueCredential({ - issuerAgent, - issuerConnectionId, - holderAgent, - credentialTemplate, -}: { - issuerAgent: Agent; - issuerConnectionId: string; - holderAgent: Agent; - credentialTemplate: CredentialOfferTemplate; -}) { - let issuerCredentialRecord = await issuerAgent.credentials.offerCredential(issuerConnectionId, credentialTemplate); - - let holderCredentialRecord = await waitForCredentialRecord(holderAgent, { - threadId: issuerCredentialRecord.tags.threadId, - state: CredentialState.OfferReceived, - }); - - holderCredentialRecord = await holderAgent.credentials.acceptOffer(holderCredentialRecord.id); - - issuerCredentialRecord = await waitForCredentialRecord(issuerAgent, { - threadId: holderCredentialRecord.tags.threadId, - state: CredentialState.RequestReceived, - }); - - issuerCredentialRecord = await issuerAgent.credentials.acceptRequest(issuerCredentialRecord.id); - - holderCredentialRecord = await waitForCredentialRecord(holderAgent, { - threadId: issuerCredentialRecord.tags.threadId, - state: CredentialState.CredentialReceived, - }); - - holderCredentialRecord = await holderAgent.credentials.acceptCredential(holderCredentialRecord.id); - - issuerCredentialRecord = await waitForCredentialRecord(issuerAgent, { - threadId: issuerCredentialRecord.tags.threadId, - state: CredentialState.Done, - }); - - return { issuerCredential: issuerCredentialRecord, holderCredential: holderCredentialRecord }; -} diff --git a/src/lib/__tests__/ledger.test.ts b/src/lib/__tests__/ledger.test.ts deleted file mode 100644 index 311e7eb067..0000000000 --- a/src/lib/__tests__/ledger.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import indy from 'indy-sdk'; -import type { SchemaId } from 'indy-sdk'; -import { Agent, InboundTransporter, OutboundTransporter } from '..'; -import { DID_IDENTIFIER_REGEX, VERKEY_REGEX, isFullVerkey, isAbbreviatedVerkey } from '../utils/did'; -import { genesisPath, sleep } from './helpers'; -import { InitConfig } from '../types'; -import testLogger from './logger'; - -const faberConfig: InitConfig = { - label: 'Faber', - walletConfig: { id: 'faber' }, - walletCredentials: { key: '00000000000000000000000000000Test01' }, - publicDidSeed: process.env.TEST_AGENT_PUBLIC_DID_SEED, - genesisPath, - poolName: 'test-pool', - indy, - logger: testLogger, -}; - -describe('ledger', () => { - let faberAgent: Agent; - let schemaId: SchemaId; - - beforeAll(async () => { - faberAgent = new Agent(faberConfig, new DummyInboundTransporter(), new DummyOutboundTransporter()); - await faberAgent.init(); - }); - - afterAll(async () => { - await faberAgent.closeAndDeleteWallet(); - }); - - test(`initialization of agent's public DID`, async () => { - const publicDid = faberAgent.publicDid; - testLogger.test('faberAgentPublicDid', publicDid); - - expect(publicDid).toEqual( - expect.objectContaining({ - did: expect.stringMatching(DID_IDENTIFIER_REGEX), - verkey: expect.stringMatching(VERKEY_REGEX), - }) - ); - }); - - test('get public DID from ledger', async () => { - if (!faberAgent.publicDid) { - throw new Error('Agent does not have public did.'); - } - - const result = await faberAgent.ledger.getPublicDid(faberAgent.publicDid.did); - - let { verkey } = faberAgent.publicDid; - // Agent’s public did stored locally in Indy wallet and created from public did seed during - // its initialization always returns full verkey. Therefore we need to align that here. - if (isFullVerkey(verkey) && isAbbreviatedVerkey(result.verkey)) { - verkey = await indy.abbreviateVerkey(faberAgent.publicDid.did, verkey); - } - - expect(result).toEqual( - expect.objectContaining({ - did: faberAgent.publicDid.did, - verkey: verkey, - role: '101', - }) - ); - }); - - test('register schema on ledger', async () => { - if (!faberAgent.publicDid) { - throw new Error('Agent does not have public did.'); - } - - const schemaName = `test-schema-${Date.now()}`; - const schemaTemplate = { - name: schemaName, - attributes: ['name', 'age'], - version: '1.0', - }; - - const schemaResponse = await faberAgent.ledger.registerSchema(schemaTemplate); - schemaId = schemaResponse[0]; - const schema = schemaResponse[1]; - - await sleep(2000); - - const ledgerSchema = await faberAgent.ledger.getSchema(schemaId); - - expect(schemaId).toBe(`${faberAgent.publicDid.did}:2:${schemaName}:1.0`); - - expect(ledgerSchema).toEqual( - expect.objectContaining({ - attrNames: expect.arrayContaining(schemaTemplate.attributes), - id: `${faberAgent.publicDid.did}:2:${schemaName}:1.0`, - name: schemaName, - seqNo: schema.seqNo, - ver: schemaTemplate.version, - version: schemaTemplate.version, - }) - ); - }); - - test('register definition on ledger', async () => { - if (!faberAgent.publicDid) { - throw new Error('Agent does not have public did.'); - } - const schema = await faberAgent.ledger.getSchema(schemaId); - const credentialDefinitionTemplate = { - schema: schema, - tag: 'TAG', - signatureType: 'CL', - config: { supportRevocation: true }, - }; - - const [credDefId] = await faberAgent.ledger.registerCredentialDefinition(credentialDefinitionTemplate); - - await sleep(2000); - - const ledgerCredDef = await faberAgent.ledger.getCredentialDefinition(credDefId); - - const credDefIdRegExp = new RegExp(`${faberAgent.publicDid.did}:3:CL:[0-9]+:TAG`); - expect(credDefId).toEqual(expect.stringMatching(credDefIdRegExp)); - expect(ledgerCredDef).toEqual( - expect.objectContaining({ - id: expect.stringMatching(credDefIdRegExp), - schemaId: String(schema.seqNo), - type: credentialDefinitionTemplate.signatureType, - tag: credentialDefinitionTemplate.tag, - ver: '1.0', - value: expect.objectContaining({ - primary: expect.anything(), - revocation: expect.anything(), - }), - }) - ); - }); -}); - -class DummyInboundTransporter implements InboundTransporter { - public start() { - testLogger.test('Starting agent...'); - } -} - -class DummyOutboundTransporter implements OutboundTransporter { - public async sendMessage() { - testLogger.test('Sending message...'); - } -} diff --git a/src/lib/__tests__/logger.ts b/src/lib/__tests__/logger.ts deleted file mode 100644 index ffc890a248..0000000000 --- a/src/lib/__tests__/logger.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { ILogObject, Logger } from 'tslog'; -import { LogLevel } from '../logger'; -import { BaseLogger } from '../logger/BaseLogger'; -import { appendFileSync } from 'fs'; - -function logToTransport(logObject: ILogObject) { - appendFileSync('logs.txt', JSON.stringify(logObject) + '\n'); -} - -export class TestLogger extends BaseLogger { - private logger: Logger; - - // Map our log levels to tslog levels - private tsLogLevelMap = { - [LogLevel.test]: 'silly', - [LogLevel.trace]: 'trace', - [LogLevel.debug]: 'debug', - [LogLevel.info]: 'info', - [LogLevel.warn]: 'warn', - [LogLevel.error]: 'error', - [LogLevel.fatal]: 'fatal', - } as const; - - public constructor(logLevel: LogLevel) { - super(logLevel); - - this.logger = new Logger({ - minLevel: this.logLevel == LogLevel.off ? undefined : this.tsLogLevelMap[this.logLevel], - ignoreStackLevels: 5, - attachedTransports: [ - { - transportLogger: { - silly: logToTransport, - debug: logToTransport, - trace: logToTransport, - info: logToTransport, - warn: logToTransport, - error: logToTransport, - fatal: logToTransport, - }, - // always log to file - minLevel: 'silly', - }, - ], - }); - } - - private log(level: Exclude, message: string, data?: Record): void { - const tsLogLevel = this.tsLogLevelMap[level]; - - if (data) { - this.logger[tsLogLevel](message, data); - } else { - this.logger[tsLogLevel](message); - } - } - - public test(message: string, data?: Record): void { - this.log(LogLevel.test, message, data); - } - - public trace(message: string, data?: Record): void { - this.log(LogLevel.trace, message, data); - } - - public debug(message: string, data?: Record): void { - this.log(LogLevel.debug, message, data); - } - - public info(message: string, data?: Record): void { - this.log(LogLevel.info, message, data); - } - - public warn(message: string, data?: Record): void { - this.log(LogLevel.warn, message, data); - } - - public error(message: string, data?: Record): void { - this.log(LogLevel.error, message, data); - } - - public fatal(message: string, data?: Record): void { - this.log(LogLevel.fatal, message, data); - } -} - -const testLogger = new TestLogger(LogLevel.error); - -export default testLogger; diff --git a/src/lib/__tests__/proofs.test.ts b/src/lib/__tests__/proofs.test.ts deleted file mode 100644 index 1ce59cce31..0000000000 --- a/src/lib/__tests__/proofs.test.ts +++ /dev/null @@ -1,262 +0,0 @@ -/* eslint-disable no-console */ -import indy from 'indy-sdk'; -import type { CredDefId } from 'indy-sdk'; -import { Subject } from 'rxjs'; -import { Agent } from '..'; -import { - ensurePublicDidIsOnLedger, - makeConnection, - registerDefinition, - registerSchema, - SubjectInboundTransporter, - SubjectOutboundTransporter, - genesisPath, - issueCredential, - waitForProofRecord, -} from './helpers'; -import { CredentialPreview, CredentialPreviewAttribute } from '../modules/credentials'; -import { InitConfig } from '../types'; -import { - PredicateType, - PresentationPreview, - PresentationPreviewAttribute, - PresentationPreviewPredicate, - ProofState, - ProofAttributeInfo, - AttributeFilter, - ProofPredicateInfo, -} from '../modules/proofs'; -import { ConnectionRecord } from '../modules/connections'; -import testLogger from './logger'; - -const faberConfig: InitConfig = { - label: 'Faber', - walletConfig: { id: 'proofs-test-faber' }, - walletCredentials: { key: '00000000000000000000000000000Test01' }, - publicDidSeed: process.env.TEST_AGENT_PUBLIC_DID_SEED, - autoAcceptConnections: true, - genesisPath, - poolName: 'proofs-test-faber-pool', - indy, - logger: testLogger, -}; - -const aliceConfig: InitConfig = { - label: 'Alice', - walletConfig: { id: 'proofs-test-alice' }, - walletCredentials: { key: '00000000000000000000000000000Test01' }, - autoAcceptConnections: true, - genesisPath, - poolName: 'proofs-test-alice-pool', - indy, - logger: testLogger, -}; - -const credentialPreview = new CredentialPreview({ - attributes: [ - new CredentialPreviewAttribute({ - name: 'name', - mimeType: 'text/plain', - value: 'John', - }), - new CredentialPreviewAttribute({ - name: 'age', - mimeType: 'text/plain', - value: '99', - }), - ], -}); - -describe('Present Proof', () => { - let faberAgent: Agent; - let aliceAgent: Agent; - let credDefId: CredDefId; - let faberConnection: ConnectionRecord; - let aliceConnection: ConnectionRecord; - let presentationPreview: PresentationPreview; - - beforeAll(async () => { - const faberMessages = new Subject(); - const aliceMessages = new Subject(); - - const faberAgentInbound = new SubjectInboundTransporter(faberMessages); - const faberAgentOutbound = new SubjectOutboundTransporter(aliceMessages); - const aliceAgentInbound = new SubjectInboundTransporter(aliceMessages); - const aliceAgentOutbound = new SubjectOutboundTransporter(faberMessages); - - faberAgent = new Agent(faberConfig, faberAgentInbound, faberAgentOutbound); - aliceAgent = new Agent(aliceConfig, aliceAgentInbound, aliceAgentOutbound); - await faberAgent.init(); - await aliceAgent.init(); - - const schemaTemplate = { - name: `test-schema-${Date.now()}`, - attributes: ['name', 'age'], - version: '1.0', - }; - const [, ledgerSchema] = await registerSchema(faberAgent, schemaTemplate); - - const definitionTemplate = { - schema: ledgerSchema, - tag: 'TAG', - signatureType: 'CL', - config: { supportRevocation: false }, - }; - const [ledgerCredDefId] = await registerDefinition(faberAgent, definitionTemplate); - credDefId = ledgerCredDefId; - - const publicDid = faberAgent.publicDid?.did; - await ensurePublicDidIsOnLedger(faberAgent, publicDid!); - const { agentAConnection, agentBConnection } = await makeConnection(faberAgent, aliceAgent); - - faberConnection = agentAConnection; - aliceConnection = agentBConnection; - - presentationPreview = new PresentationPreview({ - attributes: [ - new PresentationPreviewAttribute({ - name: 'name', - credentialDefinitionId: credDefId, - referent: '0', - value: 'John', - }), - ], - predicates: [ - new PresentationPreviewPredicate({ - name: 'age', - credentialDefinitionId: credDefId, - predicate: PredicateType.GreaterThanOrEqualTo, - threshold: 50, - }), - ], - }); - - await issueCredential({ - issuerAgent: faberAgent, - issuerConnectionId: faberConnection.id, - holderAgent: aliceAgent, - credentialTemplate: { - credentialDefinitionId: credDefId, - comment: 'some comment about credential', - preview: credentialPreview, - }, - }); - }); - - afterAll(async () => { - await faberAgent.closeAndDeleteWallet(); - await aliceAgent.closeAndDeleteWallet(); - }); - - test('Alice starts with proof proposal to Faber', async () => { - testLogger.test('Alice sends presentation proposal to Faber'); - let aliceProofRecord = await aliceAgent.proofs.proposeProof(aliceConnection.id, presentationPreview); - - testLogger.test('Faber waits for presentation proposal from Alice'); - let faberProofRecord = await waitForProofRecord(faberAgent, { - threadId: aliceProofRecord.tags.threadId, - state: ProofState.ProposalReceived, - }); - - testLogger.test('Faber accepts presentation proposal from Alice'); - faberProofRecord = await faberAgent.proofs.acceptProposal(faberProofRecord.id); - - testLogger.test('Alice waits for presentation request from Faber'); - aliceProofRecord = await waitForProofRecord(aliceAgent, { - threadId: aliceProofRecord.tags.threadId, - state: ProofState.RequestReceived, - }); - - testLogger.test('Alice accepts presentation request from Faber'); - const indyProofRequest = aliceProofRecord.requestMessage?.indyProofRequest; - const requestedCredentials = await aliceAgent.proofs.getRequestedCredentialsForProofRequest( - indyProofRequest!, - presentationPreview - ); - await aliceAgent.proofs.acceptRequest(aliceProofRecord.id, requestedCredentials); - - testLogger.test('Faber waits for presentation from Alice'); - faberProofRecord = await waitForProofRecord(faberAgent, { - threadId: aliceProofRecord.tags.threadId, - state: ProofState.PresentationReceived, - }); - - // assert presentation is valid - expect(faberProofRecord.isVerified).toBe(true); - - // Faber accepts presentation - await faberAgent.proofs.acceptPresentation(faberProofRecord.id); - - // Alice waits till it receives presentation ack - aliceProofRecord = await waitForProofRecord(aliceAgent, { - threadId: aliceProofRecord.tags.threadId, - state: ProofState.Done, - }); - }); - - test('Faber starts with proof requests to Alice', async () => { - testLogger.test('Faber sends presentation request to Alice'); - - const attributes = { - name: new ProofAttributeInfo({ - name: 'name', - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - }; - - const predicates = { - age: new ProofPredicateInfo({ - name: 'age', - predicateType: PredicateType.GreaterThanOrEqualTo, - predicateValue: 50, - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - }; - - let faberProofRecord = await faberAgent.proofs.requestProof(faberConnection.id, { - name: 'test-proof-request', - requestedAttributes: attributes, - requestedPredicates: predicates, - }); - - testLogger.test('Alice waits for presentation request from Faber'); - let aliceProofRecord = await waitForProofRecord(aliceAgent, { - threadId: faberProofRecord.tags.threadId, - state: ProofState.RequestReceived, - }); - - testLogger.test('Alice accepts presentation request from Faber'); - const indyProofRequest = aliceProofRecord.requestMessage?.indyProofRequest; - const requestedCredentials = await aliceAgent.proofs.getRequestedCredentialsForProofRequest( - indyProofRequest!, - presentationPreview - ); - await aliceAgent.proofs.acceptRequest(aliceProofRecord.id, requestedCredentials); - - testLogger.test('Faber waits for presentation from Alice'); - faberProofRecord = await waitForProofRecord(faberAgent, { - threadId: aliceProofRecord.tags.threadId, - state: ProofState.PresentationReceived, - }); - - // assert presentation is valid - expect(faberProofRecord.isVerified).toBe(true); - - // Faber accepts presentation - await faberAgent.proofs.acceptPresentation(faberProofRecord.id); - - // Alice waits till it receives presentation ack - aliceProofRecord = await waitForProofRecord(aliceAgent, { - threadId: aliceProofRecord.tags.threadId, - state: ProofState.Done, - }); - }); -}); diff --git a/src/lib/__tests__/setup.ts b/src/lib/__tests__/setup.ts deleted file mode 100644 index d2c9bc6e64..0000000000 --- a/src/lib/__tests__/setup.ts +++ /dev/null @@ -1 +0,0 @@ -import 'reflect-metadata'; diff --git a/src/lib/agent/Agent.ts b/src/lib/agent/Agent.ts deleted file mode 100644 index 3044d58303..0000000000 --- a/src/lib/agent/Agent.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { Logger } from '../logger'; -import { InitConfig } from '../types'; -import { IndyWallet } from '../wallet/IndyWallet'; -import { MessageReceiver } from './MessageReceiver'; -import { EnvelopeService } from './EnvelopeService'; -import { ConnectionService, TrustPingService, ConnectionRecord } from '../modules/connections'; -import { CredentialService, CredentialRecord } from '../modules/credentials'; -import { ProofService, ProofRecord } from '../modules/proofs'; -import { - ConsumerRoutingService, - ProviderRoutingService, - MessagePickupService, - ProvisioningService, - ProvisioningRecord, -} from '../modules/routing'; -import { BasicMessageService, BasicMessageRecord } from '../modules/basic-messages'; -import { LedgerService } from '../modules/ledger'; -import { Dispatcher } from './Dispatcher'; -import { MessageSender } from './MessageSender'; -import { InboundTransporter } from '../transport/InboundTransporter'; -import { OutboundTransporter } from '../transport/OutboundTransporter'; -import { MessageRepository } from '../storage/MessageRepository'; -import { Repository } from '../storage/Repository'; -import { IndyStorageService } from '../storage/IndyStorageService'; -import { AgentConfig } from './AgentConfig'; -import { Wallet } from '../wallet/Wallet'; -import { ConnectionsModule } from '../modules/connections/ConnectionsModule'; -import { CredentialsModule } from '../modules/credentials/CredentialsModule'; -import { ProofsModule } from '../modules/proofs/ProofsModule'; -import { RoutingModule } from '../modules/routing/RoutingModule'; -import { BasicMessagesModule } from '../modules/basic-messages/BasicMessagesModule'; -import { LedgerModule } from '../modules/ledger/LedgerModule'; - -export class Agent { - protected logger: Logger; - protected wallet: Wallet; - protected agentConfig: AgentConfig; - protected messageReceiver: MessageReceiver; - protected dispatcher: Dispatcher; - protected messageSender: MessageSender; - protected connectionService: ConnectionService; - protected proofService: ProofService; - protected basicMessageService: BasicMessageService; - protected providerRoutingService: ProviderRoutingService; - protected consumerRoutingService: ConsumerRoutingService; - protected trustPingService: TrustPingService; - protected messagePickupService: MessagePickupService; - protected provisioningService: ProvisioningService; - protected ledgerService: LedgerService; - protected credentialService: CredentialService; - protected basicMessageRepository: Repository; - protected connectionRepository: Repository; - protected provisioningRepository: Repository; - protected credentialRepository: Repository; - protected proofRepository: Repository; - - public inboundTransporter: InboundTransporter; - - public connections!: ConnectionsModule; - public proofs!: ProofsModule; - public routing!: RoutingModule; - public basicMessages!: BasicMessagesModule; - public ledger!: LedgerModule; - public credentials!: CredentialsModule; - - public constructor( - initialConfig: InitConfig, - inboundTransporter: InboundTransporter, - outboundTransporter: OutboundTransporter, - messageRepository?: MessageRepository - ) { - this.agentConfig = new AgentConfig(initialConfig); - this.logger = this.agentConfig.logger; - - this.logger.info('Creating agent with config', { - ...initialConfig, - // Prevent large object being logged. - // Will display true/false to indicate if value is present in config - indy: initialConfig.indy != undefined, - logger: initialConfig.logger != undefined, - }); - this.wallet = new IndyWallet(this.agentConfig); - const envelopeService = new EnvelopeService(this.wallet, this.agentConfig); - - this.messageSender = new MessageSender(envelopeService, outboundTransporter); - this.dispatcher = new Dispatcher(this.messageSender); - this.inboundTransporter = inboundTransporter; - - const storageService = new IndyStorageService(this.wallet); - this.basicMessageRepository = new Repository(BasicMessageRecord, storageService); - this.connectionRepository = new Repository(ConnectionRecord, storageService); - this.provisioningRepository = new Repository(ProvisioningRecord, storageService); - this.credentialRepository = new Repository(CredentialRecord, storageService); - this.proofRepository = new Repository(ProofRecord, storageService); - this.provisioningService = new ProvisioningService(this.provisioningRepository, this.agentConfig); - this.connectionService = new ConnectionService(this.wallet, this.agentConfig, this.connectionRepository); - this.basicMessageService = new BasicMessageService(this.basicMessageRepository); - this.providerRoutingService = new ProviderRoutingService(); - this.consumerRoutingService = new ConsumerRoutingService(this.messageSender, this.agentConfig); - this.trustPingService = new TrustPingService(); - this.messagePickupService = new MessagePickupService(messageRepository); - this.ledgerService = new LedgerService(this.wallet, this.agentConfig); - this.credentialService = new CredentialService( - this.wallet, - this.credentialRepository, - this.connectionService, - this.ledgerService, - this.agentConfig - ); - this.proofService = new ProofService(this.proofRepository, this.ledgerService, this.wallet, this.agentConfig); - - this.messageReceiver = new MessageReceiver( - this.agentConfig, - envelopeService, - this.connectionService, - this.dispatcher - ); - - this.registerModules(); - } - - public async init() { - await this.wallet.init(); - - const { publicDidSeed, genesisPath, poolName } = this.agentConfig; - if (publicDidSeed) { - // If an agent has publicDid it will be used as routing key. - await this.wallet.initPublicDid({ seed: publicDidSeed }); - } - - // If the genesisPath is provided in the config, we will automatically handle ledger connection - // otherwise the framework consumer needs to do this manually - if (genesisPath) { - await this.ledger.connect(poolName, { - genesis_txn: genesisPath, - }); - } - - return this.inboundTransporter.start(this); - } - - public get publicDid() { - return this.wallet.publicDid; - } - - public getMediatorUrl() { - return this.agentConfig.mediatorUrl; - } - - public async receiveMessage(inboundPackedMessage: unknown) { - return await this.messageReceiver.receiveMessage(inboundPackedMessage); - } - - public async closeAndDeleteWallet() { - await this.wallet.close(); - await this.wallet.delete(); - } - - protected registerModules() { - this.connections = new ConnectionsModule( - this.dispatcher, - this.agentConfig, - this.connectionService, - this.trustPingService, - this.consumerRoutingService, - this.messageSender - ); - - this.credentials = new CredentialsModule( - this.dispatcher, - this.connectionService, - this.credentialService, - this.messageSender - ); - - this.proofs = new ProofsModule(this.dispatcher, this.proofService, this.connectionService, this.messageSender); - - this.routing = new RoutingModule( - this.dispatcher, - this.agentConfig, - this.providerRoutingService, - this.provisioningService, - this.messagePickupService, - this.connectionService, - this.messageSender - ); - - this.basicMessages = new BasicMessagesModule(this.dispatcher, this.basicMessageService, this.messageSender); - - this.ledger = new LedgerModule(this.wallet, this.ledgerService); - } -} diff --git a/src/lib/agent/AgentConfig.ts b/src/lib/agent/AgentConfig.ts deleted file mode 100644 index 284fb15524..0000000000 --- a/src/lib/agent/AgentConfig.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { ConsoleLogger, Logger, LogLevel } from '../logger'; -import { InitConfig, InboundConnection } from '../types'; - -export class AgentConfig { - private initConfig: InitConfig; - public logger: Logger; - public inboundConnection?: InboundConnection; - - public constructor(initConfig: InitConfig) { - this.initConfig = initConfig; - this.logger = initConfig.logger ?? new ConsoleLogger(LogLevel.off); - } - - public get indy() { - return this.initConfig.indy; - } - - public get label() { - return this.initConfig.label; - } - - public get publicDid() { - return this.initConfig.publicDid; - } - - public get publicDidSeed() { - return this.initConfig.publicDidSeed; - } - - public get mediatorUrl() { - return this.initConfig.mediatorUrl; - } - - public get poolName() { - return this.initConfig.poolName ?? 'default-pool'; - } - - public get genesisPath() { - return this.initConfig.genesisPath; - } - - public get walletConfig() { - return this.initConfig.walletConfig; - } - - public get walletCredentials() { - return this.initConfig.walletCredentials; - } - - public establishInbound(inboundConnection: InboundConnection) { - this.inboundConnection = inboundConnection; - } - - public get autoAcceptConnections() { - return this.initConfig.autoAcceptConnections ?? false; - } - - public getEndpoint() { - // If a mediator is used, always return that as endpoint - const mediatorEndpoint = this.inboundConnection?.connection?.theirDidDoc?.service[0].serviceEndpoint; - if (mediatorEndpoint) return mediatorEndpoint; - - // Otherwise we check if an endpoint is set - if (this.initConfig.endpoint) return `${this.initConfig.endpoint}/msg`; - - // Otherwise we'll try to construct it from the host/port - let hostEndpoint = this.initConfig.host; - if (hostEndpoint) { - if (this.initConfig.port) hostEndpoint += `:${this.initConfig.port}`; - return `${hostEndpoint}/msg`; - } - - // If we still don't have an endpoint, return didcomm:transport/queue - // https://github.com/hyperledger/aries-rfcs/issues/405#issuecomment-582612875 - return 'didcomm:transport/queue'; - } - - public getRoutingKeys() { - const verkey = this.inboundConnection?.verkey; - return verkey ? [verkey] : []; - } -} diff --git a/src/lib/agent/AgentMessage.ts b/src/lib/agent/AgentMessage.ts deleted file mode 100644 index e52ae97b3a..0000000000 --- a/src/lib/agent/AgentMessage.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Compose } from '../utils/mixins'; -import { ThreadDecorated } from '../decorators/thread/ThreadDecoratorExtension'; -import { L10nDecorated } from '../decorators/l10n/L10nDecoratorExtension'; -import { TransportDecorated } from '../decorators/transport/TransportDecoratorExtension'; -import { TimingDecorated } from '../decorators/timing/TimingDecoratorExtension'; -import { BaseMessage } from './BaseMessage'; -import { JsonTransformer } from '../utils/JsonTransformer'; -import { AckDecorated } from '../decorators/ack/AckDecoratorExtension'; - -const DefaultDecorators = [ThreadDecorated, L10nDecorated, TransportDecorated, TimingDecorated, AckDecorated]; - -export class AgentMessage extends Compose(BaseMessage, DefaultDecorators) { - public toJSON(): Record { - return JsonTransformer.toJSON(this); - } - - public is(Class: C): this is InstanceType { - return this.type === Class.type; - } -} diff --git a/src/lib/agent/BaseMessage.ts b/src/lib/agent/BaseMessage.ts deleted file mode 100644 index 71bcc4bd18..0000000000 --- a/src/lib/agent/BaseMessage.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Expose } from 'class-transformer'; -import { Matches } from 'class-validator'; -import { v4 as uuid } from 'uuid'; - -import { Constructor } from '../utils/mixins'; - -export const MessageIdRegExp = /[-_./a-zA-Z0-9]{8,64}/; -export const MessageTypeRegExp = /(.*?)([a-z0-9._-]+)\/(\d[^/]*)\/([a-z0-9._-]+)$/; - -export type BaseMessageConstructor = Constructor; - -export class BaseMessage { - @Matches(MessageIdRegExp) - @Expose({ name: '@id' }) - public id!: string; - - @Expose({ name: '@type' }) - @Matches(MessageTypeRegExp) - public readonly type!: string; - public static readonly type: string; - - public generateId() { - return uuid(); - } -} diff --git a/src/lib/agent/Dispatcher.ts b/src/lib/agent/Dispatcher.ts deleted file mode 100644 index bbdba7a45d..0000000000 --- a/src/lib/agent/Dispatcher.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { OutboundMessage, OutboundPackage } from '../types'; -import { Handler } from './Handler'; -import { MessageSender } from './MessageSender'; -import { AgentMessage } from './AgentMessage'; -import { InboundMessageContext } from './models/InboundMessageContext'; - -class Dispatcher { - private handlers: Handler[] = []; - private messageSender: MessageSender; - - public constructor(messageSender: MessageSender) { - this.messageSender = messageSender; - } - - public registerHandler(handler: Handler) { - this.handlers.push(handler); - } - - public async dispatch(messageContext: InboundMessageContext): Promise { - const message = messageContext.message; - const handler = this.getHandlerForType(message.type); - - if (!handler) { - throw new Error(`No handler for message type "${message.type}" found`); - } - - const outboundMessage = await handler.handle(messageContext); - - if (outboundMessage) { - const threadId = outboundMessage.payload.threadId; - - // check for return routing, with thread id - if (message.hasReturnRouting(threadId)) { - return await this.messageSender.packMessage(outboundMessage); - } - - await this.messageSender.sendMessage(outboundMessage); - } - - return outboundMessage || undefined; - } - - private getHandlerForType(messageType: string): Handler | undefined { - for (const handler of this.handlers) { - for (const MessageClass of handler.supportedMessages) { - if (MessageClass.type === messageType) return handler; - } - } - } - - public getMessageClassForType(messageType: string): typeof AgentMessage | undefined { - for (const handler of this.handlers) { - for (const MessageClass of handler.supportedMessages) { - if (MessageClass.type === messageType) return MessageClass; - } - } - } -} - -export { Dispatcher }; diff --git a/src/lib/agent/EnvelopeService.ts b/src/lib/agent/EnvelopeService.ts deleted file mode 100644 index 13308177f0..0000000000 --- a/src/lib/agent/EnvelopeService.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { OutboundMessage, OutboundPackage, UnpackedMessageContext } from '../types'; -import { Wallet } from '../wallet/Wallet'; -import { ForwardMessage } from '../modules/routing/messages'; -import { AgentConfig } from './AgentConfig'; -import { Logger } from '../logger'; - -class EnvelopeService { - private wallet: Wallet; - private logger: Logger; - - public constructor(wallet: Wallet, agentConfig: AgentConfig) { - this.wallet = wallet; - this.logger = agentConfig.logger; - } - - public async packMessage(outboundMessage: OutboundMessage): Promise { - const { connection, routingKeys, recipientKeys, senderVk, payload, endpoint } = outboundMessage; - const { verkey, theirKey } = connection; - - const message = payload.toJSON(); - - this.logger.info('outboundMessage', { verkey, theirKey, routingKeys, endpoint, message }); - let outboundPackedMessage = await this.wallet.pack(message, recipientKeys, senderVk); - - if (routingKeys && routingKeys.length > 0) { - for (const routingKey of routingKeys) { - const [recipientKey] = recipientKeys; - - const forwardMessage = new ForwardMessage({ - to: recipientKey, - message: outboundPackedMessage, - }); - - this.logger.debug('Forward message created', forwardMessage); - outboundPackedMessage = await this.wallet.pack(forwardMessage.toJSON(), [routingKey], senderVk); - } - } - return { connection, payload: outboundPackedMessage, endpoint }; - } - - public async unpackMessage(packedMessage: JsonWebKey): Promise { - return this.wallet.unpack(packedMessage); - } -} - -export { EnvelopeService }; diff --git a/src/lib/agent/Handler.ts b/src/lib/agent/Handler.ts deleted file mode 100644 index 95c3ae0792..0000000000 --- a/src/lib/agent/Handler.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OutboundMessage } from '../types'; -import { AgentMessage } from './AgentMessage'; -import { InboundMessageContext } from './models/InboundMessageContext'; - -export interface Handler { - readonly supportedMessages: readonly T[]; - - handle(messageContext: InboundMessageContext): Promise; -} - -/** - * Provides exact typing for the AgentMessage in the message context in the `handle` function - * of a handler. It takes all possible types from `supportedMessageTypes` - * - * @example - * async handle(messageContext: HandlerInboundMessage) {} - */ -export type HandlerInboundMessage = InboundMessageContext< - InstanceType ->; diff --git a/src/lib/agent/MessageReceiver.ts b/src/lib/agent/MessageReceiver.ts deleted file mode 100644 index 6727f95857..0000000000 --- a/src/lib/agent/MessageReceiver.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { AgentConfig } from './AgentConfig'; -import { Dispatcher } from './Dispatcher'; -import { EnvelopeService } from './EnvelopeService'; -import { UnpackedMessageContext, UnpackedMessage } from '../types'; -import { InboundMessageContext } from './models/InboundMessageContext'; -import { RoutingMessageType as MessageType } from '../modules/routing'; -import { ConnectionService } from '../modules/connections'; -import { AgentMessage } from './AgentMessage'; -import { JsonTransformer } from '../utils/JsonTransformer'; -import { Logger } from '../logger'; - -class MessageReceiver { - private config: AgentConfig; - private envelopeService: EnvelopeService; - private connectionService: ConnectionService; - private dispatcher: Dispatcher; - private logger: Logger; - - public constructor( - config: AgentConfig, - envelopeService: EnvelopeService, - connectionService: ConnectionService, - dispatcher: Dispatcher - ) { - this.config = config; - this.envelopeService = envelopeService; - this.connectionService = connectionService; - this.dispatcher = dispatcher; - this.logger = this.config.logger; - } - - /** - * Receive and handle an inbound DIDComm message. It will unpack the message, transform it - * to it's corresponding message class and finally dispatch it to the dispatcher. - * - * @param inboundPackedMessage the message to receive and handle - */ - public async receiveMessage(inboundPackedMessage: unknown) { - if (typeof inboundPackedMessage !== 'object' || inboundPackedMessage == null) { - throw new Error('Invalid message received. Message should be object'); - } - - this.logger.debug(`Agent ${this.config.label} received message:`, inboundPackedMessage); - - const unpackedMessage = await this.unpackMessage(inboundPackedMessage as Record); - - const senderKey = unpackedMessage.sender_verkey; - let connection = undefined; - if (senderKey && unpackedMessage.recipient_verkey) { - // TODO: only attach if theirKey is present. Otherwise a connection that may not be complete, validated or correct will - // be attached to the message context. See #76 - connection = (await this.connectionService.findByVerkey(unpackedMessage.recipient_verkey)) || undefined; - - // We check whether the sender key is the same as the key we have stored in the connection - // otherwise everyone could send messages to our key and we would just accept - // it as if it was send by the key of the connection. - if (connection && connection.theirKey != null && connection.theirKey != senderKey) { - throw new Error( - `Inbound message 'sender_key' ${senderKey} is different from connection.theirKey ${connection.theirKey}` - ); - } - } - - this.logger.info(`Received message with type '${unpackedMessage.message['@type']}'`, unpackedMessage.message); - - const message = await this.transformMessage(unpackedMessage); - const messageContext = new InboundMessageContext(message, { - connection, - senderVerkey: senderKey, - recipientVerkey: unpackedMessage.recipient_verkey, - }); - - return await this.dispatcher.dispatch(messageContext); - } - - /** - * Unpack a message using the envelope service. Will perform extra unpacking steps for forward messages. - * If message is not packed, it will be returned as is, but in the unpacked message structure - * - * @param packedMessage the received, probably packed, message to unpack - */ - private async unpackMessage(packedMessage: Record): Promise { - // If the inbound message has no @type field we assume - // the message is packed and must be unpacked first - if (!this.isUnpackedMessage(packedMessage)) { - let unpackedMessage: UnpackedMessageContext; - try { - // TODO: handle when the unpacking fails. At the moment this will throw a cryptic - // indy-sdk error. Eventually we should create a problem report message - unpackedMessage = await this.envelopeService.unpackMessage(packedMessage); - } catch (error) { - this.logger.error('error while unpacking message', error); - throw error; - } - - // if the message is of type forward we should check whether the - // - forward message is intended for us (so unpack inner `msg` and pass that to dispatcher) - // - or that the message should be forwarded (pass unpacked forward message with packed `msg` to dispatcher) - if (unpackedMessage.message['@type'] === MessageType.ForwardMessage) { - this.logger.debug('unpacking forwarded message', unpackedMessage); - - try { - unpackedMessage = await this.envelopeService.unpackMessage(unpackedMessage.message.msg as JsonWebKey); - } catch { - // To check whether the `to` field is a key belonging to us could be done in two ways. - // We now just try to unpack, if it errors it means we don't have the key to unpack the message - // if we can unpack the message we certainly are the owner of the key in the to field. - // Another approach is to first check whether the key belongs to us and only unpack if so. - // I think this approach is better, but for now the current approach is easier - // It is thus okay to silently ignore this error - } - } - - return unpackedMessage; - } - // If the message does have an @type field we assume - // the message is already unpacked an use it directly - else { - const unpackedMessage: UnpackedMessageContext = { message: packedMessage }; - return unpackedMessage; - } - } - - private isUnpackedMessage(message: Record): message is UnpackedMessage { - return '@type' in message; - } - - /** - * Transform an unpacked DIDComm message into it's corresponding message class. Will look at all message types in the registered handlers. - * - * @param unpackedMessage the unpacked message for which to transform the message in to a class instance - */ - private async transformMessage(unpackedMessage: UnpackedMessageContext): Promise { - const messageType = unpackedMessage.message['@type']; - const MessageClass = this.dispatcher.getMessageClassForType(messageType); - - if (!MessageClass) { - throw new Error(`No handler for message type "${messageType}" found`); - } - - // Cast the plain JSON object to specific instance of Message extended from AgentMessage - const message = JsonTransformer.fromJSON(unpackedMessage.message, MessageClass); - - return message; - } -} - -export { MessageReceiver }; diff --git a/src/lib/agent/MessageSender.ts b/src/lib/agent/MessageSender.ts deleted file mode 100644 index 0e6cd9427a..0000000000 --- a/src/lib/agent/MessageSender.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { OutboundMessage, OutboundPackage } from '../types'; -import { OutboundTransporter } from '../transport/OutboundTransporter'; -import { EnvelopeService } from './EnvelopeService'; -import { ReturnRouteTypes } from '../decorators/transport/TransportDecorator'; -import { AgentMessage } from './AgentMessage'; -import { Constructor } from '../utils/mixins'; -import { InboundMessageContext } from './models/InboundMessageContext'; -import { JsonTransformer } from '../utils/JsonTransformer'; - -class MessageSender { - private envelopeService: EnvelopeService; - private outboundTransporter: OutboundTransporter; - - public constructor(envelopeService: EnvelopeService, outboundTransporter: OutboundTransporter) { - this.envelopeService = envelopeService; - this.outboundTransporter = outboundTransporter; - } - - public async packMessage(outboundMessage: OutboundMessage): Promise { - return this.envelopeService.packMessage(outboundMessage); - } - - public async sendMessage(outboundMessage: OutboundMessage): Promise { - const outboundPackage = await this.envelopeService.packMessage(outboundMessage); - await this.outboundTransporter.sendMessage(outboundPackage, false); - } - - public async sendAndReceiveMessage( - outboundMessage: OutboundMessage, - ReceivedMessageClass: Constructor - ): Promise> { - outboundMessage.payload.setReturnRouting(ReturnRouteTypes.all); - - const outboundPackage = await this.envelopeService.packMessage(outboundMessage); - const inboundPackedMessage = await this.outboundTransporter.sendMessage(outboundPackage, true); - const inboundUnpackedMessage = await this.envelopeService.unpackMessage(inboundPackedMessage); - - const message = JsonTransformer.fromJSON(inboundUnpackedMessage.message, ReceivedMessageClass); - - const messageContext = new InboundMessageContext(message, { - connection: outboundMessage.connection, - recipientVerkey: inboundUnpackedMessage.recipient_verkey, - senderVerkey: inboundUnpackedMessage.sender_verkey, - }); - - return messageContext; - } -} - -export { MessageSender }; diff --git a/src/lib/agent/__tests__/AgentConfig.test.ts b/src/lib/agent/__tests__/AgentConfig.test.ts deleted file mode 100644 index e35e93dc70..0000000000 --- a/src/lib/agent/__tests__/AgentConfig.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type Indy from 'indy-sdk'; -import { getMockConnection } from '../../modules/connections/__tests__/ConnectionService.test'; -import { DidDoc, IndyAgentService } from '../../modules/connections'; -import { AgentConfig } from '../AgentConfig'; - -const indy = {} as typeof Indy; - -describe('AgentConfig', () => { - describe('getEndpoint', () => { - it('should return the service endpoint of the inbound connection available', () => { - const agentConfig = new AgentConfig({ - label: 'test', - walletConfig: { id: 'test' }, - walletCredentials: { key: 'test' }, - indy, - }); - - const endpoint = 'https://mediator-url.com'; - agentConfig.establishInbound({ - verkey: 'test', - connection: getMockConnection({ - theirDidDoc: new DidDoc({ - id: 'test', - publicKey: [], - authentication: [], - service: [new IndyAgentService({ id: `test;indy`, serviceEndpoint: endpoint, recipientKeys: [] })], - }), - }), - }); - - expect(agentConfig.getEndpoint()).toBe(endpoint); - }); - - it('should return the config endpoint + /msg if no inbound connection is available', () => { - const endpoint = 'https://local-url.com'; - - const agentConfig = new AgentConfig({ - endpoint, - label: 'test', - walletConfig: { id: 'test' }, - walletCredentials: { key: 'test' }, - indy, - }); - - expect(agentConfig.getEndpoint()).toBe(endpoint + '/msg'); - }); - - it('should return the config host + /msg if no inbound connection or config endpoint is available', () => { - const host = 'https://local-url.com'; - - const agentConfig = new AgentConfig({ - host, - label: 'test', - walletConfig: { id: 'test' }, - walletCredentials: { key: 'test' }, - indy, - }); - - expect(agentConfig.getEndpoint()).toBe(host + '/msg'); - }); - - it('should return the config host and port + /msg if no inbound connection or config endpoint is available', () => { - const host = 'https://local-url.com'; - const port = 8080; - - const agentConfig = new AgentConfig({ - host, - port, - label: 'test', - walletConfig: { id: 'test' }, - walletCredentials: { key: 'test' }, - indy, - }); - - expect(agentConfig.getEndpoint()).toBe(`${host}:${port}/msg`); - }); - - // added because on first implementation this is what it did. Never again! - it('should return the endpoint + /msg without port if the endpoint and port are available', () => { - const endpoint = 'https://local-url.com'; - const port = 8080; - - const agentConfig = new AgentConfig({ - endpoint, - port, - label: 'test', - walletConfig: { id: 'test' }, - walletCredentials: { key: 'test' }, - indy, - }); - - expect(agentConfig.getEndpoint()).toBe(`${endpoint}/msg`); - }); - - it("should return 'didcomm:transport/queue' if no inbound connection or config endpoint or host/port is available", () => { - const agentConfig = new AgentConfig({ - label: 'test', - walletConfig: { id: 'test' }, - walletCredentials: { key: 'test' }, - indy, - }); - - expect(agentConfig.getEndpoint()).toBe('didcomm:transport/queue'); - }); - }); -}); diff --git a/src/lib/agent/helpers.ts b/src/lib/agent/helpers.ts deleted file mode 100644 index 3add68f1bd..0000000000 --- a/src/lib/agent/helpers.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ConnectionRecord } from '../modules/connections'; -import { AgentMessage } from './AgentMessage'; -import { OutboundMessage } from '../types'; -import { ConnectionInvitationMessage } from '../modules/connections'; -import { IndyAgentService } from '../modules/connections'; - -export function createOutboundMessage( - connection: ConnectionRecord, - payload: T, - invitation?: ConnectionInvitationMessage -): OutboundMessage { - if (invitation) { - // TODO: invitation recipientKeys, routingKeys, endpoint could be missing - // When invitation uses DID - return { - connection, - endpoint: invitation.serviceEndpoint, - payload, - recipientKeys: invitation.recipientKeys || [], - routingKeys: invitation.routingKeys || [], - senderVk: connection.verkey, - }; - } - - const { theirDidDoc } = connection; - - if (!theirDidDoc) { - throw new Error(`DidDoc for connection with verkey ${connection.verkey} not found!`); - } - - const [service] = theirDidDoc.getServicesByClassType(IndyAgentService); - - return { - connection, - endpoint: service.serviceEndpoint, - payload, - recipientKeys: service.recipientKeys, - routingKeys: service.routingKeys ?? [], - senderVk: connection.verkey, - }; -} diff --git a/src/lib/agent/models/InboundMessageContext.ts b/src/lib/agent/models/InboundMessageContext.ts deleted file mode 100644 index 75983394e3..0000000000 --- a/src/lib/agent/models/InboundMessageContext.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Verkey } from 'indy-sdk'; -import { AgentMessage } from '../AgentMessage'; -import { ConnectionRecord } from '../../modules/connections'; - -export interface MessageContextParams { - connection?: ConnectionRecord; - senderVerkey?: Verkey; - recipientVerkey?: Verkey; -} - -export class InboundMessageContext { - public message: T; - public connection?: ConnectionRecord; - public senderVerkey?: Verkey; - public recipientVerkey?: Verkey; - - public constructor(message: T, context: MessageContextParams = {}) { - this.message = message; - this.recipientVerkey = context.recipientVerkey; - - if (context.connection) { - this.connection = context.connection; - // TODO: which senderkey should we prioritize - // Or should we throw an error when they don't match? - this.senderVerkey = context.connection.theirKey || context.senderVerkey || undefined; - } else if (context.senderVerkey) { - this.senderVerkey = context.senderVerkey; - } - } -} diff --git a/src/lib/decorators/ack/AckDecorator.test.ts b/src/lib/decorators/ack/AckDecorator.test.ts deleted file mode 100644 index 70093c763d..0000000000 --- a/src/lib/decorators/ack/AckDecorator.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { JsonTransformer } from '../../utils/JsonTransformer'; -import { Compose } from '../../utils/mixins'; - -import { BaseMessage } from '../../agent/BaseMessage'; -import { AckDecorated } from './AckDecoratorExtension'; - -describe('Decorators | AckDecoratorExtension', () => { - class TestMessage extends Compose(BaseMessage, [AckDecorated]) { - public toJSON(): Record { - return JsonTransformer.toJSON(this); - } - } - - test('transforms AckDecorator class to JSON', () => { - const message = new TestMessage(); - message.setPleaseAck(); - expect(message.toJSON()).toEqual({ '~please_ack': {} }); - }); - - test('transforms Json to AckDecorator class', () => { - const transformed = JsonTransformer.fromJSON({ '~please_ack': {} }, TestMessage); - - expect(transformed).toEqual({ pleaseAck: {} }); - expect(transformed).toBeInstanceOf(TestMessage); - }); -}); diff --git a/src/lib/decorators/ack/AckDecorator.ts b/src/lib/decorators/ack/AckDecorator.ts deleted file mode 100644 index cb04be571a..0000000000 --- a/src/lib/decorators/ack/AckDecorator.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Represents `~please_ack` decorator - */ -export class AckDecorator {} diff --git a/src/lib/decorators/ack/AckDecoratorExtension.ts b/src/lib/decorators/ack/AckDecoratorExtension.ts deleted file mode 100644 index 5507490b87..0000000000 --- a/src/lib/decorators/ack/AckDecoratorExtension.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Expose, Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { BaseMessageConstructor } from '../../agent/BaseMessage'; -import { AckDecorator } from './AckDecorator'; - -export function AckDecorated(Base: T) { - class AckDecoratorExtension extends Base { - @Expose({ name: '~please_ack' }) - @Type(() => AckDecorator) - @ValidateNested() - public pleaseAck?: AckDecorator; - - public setPleaseAck() { - this.pleaseAck = new AckDecorator(); - } - - public getPleaseAck(): AckDecorator | undefined { - return this.pleaseAck; - } - - public requiresAck(): boolean { - return this.pleaseAck !== undefined; - } - } - - return AckDecoratorExtension; -} diff --git a/src/lib/decorators/attachment/Attachment.ts b/src/lib/decorators/attachment/Attachment.ts deleted file mode 100644 index 7538201122..0000000000 --- a/src/lib/decorators/attachment/Attachment.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Expose, Type } from 'class-transformer'; -import { IsBase64, IsDate, IsHash, IsInt, IsMimeType, IsOptional, IsString, ValidateNested } from 'class-validator'; -import { v4 as uuid } from 'uuid'; - -export interface AttachmentOptions { - id?: string; - description?: string; - filename?: string; - mimeType?: string; - lastmodTime?: number; - byteCount?: number; - data: AttachmentData; -} - -export interface AttachmentDataOptions { - base64?: string; - json?: Record; - links?: []; - jws?: Record; - sha256?: string; -} - -/** - * A JSON object that gives access to the actual content of the attachment - */ -export class AttachmentData { - public constructor(options: AttachmentDataOptions) { - if (options) { - this.base64 = options.base64; - this.json = options.json; - this.links = options.links; - this.jws = options.jws; - this.sha256 = options.sha256; - } - } - - /** - * Base64-encoded data, when representing arbitrary content inline instead of via links. Optional. - */ - @IsOptional() - @IsBase64() - public base64?: string; - - /** - * Directly embedded JSON data, when representing content inline instead of via links, and when the content is natively conveyable as JSON. Optional. - */ - @IsOptional() - public json?: Record; - - /** - * A list of zero or more locations at which the content may be fetched. Optional. - */ - @IsOptional() - @IsString({ each: true }) - public links?: string[]; - - /** - * A JSON Web Signature over the content of the attachment. Optional. - */ - @IsOptional() - public jws?: Record; - - /** - * The hash of the content. Optional. - */ - @IsOptional() - @IsHash('sha256') - public sha256?: string; -} - -/** - * Represents DIDComm attachment - * https://github.com/hyperledger/aries-rfcs/blob/master/concepts/0017-attachments/README.md - */ -export class Attachment { - public constructor(options: AttachmentOptions) { - if (options) { - this.id = options.id ?? uuid(); - this.description = options.description; - this.filename = options.filename; - this.mimeType = options.mimeType; - this.lastmodTime = options.lastmodTime; - this.byteCount = options.byteCount; - this.data = options.data; - } - } - - @Expose({ name: '@id' }) - public id!: string; - - /** - * An optional human-readable description of the content. - */ - @IsOptional() - @IsString() - public description?: string; - - /** - * A hint about the name that might be used if this attachment is persisted as a file. It is not required, and need not be unique. If this field is present and mime-type is not, the extension on the filename may be used to infer a MIME type. - */ - @IsOptional() - @IsString() - public filename?: string; - - /** - * Describes the MIME type of the attached content. Optional but recommended. - */ - @Expose({ name: 'mime-type' }) - @IsOptional() - @IsMimeType() - public mimeType?: string; - - /** - * A hint about when the content in this attachment was last modified. - */ - @Expose({ name: 'lastmod_time' }) - @Type(() => Date) - @IsOptional() - @IsDate() - public lastmodTime?: number; - - /** - * Optional, and mostly relevant when content is included by reference instead of by value. Lets the receiver guess how expensive it will be, in time, bandwidth, and storage, to fully fetch the attachment. - */ - @Expose({ name: 'byte_count' }) - @IsOptional() - @IsInt() - public byteCount?: number; - - @Type(() => AttachmentData) - @ValidateNested() - public data!: AttachmentData; -} diff --git a/src/lib/decorators/l10n/L10nDecorator.test.ts b/src/lib/decorators/l10n/L10nDecorator.test.ts deleted file mode 100644 index 5827a8d055..0000000000 --- a/src/lib/decorators/l10n/L10nDecorator.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { JsonTransformer } from '../../utils/JsonTransformer'; - -import { L10nDecorator } from './L10nDecorator'; - -describe('Decorators | L10nDecorator', () => { - it('should correctly transform Json to L10nDecorator class', () => { - const locale = 'en'; - const decorator = JsonTransformer.fromJSON({ locale }, L10nDecorator); - - expect(decorator.locale).toBe(locale); - }); - - it('should correctly transform L10nDecorator class to Json', () => { - const locale = 'nl'; - - const decorator = new L10nDecorator({ - locale, - }); - - const json = JsonTransformer.toJSON(decorator); - const transformed = { - locale, - }; - - expect(json).toEqual(transformed); - }); -}); diff --git a/src/lib/decorators/l10n/L10nDecorator.ts b/src/lib/decorators/l10n/L10nDecorator.ts deleted file mode 100644 index 684890d386..0000000000 --- a/src/lib/decorators/l10n/L10nDecorator.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Represents `~l10n` decorator - */ -export class L10nDecorator { - public constructor(partial?: Partial) { - this.locale = partial?.locale; - } - - public locale?: string; -} diff --git a/src/lib/decorators/l10n/L10nDecoratorExtension.ts b/src/lib/decorators/l10n/L10nDecoratorExtension.ts deleted file mode 100644 index 455cf64aac..0000000000 --- a/src/lib/decorators/l10n/L10nDecoratorExtension.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Expose, Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { BaseMessageConstructor } from '../../agent/BaseMessage'; -import { L10nDecorator } from './L10nDecorator'; - -export function L10nDecorated(Base: T) { - class L10nDecoratorExtension extends Base { - @Expose({ name: '~l10n' }) - @Type(() => L10nDecorator) - @ValidateNested() - public l10n?: L10nDecorator; - - public addLocale(locale: string) { - this.l10n = new L10nDecorator({ - locale, - }); - } - - public getLocale(): string | undefined { - if (this.l10n?.locale) return this.l10n.locale; - - return undefined; - } - } - - return L10nDecoratorExtension; -} diff --git a/src/lib/decorators/signature/SignatureDecorator.ts b/src/lib/decorators/signature/SignatureDecorator.ts deleted file mode 100644 index 569ac8457e..0000000000 --- a/src/lib/decorators/signature/SignatureDecorator.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Expose } from 'class-transformer'; -import { Matches } from 'class-validator'; - -import { MessageTypeRegExp } from '../../agent/BaseMessage'; - -/** - * Represents `[field]~sig` decorator - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0234-signature-decorator/README.md - */ -export class SignatureDecorator { - public constructor(options: SignatureDecorator) { - if (options) { - this.signatureType = options.signatureType; - this.signatureData = options.signatureData; - this.signer = options.signer; - this.signature = options.signature; - } - } - - @Expose({ name: '@type' }) - @Matches(MessageTypeRegExp) - public signatureType!: string; - - @Expose({ name: 'sig_data' }) - public signatureData!: string; - - @Expose({ name: 'signer' }) - public signer!: string; - - @Expose({ name: 'signature' }) - public signature!: string; -} diff --git a/src/lib/decorators/signature/SignatureDecoratorUtils.test.ts b/src/lib/decorators/signature/SignatureDecoratorUtils.test.ts deleted file mode 100644 index 7efb497fc2..0000000000 --- a/src/lib/decorators/signature/SignatureDecoratorUtils.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import indy from 'indy-sdk'; -import { signData, unpackAndVerifySignatureDecorator } from './SignatureDecoratorUtils'; -import { IndyWallet } from '../../wallet/IndyWallet'; -import { SignatureDecorator } from './SignatureDecorator'; -import { AgentConfig } from '../../agent/AgentConfig'; - -jest.mock('../../utils/timestamp', () => { - return { - __esModule: true, - default: jest.fn(() => Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0)), - }; -}); - -describe('Decorators | Signature | SignatureDecoratorUtils', () => { - const walletConfig = { id: 'wallet-1' + 'test1' }; - const walletCredentials = { key: 'key' }; - - const data = { - did: 'did', - did_doc: { - '@context': 'https://w3id.org/did/v1', - service: [ - { - id: 'did:example:123456789abcdefghi#did-communication', - type: 'did-communication', - priority: 0, - recipientKeys: ['someVerkey'], - routingKeys: [], - serviceEndpoint: 'https://agent.example.com/', - }, - ], - }, - }; - - const signedData = new SignatureDecorator({ - signatureType: 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/signature/1.0/ed25519Sha512_single', - signature: 'zOSmKNCHKqOJGDJ6OlfUXTPJiirEAXrFn1kPiFDZfvG5hNTBKhsSzqAvlg44apgWBu7O57vGWZsXBF2BWZ5JAw', - signatureData: - 'AAAAAAAAAAB7ImRpZCI6ImRpZCIsImRpZF9kb2MiOnsiQGNvbnRleHQiOiJodHRwczovL3czaWQub3JnL2RpZC92MSIsInNlcnZpY2UiOlt7ImlkIjoiZGlkOmV4YW1wbGU6MTIzNDU2Nzg5YWJjZGVmZ2hpI2RpZC1jb21tdW5pY2F0aW9uIiwidHlwZSI6ImRpZC1jb21tdW5pY2F0aW9uIiwicHJpb3JpdHkiOjAsInJlY2lwaWVudEtleXMiOlsic29tZVZlcmtleSJdLCJyb3V0aW5nS2V5cyI6W10sInNlcnZpY2VFbmRwb2ludCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20vIn1dfX0', - signer: 'GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa', - }); - - let wallet: IndyWallet; - - beforeAll(async () => { - wallet = new IndyWallet( - new AgentConfig({ - walletConfig, - walletCredentials, - indy, - label: 'test', - }) - ); - await wallet.init(); - }); - - afterAll(async () => { - await wallet.close(); - await wallet.delete(); - }); - - test('signData signs json object and returns SignatureDecorator', async () => { - const seed1 = '00000000000000000000000000000My1'; - const [, verkey] = await wallet.createDid({ seed: seed1 }); - - const result = await signData(data, wallet, verkey); - - expect(result).toEqual(signedData); - }); - - test('unpackAndVerifySignatureDecorator unpacks signature decorator and verifies signature', async () => { - const result = await unpackAndVerifySignatureDecorator(signedData, wallet); - expect(result).toEqual(data); - }); - - test('unpackAndVerifySignatureDecorator throws when signature is not valid', async () => { - const wrongSignature = '6sblL1+OMlTFB3KhIQ8HKKZga8te7NAJAmBVPg2WzNYjMHVjfm+LJP6ZS1GUc2FRtfczRyLEfXrXb86SnzBmBA=='; - - const wronglySignedData = new SignatureDecorator({ - ...signedData, - signature: wrongSignature, - }); - - expect.assertions(1); - try { - await unpackAndVerifySignatureDecorator(wronglySignedData, wallet); - } catch (error) { - expect(error.message).toEqual('Signature is not valid!'); - } - }); -}); diff --git a/src/lib/decorators/signature/SignatureDecoratorUtils.ts b/src/lib/decorators/signature/SignatureDecoratorUtils.ts deleted file mode 100644 index 83f8242128..0000000000 --- a/src/lib/decorators/signature/SignatureDecoratorUtils.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { Verkey } from 'indy-sdk'; -import { SignatureDecorator } from './SignatureDecorator'; -import timestamp from '../../utils/timestamp'; -import { Wallet } from '../../wallet/Wallet'; -import { Buffer } from '../../utils/buffer'; -import { JsonEncoder } from '../../utils/JsonEncoder'; -import { BufferEncoder } from '../../utils/BufferEncoder'; - -/** - * Unpack and verify signed data before casting it to the supplied type. - * - * @param decorator Signature decorator to unpack and verify - * @param wallet wallet instance - * - * @return Resulting data - */ -export async function unpackAndVerifySignatureDecorator( - decorator: SignatureDecorator, - wallet: Wallet -): Promise> { - const signerVerkey = decorator.signer; - - // first 8 bytes are for 64 bit integer from unix epoch - const signedData = BufferEncoder.fromBase64(decorator.signatureData); - const signature = BufferEncoder.fromBase64(decorator.signature); - - const isValid = await wallet.verify(signerVerkey, signedData, signature); - - if (!isValid) { - throw new Error('Signature is not valid!'); - } - - // TODO: return Connection instance instead of raw json - return JsonEncoder.fromBuffer(signedData.slice(8)); -} - -/** - * Sign data supplied and return a signature decorator. - * - * @param data the data to sign - * @param wallet the wallet contianing a key to use for signing - * @param signerKey signers verkey - * - * @returns Resulting signature decorator. - */ -export async function signData(data: unknown, wallet: Wallet, signerKey: Verkey): Promise { - const dataBuffer = Buffer.concat([timestamp(), JsonEncoder.toBuffer(data)]); - - const signatureBuffer = await wallet.sign(dataBuffer, signerKey); - - const signatureDecorator = new SignatureDecorator({ - signatureType: 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/signature/1.0/ed25519Sha512_single', - signature: BufferEncoder.toBase64URL(signatureBuffer), - signatureData: BufferEncoder.toBase64URL(dataBuffer), - signer: signerKey, - }); - - return signatureDecorator; -} diff --git a/src/lib/decorators/thread/ThreadDecorator.test.ts b/src/lib/decorators/thread/ThreadDecorator.test.ts deleted file mode 100644 index a8b1ac9e4c..0000000000 --- a/src/lib/decorators/thread/ThreadDecorator.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { JsonTransformer } from '../../utils/JsonTransformer'; -import { ThreadDecorator } from './ThreadDecorator'; - -describe('Decorators | ThreadDecorator', () => { - it('should correctly transform Json to ThreadDecorator class', () => { - const json = { - thid: 'ceffce22-6471-43e4-8945-b604091981c9', - pthid: '917a109d-eae3-42bc-9436-b02426d3ce2c', - sender_order: 2, - received_orders: { - 'did:sov:3ecf688c-cb3f-467b-8636-6b0c7f1d9022': 1, - }, - }; - const decorator = JsonTransformer.fromJSON(json, ThreadDecorator); - - expect(decorator.threadId).toBe(json.thid); - expect(decorator.parentThreadId).toBe(json.pthid); - expect(decorator.senderOrder).toBe(json.sender_order); - expect(decorator.receivedOrders).toEqual(json.received_orders); - }); - - it('should correctly transform ThreadDecorator class to Json', () => { - const threadId = 'ceffce22-6471-43e4-8945-b604091981c9'; - const parentThreadId = '917a109d-eae3-42bc-9436-b02426d3ce2c'; - const senderOrder = 2; - const receivedOrders = { - 'did:sov:3ecf688c-cb3f-467b-8636-6b0c7f1d9022': 1, - }; - - const decorator = new ThreadDecorator({ - threadId, - parentThreadId, - senderOrder, - receivedOrders, - }); - - const json = JsonTransformer.toJSON(decorator); - const transformed = { - thid: threadId, - pthid: parentThreadId, - sender_order: senderOrder, - received_orders: receivedOrders, - }; - - expect(json).toEqual(transformed); - }); -}); diff --git a/src/lib/decorators/thread/ThreadDecorator.ts b/src/lib/decorators/thread/ThreadDecorator.ts deleted file mode 100644 index 682414cb27..0000000000 --- a/src/lib/decorators/thread/ThreadDecorator.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Expose } from 'class-transformer'; -import { Matches } from 'class-validator'; -import { MessageIdRegExp } from '../../agent/BaseMessage'; - -/** - * Represents `~thread` decorator - * @see https://github.com/hyperledger/aries-rfcs/blob/master/concepts/0008-message-id-and-threading/README.md - */ -export class ThreadDecorator { - public constructor(partial?: Partial) { - this.threadId = partial?.threadId; - this.parentThreadId = partial?.parentThreadId; - this.senderOrder = partial?.senderOrder; - this.receivedOrders = partial?.receivedOrders; - } - - /** - * The ID of the message that serves as the thread start. - */ - @Expose({ name: 'thid' }) - @Matches(MessageIdRegExp) - public threadId?: string; - - /** - * An optional parent `thid`. Used when branching or nesting a new interaction off of an existing one. - */ - @Expose({ name: 'pthid' }) - @Matches(MessageIdRegExp) - public parentThreadId?: string; - - /** - * A number that tells where this message fits in the sequence of all messages that the current sender has contributed to this thread. - */ - @Expose({ name: 'sender_order' }) - public senderOrder?: number; - - /** - * Reports the highest `sender_order` value that the sender has seen from other sender(s) on the thread. - * This value is often missing if it is the first message in an interaction, but should be used otherwise, as it provides an implicit ACK. - */ - @Expose({ name: 'received_orders' }) - public receivedOrders?: { [key: string]: number }; -} diff --git a/src/lib/decorators/thread/ThreadDecoratorExtension.ts b/src/lib/decorators/thread/ThreadDecoratorExtension.ts deleted file mode 100644 index 334934fb9f..0000000000 --- a/src/lib/decorators/thread/ThreadDecoratorExtension.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Expose, Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { BaseMessageConstructor } from '../../agent/BaseMessage'; -import { ThreadDecorator } from './ThreadDecorator'; - -export function ThreadDecorated(Base: T) { - class ThreadDecoratorExtension extends Base { - /** - * The ~thread decorator is generally required on any type of response, since this is what connects it with the original request. - */ - @Expose({ name: '~thread' }) - @Type(() => ThreadDecorator) - @ValidateNested() - public thread?: ThreadDecorator; - - public get threadId(): string { - return this.thread?.threadId ?? this.id; - } - - public setThread(options: Partial) { - this.thread = new ThreadDecorator(options); - } - } - - return ThreadDecoratorExtension; -} diff --git a/src/lib/decorators/timing/TimingDecorator.test.ts b/src/lib/decorators/timing/TimingDecorator.test.ts deleted file mode 100644 index c6a09bc5e6..0000000000 --- a/src/lib/decorators/timing/TimingDecorator.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { JsonTransformer } from '../../utils/JsonTransformer'; -import { TimingDecorator } from './TimingDecorator'; - -describe('Decorators | TimingDecorator', () => { - it('should correctly transform Json to TimingDecorator class', () => { - const json = { - in_time: '2019-01-23 18:03:27.123Z', - out_time: '2019-01-23 18:03:27.123Z', - stale_time: '2019-01-24 18:25Z', - expires_time: '2019-01-25 18:25Z', - delay_milli: 12345, - wait_until_time: '2019-01-24 00:00Z', - }; - const decorator = JsonTransformer.fromJSON(json, TimingDecorator); - - expect(decorator.inTime).toBeInstanceOf(Date); - expect(decorator.outTime).toBeInstanceOf(Date); - expect(decorator.staleTime).toBeInstanceOf(Date); - expect(decorator.expiresTime).toBeInstanceOf(Date); - expect(decorator.delayMilli).toBe(json.delay_milli); - expect(decorator.waitUntilTime).toBeInstanceOf(Date); - }); - - it('should correctly transform TimingDecorator class to Json', () => { - const inTime = new Date('2019-01-23 18:03:27.123Z'); - const outTime = new Date('2019-01-23 18:03:27.123Z'); - const staleTime = new Date('2019-01-24 18:25:00.000Z'); - const expiresTime = new Date('2019-01-25 18:25:00:000Z'); - const delayMilli = 12345; - const waitUntilTime = new Date('2019-01-24 00:00:00.000Z'); - - const decorator = new TimingDecorator({ - inTime, - outTime, - staleTime, - expiresTime, - delayMilli, - waitUntilTime, - }); - - const jsonString = JsonTransformer.serialize(decorator); - const transformed = JSON.stringify({ - in_time: '2019-01-23T18:03:27.123Z', - out_time: '2019-01-23T18:03:27.123Z', - stale_time: '2019-01-24T18:25:00.000Z', - expires_time: '2019-01-25T18:25:00.000Z', - delay_milli: 12345, - wait_until_time: '2019-01-24T00:00:00.000Z', - }); - - expect(jsonString).toEqual(transformed); - }); -}); diff --git a/src/lib/decorators/timing/TimingDecoratorExtension.ts b/src/lib/decorators/timing/TimingDecoratorExtension.ts deleted file mode 100644 index eb97d99e36..0000000000 --- a/src/lib/decorators/timing/TimingDecoratorExtension.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Expose, Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { BaseMessageConstructor } from '../../agent/BaseMessage'; -import { TimingDecorator } from './TimingDecorator'; - -export function TimingDecorated(Base: T) { - class TimingDecoratorExtension extends Base { - /** - * Timing attributes of messages can be described with the ~timing decorator. - */ - @Expose({ name: '~timing' }) - @Type(() => TimingDecorator) - @ValidateNested() - public timing?: TimingDecorator; - - public setTiming(options: Partial) { - this.timing = new TimingDecorator(options); - } - } - - return TimingDecoratorExtension; -} diff --git a/src/lib/decorators/transport/TransportDecorator.test.ts b/src/lib/decorators/transport/TransportDecorator.test.ts deleted file mode 100644 index 06512c9e07..0000000000 --- a/src/lib/decorators/transport/TransportDecorator.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { TransportDecorator, ReturnRouteTypes } from './TransportDecorator'; -import { validateOrReject } from 'class-validator'; -import { JsonTransformer } from '../../utils/JsonTransformer'; - -const validTranport = (obj: Record) => - validateOrReject(JsonTransformer.fromJSON(obj, TransportDecorator)); -const expectValid = (obj: Record) => expect(validTranport(obj)).resolves.toBeUndefined(); -const expectInvalid = (obj: Record) => expect(validTranport(obj)).rejects.not.toBeNull(); - -const valid = { - all: { - return_route: 'all', - }, - none: { - return_route: 'none', - }, - thread: { - return_route: 'thread', - return_route_thread: '7d5d797c-db60-489f-8787-87bbd1acdb7e', - }, -}; - -const invalid = { - random: { - return_route: 'random', - }, - invalidThreadId: { - return_route: 'thread', - return_route_thread: 'invalid', - }, - missingThreadId: { - return_route: 'thread', - }, -}; - -describe('Decorators | TransportDecorator', () => { - it('should correctly transform Json to TransportDecorator class', () => { - const decorator = JsonTransformer.fromJSON(valid.thread, TransportDecorator); - - expect(decorator.returnRoute).toBe(valid.thread.return_route); - expect(decorator.returnRouteThread).toBe(valid.thread.return_route_thread); - }); - - it('should correctly transform TransportDecorator class to Json', () => { - const id = 'f6ce6225-087b-46c1-834a-3e7e24116a00'; - const decorator = new TransportDecorator({ - returnRoute: ReturnRouteTypes.thread, - returnRouteThread: id, - }); - - const json = JsonTransformer.toJSON(decorator); - const transformed = { - return_route: 'thread', - return_route_thread: id, - }; - - expect(json).toEqual(transformed); - }); - - it('should only allow correct return_route values', async () => { - expect.assertions(4); - await expectValid(valid.all); - await expectValid(valid.none); - await expectValid(valid.thread); - await expectInvalid(invalid.random); - }); - - it('should require return_route_thread when return_route is thread', async () => { - expect.assertions(3); - await expectValid(valid.thread); - await expectInvalid(invalid.invalidThreadId); - await expectInvalid(invalid.missingThreadId); - }); -}); diff --git a/src/lib/decorators/transport/TransportDecorator.ts b/src/lib/decorators/transport/TransportDecorator.ts deleted file mode 100644 index 323f462f62..0000000000 --- a/src/lib/decorators/transport/TransportDecorator.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Expose } from 'class-transformer'; -import { IsEnum, ValidateIf, Matches } from 'class-validator'; - -import { MessageIdRegExp } from '../../agent/BaseMessage'; - -/** - * Return route types. - */ -export enum ReturnRouteTypes { - /** No messages should be returned over this connection. */ - none = 'none', - /** All messages for this key should be returned over this connection. */ - all = 'all', - /** Send all messages matching this thread over this connection. */ - thread = 'thread', -} - -/** - * Represents `~transport` decorator - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0092-transport-return-route/README.md - */ -export class TransportDecorator { - public constructor(partial?: Partial) { - this.returnRoute = partial?.returnRoute; - this.returnRouteThread = partial?.returnRouteThread; - } - - @Expose({ name: 'return_route' }) - @IsEnum(ReturnRouteTypes) - public returnRoute?: ReturnRouteTypes; - - @Expose({ name: 'return_route_thread' }) - @ValidateIf((o: TransportDecorator) => o.returnRoute === ReturnRouteTypes.thread) - @Matches(MessageIdRegExp) - public returnRouteThread?: string; -} diff --git a/src/lib/decorators/transport/TransportDecoratorExtension.ts b/src/lib/decorators/transport/TransportDecoratorExtension.ts deleted file mode 100644 index d7bee3abe7..0000000000 --- a/src/lib/decorators/transport/TransportDecoratorExtension.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Expose, Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; -import { TransportDecorator, ReturnRouteTypes } from './TransportDecorator'; -import { BaseMessageConstructor } from '../../agent/BaseMessage'; - -export function TransportDecorated(Base: T) { - class TransportDecoratorExtension extends Base { - @Expose({ name: '~transport' }) - @Type(() => TransportDecorator) - @ValidateNested() - public transport?: TransportDecorator; - - public setReturnRouting(type: ReturnRouteTypes, thread?: string) { - this.transport = new TransportDecorator({ - returnRoute: type, - returnRouteThread: thread, - }); - } - - public hasReturnRouting(threadId?: string): boolean { - // transport 'none' or undefined always false - if (!this.transport || this.transport.returnRoute === ReturnRouteTypes.none) return false; - // transport 'all' always true - else if (this.transport.returnRoute === ReturnRouteTypes.all) return true; - // transport 'thread' with matching thread id is true - else if (this.transport.returnRoute === ReturnRouteTypes.thread && this.transport.returnRouteThread === threadId) - return true; - - // transport is thread but threadId is either missing or doesn't match. Return false - return false; - } - } - - return TransportDecoratorExtension; -} diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts deleted file mode 100644 index e25448bff6..0000000000 --- a/src/lib/helpers.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { validateOrReject } from 'class-validator'; - -import { ConnectionInvitationMessage } from './modules/connections'; -import { JsonEncoder } from './utils/JsonEncoder'; -import { JsonTransformer } from './utils/JsonTransformer'; - -/** - * Create a `ConnectionInvitationMessage` instance from the `c_i` parameter of an URL - * - * @param invitationUrl invitation url containing c_i parameter - * - * @throws Error when url can not be decoded to JSON, or decoded message is not a valid `ConnectionInvitationMessage` - */ -export async function decodeInvitationFromUrl(invitationUrl: string): Promise { - // TODO: properly extract c_i param from invitation URL - const [, encodedInvitation] = invitationUrl.split('c_i='); - const invitationJson = JsonEncoder.fromBase64(encodedInvitation); - - const invitation = JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage); - - // TODO: should validation happen here? - await validateOrReject(invitation); - - return invitation; -} - -/** - * Create an invitation url from this instance - * - * @param invitation invitation message - * @param domain domain name to use for invitation url - */ -export function encodeInvitationToUrl( - invitation: ConnectionInvitationMessage, - domain = 'https://example.com/ssi' -): string { - const invitationJson = JsonTransformer.toJSON(invitation); - const encodedInvitation = JsonEncoder.toBase64URL(invitationJson); - const invitationUrl = `${domain}?c_i=${encodedInvitation}`; - - return invitationUrl; -} diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index cca9a745b5..0000000000 --- a/src/lib/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -// reflect-metadata used for class-transfomer + class-validator -import 'reflect-metadata'; - -export { Agent } from './agent/Agent'; -export { InboundTransporter } from './transport/InboundTransporter'; -export { OutboundTransporter } from './transport/OutboundTransporter'; -export { encodeInvitationToUrl, decodeInvitationFromUrl } from './helpers'; -export { InitConfig, OutboundPackage } from './types'; - -export * from './modules/basic-messages'; -export * from './modules/credentials'; -export * from './modules/proofs'; -export * from './modules/connections'; -export * from './utils/JsonTransformer'; -export * from './logger'; diff --git a/src/lib/logger/BaseLogger.ts b/src/lib/logger/BaseLogger.ts deleted file mode 100644 index 3000e1fc60..0000000000 --- a/src/lib/logger/BaseLogger.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Logger, LogLevel } from './Logger'; - -export abstract class BaseLogger implements Logger { - public logLevel: LogLevel; - - public constructor(logLevel: LogLevel = LogLevel.off) { - this.logLevel = logLevel; - } - - public isEnabled(logLevel: LogLevel) { - return logLevel >= this.logLevel; - } - - public abstract test(message: string, data?: Record): void; - public abstract trace(message: string, data?: Record): void; - public abstract debug(message: string, data?: Record): void; - public abstract info(message: string, data?: Record): void; - public abstract warn(message: string, data?: Record): void; - public abstract error(message: string, data?: Record): void; - public abstract fatal(message: string, data?: Record): void; -} diff --git a/src/lib/logger/ConsoleLogger.ts b/src/lib/logger/ConsoleLogger.ts deleted file mode 100644 index 1f0d4ade71..0000000000 --- a/src/lib/logger/ConsoleLogger.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable no-console,@typescript-eslint/no-explicit-any */ - -import { BaseLogger } from './BaseLogger'; -import { LogLevel } from './Logger'; - -export class ConsoleLogger extends BaseLogger { - // Map our log levels to console levels - private consoleLogMap = { - [LogLevel.test]: 'log', - [LogLevel.trace]: 'log', - [LogLevel.debug]: 'debug', - [LogLevel.info]: 'info', - [LogLevel.warn]: 'warn', - [LogLevel.error]: 'error', - [LogLevel.fatal]: 'error', - } as const; - - private log(level: Exclude, message: string, data?: Record): void { - // Get console method from mapping - const consoleLevel = this.consoleLogMap[level]; - - // Get logger prefix from log level names in enum - const prefix = LogLevel[level].toUpperCase(); - - // Return early if logging is not enabled for this level - if (!this.isEnabled(level)) return; - - // Log, with or without data - if (data) { - console[consoleLevel](`${prefix}: ${message}`, JSON.stringify(data, null, 2)); - } else { - console[consoleLevel](`${prefix}: ${message}`); - } - } - - public test(message: string, data?: Record): void { - this.log(LogLevel.test, message, data); - } - - public trace(message: string, data?: Record): void { - this.log(LogLevel.trace, message, data); - } - - public debug(message: string, data?: Record): void { - this.log(LogLevel.debug, message, data); - } - - public info(message: string, data?: Record): void { - this.log(LogLevel.info, message, data); - } - - public warn(message: string, data?: Record): void { - this.log(LogLevel.warn, message, data); - } - - public error(message: string, data?: Record): void { - this.log(LogLevel.error, message, data); - } - - public fatal(message: string, data?: Record): void { - this.log(LogLevel.fatal, message, data); - } -} diff --git a/src/lib/logger/Logger.ts b/src/lib/logger/Logger.ts deleted file mode 100644 index 51e8d16928..0000000000 --- a/src/lib/logger/Logger.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export enum LogLevel { - test = 0, - trace = 1, - debug = 2, - info = 3, - warn = 4, - error = 5, - fatal = 6, - off = 7, -} - -export interface Logger { - logLevel: LogLevel; - - test(message: string, data?: Record): void; - trace(message: string, data?: Record): void; - debug(message: string, data?: Record): void; - info(message: string, data?: Record): void; - warn(message: string, data?: Record): void; - error(message: string, data?: Record): void; - fatal(message: string, data?: Record): void; -} diff --git a/src/lib/logger/index.ts b/src/lib/logger/index.ts deleted file mode 100644 index 5323ebad6c..0000000000 --- a/src/lib/logger/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './ConsoleLogger'; -export * from './BaseLogger'; -export * from './Logger'; diff --git a/src/lib/modules/basic-messages/BasicMessagesModule.ts b/src/lib/modules/basic-messages/BasicMessagesModule.ts deleted file mode 100644 index 4b0b20d249..0000000000 --- a/src/lib/modules/basic-messages/BasicMessagesModule.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { WalletQuery } from 'indy-sdk'; -import { EventEmitter } from 'events'; -import { BasicMessageService } from './services'; -import { MessageSender } from '../../agent/MessageSender'; -import { ConnectionRecord } from '../connections'; -import { Dispatcher } from '../../agent/Dispatcher'; -import { BasicMessageHandler } from './handlers'; - -export class BasicMessagesModule { - private basicMessageService: BasicMessageService; - private messageSender: MessageSender; - - public constructor(dispatcher: Dispatcher, basicMessageService: BasicMessageService, messageSender: MessageSender) { - this.basicMessageService = basicMessageService; - this.messageSender = messageSender; - this.registerHandlers(dispatcher); - } - - /** - * Get the event emitter for the basic message service. Will emit message received events - * when basic messages are received. - * - * @returns event emitter for basic message related events - */ - public get events(): EventEmitter { - return this.basicMessageService; - } - - public async sendMessage(connection: ConnectionRecord, message: string) { - const outboundMessage = await this.basicMessageService.send(message, connection); - await this.messageSender.sendMessage(outboundMessage); - } - - public async findAllByQuery(query: WalletQuery) { - return this.basicMessageService.findAllByQuery(query); - } - - private registerHandlers(dispatcher: Dispatcher) { - dispatcher.registerHandler(new BasicMessageHandler(this.basicMessageService)); - } -} diff --git a/src/lib/modules/basic-messages/__tests__/BasicMessageService.test.ts b/src/lib/modules/basic-messages/__tests__/BasicMessageService.test.ts deleted file mode 100644 index 6acb4bfc12..0000000000 --- a/src/lib/modules/basic-messages/__tests__/BasicMessageService.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import indy from 'indy-sdk'; -import { IndyWallet } from '../../../wallet/IndyWallet'; -import { Wallet } from '../../../wallet/Wallet'; -import { Repository } from '../../../storage/Repository'; -import { StorageService } from '../../../storage/StorageService'; -import { IndyStorageService } from '../../../storage/IndyStorageService'; -import { BasicMessageService, BasicMessageEventType } from '../services'; -import { BasicMessageRecord } from '../repository/BasicMessageRecord'; -import { BasicMessage } from '../messages'; -import { InboundMessageContext } from '../../../agent/models/InboundMessageContext'; -import { ConnectionRecord } from '../../connections'; -import { AgentConfig } from '../../../agent/AgentConfig'; - -describe('BasicMessageService', () => { - const walletConfig = { id: 'test-wallet' + '-BasicMessageServiceTest' }; - const walletCredentials = { key: 'key' }; - const mockConnectionRecord = { - id: 'd3849ac3-c981-455b-a1aa-a10bea6cead8', - verkey: '71X9Y1aSPK11ariWUYQCYMjSewf2Kw2JFGeygEf9uZd9', - did: 'did:sov:C2SsBf5QUQpqSAQfhu3sd2', - didDoc: {}, - tags: {}, - }; - - let wallet: Wallet; - let storageService: StorageService; - - beforeAll(async () => { - wallet = new IndyWallet( - new AgentConfig({ - walletConfig, - walletCredentials, - indy, - label: 'test', - }) - ); - await wallet.init(); - storageService = new IndyStorageService(wallet); - }); - - afterAll(async () => { - await wallet.close(); - await wallet.delete(); - }); - - describe('save', () => { - let basicMessageRepository: Repository; - let basicMessageService: BasicMessageService; - - beforeEach(() => { - basicMessageRepository = new Repository(BasicMessageRecord, storageService); - basicMessageService = new BasicMessageService(basicMessageRepository); - }); - - it(`emits newMessage with connection verkey and message itself`, async () => { - const eventListenerMock = jest.fn(); - basicMessageService.on(BasicMessageEventType.MessageReceived, eventListenerMock); - - const basicMessage = new BasicMessage({ - id: '123', - content: 'message', - }); - - const messageContext = new InboundMessageContext(basicMessage, { - senderVerkey: 'senderKey', - recipientVerkey: 'recipientKey', - }); - - // TODO - // Currently, it's not so easy to create instance of ConnectionRecord object. - // We use simple `mockConnectionRecord` as ConnectionRecord type - await basicMessageService.save(messageContext, mockConnectionRecord as ConnectionRecord); - - expect(eventListenerMock).toHaveBeenCalledWith({ - verkey: mockConnectionRecord.verkey, - message: messageContext.message, - }); - }); - }); -}); diff --git a/src/lib/modules/basic-messages/handlers/BasicMessageHandler.ts b/src/lib/modules/basic-messages/handlers/BasicMessageHandler.ts deleted file mode 100644 index 83665b5266..0000000000 --- a/src/lib/modules/basic-messages/handlers/BasicMessageHandler.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { BasicMessageService } from '../services/BasicMessageService'; -import { BasicMessage } from '../messages'; - -export class BasicMessageHandler implements Handler { - private basicMessageService: BasicMessageService; - public supportedMessages = [BasicMessage]; - - public constructor(basicMessageService: BasicMessageService) { - this.basicMessageService = basicMessageService; - } - - public async handle(messageContext: HandlerInboundMessage) { - const connection = messageContext.connection; - - if (!connection) { - throw new Error(`Connection for verkey ${messageContext.recipientVerkey} not found!`); - } - - if (!connection.theirKey) { - throw new Error(`Connection with verkey ${connection.verkey} has no recipient keys.`); - } - - await this.basicMessageService.save(messageContext, connection); - } -} diff --git a/src/lib/modules/basic-messages/handlers/index.ts b/src/lib/modules/basic-messages/handlers/index.ts deleted file mode 100644 index 68437fecc6..0000000000 --- a/src/lib/modules/basic-messages/handlers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BasicMessageHandler'; diff --git a/src/lib/modules/basic-messages/index.ts b/src/lib/modules/basic-messages/index.ts deleted file mode 100644 index 08cce0ed46..0000000000 --- a/src/lib/modules/basic-messages/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './messages'; -export * from './services'; -export * from './repository/BasicMessageRecord'; diff --git a/src/lib/modules/basic-messages/messages/BasicMessage.ts b/src/lib/modules/basic-messages/messages/BasicMessage.ts deleted file mode 100644 index 3fee8a5651..0000000000 --- a/src/lib/modules/basic-messages/messages/BasicMessage.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Equals, IsDate, IsString } from 'class-validator'; -import { Expose, Type } from 'class-transformer'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { MessageType } from './BasicMessageMessageType'; - -export class BasicMessage extends AgentMessage { - /** - * Create new BasicMessage instance. - * sentTime will be assigned to new Date if not passed, id will be assigned to uuid/v4 if not passed - * @param options - */ - public constructor(options: { content: string; sentTime?: Date; id?: string; locale?: string }) { - super(); - - if (options) { - this.id = options.id || this.generateId(); - this.sentTime = options.sentTime || new Date(); - this.content = options.content; - this.addLocale(options.locale || 'en'); - } - } - - @Equals(BasicMessage.type) - public readonly type = BasicMessage.type; - public static readonly type = MessageType.BasicMessage; - - @Expose({ name: 'sent_time' }) - @Type(() => Date) - @IsDate() - public sentTime!: Date; - - @Expose({ name: 'content' }) - @IsString() - public content!: string; -} diff --git a/src/lib/modules/basic-messages/messages/BasicMessageMessageType.ts b/src/lib/modules/basic-messages/messages/BasicMessageMessageType.ts deleted file mode 100644 index f97d0430a6..0000000000 --- a/src/lib/modules/basic-messages/messages/BasicMessageMessageType.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum MessageType { - BasicMessage = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/basicmessage/1.0/message', -} diff --git a/src/lib/modules/basic-messages/messages/index.ts b/src/lib/modules/basic-messages/messages/index.ts deleted file mode 100644 index 66f3bc7d26..0000000000 --- a/src/lib/modules/basic-messages/messages/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './BasicMessage'; -export * from './BasicMessageMessageType'; diff --git a/src/lib/modules/basic-messages/repository/BasicMessageRecord.ts b/src/lib/modules/basic-messages/repository/BasicMessageRecord.ts deleted file mode 100644 index aeb9379906..0000000000 --- a/src/lib/modules/basic-messages/repository/BasicMessageRecord.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { v4 as uuid } from 'uuid'; -import { BaseRecord, RecordType, Tags } from '../../../storage/BaseRecord'; - -export interface BasicMessageStorageProps { - id?: string; - createdAt?: number; - tags: Tags; - - content: string; - sentTime: string; -} - -export class BasicMessageRecord extends BaseRecord implements BasicMessageStorageProps { - public content: string; - public sentTime: string; - - public static readonly type: RecordType = RecordType.BasicMessageRecord; - public readonly type = BasicMessageRecord.type; - - public constructor(props: BasicMessageStorageProps) { - super(props.id ?? uuid(), props.createdAt ?? Date.now()); - this.content = props.content; - this.sentTime = props.sentTime; - this.tags = props.tags; - } -} diff --git a/src/lib/modules/basic-messages/services/BasicMessageService.ts b/src/lib/modules/basic-messages/services/BasicMessageService.ts deleted file mode 100644 index feb7bc6a8c..0000000000 --- a/src/lib/modules/basic-messages/services/BasicMessageService.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { Verkey, WalletQuery } from 'indy-sdk'; -import { EventEmitter } from 'events'; -import { OutboundMessage } from '../../../types'; -import { createOutboundMessage } from '../../../agent/helpers'; -import { Repository } from '../../../storage/Repository'; -import { BasicMessageRecord } from '../repository/BasicMessageRecord'; -import { ConnectionRecord } from '../../connections/repository/ConnectionRecord'; -import { InboundMessageContext } from '../../../agent/models/InboundMessageContext'; -import { BasicMessage } from '../messages'; - -export enum BasicMessageEventType { - MessageReceived = 'messageReceived', -} - -export interface BasicMessageReceivedEvent { - message: BasicMessage; - verkey: Verkey; -} - -export class BasicMessageService extends EventEmitter { - private basicMessageRepository: Repository; - - public constructor(basicMessageRepository: Repository) { - super(); - this.basicMessageRepository = basicMessageRepository; - } - - public async send(message: string, connection: ConnectionRecord): Promise> { - const basicMessage = new BasicMessage({ - content: message, - }); - - const basicMessageRecord = new BasicMessageRecord({ - id: basicMessage.id, - sentTime: basicMessage.sentTime.toISOString(), - content: basicMessage.content, - tags: { from: connection.did || '', to: connection.theirDid || '' }, - }); - - await this.basicMessageRepository.save(basicMessageRecord); - return createOutboundMessage(connection, basicMessage); - } - - /** - * @todo use connection from message context - */ - public async save({ message }: InboundMessageContext, connection: ConnectionRecord) { - const basicMessageRecord = new BasicMessageRecord({ - id: message.id, - sentTime: message.sentTime.toISOString(), - content: message.content, - tags: { from: connection.theirDid || '', to: connection.did || '' }, - }); - - await this.basicMessageRepository.save(basicMessageRecord); - const event: BasicMessageReceivedEvent = { - message, - verkey: connection.verkey, - }; - this.emit(BasicMessageEventType.MessageReceived, event); - } - - public async findAllByQuery(query: WalletQuery) { - return this.basicMessageRepository.findByQuery(query); - } -} diff --git a/src/lib/modules/basic-messages/services/index.ts b/src/lib/modules/basic-messages/services/index.ts deleted file mode 100644 index e8e9932ea7..0000000000 --- a/src/lib/modules/basic-messages/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BasicMessageService'; diff --git a/src/lib/modules/common/index.ts b/src/lib/modules/common/index.ts deleted file mode 100644 index 135f0bc41a..0000000000 --- a/src/lib/modules/common/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './messages/AckMessage'; diff --git a/src/lib/modules/common/messages/AckMessage.ts b/src/lib/modules/common/messages/AckMessage.ts deleted file mode 100644 index 5120615551..0000000000 --- a/src/lib/modules/common/messages/AckMessage.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Equals, IsEnum } from 'class-validator'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { CommonMessageType } from './CommonMessageType'; - -/** - * Ack message status types - */ -export enum AckStatus { - OK = 'OK', - FAIL = 'FAIL', - PENDING = 'PENDING', -} - -export interface AckMessageOptions { - id?: string; - threadId: string; - status: AckStatus; -} - -/** - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0015-acks/README.md#explicit-acks - */ -export class AckMessage extends AgentMessage { - /** - * Create new AckMessage instance. - * @param options - */ - public constructor(options: AckMessageOptions) { - super(); - - if (options) { - this.id = options.id || this.generateId(); - this.status = options.status; - - this.setThread({ - threadId: options.threadId, - }); - } - } - - @Equals(AckMessage.type) - public readonly type: string = AckMessage.type; - public static readonly type: string = CommonMessageType.Ack; - - @IsEnum(AckStatus) - public status!: AckStatus; -} diff --git a/src/lib/modules/common/messages/CommonMessageType.ts b/src/lib/modules/common/messages/CommonMessageType.ts deleted file mode 100644 index a488938203..0000000000 --- a/src/lib/modules/common/messages/CommonMessageType.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum CommonMessageType { - Ack = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/notification/1.0/ack', -} diff --git a/src/lib/modules/connections/ConnectionsModule.ts b/src/lib/modules/connections/ConnectionsModule.ts deleted file mode 100644 index 92b72ac5fd..0000000000 --- a/src/lib/modules/connections/ConnectionsModule.ts +++ /dev/null @@ -1,223 +0,0 @@ -import type { Verkey } from 'indy-sdk'; -import { EventEmitter } from 'events'; - -import { AgentConfig } from '../../agent/AgentConfig'; -import { MessageSender } from '../../agent/MessageSender'; -import { createOutboundMessage } from '../../agent/helpers'; -import { Dispatcher } from '../../agent/Dispatcher'; -import { ConnectionService, ConnectionEventType, ConnectionStateChangedEvent, TrustPingService } from './services'; -import { ConsumerRoutingService } from '../routing'; -import { ConnectionRecord } from './repository/ConnectionRecord'; -import { ConnectionState } from './models'; -import { ConnectionInvitationMessage } from './messages'; -import { - ConnectionRequestHandler, - ConnectionResponseHandler, - AckMessageHandler, - TrustPingMessageHandler, - TrustPingResponseMessageHandler, -} from './handlers'; - -export class ConnectionsModule { - private agentConfig: AgentConfig; - private connectionService: ConnectionService; - private consumerRoutingService: ConsumerRoutingService; - private messageSender: MessageSender; - private trustPingService: TrustPingService; - - public constructor( - dispatcher: Dispatcher, - agentConfig: AgentConfig, - connectionService: ConnectionService, - trustPingService: TrustPingService, - consumerRoutingService: ConsumerRoutingService, - messageSender: MessageSender - ) { - this.agentConfig = agentConfig; - this.connectionService = connectionService; - this.trustPingService = trustPingService; - this.consumerRoutingService = consumerRoutingService; - this.messageSender = messageSender; - this.registerHandlers(dispatcher); - } - - /** - * Get the event emitter for the connection service. Will emit state changed events - * when the state of connections records changes. - * - * @returns event emitter for connection related state changes - */ - public get events(): EventEmitter { - return this.connectionService; - } - - public async createConnection(config?: { - autoAcceptConnection?: boolean; - alias?: string; - }): Promise<{ invitation: ConnectionInvitationMessage; connectionRecord: ConnectionRecord }> { - const { connectionRecord: connectionRecord, message: invitation } = await this.connectionService.createInvitation({ - autoAcceptConnection: config?.autoAcceptConnection, - alias: config?.alias, - }); - - // If agent has inbound connection, which means it's using a mediator, we need to create a route for newly created - // connection verkey at mediator. - if (this.agentConfig.inboundConnection) { - this.consumerRoutingService.createRoute(connectionRecord.verkey); - } - - return { connectionRecord, invitation }; - } - - /** - * Receive connection invitation as invitee and create connection. If auto accepting is enabled - * via either the config passed in the function or the global agent config, a connection - * request message will be send. - * - * @param invitationJson json object containing the invitation to receive - * @param config config for handling of invitation - * @returns new connection record - */ - public async receiveInvitation( - invitation: ConnectionInvitationMessage, - config?: { - autoAcceptConnection?: boolean; - alias?: string; - } - ): Promise { - let connection = await this.connectionService.processInvitation(invitation, { - autoAcceptConnection: config?.autoAcceptConnection, - alias: config?.alias, - }); - - // if auto accept is enabled (either on the record or the global agent config) - // we directly send a connection request - if (connection.autoAcceptConnection ?? this.agentConfig.autoAcceptConnections) { - connection = await this.acceptInvitation(connection.id); - } - - return connection; - } - - /** - * Receive connection invitation as invitee encoded as url and create connection. If auto accepting is enabled - * via either the config passed in the function or the global agent config, a connection - * request message will be send. - * - * @param invitationUrl url containing a base64 encoded invitation to receive - * @param config config for handling of invitation - * @returns new connection record - */ - public async receiveInvitationFromUrl( - invitationUrl: string, - config?: { - autoAcceptConnection?: boolean; - alias?: string; - } - ): Promise { - const invitation = await ConnectionInvitationMessage.fromUrl(invitationUrl); - return this.receiveInvitation(invitation, config); - } - - /** - * Accept a connection invitation as invitee (by sending a connection request message) for the connection with the specified connection id. - * This is not needed when auto accepting of connections is enabled. - * - * @param connectionId the id of the connection for which to accept the invitation - * @returns connection record - */ - public async acceptInvitation(connectionId: string): Promise { - const { message, connectionRecord: connectionRecord } = await this.connectionService.createRequest(connectionId); - - // If agent has inbound connection, which means it's using a mediator, - // we need to create a route for newly created connection verkey at mediator. - if (this.agentConfig.inboundConnection) { - await this.consumerRoutingService.createRoute(connectionRecord.verkey); - } - - const outbound = createOutboundMessage(connectionRecord, message, connectionRecord.invitation); - await this.messageSender.sendMessage(outbound); - - return connectionRecord; - } - - /** - * Accept a connection request as inviter (by sending a connection response message) for the connection with the specified connection id. - * This is not needed when auto accepting of connection is enabled. - * - * @param connectionId the id of the connection for which to accept the request - * @returns connection record - */ - public async acceptRequest(connectionId: string): Promise { - const { message, connectionRecord: connectionRecord } = await this.connectionService.createResponse(connectionId); - - const outbound = createOutboundMessage(connectionRecord, message); - await this.messageSender.sendMessage(outbound); - - return connectionRecord; - } - - /** - * Accept a connection response as invitee (by sending a trust ping message) for the connection with the specified connection id. - * This is not needed when auto accepting of connection is enabled. - * - * @param connectionId the id of the connection for which to accept the response - * @returns connection record - */ - public async acceptResponse(connectionId: string): Promise { - const { message, connectionRecord: connectionRecord } = await this.connectionService.createTrustPing(connectionId); - - const outbound = createOutboundMessage(connectionRecord, message); - await this.messageSender.sendMessage(outbound); - - return connectionRecord; - } - - public async returnWhenIsConnected(connectionId: string): Promise { - const isConnected = (connection: ConnectionRecord) => { - return connection.id === connectionId && connection.state === ConnectionState.Complete; - }; - - const connection = await this.connectionService.find(connectionId); - if (connection && isConnected(connection)) return connection; - - return new Promise(resolve => { - const listener = ({ connectionRecord: connectionRecord }: ConnectionStateChangedEvent) => { - if (isConnected(connectionRecord)) { - this.events.off(ConnectionEventType.StateChanged, listener); - resolve(connectionRecord); - } - }; - - this.events.on(ConnectionEventType.StateChanged, listener); - }); - } - - public async getAll() { - return this.connectionService.getConnections(); - } - - public async find(connectionId: string): Promise { - return this.connectionService.find(connectionId); - } - - public async getById(connectionId: string): Promise { - return this.connectionService.getById(connectionId); - } - - public async findConnectionByVerkey(verkey: Verkey): Promise { - return this.connectionService.findByVerkey(verkey); - } - - public async findConnectionByTheirKey(verkey: Verkey): Promise { - return this.connectionService.findByTheirKey(verkey); - } - - private registerHandlers(dispatcher: Dispatcher) { - dispatcher.registerHandler(new ConnectionRequestHandler(this.connectionService, this.agentConfig)); - dispatcher.registerHandler(new ConnectionResponseHandler(this.connectionService, this.agentConfig)); - dispatcher.registerHandler(new AckMessageHandler(this.connectionService)); - dispatcher.registerHandler(new TrustPingMessageHandler(this.trustPingService, this.connectionService)); - dispatcher.registerHandler(new TrustPingResponseMessageHandler(this.trustPingService)); - } -} diff --git a/src/lib/modules/connections/__tests__/ConnectionInvitationMessage.test.ts b/src/lib/modules/connections/__tests__/ConnectionInvitationMessage.test.ts deleted file mode 100644 index 1425045c75..0000000000 --- a/src/lib/modules/connections/__tests__/ConnectionInvitationMessage.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { validateOrReject } from 'class-validator'; - -import { ConnectionInvitationMessage } from '../messages/ConnectionInvitationMessage'; -import { JsonTransformer } from '../../../utils/JsonTransformer'; - -describe('ConnectionInvitationMessage', () => { - it('should allow routingKeys to be left out of inline invitation', async () => { - const json = { - '@type': ConnectionInvitationMessage.type, - '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', - recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'], - serviceEndpoint: 'https://example.com', - label: 'test', - }; - const invitation = JsonTransformer.fromJSON(json, ConnectionInvitationMessage); - await expect(validateOrReject(invitation)).resolves.toBeUndefined(); - }); - - it('should throw error if both did and inline keys / endpoint are missing', async () => { - const json = { - '@type': ConnectionInvitationMessage.type, - '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', - label: 'test', - }; - const invitation = JsonTransformer.fromJSON(json, ConnectionInvitationMessage); - await expect(validateOrReject(invitation)).rejects.not.toBeNull(); - }); -}); diff --git a/src/lib/modules/connections/__tests__/ConnectionService.test.ts b/src/lib/modules/connections/__tests__/ConnectionService.test.ts deleted file mode 100644 index a8b92f2a54..0000000000 --- a/src/lib/modules/connections/__tests__/ConnectionService.test.ts +++ /dev/null @@ -1,937 +0,0 @@ -import indy from 'indy-sdk'; -import { v4 as uuid } from 'uuid'; -import { IndyWallet } from '../../../wallet/IndyWallet'; -import { Wallet } from '../../../wallet/Wallet'; -import { ConnectionService } from '../services/ConnectionService'; -import { ConnectionRecord, ConnectionStorageProps } from '../repository/ConnectionRecord'; -import { AgentConfig } from '../../../agent/AgentConfig'; -import { Connection, ConnectionState, ConnectionRole, DidDoc, IndyAgentService } from '../models'; -import { InitConfig } from '../../../types'; -import { - ConnectionInvitationMessage, - ConnectionRequestMessage, - ConnectionResponseMessage, - TrustPingMessage, -} from '../messages'; -import { AckMessage, AckStatus } from '../../common'; -import { Repository } from '../../../storage/Repository'; -import { signData, unpackAndVerifySignatureDecorator } from '../../../decorators/signature/SignatureDecoratorUtils'; -import { InboundMessageContext } from '../../../agent/models/InboundMessageContext'; -import { SignatureDecorator } from '../../../decorators/signature/SignatureDecorator'; -import { JsonTransformer } from '../../../utils/JsonTransformer'; -import testLogger from '../../../__tests__/logger'; - -jest.mock('./../../../storage/Repository'); -const ConnectionRepository = >>(Repository); - -export function getMockConnection({ - state = ConnectionState.Invited, - role = ConnectionRole.Invitee, - id = 'test', - did = 'test-did', - verkey = 'key-1', - didDoc = new DidDoc({ - id: did, - publicKey: [], - authentication: [], - service: [ - new IndyAgentService({ id: `${did};indy`, serviceEndpoint: 'https://endpoint.com', recipientKeys: [verkey] }), - ], - }), - tags = {}, - invitation = new ConnectionInvitationMessage({ - label: 'test', - recipientKeys: [verkey], - serviceEndpoint: 'https:endpoint.com/msg', - }), - theirDid = 'their-did', - theirDidDoc = new DidDoc({ - id: theirDid, - publicKey: [], - authentication: [], - service: [ - new IndyAgentService({ id: `${did};indy`, serviceEndpoint: 'https://endpoint.com', recipientKeys: [verkey] }), - ], - }), -}: Partial = {}) { - return new ConnectionRecord({ - did, - didDoc, - theirDid, - theirDidDoc, - id, - role, - state, - tags, - verkey, - invitation, - }); -} - -describe('ConnectionService', () => { - const walletConfig = { id: 'test-wallet' + '-ConnectionServiceTest' }; - const walletCredentials = { key: 'key' }; - const initConfig: InitConfig = { - label: 'agent label', - host: 'http://agent.com', - port: 8080, - walletConfig, - walletCredentials, - indy, - logger: testLogger, - }; - - let wallet: Wallet; - let agentConfig: AgentConfig; - let connectionRepository: Repository; - let connectionService: ConnectionService; - - beforeAll(async () => { - agentConfig = new AgentConfig(initConfig); - wallet = new IndyWallet(agentConfig); - await wallet.init(); - }); - - afterAll(async () => { - await wallet.close(); - await wallet.delete(); - }); - - beforeEach(() => { - // Clear all instances and calls to constructor and all methods: - ConnectionRepository.mockClear(); - - connectionRepository = new ConnectionRepository(); - connectionService = new ConnectionService(wallet, agentConfig, connectionRepository); - }); - - describe('createConnectionWithInvitation', () => { - it('returns a connection record with values set', async () => { - expect.assertions(6); - - const { connectionRecord: connectionRecord } = await connectionService.createInvitation(); - - expect(connectionRecord.role).toBe(ConnectionRole.Inviter); - expect(connectionRecord.state).toBe(ConnectionState.Invited); - expect(connectionRecord.autoAcceptConnection).toBeUndefined(); - expect(connectionRecord.id).toEqual(expect.any(String)); - expect(connectionRecord.verkey).toEqual(expect.any(String)); - expect(connectionRecord.tags).toEqual( - expect.objectContaining({ - verkey: connectionRecord.verkey, - }) - ); - }); - - it('returns a connection record with invitation', async () => { - expect.assertions(1); - - const { message: invitation } = await connectionService.createInvitation(); - - expect(invitation).toEqual( - expect.objectContaining({ - label: initConfig.label, - recipientKeys: [expect.any(String)], - routingKeys: [], - serviceEndpoint: `${initConfig.host}:${initConfig.port}/msg`, - }) - ); - }); - - it('saves the connection record in the connection repository', async () => { - expect.assertions(1); - - const saveSpy = jest.spyOn(connectionRepository, 'save'); - - await connectionService.createInvitation(); - - expect(saveSpy).toHaveBeenCalledWith(expect.any(ConnectionRecord)); - }); - - it('returns a connection record with the autoAcceptConnection parameter from the config', async () => { - expect.assertions(3); - - const { connectionRecord: connectionTrue } = await connectionService.createInvitation({ - autoAcceptConnection: true, - }); - const { connectionRecord: connectionFalse } = await connectionService.createInvitation({ - autoAcceptConnection: false, - }); - const { connectionRecord: connectionUndefined } = await connectionService.createInvitation(); - - expect(connectionTrue.autoAcceptConnection).toBe(true); - expect(connectionFalse.autoAcceptConnection).toBe(false); - expect(connectionUndefined.autoAcceptConnection).toBeUndefined(); - }); - - it('returns a connection record with the alias parameter from the config', async () => { - expect.assertions(2); - - const { connectionRecord: aliasDefined } = await connectionService.createInvitation({ alias: 'test-alias' }); - const { connectionRecord: aliasUndefined } = await connectionService.createInvitation(); - - expect(aliasDefined.alias).toBe('test-alias'); - expect(aliasUndefined.alias).toBeUndefined(); - }); - }); - - describe('processInvitation', () => { - it('returns a connection record containing the information from the connection invitation', async () => { - expect.assertions(9); - - const recipientKey = 'key-1'; - const invitation = new ConnectionInvitationMessage({ - label: 'test label', - recipientKeys: [recipientKey], - serviceEndpoint: 'https://test.com/msg', - }); - - const connection = await connectionService.processInvitation(invitation); - const connectionAlias = await connectionService.processInvitation(invitation, { alias: 'test-alias' }); - - expect(connection.role).toBe(ConnectionRole.Invitee); - expect(connection.state).toBe(ConnectionState.Invited); - expect(connection.autoAcceptConnection).toBeUndefined(); - expect(connection.id).toEqual(expect.any(String)); - expect(connection.verkey).toEqual(expect.any(String)); - expect(connection.tags).toEqual( - expect.objectContaining({ - verkey: connection.verkey, - invitationKey: recipientKey, - }) - ); - expect(connection.invitation).toMatchObject(invitation); - expect(connection.alias).toBeUndefined(); - expect(connectionAlias.alias).toBe('test-alias'); - }); - - it('returns a connection record with the autoAcceptConnection parameter from the config', async () => { - expect.assertions(3); - - const invitation = new ConnectionInvitationMessage({ - did: 'did:sov:test', - label: 'test label', - }); - - const connectionTrue = await connectionService.processInvitation(invitation, { autoAcceptConnection: true }); - const connectionFalse = await connectionService.processInvitation(invitation, { - autoAcceptConnection: false, - }); - const connectionUndefined = await connectionService.processInvitation(invitation); - - expect(connectionTrue.autoAcceptConnection).toBe(true); - expect(connectionFalse.autoAcceptConnection).toBe(false); - expect(connectionUndefined.autoAcceptConnection).toBeUndefined(); - }); - - it('returns a connection record with the alias parameter from the config', async () => { - expect.assertions(2); - - const invitation = new ConnectionInvitationMessage({ - did: 'did:sov:test', - label: 'test label', - }); - - const aliasDefined = await connectionService.processInvitation(invitation, { alias: 'test-alias' }); - const aliasUndefined = await connectionService.processInvitation(invitation); - - expect(aliasDefined.alias).toBe('test-alias'); - expect(aliasUndefined.alias).toBeUndefined(); - }); - }); - - describe('createRequest', () => { - it('returns a connection request message containing the information from the connection record', async () => { - expect.assertions(4); - - const connection = getMockConnection(); - - // make separate mockFind variable to get the correct jest mock typing - const mockFind = connectionRepository.find as jest.Mock, [string]>; - mockFind.mockReturnValue(Promise.resolve(connection)); - - const { connectionRecord: connectionRecord, message } = await connectionService.createRequest('test'); - - expect(connectionRecord.state).toBe(ConnectionState.Requested); - expect(message.label).toBe(initConfig.label); - expect(message.connection.did).toBe('test-did'); - expect(message.connection.didDoc).toEqual(connection.didDoc); - }); - - it(`throws an error when connection role is ${ConnectionRole.Inviter} and not ${ConnectionRole.Invitee}`, async () => { - expect.assertions(1); - - // make separate mockFind variable to get the correct jest mock typing - const mockFind = connectionRepository.find as jest.Mock, [string]>; - - mockFind.mockReturnValue(Promise.resolve(getMockConnection({ role: ConnectionRole.Inviter }))); - return expect(connectionService.createRequest('test')).rejects.toThrowError( - `Connection record has invalid role ${ConnectionRole.Inviter}. Expected role ${ConnectionRole.Invitee}.` - ); - }); - - const invalidConnectionStates = [ - ConnectionState.Init, - ConnectionState.Requested, - ConnectionState.Responded, - ConnectionState.Complete, - ]; - test.each(invalidConnectionStates)( - `throws an error when connection state is %s and not ${ConnectionState.Invited}`, - state => { - expect.assertions(1); - - // make separate mockFind variable to get the correct jest mock typing - const mockFind = connectionRepository.find as jest.Mock, [string]>; - - mockFind.mockReturnValue(Promise.resolve(getMockConnection({ state }))); - return expect(connectionService.createRequest('test')).rejects.toThrowError( - `Connection record is in invalid state ${state}. Valid states are: ${ConnectionState.Invited}.` - ); - } - ); - }); - - describe('processRequest', () => { - it('returns a connection record containing the information from the connection request', async () => { - expect.assertions(5); - - const connectionRecord = getMockConnection({ - state: ConnectionState.Invited, - verkey: 'my-key', - role: ConnectionRole.Inviter, - }); - - const theirDid = 'their-did'; - const theirVerkey = 'their-verkey'; - const theirDidDoc = new DidDoc({ - id: theirDid, - publicKey: [], - authentication: [], - service: [ - new IndyAgentService({ - id: `${theirDid};indy`, - serviceEndpoint: 'https://endpoint.com', - recipientKeys: [theirVerkey], - }), - ], - }); - - const connectionRequest = new ConnectionRequestMessage({ - did: theirDid, - didDoc: theirDidDoc, - label: 'test-label', - }); - - const messageContext = new InboundMessageContext(connectionRequest, { - connection: connectionRecord, - senderVerkey: theirVerkey, - recipientVerkey: 'my-key', - }); - - const processedConnection = await connectionService.processRequest(messageContext); - - expect(processedConnection.state).toBe(ConnectionState.Requested); - expect(processedConnection.theirDid).toBe(theirDid); - // TODO: we should transform theirDidDoc to didDoc instance after retrieving from persistence - expect(processedConnection.theirDidDoc).toEqual(theirDidDoc); - expect(processedConnection.tags.theirKey).toBe(theirVerkey); - expect(processedConnection.tags.threadId).toBe(connectionRequest.id); - }); - - it('throws an error when the message context does not have a connection', async () => { - expect.assertions(1); - - const connectionRequest = new ConnectionRequestMessage({ - did: 'did', - label: 'test-label', - }); - - const messageContext = new InboundMessageContext(connectionRequest, { - recipientVerkey: 'test-verkey', - }); - - return expect(connectionService.processRequest(messageContext)).rejects.toThrowError( - 'Connection for verkey test-verkey not found!' - ); - }); - - it('throws an error when the message does not contain a connection parameter', async () => { - expect.assertions(1); - - const connection = getMockConnection({ - role: ConnectionRole.Inviter, - }); - - const connectionRequest = new ConnectionRequestMessage({ - did: 'did', - label: 'test-label', - }); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - delete connectionRequest.connection; - - const messageContext = new InboundMessageContext(connectionRequest, { - connection, - recipientVerkey: 'test-verkey', - }); - - return expect(connectionService.processRequest(messageContext)).rejects.toThrowError('Invalid message'); - }); - - it(`throws an error when connection role is ${ConnectionRole.Invitee} and not ${ConnectionRole.Inviter}`, async () => { - expect.assertions(1); - - const inboundMessage = new InboundMessageContext(jest.fn()(), { - connection: getMockConnection({ role: ConnectionRole.Invitee }), - }); - - return expect(connectionService.processRequest(inboundMessage)).rejects.toThrowError( - `Connection record has invalid role ${ConnectionRole.Invitee}. Expected role ${ConnectionRole.Inviter}.` - ); - }); - - it('throws an error when the message does not contain a did doc with any recipientKeys', async () => { - expect.assertions(1); - - const connection = getMockConnection({ - role: ConnectionRole.Inviter, - }); - - const connectionRequest = new ConnectionRequestMessage({ - did: 'did', - label: 'test-label', - }); - - const messageContext = new InboundMessageContext(connectionRequest, { - connection, - recipientVerkey: 'test-verkey', - }); - - return expect(connectionService.processRequest(messageContext)).rejects.toThrowError( - `Connection with id ${connection.id} has no recipient keys.` - ); - }); - }); - - describe('createResponse', () => { - it('returns a connection response message containing the information from the connection record', async () => { - expect.assertions(2); - - // Needed for signing connection~sig - const [did, verkey] = await wallet.createDid(); - const mockConnection = getMockConnection({ - did, - verkey, - state: ConnectionState.Requested, - role: ConnectionRole.Inviter, - }); - - // make separate mockFind variable to get the correct jest mock typing - const mockFind = connectionRepository.find as jest.Mock, [string]>; - mockFind.mockReturnValue(Promise.resolve(mockConnection)); - - const { message, connectionRecord: connectionRecord } = await connectionService.createResponse('test'); - - const connection = new Connection({ - did: mockConnection.did, - didDoc: mockConnection.didDoc, - }); - const plainConnection = JsonTransformer.toJSON(connection); - - expect(connectionRecord.state).toBe(ConnectionState.Responded); - expect(await unpackAndVerifySignatureDecorator(message.connectionSig, wallet)).toEqual(plainConnection); - }); - - it(`throws an error when connection role is ${ConnectionRole.Invitee} and not ${ConnectionRole.Inviter}`, async () => { - expect.assertions(1); - - // make separate mockFind variable to get the correct jest mock typing - const mockFind = connectionRepository.find as jest.Mock, [string]>; - - mockFind.mockReturnValue( - Promise.resolve(getMockConnection({ role: ConnectionRole.Invitee, state: ConnectionState.Requested })) - ); - return expect(connectionService.createResponse('test')).rejects.toThrowError( - `Connection record has invalid role ${ConnectionRole.Invitee}. Expected role ${ConnectionRole.Inviter}.` - ); - }); - - const invalidConnectionStates = [ - ConnectionState.Init, - ConnectionState.Invited, - ConnectionState.Responded, - ConnectionState.Complete, - ]; - test.each(invalidConnectionStates)( - `throws an error when connection state is %s and not ${ConnectionState.Requested}`, - async state => { - expect.assertions(1); - - // make separate mockFind variable to get the correct jest mock typing - const mockFind = connectionRepository.find as jest.Mock, [string]>; - mockFind.mockReturnValue(Promise.resolve(getMockConnection({ state }))); - - return expect(connectionService.createResponse('test')).rejects.toThrowError( - `Connection record is in invalid state ${state}. Valid states are: ${ConnectionState.Requested}.` - ); - } - ); - }); - - describe('processResponse', () => { - it('returns a connection record containing the information from the connection response', async () => { - expect.assertions(3); - - const [did, verkey] = await wallet.createDid(); - const [theirDid, theirVerkey] = await wallet.createDid(); - - const connectionRecord = getMockConnection({ - did, - verkey, - state: ConnectionState.Requested, - role: ConnectionRole.Invitee, - tags: { - // processResponse checks wether invitation key is same as signing key for connetion~sig - invitationKey: theirVerkey, - }, - }); - - const otherPartyConnection = new Connection({ - did: theirDid, - didDoc: new DidDoc({ - id: theirDid, - publicKey: [], - authentication: [], - service: [ - new IndyAgentService({ - id: `${did};indy`, - serviceEndpoint: 'https://endpoint.com', - recipientKeys: [theirVerkey], - }), - ], - }), - }); - - const plainConnection = JsonTransformer.toJSON(otherPartyConnection); - const connectionSig = await signData(plainConnection, wallet, theirVerkey); - - const connectionResponse = new ConnectionResponseMessage({ - threadId: uuid(), - connectionSig, - }); - - const messageContext = new InboundMessageContext(connectionResponse, { - connection: connectionRecord, - senderVerkey: connectionRecord.theirKey!, - recipientVerkey: connectionRecord.myKey!, - }); - - const processedConnection = await connectionService.processResponse(messageContext); - - expect(processedConnection.state).toBe(ConnectionState.Responded); - expect(processedConnection.theirDid).toBe(theirDid); - // TODO: we should transform theirDidDoc to didDoc instance after retrieving from persistence - expect(processedConnection.theirDidDoc).toEqual(otherPartyConnection.didDoc); - }); - - it(`throws an error when connection role is ${ConnectionRole.Inviter} and not ${ConnectionRole.Invitee}`, async () => { - expect.assertions(1); - - const inboundMessage = new InboundMessageContext(jest.fn()(), { - connection: getMockConnection({ role: ConnectionRole.Inviter, state: ConnectionState.Requested }), - }); - - return expect(connectionService.processResponse(inboundMessage)).rejects.toThrowError( - `Connection record has invalid role ${ConnectionRole.Inviter}. Expected role ${ConnectionRole.Invitee}.` - ); - }); - - it('throws an error when the connection sig is not signed with the same key as the recipient key from the invitation', async () => { - expect.assertions(1); - - const [did, verkey] = await wallet.createDid(); - const [theirDid, theirVerkey] = await wallet.createDid(); - const connectionRecord = getMockConnection({ - did, - verkey, - role: ConnectionRole.Invitee, - state: ConnectionState.Requested, - tags: { - // processResponse checks wether invitation key is same as signing key for connetion~sig - invitationKey: 'some-random-key', - }, - }); - - const otherPartyConnection = new Connection({ - did: theirDid, - didDoc: new DidDoc({ - id: theirDid, - publicKey: [], - authentication: [], - service: [ - new IndyAgentService({ - id: `${did};indy`, - serviceEndpoint: 'https://endpoint.com', - recipientKeys: [theirVerkey], - }), - ], - }), - }); - const plainConnection = JsonTransformer.toJSON(otherPartyConnection); - const connectionSig = await signData(plainConnection, wallet, theirVerkey); - - const connectionResponse = new ConnectionResponseMessage({ - threadId: uuid(), - connectionSig, - }); - - const messageContext = new InboundMessageContext(connectionResponse, { - connection: connectionRecord, - senderVerkey: connectionRecord.theirKey!, - recipientVerkey: connectionRecord.myKey!, - }); - - return expect(connectionService.processResponse(messageContext)).rejects.toThrowError( - 'Connection in connection response is not signed with same key as recipient key in invitation' - ); - }); - - it('throws an error when the message context does not have a connection', async () => { - expect.assertions(1); - - const connectionResponse = new ConnectionResponseMessage({ - threadId: uuid(), - connectionSig: new SignatureDecorator({ - signature: '', - signatureData: '', - signatureType: '', - signer: '', - }), - }); - - const messageContext = new InboundMessageContext(connectionResponse, { - recipientVerkey: 'test-verkey', - }); - - return expect(connectionService.processResponse(messageContext)).rejects.toThrowError( - 'Connection for verkey test-verkey not found!' - ); - }); - - it('throws an error when the message does not contain a did doc with any recipientKeys', async () => { - expect.assertions(1); - - const [did, verkey] = await wallet.createDid(); - const [theirDid, theirVerkey] = await wallet.createDid(); - const connectionRecord = getMockConnection({ - did, - verkey, - state: ConnectionState.Requested, - tags: { - // processResponse checks wether invitation key is same as signing key for connetion~sig - invitationKey: theirVerkey, - }, - theirDid: undefined, - theirDidDoc: undefined, - }); - - const otherPartyConnection = new Connection({ - did: theirDid, - }); - const plainConnection = JsonTransformer.toJSON(otherPartyConnection); - const connectionSig = await signData(plainConnection, wallet, theirVerkey); - - const connectionResponse = new ConnectionResponseMessage({ - threadId: uuid(), - connectionSig, - }); - - const messageContext = new InboundMessageContext(connectionResponse, { - connection: connectionRecord, - }); - - return expect(connectionService.processResponse(messageContext)).rejects.toThrowError( - `Connection with id ${connectionRecord.id} has no recipient keys.` - ); - }); - }); - - describe('createTrustPing', () => { - it('returns a trust ping message', async () => { - expect.assertions(2); - - const mockConnection = getMockConnection({ - state: ConnectionState.Responded, - }); - - // make separate mockFind variable to get the correct jest mock typing - const mockFind = connectionRepository.find as jest.Mock, [string]>; - mockFind.mockReturnValue(Promise.resolve(mockConnection)); - - const { message, connectionRecord: connectionRecord } = await connectionService.createTrustPing('test'); - - expect(connectionRecord.state).toBe(ConnectionState.Complete); - expect(message).toEqual(expect.any(TrustPingMessage)); - }); - - const invalidConnectionStates = [ConnectionState.Init, ConnectionState.Invited, ConnectionState.Requested]; - test.each(invalidConnectionStates)( - `throws an error when connection state is %s and not ${ConnectionState.Responded} or ${ConnectionState.Complete}`, - state => { - expect.assertions(1); - - // make separate mockFind variable to get the correct jest mock typing - const mockFind = connectionRepository.find as jest.Mock, [string]>; - - mockFind.mockReturnValue(Promise.resolve(getMockConnection({ state }))); - return expect(connectionService.createTrustPing('test')).rejects.toThrowError( - `Connection record is in invalid state ${state}. Valid states are: ${ConnectionState.Responded}, ${ConnectionState.Complete}.` - ); - } - ); - }); - - describe('processAck', () => { - it('throws an error when the message context does not have a connection', async () => { - expect.assertions(1); - - const ack = new AckMessage({ - status: AckStatus.OK, - threadId: 'thread-id', - }); - - const messageContext = new InboundMessageContext(ack, { - recipientVerkey: 'test-verkey', - }); - - return expect(connectionService.processAck(messageContext)).rejects.toThrowError( - 'Connection for verkey test-verkey not found!' - ); - }); - - it('updates the state to Completed when the state is Responded and role is Inviter', async () => { - expect.assertions(1); - - const connection = getMockConnection({ - state: ConnectionState.Responded, - role: ConnectionRole.Inviter, - }); - - const ack = new AckMessage({ - status: AckStatus.OK, - threadId: 'thread-id', - }); - - const messageContext = new InboundMessageContext(ack, { - recipientVerkey: 'test-verkey', - connection, - }); - - const updatedConnection = await connectionService.processAck(messageContext); - - expect(updatedConnection.state).toBe(ConnectionState.Complete); - }); - - it('does not update the state when the state is not Responded or the role is not Inviter', async () => { - expect.assertions(1); - - const connection = getMockConnection({ - state: ConnectionState.Responded, - role: ConnectionRole.Invitee, - }); - - const ack = new AckMessage({ - status: AckStatus.OK, - threadId: 'thread-id', - }); - - const messageContext = new InboundMessageContext(ack, { - recipientVerkey: 'test-verkey', - connection, - }); - - const updatedConnection = await connectionService.processAck(messageContext); - - expect(updatedConnection.state).toBe(ConnectionState.Responded); - }); - }); - - describe('getConnections', () => { - it('returns the connections from the connections repository', async () => { - expect.assertions(2); - - const expectedConnections = [getMockConnection(), getMockConnection(), getMockConnection()]; - - // make separate mockFind variable to get the correct jest mock typing - const mockFindAll = connectionRepository.findAll as jest.Mock, []>; - mockFindAll.mockReturnValue(Promise.resolve(expectedConnections)); - - const connections = await connectionService.getConnections(); - - expect(connections).toEqual(expectedConnections); - expect(mockFindAll).toBeCalled(); - }); - }); - - describe('find', () => { - it('returns the connection from the connections repository', async () => { - expect.assertions(2); - - const id = 'test-id'; - - const expectedConnection = getMockConnection({ - id, - }); - - // make separate mockFind variable to get the correct jest mock typing - const mockFind = connectionRepository.find as jest.Mock, [string]>; - mockFind.mockReturnValue(Promise.resolve(expectedConnection)); - - const connection = await connectionService.find(id); - - expect(connection).toEqual(expectedConnection); - expect(mockFind).toBeCalledWith(id); - }); - - it('returns null when the connections repository throws an error', async () => { - expect.assertions(2); - - const id = 'test-id'; - - // make separate mockFind variable to get the correct jest mock typing - const mockFind = connectionRepository.find as jest.Mock, [string]>; - mockFind.mockReturnValue(Promise.reject()); - - const connection = await connectionService.find(id); - - expect(connection).toBeNull(); - expect(mockFind).toBeCalledWith(id); - }); - }); - - describe('findByVerkey', () => { - it('returns the connection from the connections repository', async () => { - expect.assertions(2); - - const verkey = 'test-verkey'; - - const expectedConnection = getMockConnection({ - verkey, - }); - - // make separate mockFind variable to get the correct jest mock typing - const mockFindByQuery = connectionRepository.findByQuery as jest.Mock< - Promise, - [Record] - >; - mockFindByQuery.mockReturnValue(Promise.resolve([expectedConnection])); - - const connection = await connectionService.findByVerkey(verkey); - - expect(connection).toEqual(expectedConnection); - expect(mockFindByQuery).toBeCalledWith({ verkey }); - }); - - it('returns null when the connection repository does not return any connections', async () => { - expect.assertions(2); - - const verkey = 'test-verkey'; - - // make separate mockFind variable to get the correct jest mock typing - const mockFindByQuery = connectionRepository.findByQuery as jest.Mock< - Promise, - [Record] - >; - mockFindByQuery.mockReturnValue(Promise.resolve([])); - - const connection = await connectionService.findByVerkey(verkey); - - expect(connection).toBeNull(); - expect(mockFindByQuery).toBeCalledWith({ verkey }); - }); - - it('throws an error when the connection repository returns more than one connection', async () => { - expect.assertions(2); - - const verkey = 'test-verkey'; - - const expectedConnections = [getMockConnection({ verkey }), getMockConnection({ verkey })]; - - // make separate mockFind variable to get the correct jest mock typing - const mockFindByQuery = connectionRepository.findByQuery as jest.Mock< - Promise, - [Record] - >; - mockFindByQuery.mockReturnValue(Promise.resolve(expectedConnections)); - - expect(connectionService.findByVerkey(verkey)).rejects.toThrowError( - 'There is more than one connection for given verkey test-verkey' - ); - - expect(mockFindByQuery).toBeCalledWith({ verkey }); - }); - }); - - describe('findByTheirKey', () => { - it('returns the connection from the connections repository', async () => { - expect.assertions(2); - - const theirKey = 'test-theirVerkey'; - - const expectedConnection = getMockConnection(); - - // make separate mockFind variable to get the correct jest mock typing - const mockFindByQuery = connectionRepository.findByQuery as jest.Mock< - Promise, - [Record] - >; - mockFindByQuery.mockReturnValue(Promise.resolve([expectedConnection])); - - const connection = await connectionService.findByTheirKey(theirKey); - - expect(connection).toEqual(expectedConnection); - expect(mockFindByQuery).toBeCalledWith({ theirKey }); - }); - - it('returns null when the connection repository does not return any connections', async () => { - expect.assertions(2); - - const theirKey = 'test-theirVerkey'; - - // make separate mockFind variable to get the correct jest mock typing - const mockFindByQuery = connectionRepository.findByQuery as jest.Mock< - Promise, - [Record] - >; - mockFindByQuery.mockReturnValue(Promise.resolve([])); - - const connection = await connectionService.findByTheirKey(theirKey); - - expect(connection).toBeNull(); - expect(mockFindByQuery).toBeCalledWith({ theirKey }); - }); - - it('throws an error when the connection repository returns more than one connection', async () => { - expect.assertions(2); - - const theirKey = 'test-theirVerkey'; - - const expectedConnections = [getMockConnection(), getMockConnection()]; - - // make separate mockFind variable to get the correct jest mock typing - const mockFindByQuery = connectionRepository.findByQuery as jest.Mock< - Promise, - [Record] - >; - mockFindByQuery.mockReturnValue(Promise.resolve(expectedConnections)); - - expect(connectionService.findByTheirKey(theirKey)).rejects.toThrowError( - 'There is more than one connection for given verkey test-theirVerkey' - ); - - expect(mockFindByQuery).toBeCalledWith({ theirKey }); - }); - }); -}); diff --git a/src/lib/modules/connections/__tests__/ConnectionState.test.ts b/src/lib/modules/connections/__tests__/ConnectionState.test.ts deleted file mode 100644 index 9112d743e0..0000000000 --- a/src/lib/modules/connections/__tests__/ConnectionState.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ConnectionState } from '../models/ConnectionState'; - -describe('ConnectionState', () => { - test('state matches Connection 1.0 (RFC 0160) state value', () => { - expect(ConnectionState.Init).toBe('init'); - expect(ConnectionState.Invited).toBe('invited'); - expect(ConnectionState.Requested).toBe('requested'); - expect(ConnectionState.Responded).toBe('responded'); - expect(ConnectionState.Complete).toBe('complete'); - }); -}); diff --git a/src/lib/modules/connections/handlers/AckMessageHandler.ts b/src/lib/modules/connections/handlers/AckMessageHandler.ts deleted file mode 100644 index 4267ede53a..0000000000 --- a/src/lib/modules/connections/handlers/AckMessageHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { ConnectionService } from '../services/ConnectionService'; -import { AckMessage } from '../../common'; - -export class AckMessageHandler implements Handler { - private connectionService: ConnectionService; - public supportedMessages = [AckMessage]; - - public constructor(connectionService: ConnectionService) { - this.connectionService = connectionService; - } - - public async handle(inboundMessage: HandlerInboundMessage) { - await this.connectionService.processAck(inboundMessage); - } -} diff --git a/src/lib/modules/connections/handlers/ConnectionRequestHandler.ts b/src/lib/modules/connections/handlers/ConnectionRequestHandler.ts deleted file mode 100644 index 1bf274d426..0000000000 --- a/src/lib/modules/connections/handlers/ConnectionRequestHandler.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { ConnectionService } from '../services/ConnectionService'; -import { ConnectionRequestMessage } from '../messages'; -import { AgentConfig } from '../../../agent/AgentConfig'; -import { createOutboundMessage } from '../../../agent/helpers'; - -export class ConnectionRequestHandler implements Handler { - private connectionService: ConnectionService; - private agentConfig: AgentConfig; - public supportedMessages = [ConnectionRequestMessage]; - - public constructor(connectionService: ConnectionService, agentConfig: AgentConfig) { - this.connectionService = connectionService; - this.agentConfig = agentConfig; - } - - public async handle(messageContext: HandlerInboundMessage) { - if (!messageContext.connection) { - throw new Error(`Connection for verkey ${messageContext.recipientVerkey} not found!`); - } - - await this.connectionService.processRequest(messageContext); - - if (messageContext.connection?.autoAcceptConnection ?? this.agentConfig.autoAcceptConnections) { - const { message } = await this.connectionService.createResponse(messageContext.connection.id); - return createOutboundMessage(messageContext.connection, message); - } - } -} diff --git a/src/lib/modules/connections/handlers/ConnectionResponseHandler.ts b/src/lib/modules/connections/handlers/ConnectionResponseHandler.ts deleted file mode 100644 index f4422f37a1..0000000000 --- a/src/lib/modules/connections/handlers/ConnectionResponseHandler.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { AgentConfig } from '../../../agent/AgentConfig'; -import { createOutboundMessage } from '../../../agent/helpers'; -import { ConnectionService } from '../services/ConnectionService'; -import { ConnectionResponseMessage } from '../messages'; - -export class ConnectionResponseHandler implements Handler { - private connectionService: ConnectionService; - private agentConfig: AgentConfig; - public supportedMessages = [ConnectionResponseMessage]; - - public constructor(connectionService: ConnectionService, agentConfig: AgentConfig) { - this.connectionService = connectionService; - this.agentConfig = agentConfig; - } - - public async handle(messageContext: HandlerInboundMessage) { - if (!messageContext.connection) { - throw new Error(`Connection for verkey ${messageContext.recipientVerkey} not found!`); - } - - await this.connectionService.processResponse(messageContext); - - // TODO: should we only send ping message in case of autoAcceptConnection or always? - // In AATH we have a separate step to send the ping. So for now we'll only do it - // if auto accept is enable - if (messageContext.connection?.autoAcceptConnection ?? this.agentConfig.autoAcceptConnections) { - const { message } = await this.connectionService.createTrustPing(messageContext.connection.id); - return createOutboundMessage(messageContext.connection, message); - } - } -} diff --git a/src/lib/modules/connections/handlers/TrustPingMessageHandler.ts b/src/lib/modules/connections/handlers/TrustPingMessageHandler.ts deleted file mode 100644 index 765628dd38..0000000000 --- a/src/lib/modules/connections/handlers/TrustPingMessageHandler.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { TrustPingService } from '../services/TrustPingService'; -import { ConnectionService } from '../services/ConnectionService'; -import { ConnectionState } from '../models'; -import { TrustPingMessage } from '../messages'; - -export class TrustPingMessageHandler implements Handler { - private trustPingService: TrustPingService; - private connectionService: ConnectionService; - public supportedMessages = [TrustPingMessage]; - - public constructor(trustPingService: TrustPingService, connectionService: ConnectionService) { - this.trustPingService = trustPingService; - this.connectionService = connectionService; - } - - public async handle(messageContext: HandlerInboundMessage) { - const { connection, recipientVerkey } = messageContext; - if (!connection) { - throw new Error(`Connection for verkey ${recipientVerkey} not found!`); - } - - // TODO: This is better addressed in a middleware of some kind because - // any message can transition the state to complete, not just an ack or trust ping - if (connection.state === ConnectionState.Responded) { - await this.connectionService.updateState(connection, ConnectionState.Complete); - } - - return this.trustPingService.processPing(messageContext, connection); - } -} diff --git a/src/lib/modules/connections/handlers/TrustPingResponseMessageHandler.ts b/src/lib/modules/connections/handlers/TrustPingResponseMessageHandler.ts deleted file mode 100644 index c2976ebb2e..0000000000 --- a/src/lib/modules/connections/handlers/TrustPingResponseMessageHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { TrustPingService } from '../services/TrustPingService'; -import { TrustPingResponseMessage } from '../messages'; - -export class TrustPingResponseMessageHandler implements Handler { - private trustPingService: TrustPingService; - public supportedMessages = [TrustPingResponseMessage]; - - public constructor(trustPingService: TrustPingService) { - this.trustPingService = trustPingService; - } - - public async handle(inboundMessage: HandlerInboundMessage) { - return this.trustPingService.processPingResponse(inboundMessage); - } -} diff --git a/src/lib/modules/connections/handlers/index.ts b/src/lib/modules/connections/handlers/index.ts deleted file mode 100644 index ec456749ad..0000000000 --- a/src/lib/modules/connections/handlers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './AckMessageHandler'; -export * from './ConnectionRequestHandler'; -export * from './ConnectionResponseHandler'; -export * from './TrustPingMessageHandler'; -export * from './TrustPingResponseMessageHandler'; diff --git a/src/lib/modules/connections/index.ts b/src/lib/modules/connections/index.ts deleted file mode 100644 index 6df7fb8909..0000000000 --- a/src/lib/modules/connections/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './messages'; -export * from './services'; -export * from './models'; -export * from './repository/ConnectionRecord'; diff --git a/src/lib/modules/connections/messages/ConnectionInvitationMessage.ts b/src/lib/modules/connections/messages/ConnectionInvitationMessage.ts deleted file mode 100644 index aa59a5c631..0000000000 --- a/src/lib/modules/connections/messages/ConnectionInvitationMessage.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Equals, IsString, ValidateIf, IsArray, IsOptional } from 'class-validator'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { decodeInvitationFromUrl, encodeInvitationToUrl } from '../../../helpers'; -import { ConnectionMessageType } from './ConnectionMessageType'; - -// TODO: improve typing of `DIDInvitationData` and `InlineInvitationData` so properties can't be mixed -export interface InlineInvitationData { - recipientKeys: string[]; - serviceEndpoint: string; - routingKeys?: string[]; -} - -export interface DIDInvitationData { - did: string; -} - -/** - * Message to invite another agent to create a connection - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0160-connection-protocol/README.md#0-invitation-to-connect - */ -export class ConnectionInvitationMessage extends AgentMessage { - /** - * Create new ConnectionInvitationMessage instance. - * @param options - */ - public constructor(options: { id?: string; label: string } & (DIDInvitationData | InlineInvitationData)) { - super(); - - if (options) { - this.id = options.id || this.generateId(); - this.label = options.label; - - if (isDidInvitation(options)) { - this.did = options.did; - } else { - this.recipientKeys = options.recipientKeys; - this.serviceEndpoint = options.serviceEndpoint; - this.routingKeys = options.routingKeys; - } - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - if (options.did && (options.recipientKeys || options.routingKeys || options.serviceEndpoint)) { - throw new Error('either the did or the recipientKeys/serviceEndpoint/routingKeys must be set, but not both'); - } - } - } - - @Equals(ConnectionInvitationMessage.type) - public readonly type = ConnectionInvitationMessage.type; - public static readonly type = ConnectionMessageType.ConnectionInvitation; - - @IsString() - public label!: string; - - @IsString() - @ValidateIf((o: ConnectionInvitationMessage) => o.recipientKeys === undefined) - public did?: string; - - @IsString({ - each: true, - }) - @IsArray() - @ValidateIf((o: ConnectionInvitationMessage) => o.did === undefined) - public recipientKeys?: string[]; - - @IsString() - @ValidateIf((o: ConnectionInvitationMessage) => o.did === undefined) - public serviceEndpoint?: string; - - @IsString({ - each: true, - }) - @IsOptional() - @ValidateIf((o: ConnectionInvitationMessage) => o.did === undefined) - public routingKeys?: string[]; - - public toUrl(domain?: string) { - return encodeInvitationToUrl(this, domain); - } - - public static async fromUrl(invitationUrl: string) { - return decodeInvitationFromUrl(invitationUrl); - } -} - -/** - * Check whether an invitation is a `DIDInvitationData` object - * - * @param invitation invitation object - */ -function isDidInvitation(invitation: InlineInvitationData | DIDInvitationData): invitation is DIDInvitationData { - return (invitation as DIDInvitationData).did !== undefined; -} diff --git a/src/lib/modules/connections/messages/ConnectionMessageType.ts b/src/lib/modules/connections/messages/ConnectionMessageType.ts deleted file mode 100644 index 414d7f8833..0000000000 --- a/src/lib/modules/connections/messages/ConnectionMessageType.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum ConnectionMessageType { - ConnectionInvitation = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation', - ConnectionRequest = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/request', - ConnectionResponse = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/response', - TrustPingMessage = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/trust_ping/1.0/ping', - TrustPingResponseMessage = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/trust_ping/1.0/ping_response', -} diff --git a/src/lib/modules/connections/messages/ConnectionRequestMessage.ts b/src/lib/modules/connections/messages/ConnectionRequestMessage.ts deleted file mode 100644 index 88e5aad53f..0000000000 --- a/src/lib/modules/connections/messages/ConnectionRequestMessage.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Type } from 'class-transformer'; -import { Equals, IsString, ValidateNested } from 'class-validator'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { ConnectionMessageType } from './ConnectionMessageType'; -import { Connection, DidDoc } from '../models'; - -export interface ConnectionRequestMessageOptions { - id?: string; - label: string; - did: string; - didDoc?: DidDoc; -} - -/** - * Message to communicate the DID document to the other agent when creating a connectino - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0160-connection-protocol/README.md#1-connection-request - */ -export class ConnectionRequestMessage extends AgentMessage { - /** - * Create new ConnectionRequestMessage instance. - * @param options - */ - public constructor(options: ConnectionRequestMessageOptions) { - super(); - - if (options) { - this.id = options.id || this.generateId(); - this.label = options.label; - - this.connection = new Connection({ - did: options.did, - didDoc: options.didDoc, - }); - } - } - - @Equals(ConnectionRequestMessage.type) - public readonly type = ConnectionRequestMessage.type; - public static readonly type = ConnectionMessageType.ConnectionRequest; - - @IsString() - public label!: string; - - @Type(() => Connection) - @ValidateNested() - public connection!: Connection; -} diff --git a/src/lib/modules/connections/messages/ConnectionResponseMessage.ts b/src/lib/modules/connections/messages/ConnectionResponseMessage.ts deleted file mode 100644 index 49b37af22c..0000000000 --- a/src/lib/modules/connections/messages/ConnectionResponseMessage.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Equals, ValidateNested } from 'class-validator'; -import { Type, Expose } from 'class-transformer'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { ConnectionMessageType } from './ConnectionMessageType'; -import { SignatureDecorator } from '../../../decorators/signature/SignatureDecorator'; - -export interface ConnectionResponseMessageOptions { - id?: string; - threadId: string; - connectionSig: SignatureDecorator; -} - -/** - * Message part of connection protocol used to complete the connection - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0160-connection-protocol/README.md#2-connection-response - */ -export class ConnectionResponseMessage extends AgentMessage { - /** - * Create new ConnectionResponseMessage instance. - * @param options - */ - public constructor(options: ConnectionResponseMessageOptions) { - super(); - - if (options) { - this.id = options.id || this.generateId(); - this.connectionSig = options.connectionSig; - - this.setThread({ threadId: options.threadId }); - } - } - - @Equals(ConnectionResponseMessage.type) - public readonly type = ConnectionResponseMessage.type; - public static readonly type = ConnectionMessageType.ConnectionResponse; - - @Type(() => SignatureDecorator) - @ValidateNested() - @Expose({ name: 'connection~sig' }) - public connectionSig!: SignatureDecorator; -} diff --git a/src/lib/modules/connections/messages/TrustPingMessage.ts b/src/lib/modules/connections/messages/TrustPingMessage.ts deleted file mode 100644 index 7a00bef844..0000000000 --- a/src/lib/modules/connections/messages/TrustPingMessage.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Equals, IsString, IsBoolean } from 'class-validator'; -import { Expose } from 'class-transformer'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { ConnectionMessageType } from './ConnectionMessageType'; -import { TimingDecorator } from '../../../decorators/timing/TimingDecorator'; - -export interface TrustPingMessageOptions { - comment?: string; - id?: string; - responseRequested?: boolean; - timing?: Pick; -} - -/** - * Message to initiate trust ping interaction - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0048-trust-ping/README.md#messages - */ -export class TrustPingMessage extends AgentMessage { - /** - * Create new TrustPingMessage instance. - * responseRequested will be true if not passed - * @param options - */ - public constructor(options?: TrustPingMessageOptions) { - super(); - - if (options) { - this.id = options.id || this.generateId(); - this.comment = options.comment; - this.responseRequested = options.responseRequested !== undefined ? options.responseRequested : true; - - if (options.timing) { - this.setTiming({ - outTime: options.timing.outTime, - expiresTime: options.timing.expiresTime, - delayMilli: options.timing.delayMilli, - }); - } - } - } - - @Equals(TrustPingMessage.type) - public readonly type = TrustPingMessage.type; - public static readonly type = ConnectionMessageType.TrustPingMessage; - - @IsString() - public comment?: string; - - @IsBoolean() - @Expose({ name: 'response_requested' }) - public responseRequested = true; -} diff --git a/src/lib/modules/connections/messages/TrustPingResponseMessage.ts b/src/lib/modules/connections/messages/TrustPingResponseMessage.ts deleted file mode 100644 index 1165786f5a..0000000000 --- a/src/lib/modules/connections/messages/TrustPingResponseMessage.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Equals, IsString } from 'class-validator'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { ConnectionMessageType } from './ConnectionMessageType'; -import { TimingDecorator } from '../../../decorators/timing/TimingDecorator'; - -export interface TrustPingResponseMessageOptions { - comment?: string; - id?: string; - threadId: string; - timing?: Pick; -} - -/** - * Message to respond to a trust ping message - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0048-trust-ping/README.md#messages - */ -export class TrustPingResponseMessage extends AgentMessage { - /** - * Create new TrustPingResponseMessage instance. - * responseRequested will be true if not passed - * @param options - */ - public constructor(options: TrustPingResponseMessageOptions) { - super(); - - if (options) { - this.id = options.id || this.generateId(); - this.comment = options.comment; - - this.setThread({ - threadId: options.threadId, - }); - - if (options.timing) { - this.setTiming({ - inTime: options.timing.inTime, - outTime: options.timing.outTime, - }); - } - } - } - - @Equals(TrustPingResponseMessage.type) - public static readonly type = ConnectionMessageType.TrustPingResponseMessage; - public readonly type = TrustPingResponseMessage.type; - - @IsString() - public comment?: string; -} diff --git a/src/lib/modules/connections/messages/index.ts b/src/lib/modules/connections/messages/index.ts deleted file mode 100644 index 9e6df92a0e..0000000000 --- a/src/lib/modules/connections/messages/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './ConnectionInvitationMessage'; -export * from './ConnectionMessageType'; -export * from './ConnectionRequestMessage'; -export * from './ConnectionResponseMessage'; -export * from './TrustPingMessage'; -export * from './TrustPingResponseMessage'; diff --git a/src/lib/modules/connections/models/Connection.ts b/src/lib/modules/connections/models/Connection.ts deleted file mode 100644 index 978aa90c19..0000000000 --- a/src/lib/modules/connections/models/Connection.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { IsString, ValidateNested } from 'class-validator'; -import { Expose, Type } from 'class-transformer'; - -import { DidDoc } from './did/DidDoc'; - -export interface ConnectionOptions { - did: string; - didDoc?: DidDoc; -} - -export class Connection { - public constructor(options: ConnectionOptions) { - if (options) { - this.did = options.did; - this.didDoc = options.didDoc; - } - } - - @IsString() - @Expose({ name: 'DID' }) - public did!: string; - - @Expose({ name: 'DIDDoc' }) - @Type(() => DidDoc) - @ValidateNested() - public didDoc?: DidDoc; -} diff --git a/src/lib/modules/connections/models/ConnectionRole.ts b/src/lib/modules/connections/models/ConnectionRole.ts deleted file mode 100644 index e3ea905f90..0000000000 --- a/src/lib/modules/connections/models/ConnectionRole.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum ConnectionRole { - Inviter = 'INVITER', - Invitee = 'INVITEE', -} diff --git a/src/lib/modules/connections/models/ConnectionState.ts b/src/lib/modules/connections/models/ConnectionState.ts deleted file mode 100644 index bb15f0860d..0000000000 --- a/src/lib/modules/connections/models/ConnectionState.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Connection states as defined in RFC 0160. - * - * State 'null' from RFC is changed to 'init' - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0160-connection-protocol/README.md#states - */ -export enum ConnectionState { - Init = 'init', - Invited = 'invited', - Requested = 'requested', - Responded = 'responded', - Complete = 'complete', -} diff --git a/src/lib/modules/connections/models/InvitationDetails.ts b/src/lib/modules/connections/models/InvitationDetails.ts deleted file mode 100644 index 8fe8e07aef..0000000000 --- a/src/lib/modules/connections/models/InvitationDetails.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Verkey } from 'indy-sdk'; - -export interface InvitationDetails { - label: string; - recipientKeys: Verkey[]; - serviceEndpoint: string; - routingKeys: Verkey[]; -} diff --git a/src/lib/modules/connections/models/did/DidDoc.ts b/src/lib/modules/connections/models/did/DidDoc.ts deleted file mode 100644 index e2e339ce10..0000000000 --- a/src/lib/modules/connections/models/did/DidDoc.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Expose } from 'class-transformer'; -import { Equals, IsArray, IsString, ValidateNested } from 'class-validator'; - -import { AuthenticationTransformer, Authentication } from './authentication'; -import { PublicKey, PublicKeyTransformer } from './publicKey'; -import { Service, ServiceTransformer } from './service'; - -type DidDocOptions = Pick; - -export class DidDoc { - @Expose({ name: '@context' }) - @Equals('https://w3id.org/did/v1') - public context = 'https://w3id.org/did/v1'; - - @IsString() - public id!: string; - - @IsArray() - @ValidateNested() - @PublicKeyTransformer() - public publicKey: PublicKey[] = []; - - @IsArray() - @ValidateNested() - @ServiceTransformer() - public service: Service[] = []; - - @IsArray() - @ValidateNested() - @AuthenticationTransformer() - public authentication: Authentication[] = []; - - public constructor(options: DidDocOptions) { - if (options) { - this.id = options.id; - this.publicKey = options.publicKey; - this.service = options.service; - this.authentication = options.authentication; - } - } - - /** - * Gets the matching public key for a given key id - * - * @param id fully qualified key id - */ - public getPublicKey(id: string): PublicKey | undefined { - return this.publicKey.find(item => item.id === id); - } - - /** - * Returns all of the service endpoints matching the given type. - * - * @param type The type of service(s) to query. - */ - public getServicesByType(type: string): S[] { - return this.service.filter(service => service.type === type) as S[]; - } - - /** - * Returns all of the service endpoints matching the given class - * - * @param classType The class to query services. - */ - public getServicesByClassType(classType: new (...args: never[]) => S): S[] { - return this.service.filter(service => service instanceof classType) as S[]; - } -} diff --git a/src/lib/modules/connections/models/did/__tests__/Authentication.test.ts b/src/lib/modules/connections/models/did/__tests__/Authentication.test.ts deleted file mode 100644 index 232eeaaa26..0000000000 --- a/src/lib/modules/connections/models/did/__tests__/Authentication.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { classToPlain, plainToClass } from 'class-transformer'; -import { - Authentication, - AuthenticationTransformer, - ReferencedAuthentication, - EmbeddedAuthentication, -} from '../authentication'; -import { PublicKey, RsaSig2018 } from '../publicKey'; - -describe('Did | Authentication', () => { - describe('EmbeddedAuthentication', () => { - it('should correctly transform ReferencedAuthentication class to Json', async () => { - const publicKey = new RsaSig2018({ - controller: 'test', - publicKeyPem: 'test', - id: 'test#1', - }); - - const referencedAuthentication = new ReferencedAuthentication(publicKey, 'RsaSignatureAuthentication2018'); - const transformed = classToPlain(referencedAuthentication); - - expect(transformed).toMatchObject({ - type: 'RsaSignatureAuthentication2018', - publicKey: 'test#1', - }); - }); - }); - - describe('AuthenticationTransformer', () => { - class AuthenticationTransformerTest { - public publicKey: PublicKey[] = []; - - @AuthenticationTransformer() - public authentication: Authentication[] = []; - } - - it("should use generic 'publicKey' type when no matching public key type class is present", async () => { - const embeddedAuthenticationJson = { - controller: 'did:sov:1123123', - id: 'did:sov:1123123#1', - type: 'RandomType', - publicKeyPem: '-----BEGIN PUBLIC X...', - }; - - const referencedAuthenticationJson = { - type: 'RandomType', - publicKey: 'did:sov:1123123#1', - }; - - const authenticationWrapperJson = { - publicKey: [embeddedAuthenticationJson], - authentication: [referencedAuthenticationJson, embeddedAuthenticationJson], - }; - const authenticationWrapper = plainToClass(AuthenticationTransformerTest, authenticationWrapperJson); - - expect(authenticationWrapper.authentication.length).toBe(2); - - const [referencedAuthentication, embeddedAuthentication] = authenticationWrapper.authentication as [ - ReferencedAuthentication, - EmbeddedAuthentication - ]; - expect(referencedAuthentication.publicKey).toBeInstanceOf(PublicKey); - expect(embeddedAuthentication.publicKey).toBeInstanceOf(PublicKey); - }); - - it("should transform Json to ReferencedAuthentication class when the 'publicKey' key is present on the authentication object", async () => { - const publicKeyJson = { - controller: 'did:sov:1123123', - id: 'did:sov:1123123#1', - type: 'RsaVerificationKey2018', - publicKeyPem: '-----BEGIN PUBLIC X...', - }; - const referencedAuthenticationJson = { - type: 'RsaSignatureAuthentication2018', - publicKey: 'did:sov:1123123#1', - }; - - const authenticationWrapperJson = { - publicKey: [publicKeyJson], - authentication: [referencedAuthenticationJson], - }; - const authenticationWrapper = plainToClass(AuthenticationTransformerTest, authenticationWrapperJson); - - expect(authenticationWrapper.authentication.length).toBe(1); - - const firstAuth = authenticationWrapper.authentication[0] as ReferencedAuthentication; - expect(firstAuth).toBeInstanceOf(ReferencedAuthentication); - expect(firstAuth.publicKey).toBeInstanceOf(RsaSig2018); - expect(firstAuth.type).toBe(referencedAuthenticationJson.type); - }); - - it("should throw an error when the 'publicKey' is present, but no publicKey entry exists with the corresponding id", async () => { - const referencedAuthenticationJson = { - type: 'RsaVerificationKey2018', - publicKey: 'did:sov:1123123#1', - }; - - const authenticationWrapperJson = { - publicKey: [], - authentication: [referencedAuthenticationJson], - }; - - expect(() => plainToClass(AuthenticationTransformerTest, authenticationWrapperJson)).toThrowError( - `Invalid public key referenced ${referencedAuthenticationJson.publicKey}` - ); - }); - - it("should transform Json to EmbeddedAuthentication class when the 'publicKey' key is not present on the authentication object", async () => { - const publicKeyJson = { - controller: 'did:sov:1123123', - id: 'did:sov:1123123#1', - type: 'RsaVerificationKey2018', - publicKeyPem: '-----BEGIN PUBLIC X...', - }; - - const authenticationWrapperJson = { - authentication: [publicKeyJson], - }; - const authenticationWrapper = plainToClass(AuthenticationTransformerTest, authenticationWrapperJson); - - expect(authenticationWrapper.authentication.length).toBe(1); - - const firstAuth = authenticationWrapper.authentication[0] as EmbeddedAuthentication; - expect(firstAuth).toBeInstanceOf(EmbeddedAuthentication); - expect(firstAuth.publicKey).toBeInstanceOf(RsaSig2018); - expect(firstAuth.publicKey.value).toBe(publicKeyJson.publicKeyPem); - }); - - it('should transform EmbeddedAuthentication and ReferencedAuthentication class to Json', async () => { - const authenticationWrapper = new AuthenticationTransformerTest(); - authenticationWrapper.authentication = [ - new EmbeddedAuthentication( - new RsaSig2018({ - controller: 'test', - publicKeyPem: 'test', - id: 'test#1', - }) - ), - new ReferencedAuthentication( - new RsaSig2018({ - controller: 'test', - publicKeyPem: 'test', - id: 'test#1', - }), - 'RsaSignatureAuthentication2018' - ), - ]; - - expect(authenticationWrapper.authentication.length).toBe(2); - const [embeddedJson, referencedJson] = classToPlain(authenticationWrapper).authentication; - - expect(embeddedJson).toMatchObject({ - controller: 'test', - publicKeyPem: 'test', - id: 'test#1', - type: 'RsaVerificationKey2018', - }); - - expect(referencedJson).toMatchObject({ - type: 'RsaSignatureAuthentication2018', - publicKey: 'test#1', - }); - }); - }); -}); diff --git a/src/lib/modules/connections/models/did/__tests__/DidDoc.test.ts b/src/lib/modules/connections/models/did/__tests__/DidDoc.test.ts deleted file mode 100644 index 3fc9de5766..0000000000 --- a/src/lib/modules/connections/models/did/__tests__/DidDoc.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { classToPlain, plainToClass } from 'class-transformer'; - -import { ReferencedAuthentication, EmbeddedAuthentication } from '../authentication'; -import { DidDoc } from '../DidDoc'; -import { Ed25119Sig2018, EddsaSaSigSecp256k1, RsaSig2018 } from '../publicKey'; -import { Service, IndyAgentService } from '../service'; - -import diddoc from './diddoc.json'; - -const didDoc = new DidDoc({ - authentication: [ - new ReferencedAuthentication( - new RsaSig2018({ - id: '3', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyPem: '-----BEGIN PUBLIC X...', - }), - 'RsaSignatureAuthentication2018' - ), - new EmbeddedAuthentication( - new EddsaSaSigSecp256k1({ - id: '6', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyHex: '-----BEGIN PUBLIC A...', - }) - ), - ], - id: 'test-id', - publicKey: [ - new RsaSig2018({ - id: '3', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyPem: '-----BEGIN PUBLIC X...', - }), - new Ed25119Sig2018({ - id: 'did:sov:LjgpST2rjsoxYegQDRm7EL#4', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyBase58: '-----BEGIN PUBLIC 9...', - }), - new EddsaSaSigSecp256k1({ - id: '6', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyHex: '-----BEGIN PUBLIC A...', - }), - ], - service: [ - new Service({ - id: '0', - type: 'Mediator', - serviceEndpoint: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', - }), - new IndyAgentService({ - id: '6', - serviceEndpoint: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', - recipientKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], - routingKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], - priority: 5, - }), - ], -}); - -// Test adopted from ACA-Py -// TODO: add more tests -describe('Did | DidDoc', () => { - it('should correctly transforms Json to DidDoc class', () => { - const didDoc = plainToClass(DidDoc, diddoc); - - // Check array length of all items - expect(didDoc.publicKey.length).toBe(diddoc.publicKey.length); - expect(didDoc.service.length).toBe(diddoc.service.length); - expect(didDoc.authentication.length).toBe(diddoc.authentication.length); - - // Check other properties - expect(didDoc.id).toBe(diddoc.id); - expect(didDoc.context).toBe(diddoc['@context']); - - // Check publicKey - expect(didDoc.publicKey[0]).toBeInstanceOf(RsaSig2018); - expect(didDoc.publicKey[1]).toBeInstanceOf(Ed25119Sig2018); - expect(didDoc.publicKey[2]).toBeInstanceOf(EddsaSaSigSecp256k1); - - // Check Service - expect(didDoc.service[0]).toBeInstanceOf(Service); - expect(didDoc.service[1]).toBeInstanceOf(IndyAgentService); - - // Check Authentication - expect(didDoc.authentication[0]).toBeInstanceOf(ReferencedAuthentication); - expect(didDoc.authentication[1]).toBeInstanceOf(EmbeddedAuthentication); - }); - - it('should correctly transforms DidDoc class to Json', () => { - const json = classToPlain(didDoc); - - // Check array length of all items - expect(json.publicKey.length).toBe(didDoc.publicKey.length); - expect(json.service.length).toBe(didDoc.service.length); - expect(json.authentication.length).toBe(didDoc.authentication.length); - - // Check other properties - expect(json.id).toBe(didDoc.id); - expect(json['@context']).toBe(didDoc.context); - - // Check publicKey - expect(json.publicKey[0]).toMatchObject({ - id: '3', - type: 'RsaVerificationKey2018', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyPem: '-----BEGIN PUBLIC X...', - }); - expect(json.publicKey[1]).toMatchObject({ - id: 'did:sov:LjgpST2rjsoxYegQDRm7EL#4', - type: 'Ed25519VerificationKey2018', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyBase58: '-----BEGIN PUBLIC 9...', - }); - expect(json.publicKey[2]).toMatchObject({ - id: '6', - type: 'Secp256k1VerificationKey2018', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyHex: '-----BEGIN PUBLIC A...', - }); - - // Check Service - expect(json.service[0]).toMatchObject({ - id: '0', - type: 'Mediator', - serviceEndpoint: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', - }); - expect(json.service[1]).toMatchObject({ - id: '6', - type: 'IndyAgent', - serviceEndpoint: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', - recipientKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], - routingKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], - priority: 5, - }); - - // Check Authentication - expect(json.authentication[0]).toMatchObject({ - type: 'RsaSignatureAuthentication2018', - publicKey: '3', - }); - expect(json.authentication[1]).toMatchObject({ - id: '6', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - type: 'Secp256k1VerificationKey2018', - publicKeyHex: '-----BEGIN PUBLIC A...', - }); - }); - - describe('getPublicKey', () => { - it('return the public key with the specified id', async () => { - expect(didDoc.getPublicKey('3')).toEqual(didDoc.publicKey.find(item => item.id === '3')); - }); - }); - - describe('getServicesByType', () => { - it('returns all services with specified type', async () => { - expect(didDoc.getServicesByType('IndyAgent')).toEqual( - didDoc.service.filter(service => service.type === 'IndyAgent') - ); - }); - }); - - describe('getServicesByType', () => { - it('returns all services with specified class', async () => { - expect(didDoc.getServicesByClassType(IndyAgentService)).toEqual( - didDoc.service.filter(service => service instanceof IndyAgentService) - ); - }); - }); -}); diff --git a/src/lib/modules/connections/models/did/__tests__/PublicKey.test.ts b/src/lib/modules/connections/models/did/__tests__/PublicKey.test.ts deleted file mode 100644 index 9788ee1703..0000000000 --- a/src/lib/modules/connections/models/did/__tests__/PublicKey.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { ClassConstructor, classToPlain, plainToClass } from 'class-transformer'; -import { - PublicKeyTransformer, - PublicKey, - publicKeyTypes, - EddsaSaSigSecp256k1, - Ed25119Sig2018, - RsaSig2018, -} from '../publicKey'; - -const publicKeysJson = [ - { - class: RsaSig2018, - valueKey: 'publicKeyPem', - json: { - id: '3', - type: 'RsaVerificationKey2018', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyPem: '-----BEGIN PUBLIC X...', - }, - }, - { - class: Ed25119Sig2018, - valueKey: 'publicKeyBase58', - json: { - id: '4', - type: 'Ed25519VerificationKey2018', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyBase58: '-----BEGIN PUBLIC X...', - }, - }, - { - class: EddsaSaSigSecp256k1, - valueKey: 'publicKeyHex', - json: { - id: 'did:sov:LjgpST2rjsoxYegQDRm7EL#5', - type: 'Secp256k1VerificationKey2018', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyHex: '-----BEGIN PUBLIC X...', - }, - }, -]; - -describe('Did | PublicKey', () => { - it('should correctly transform Json to PublicKey class', async () => { - const json = { - id: 'did:sov:LjgpST2rjsoxYegQDRm7EL#5', - type: 'RandomType', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - }; - - const service = plainToClass(PublicKey, json); - expect(service.id).toBe(json.id); - expect(service.type).toBe(json.type); - expect(service.controller).toBe(json.controller); - }); - - it('should correctly transform PublicKey class to Json', async () => { - const json = { - id: 'did:sov:LjgpST2rjsoxYegQDRm7EL#5', - type: 'RandomType', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - }; - const publicKey = new PublicKey({ - ...json, - }); - const transformed = classToPlain(publicKey); - expect(transformed).toEqual(json); - }); - - const publicKeyJsonToClassTests: [ - string, - ClassConstructor, - Record, - string - ][] = publicKeysJson.map(pk => [pk.class.name, pk.class, pk.json, pk.valueKey]); - test.each(publicKeyJsonToClassTests)( - 'should correctly transform Json to %s class', - async (_, publicKeyClass, json, valueKey) => { - const publicKey = plainToClass(publicKeyClass, json); - - expect(publicKey.id).toBe(json.id); - expect(publicKey.type).toBe(json.type); - expect(publicKey.controller).toBe(json.controller); - expect(publicKey.value).toBe(json[valueKey]); - } - ); - - const publicKeyClassToJsonTests: [ - string, - PublicKey, - Record, - string - ][] = publicKeysJson.map(pk => [pk.class.name, new pk.class({ ...(pk.json as any) }), pk.json, pk.valueKey]); - - test.each(publicKeyClassToJsonTests)( - 'should correctly transform %s class to Json', - async (_, publicKey, json, valueKey) => { - const publicKeyJson = classToPlain(publicKey); - - expect(publicKey.value).toBe(json[valueKey]); - expect(publicKeyJson).toMatchObject(json); - } - ); - - describe('PublicKeyTransformer', () => { - class PublicKeyTransformerTest { - @PublicKeyTransformer() - public publicKey: PublicKey[] = []; - } - - it("should transform Json to default PublicKey class when the 'type' key is not present in 'publicKeyTypes'", async () => { - const publicKeyJson = { - id: '3', - type: 'RsaVerificationKey2018--unknown', - controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', - publicKeyPem: '-----BEGIN PUBLIC X...', - }; - - const publicKeyWrapperJson = { - publicKey: [publicKeyJson], - }; - const publicKeyWrapper = plainToClass(PublicKeyTransformerTest, publicKeyWrapperJson); - - expect(publicKeyWrapper.publicKey.length).toBe(1); - - const firstPublicKey = publicKeyWrapper.publicKey[0]; - expect(firstPublicKey).toBeInstanceOf(PublicKey); - expect(firstPublicKey.id).toBe(publicKeyJson.id); - expect(firstPublicKey.type).toBe(publicKeyJson.type); - expect(firstPublicKey.controller).toBe(publicKeyJson.controller); - expect(firstPublicKey.value).toBeUndefined(); - }); - - it("should transform Json to corresponding class when the 'type' key is present in 'publicKeyTypes'", async () => { - const publicKeyArray = publicKeysJson.map(pk => pk.json); - - const publicKeyWrapperJson = { - publicKey: publicKeyArray, - }; - const publicKeyWrapper = plainToClass(PublicKeyTransformerTest, publicKeyWrapperJson); - - expect(publicKeyWrapper.publicKey.length).toBe(publicKeyArray.length); - - for (let i = 0; i < publicKeyArray.length; i++) { - const publicKeyJson = publicKeyArray[i]; - const publicKey = publicKeyWrapper.publicKey[i]; - - expect(publicKey).toBeInstanceOf(publicKeyTypes[publicKeyJson.type]); - } - }); - }); -}); diff --git a/src/lib/modules/connections/models/did/__tests__/Service.test.ts b/src/lib/modules/connections/models/did/__tests__/Service.test.ts deleted file mode 100644 index 8a52c55a74..0000000000 --- a/src/lib/modules/connections/models/did/__tests__/Service.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { classToPlain, plainToClass } from 'class-transformer'; -import { Service, ServiceTransformer, serviceTypes, IndyAgentService } from '../service'; - -describe('Did | Service', () => { - it('should correctly transform Json to Service class', async () => { - const json = { - id: 'test-id', - type: 'Mediator', - serviceEndpoint: 'https://example.com', - }; - const service = plainToClass(Service, json); - - expect(service.id).toBe(json.id); - expect(service.type).toBe(json.type); - expect(service.serviceEndpoint).toBe(json.serviceEndpoint); - }); - - it('should correctly transform Service class to Json', async () => { - const json = { - id: 'test-id', - type: 'Mediator', - serviceEndpoint: 'https://example.com', - }; - - const service = new Service({ - ...json, - }); - - const transformed = classToPlain(service); - - expect(transformed).toEqual(json); - }); - - // TODO: make more generic like in PublicKey.test.ts - describe('IndyAgentService', () => { - it('should correctly transform Json to IndyAgentService class', async () => { - const json = { - id: 'test-id', - type: 'IndyAgent', - recipientKeys: ['917a109d-eae3-42bc-9436-b02426d3ce2c', '348d5200-0f8f-42cc-aad9-61e0d082a674'], - routingKeys: ['0094df0b-7b6d-4ebb-82de-234a621fb615'], - priority: 10, - serviceEndpoint: 'https://example.com', - }; - const service = plainToClass(IndyAgentService, json); - - expect(service).toMatchObject(json); - }); - - it('should correctly transform IndyAgentService class to Json', async () => { - const json = { - id: 'test-id', - type: 'IndyAgent', - recipientKeys: ['917a109d-eae3-42bc-9436-b02426d3ce2c', '348d5200-0f8f-42cc-aad9-61e0d082a674'], - routingKeys: ['0094df0b-7b6d-4ebb-82de-234a621fb615'], - priority: 10, - serviceEndpoint: 'https://example.com', - }; - - const service = new IndyAgentService({ - ...json, - }); - - const transformed = classToPlain(service); - - expect(transformed).toEqual(json); - }); - - it("should set 'priority' to default (0) when not present in constructor or during transformation", async () => { - const json = { - id: 'test-id', - type: 'IndyAgent', - recipientKeys: ['917a109d-eae3-42bc-9436-b02426d3ce2c', '348d5200-0f8f-42cc-aad9-61e0d082a674'], - routingKeys: ['0094df0b-7b6d-4ebb-82de-234a621fb615'], - serviceEndpoint: 'https://example.com', - }; - - const transformService = plainToClass(IndyAgentService, json); - const constructorService = new IndyAgentService({ ...json }); - - expect(transformService.priority).toBe(0); - expect(constructorService.priority).toBe(0); - - expect(classToPlain(transformService).priority).toBe(0); - expect(classToPlain(constructorService).priority).toBe(0); - }); - }); - - describe('ServiceTransformer', () => { - class ServiceTransformerTest { - @ServiceTransformer() - public service: Service[] = []; - } - - it("should transform Json to default Service class when the 'type' key is not present in 'serviceTypes'", async () => { - const serviceJson = { - id: 'test-id', - type: 'Mediator', - serviceEndpoint: 'https://example.com', - }; - - const serviceWrapperJson = { - service: [serviceJson], - }; - const serviceWrapper = plainToClass(ServiceTransformerTest, serviceWrapperJson); - - expect(serviceWrapper.service.length).toBe(1); - - const firstService = serviceWrapper.service[0]; - expect(firstService).toBeInstanceOf(Service); - expect(firstService.id).toBe(serviceJson.id); - expect(firstService.type).toBe(serviceJson.type); - expect(firstService.serviceEndpoint).toBe(serviceJson.serviceEndpoint); - }); - - it("should transform Json to corresponding class when the 'type' key is present in 'serviceTypes'", async () => { - const serviceArray = [ - { - id: 'test-id', - type: 'IndyAgent', - recipientKeys: ['917a109d-eae3-42bc-9436-b02426d3ce2c', '348d5200-0f8f-42cc-aad9-61e0d082a674'], - routingKeys: ['0094df0b-7b6d-4ebb-82de-234a621fb615'], - priority: 10, - serviceEndpoint: 'https://example.com', - }, - ]; - - const serviceWrapperJson = { - service: serviceArray, - }; - const serviceWrapper = plainToClass(ServiceTransformerTest, serviceWrapperJson); - - expect(serviceWrapper.service.length).toBe(serviceArray.length); - - serviceArray.forEach((serviceJson, i) => { - const service = serviceWrapper.service[i]; - expect(service).toBeInstanceOf(serviceTypes[serviceJson.type]); - }); - }); - }); -}); diff --git a/src/lib/modules/connections/models/did/authentication/Authentication.ts b/src/lib/modules/connections/models/did/authentication/Authentication.ts deleted file mode 100644 index 80b87fe5ba..0000000000 --- a/src/lib/modules/connections/models/did/authentication/Authentication.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PublicKey } from '../publicKey/PublicKey'; - -export abstract class Authentication { - abstract publicKey: PublicKey; -} diff --git a/src/lib/modules/connections/models/did/authentication/EmbeddedAuthentication.ts b/src/lib/modules/connections/models/did/authentication/EmbeddedAuthentication.ts deleted file mode 100644 index 35a230e030..0000000000 --- a/src/lib/modules/connections/models/did/authentication/EmbeddedAuthentication.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IsNotEmpty, ValidateNested } from 'class-validator'; -import { PublicKey } from '../publicKey/PublicKey'; -import { Authentication } from './Authentication'; - -export class EmbeddedAuthentication extends Authentication { - @IsNotEmpty() - @ValidateNested() - public publicKey!: PublicKey; - - public constructor(publicKey: PublicKey) { - super(); - - this.publicKey = publicKey; - } -} diff --git a/src/lib/modules/connections/models/did/authentication/ReferencedAuthentication.ts b/src/lib/modules/connections/models/did/authentication/ReferencedAuthentication.ts deleted file mode 100644 index 23a080f67f..0000000000 --- a/src/lib/modules/connections/models/did/authentication/ReferencedAuthentication.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Transform } from 'class-transformer'; -import { IsString } from 'class-validator'; -import { PublicKey } from '../publicKey/PublicKey'; -import { Authentication } from './Authentication'; - -export class ReferencedAuthentication extends Authentication { - public constructor(publicKey: PublicKey, type: string) { - super(); - - this.publicKey = publicKey; - this.type = type; - } - - @IsString() - public type!: string; - - @Transform(({ value }: { value: PublicKey }) => value.id, { toPlainOnly: true }) - public publicKey!: PublicKey; -} diff --git a/src/lib/modules/connections/models/did/authentication/index.ts b/src/lib/modules/connections/models/did/authentication/index.ts deleted file mode 100644 index c336441936..0000000000 --- a/src/lib/modules/connections/models/did/authentication/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Transform, TransformationType, ClassConstructor, plainToClass, classToPlain } from 'class-transformer'; - -import { PublicKey, publicKeyTypes } from '../publicKey'; -import { Authentication } from './Authentication'; -import { EmbeddedAuthentication } from './EmbeddedAuthentication'; -import { ReferencedAuthentication } from './ReferencedAuthentication'; - -export const authenticationTypes = { - RsaVerificationKey2018: 'RsaSignatureAuthentication2018', - Ed25519VerificationKey2018: 'Ed25519SignatureAuthentication2018', - Secp256k1VerificationKey2018: 'Secp256k1SignatureAuthenticationKey2018', -}; - -/** - * Decorator that transforms authentication json to corresonding class instances. See {@link authenticationTypes} - * - * @example - * class Example { - * AuthenticationTransformer() - * private authentication: Authentication - * } - */ -export function AuthenticationTransformer() { - return Transform( - ({ - value, - obj, - type, - }: { - value: { type: string; publicKey?: string | PublicKey }[]; - obj: { publicKey: { id: string; type: string }[] }; - type: TransformationType; - }) => { - // TODO: PLAIN_TO_PLAIN - - if (type === TransformationType.PLAIN_TO_CLASS) { - return value.map(auth => { - // referenced public key - if (auth.publicKey) { - //referenced - const publicKeyJson = obj.publicKey.find(publicKey => publicKey.id === auth.publicKey); - - if (!publicKeyJson) { - throw new Error(`Invalid public key referenced ${auth.publicKey}`); - } - - // Referenced keys use other types than embedded keys. - const publicKeyClass = (publicKeyTypes[publicKeyJson.type] ?? PublicKey) as ClassConstructor; - const publicKey = plainToClass(publicKeyClass, publicKeyJson); - return new ReferencedAuthentication(publicKey, auth.type); - } else { - // embedded - const publicKeyClass = (publicKeyTypes[auth.type] ?? PublicKey) as ClassConstructor; - const publicKey = plainToClass(publicKeyClass, auth); - return new EmbeddedAuthentication(publicKey); - } - }); - } else { - return value.map(auth => (auth instanceof EmbeddedAuthentication ? classToPlain(auth.publicKey) : auth)); - } - } - ); -} - -export { Authentication, EmbeddedAuthentication, ReferencedAuthentication }; diff --git a/src/lib/modules/connections/models/did/index.ts b/src/lib/modules/connections/models/did/index.ts deleted file mode 100644 index d5259e4ac6..0000000000 --- a/src/lib/modules/connections/models/did/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './DidDoc'; -export * from './service'; -export * from './publicKey'; -export * from './authentication'; diff --git a/src/lib/modules/connections/models/did/publicKey/Ed25119Sig2018.ts b/src/lib/modules/connections/models/did/publicKey/Ed25119Sig2018.ts deleted file mode 100644 index 6a821fc370..0000000000 --- a/src/lib/modules/connections/models/did/publicKey/Ed25119Sig2018.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Expose } from 'class-transformer'; -import { Equals, IsString } from 'class-validator'; -import { PublicKey } from './PublicKey'; - -export class Ed25119Sig2018 extends PublicKey { - public constructor(options: { id: string; controller: string; publicKeyBase58: string }) { - super({ ...options, type: 'Ed25519VerificationKey2018' }); - - if (options) { - this.value = options.publicKeyBase58; - } - } - - @Equals('Ed25519VerificationKey2018') - public type = 'Ed25519VerificationKey2018' as const; - - @Expose({ name: 'publicKeyBase58' }) - @IsString() - public value!: string; -} diff --git a/src/lib/modules/connections/models/did/publicKey/EddsaSaSigSecp256k1.ts b/src/lib/modules/connections/models/did/publicKey/EddsaSaSigSecp256k1.ts deleted file mode 100644 index 3e1c35abe5..0000000000 --- a/src/lib/modules/connections/models/did/publicKey/EddsaSaSigSecp256k1.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Expose } from 'class-transformer'; -import { Equals, IsString } from 'class-validator'; -import { PublicKey } from './PublicKey'; - -export class EddsaSaSigSecp256k1 extends PublicKey { - public constructor(options: { id: string; controller: string; publicKeyHex: string }) { - super({ ...options, type: 'Secp256k1VerificationKey2018' }); - - if (options) { - this.value = options.publicKeyHex; - } - } - - @Equals('Secp256k1VerificationKey2018') - public type = 'Secp256k1VerificationKey2018' as const; - - @Expose({ name: 'publicKeyHex' }) - @IsString() - public value!: string; -} diff --git a/src/lib/modules/connections/models/did/publicKey/PublicKey.ts b/src/lib/modules/connections/models/did/publicKey/PublicKey.ts deleted file mode 100644 index 9a5b5f54e6..0000000000 --- a/src/lib/modules/connections/models/did/publicKey/PublicKey.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { IsString } from 'class-validator'; - -export class PublicKey { - public constructor(options: { id: string; controller: string; type: string; value?: string }) { - if (options) { - this.id = options.id; - this.controller = options.controller; - this.type = options.type; - this.value = options.value; - } - } - - @IsString() - public id!: string; - - @IsString() - public controller!: string; - - @IsString() - public type!: string; - public value?: string; -} diff --git a/src/lib/modules/connections/models/did/publicKey/RsaSig2018.ts b/src/lib/modules/connections/models/did/publicKey/RsaSig2018.ts deleted file mode 100644 index c90b16c19b..0000000000 --- a/src/lib/modules/connections/models/did/publicKey/RsaSig2018.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Expose } from 'class-transformer'; -import { Equals, IsString } from 'class-validator'; -import { PublicKey } from './PublicKey'; - -export class RsaSig2018 extends PublicKey { - public constructor(options: { id: string; controller: string; publicKeyPem: string }) { - super({ ...options, type: 'RsaVerificationKey2018' }); - - if (options) { - this.value = options.publicKeyPem; - } - } - - @Equals('RsaVerificationKey2018') - public type = 'RsaVerificationKey2018' as const; - - @Expose({ name: 'publicKeyPem' }) - @IsString() - public value!: string; -} diff --git a/src/lib/modules/connections/models/did/publicKey/index.ts b/src/lib/modules/connections/models/did/publicKey/index.ts deleted file mode 100644 index 5d6aee53fe..0000000000 --- a/src/lib/modules/connections/models/did/publicKey/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Transform, ClassConstructor, plainToClass } from 'class-transformer'; - -import { PublicKey } from './PublicKey'; -import { Ed25119Sig2018 } from './Ed25119Sig2018'; -import { EddsaSaSigSecp256k1 } from './EddsaSaSigSecp256k1'; -import { RsaSig2018 } from './RsaSig2018'; - -export const publicKeyTypes: { [key: string]: unknown | undefined } = { - RsaVerificationKey2018: RsaSig2018, - Ed25519VerificationKey2018: Ed25119Sig2018, - Secp256k1VerificationKey2018: EddsaSaSigSecp256k1, -}; - -/** - * Decorator that transforms public key json to corresonding class instances. See {@link publicKeyTypes} - * - * @example - * class Example { - * @PublicKeyTransformer() - * private publicKey: PublicKey - * } - */ -export function PublicKeyTransformer() { - return Transform( - ({ value }: { value: { type: string }[] }) => { - return value.map(publicKeyJson => { - const publicKeyClass = (publicKeyTypes[publicKeyJson.type] ?? PublicKey) as ClassConstructor; - const publicKey = plainToClass(publicKeyClass, publicKeyJson); - - return publicKey; - }); - }, - { - toClassOnly: true, - } - ); -} - -export { Ed25119Sig2018, PublicKey, EddsaSaSigSecp256k1, RsaSig2018 }; diff --git a/src/lib/modules/connections/models/did/service/IndyAgentService.ts b/src/lib/modules/connections/models/did/service/IndyAgentService.ts deleted file mode 100644 index 90d5435a50..0000000000 --- a/src/lib/modules/connections/models/did/service/IndyAgentService.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ArrayNotEmpty, IsOptional, IsString } from 'class-validator'; -import { Service } from './Service'; - -export class IndyAgentService extends Service { - public constructor(options: { - id: string; - serviceEndpoint: string; - recipientKeys: string[]; - routingKeys?: string[]; - priority?: number; - }) { - super({ ...options, type: 'IndyAgent' }); - - if (options) { - this.recipientKeys = options.recipientKeys; - this.routingKeys = options.routingKeys; - if (options.priority) this.priority = options.priority; - } - } - - public type = 'IndyAgent'; - - @ArrayNotEmpty() - @IsString({ each: true }) - public recipientKeys!: string[]; - - @IsString({ each: true }) - @IsOptional() - public routingKeys?: string[]; - - public priority = 0; -} diff --git a/src/lib/modules/connections/models/did/service/Service.ts b/src/lib/modules/connections/models/did/service/Service.ts deleted file mode 100644 index 286a82b72c..0000000000 --- a/src/lib/modules/connections/models/did/service/Service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { IsString } from 'class-validator'; - -export class Service { - public constructor(options: { id: string; serviceEndpoint: string; type: string }) { - if (options) { - this.id = options.id; - this.serviceEndpoint = options.serviceEndpoint; - this.type = options.type; - } - } - - @IsString() - public id!: string; - - @IsString() - public serviceEndpoint!: string; - - @IsString() - public type!: string; -} diff --git a/src/lib/modules/connections/models/did/service/index.ts b/src/lib/modules/connections/models/did/service/index.ts deleted file mode 100644 index f0e70f09f4..0000000000 --- a/src/lib/modules/connections/models/did/service/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Transform, ClassConstructor, plainToClass } from 'class-transformer'; - -import { IndyAgentService } from './IndyAgentService'; -import { Service } from './Service'; - -export const serviceTypes: { [key: string]: unknown | undefined } = { - IndyAgent: IndyAgentService, -}; - -/** - * Decorator that transforms service json to corresponding class instances. See {@link serviceTypes} - * - * @example - * class Example { - * ServiceTransformer() - * private service: Service - * } - */ -export function ServiceTransformer() { - return Transform( - ({ value }: { value: { type: string }[] }) => { - return value.map(serviceJson => { - const serviceClass = (serviceTypes[serviceJson.type] ?? Service) as ClassConstructor; - const service = plainToClass(serviceClass, serviceJson); - - return service; - }); - }, - { - toClassOnly: true, - } - ); -} - -export { IndyAgentService, Service }; diff --git a/src/lib/modules/connections/models/index.ts b/src/lib/modules/connections/models/index.ts deleted file mode 100644 index 18f620223b..0000000000 --- a/src/lib/modules/connections/models/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './Connection'; -export * from './ConnectionRole'; -export * from './ConnectionState'; -export * from './did'; diff --git a/src/lib/modules/connections/repository/ConnectionRecord.ts b/src/lib/modules/connections/repository/ConnectionRecord.ts deleted file mode 100644 index 2d14082bad..0000000000 --- a/src/lib/modules/connections/repository/ConnectionRecord.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { v4 as uuid } from 'uuid'; -import { classToPlain, plainToClass } from 'class-transformer'; -import { BaseRecord, RecordType, Tags } from '../../../storage/BaseRecord'; -import { ConnectionState } from '..'; -import { ConnectionInvitationMessage } from '..'; -import { ConnectionRole } from '..'; -import { JsonTransformer } from '../../../utils/JsonTransformer'; -import { DidDoc } from '..'; -import { IndyAgentService } from '..'; -import type { Did, Verkey } from 'indy-sdk'; - -interface ConnectionProps { - id?: string; - createdAt?: number; - did: Did; - didDoc: DidDoc; - verkey: Verkey; - theirDid?: Did; - theirDidDoc?: DidDoc; - invitation?: ConnectionInvitationMessage; - state: ConnectionState; - role: ConnectionRole; - endpoint?: string; - alias?: string; - autoAcceptConnection?: boolean; -} - -export interface ConnectionTags extends Tags { - invitationKey?: string; - threadId?: string; - verkey?: string; - theirKey?: string; -} - -export interface ConnectionStorageProps extends ConnectionProps { - tags: ConnectionTags; -} - -export class ConnectionRecord extends BaseRecord implements ConnectionStorageProps { - public did: Did; - private _didDoc!: Record; - public verkey: Verkey; - public theirDid?: Did; - private _theirDidDoc?: Record; - private _invitation?: Record; - public state: ConnectionState; - public role: ConnectionRole; - public endpoint?: string; - public alias?: string; - public autoAcceptConnection?: boolean; - public tags: ConnectionTags; - - public static readonly type: RecordType = RecordType.ConnectionRecord; - public readonly type = ConnectionRecord.type; - - public constructor(props: ConnectionStorageProps) { - super(props.id ?? uuid(), props.createdAt ?? Date.now()); - this.did = props.did; - this.didDoc = props.didDoc; - this.verkey = props.verkey; - this.theirDid = props.theirDid; - this.theirDidDoc = props.theirDidDoc; - this.state = props.state; - this.role = props.role; - this.endpoint = props.endpoint; - this.alias = props.alias; - this.autoAcceptConnection = props.autoAcceptConnection; - this.tags = props.tags; - this.invitation = props.invitation; - - // We need a better approach for this. After retrieving the connection message from - // persistence it is plain json, so we need to transform it to a message class - // if transform all record classes with class transformer this wouldn't be needed anymore - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const _invitation = props._invitation; - if (_invitation) { - this._invitation = _invitation; - } - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const _didDoc = props._didDoc; - if (_didDoc) { - this._didDoc = _didDoc; - } - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const _theirDidDoc = props._theirDidDoc; - if (_theirDidDoc) { - this._theirDidDoc = _theirDidDoc; - } - } - - public get invitation() { - if (this._invitation) return JsonTransformer.fromJSON(this._invitation, ConnectionInvitationMessage); - } - - public set invitation(invitation: ConnectionInvitationMessage | undefined) { - if (invitation) this._invitation = JsonTransformer.toJSON(invitation); - } - - public get didDoc() { - return plainToClass(DidDoc, this._didDoc); - } - - public set didDoc(didDoc: DidDoc) { - this._didDoc = classToPlain(didDoc); - } - - public get theirDidDoc() { - if (this._theirDidDoc) return plainToClass(DidDoc, this._theirDidDoc); - } - - public set theirDidDoc(didDoc: DidDoc | undefined) { - this._theirDidDoc = classToPlain(didDoc); - } - - public get myKey() { - const [service] = this.didDoc?.getServicesByClassType(IndyAgentService) ?? []; - - if (!service) { - return null; - } - - return service.recipientKeys[0]; - } - - public get theirKey() { - const [service] = this.theirDidDoc?.getServicesByClassType(IndyAgentService) ?? []; - - if (!service) { - return null; - } - - return service.recipientKeys[0]; - } - - public get isReady() { - return [ConnectionState.Responded, ConnectionState.Complete].includes(this.state); - } - - public assertReady() { - if (!this.isReady) { - throw new Error( - `Connection record is not ready to be used. Expected ${ConnectionState.Responded} or ${ConnectionState.Complete}, found invalid state ${this.state}` - ); - } - } - - public assertState(expectedStates: ConnectionState | ConnectionState[]) { - if (!Array.isArray(expectedStates)) { - expectedStates = [expectedStates]; - } - - if (!expectedStates.includes(this.state)) { - throw new Error( - `Connection record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.` - ); - } - } - - public assertRole(expectedRole: ConnectionRole) { - if (this.role !== expectedRole) { - throw new Error(`Connection record has invalid role ${this.role}. Expected role ${expectedRole}.`); - } - } -} diff --git a/src/lib/modules/connections/services/ConnectionService.ts b/src/lib/modules/connections/services/ConnectionService.ts deleted file mode 100644 index ace996d645..0000000000 --- a/src/lib/modules/connections/services/ConnectionService.ts +++ /dev/null @@ -1,458 +0,0 @@ -import type { Verkey } from 'indy-sdk'; -import { EventEmitter } from 'events'; -import { validateOrReject } from 'class-validator'; - -import { AgentConfig } from '../../../agent/AgentConfig'; -import { ConnectionRecord, ConnectionTags } from '../repository/ConnectionRecord'; -import { Repository } from '../../../storage/Repository'; -import { Wallet } from '../../../wallet/Wallet'; -import { - ConnectionInvitationMessage, - ConnectionRequestMessage, - ConnectionResponseMessage, - TrustPingMessage, -} from '../messages'; -import { AckMessage } from '../../common'; -import { signData, unpackAndVerifySignatureDecorator } from '../../../decorators/signature/SignatureDecoratorUtils'; -import { - Connection, - ConnectionState, - ConnectionRole, - DidDoc, - Ed25119Sig2018, - IndyAgentService, - authenticationTypes, - ReferencedAuthentication, -} from '../models'; -import { InboundMessageContext } from '../../../agent/models/InboundMessageContext'; -import { JsonTransformer } from '../../../utils/JsonTransformer'; -import { AgentMessage } from '../../../agent/AgentMessage'; - -export enum ConnectionEventType { - StateChanged = 'stateChanged', -} - -export interface ConnectionStateChangedEvent { - connectionRecord: ConnectionRecord; - previousState: ConnectionState | null; -} - -export interface ConnectionProtocolMsgReturnType { - message: MessageType; - connectionRecord: ConnectionRecord; -} - -export class ConnectionService extends EventEmitter { - private wallet: Wallet; - private config: AgentConfig; - private connectionRepository: Repository; - - public constructor(wallet: Wallet, config: AgentConfig, connectionRepository: Repository) { - super(); - this.wallet = wallet; - this.config = config; - this.connectionRepository = connectionRepository; - } - - /** - * Create a new connection record containing a connection invitation message - * - * @param config config for creation of connection and invitation - * @returns new connection record - */ - public async createInvitation(config?: { - autoAcceptConnection?: boolean; - alias?: string; - }): Promise> { - // TODO: public did, multi use - const connectionRecord = await this.createConnection({ - role: ConnectionRole.Inviter, - state: ConnectionState.Invited, - alias: config?.alias, - autoAcceptConnection: config?.autoAcceptConnection, - }); - - const { didDoc } = connectionRecord; - const [service] = didDoc.getServicesByClassType(IndyAgentService); - const invitation = new ConnectionInvitationMessage({ - label: this.config.label, - recipientKeys: service.recipientKeys, - serviceEndpoint: service.serviceEndpoint, - routingKeys: service.routingKeys, - }); - - connectionRecord.invitation = invitation; - - await this.connectionRepository.update(connectionRecord); - - const event: ConnectionStateChangedEvent = { - connectionRecord: connectionRecord, - previousState: null, - }; - this.emit(ConnectionEventType.StateChanged, event); - - return { connectionRecord: connectionRecord, message: invitation }; - } - - /** - * Process a received invitation message. This will not accept the invitation - * or send an invitation request message. It will only create a connection record - * with all the information about the invitation stored. Use {@link ConnectionService#createRequest} - * after calling this function to create a connection request. - * - * @param invitation the invitation message to process - * @returns new connection record. - */ - public async processInvitation( - invitation: ConnectionInvitationMessage, - config?: { - autoAcceptConnection?: boolean; - alias?: string; - } - ): Promise { - const connectionRecord = await this.createConnection({ - role: ConnectionRole.Invitee, - state: ConnectionState.Invited, - alias: config?.alias, - autoAcceptConnection: config?.autoAcceptConnection, - invitation, - tags: { - invitationKey: invitation.recipientKeys && invitation.recipientKeys[0], - }, - }); - - await this.connectionRepository.update(connectionRecord); - - const event: ConnectionStateChangedEvent = { - connectionRecord: connectionRecord, - previousState: null, - }; - this.emit(ConnectionEventType.StateChanged, event); - - return connectionRecord; - } - - /** - * Create a connection request message for the connection with the specified connection id. - * - * @param connectionId the id of the connection for which to create a connection request - * @returns outbound message containing connection request - */ - public async createRequest(connectionId: string): Promise> { - const connectionRecord = await this.connectionRepository.find(connectionId); - - connectionRecord.assertState(ConnectionState.Invited); - connectionRecord.assertRole(ConnectionRole.Invitee); - - const connectionRequest = new ConnectionRequestMessage({ - label: this.config.label, - did: connectionRecord.did, - didDoc: connectionRecord.didDoc, - }); - - await this.updateState(connectionRecord, ConnectionState.Requested); - - return { - connectionRecord: connectionRecord, - message: connectionRequest, - }; - } - - /** - * Process a received connection request message. This will not accept the connection request - * or send a connection response message. It will only update the existing connection record - * with all the new information from the connection request message. Use {@link ConnectionService#createResponse} - * after calling this function to create a connection respone. - * - * @param messageContext the message context containing a connetion request message - * @returns updated connection record - */ - public async processRequest( - messageContext: InboundMessageContext - ): Promise { - const { message, connection: connectionRecord, recipientVerkey } = messageContext; - - if (!connectionRecord) { - throw new Error(`Connection for verkey ${recipientVerkey} not found!`); - } - - connectionRecord.assertState(ConnectionState.Invited); - connectionRecord.assertRole(ConnectionRole.Inviter); - - // TODO: validate using class-validator - if (!message.connection) { - throw new Error('Invalid message'); - } - - connectionRecord.theirDid = message.connection.did; - connectionRecord.theirDidDoc = message.connection.didDoc; - - if (!connectionRecord.theirKey) { - throw new Error(`Connection with id ${connectionRecord.id} has no recipient keys.`); - } - - connectionRecord.tags = { - ...connectionRecord.tags, - theirKey: connectionRecord.theirKey, - threadId: message.id, - }; - - await this.updateState(connectionRecord, ConnectionState.Requested); - - return connectionRecord; - } - - /** - * Create a connection response message for the connection with the specified connection id. - * - * @param connectionId the id of the connection for which to create a connection response - * @returns outbound message contaning connection response - */ - public async createResponse( - connectionId: string - ): Promise> { - const connectionRecord = await this.connectionRepository.find(connectionId); - - connectionRecord.assertState(ConnectionState.Requested); - connectionRecord.assertRole(ConnectionRole.Inviter); - - const connection = new Connection({ - did: connectionRecord.did, - didDoc: connectionRecord.didDoc, - }); - - const connectionJson = JsonTransformer.toJSON(connection); - - const connectionResponse = new ConnectionResponseMessage({ - threadId: connectionRecord.tags.threadId!, - connectionSig: await signData(connectionJson, this.wallet, connectionRecord.verkey), - }); - - await this.updateState(connectionRecord, ConnectionState.Responded); - - return { - connectionRecord: connectionRecord, - message: connectionResponse, - }; - } - - /** - * Process a received connection response message. This will not accept the connection request - * or send a connection acknowledgement message. It will only update the existing connection record - * with all the new information from the connection response message. Use {@link ConnectionService#createTrustPing} - * after calling this function to create a trust ping message. - * - * @param messageContext the message context containing a connetion response message - * @returns updated connection record - */ - public async processResponse( - messageContext: InboundMessageContext - ): Promise { - const { message, connection: connectionRecord, recipientVerkey } = messageContext; - - if (!connectionRecord) { - throw new Error(`Connection for verkey ${recipientVerkey} not found!`); - } - connectionRecord.assertState(ConnectionState.Requested); - connectionRecord.assertRole(ConnectionRole.Invitee); - - const connectionJson = await unpackAndVerifySignatureDecorator(message.connectionSig, this.wallet); - - const connection = JsonTransformer.fromJSON(connectionJson, Connection); - // TODO: throw framework error stating the connection object is invalid - await validateOrReject(connection); - - // Per the Connection RFC we must check if the key used to sign the connection~sig is the same key - // as the recipient key(s) in the connection invitation message - const signerVerkey = message.connectionSig.signer; - const invitationKey = connectionRecord.tags.invitationKey; - if (signerVerkey !== invitationKey) { - throw new Error('Connection in connection response is not signed with same key as recipient key in invitation'); - } - - connectionRecord.theirDid = connection.did; - connectionRecord.theirDidDoc = connection.didDoc; - - if (!connectionRecord.theirKey) { - throw new Error(`Connection with id ${connectionRecord.id} has no recipient keys.`); - } - - connectionRecord.tags = { - ...connectionRecord.tags, - theirKey: connectionRecord.theirKey, - threadId: message.threadId, - }; - - await this.updateState(connectionRecord, ConnectionState.Responded); - return connectionRecord; - } - - /** - * Create a trust ping message for the connection with the specified connection id. - * - * @param connectionId the id of the connection for which to create a trust ping message - * @returns outbound message contaning trust ping message - */ - public async createTrustPing(connectionId: string): Promise> { - const connectionRecord = await this.connectionRepository.find(connectionId); - - connectionRecord.assertState([ConnectionState.Responded, ConnectionState.Complete]); - - // TODO: - // - create ack message - // - allow for options - // - maybe this shouldn't be in the connection service? - const trustPing = new TrustPingMessage(); - - await this.updateState(connectionRecord, ConnectionState.Complete); - - return { - connectionRecord: connectionRecord, - message: trustPing, - }; - } - - /** - * Process a received ack message. This will update the state of the connection - * to Completed if this is not already the case. - * - * @param messageContext the message context containing an ack message - * @returns updated connection record - */ - public async processAck(messageContext: InboundMessageContext): Promise { - const connection = messageContext.connection; - - if (!connection) { - throw new Error(`Connection for verkey ${messageContext.recipientVerkey} not found!`); - } - - // TODO: This is better addressed in a middleware of some kind because - // any message can transition the state to complete, not just an ack or trust ping - if (connection.state === ConnectionState.Responded && connection.role === ConnectionRole.Inviter) { - await this.updateState(connection, ConnectionState.Complete); - } - - return connection; - } - - public async updateState(connectionRecord: ConnectionRecord, newState: ConnectionState) { - const previousState = connectionRecord.state; - connectionRecord.state = newState; - await this.connectionRepository.update(connectionRecord); - - const event: ConnectionStateChangedEvent = { - connectionRecord: connectionRecord, - previousState, - }; - - this.emit(ConnectionEventType.StateChanged, event); - } - - private async createConnection(options: { - role: ConnectionRole; - state: ConnectionState; - invitation?: ConnectionInvitationMessage; - alias?: string; - autoAcceptConnection?: boolean; - tags?: ConnectionTags; - }): Promise { - const [did, verkey] = await this.wallet.createDid(); - - const publicKey = new Ed25119Sig2018({ - id: `${did}#1`, - controller: did, - publicKeyBase58: verkey, - }); - - const service = new IndyAgentService({ - id: `${did};indy`, - serviceEndpoint: this.config.getEndpoint(), - recipientKeys: [verkey], - routingKeys: this.config.getRoutingKeys(), - }); - - // TODO: abstract the second parameter for ReferencedAuthentication away. This can be - // inferred from the publicKey class instance - const auth = new ReferencedAuthentication(publicKey, authenticationTypes[publicKey.type]); - - const didDoc = new DidDoc({ - id: did, - authentication: [auth], - service: [service], - publicKey: [publicKey], - }); - - const connectionRecord = new ConnectionRecord({ - did, - didDoc, - verkey, - state: options.state, - role: options.role, - tags: { - verkey, - ...options.tags, - }, - invitation: options.invitation, - alias: options.alias, - autoAcceptConnection: options.autoAcceptConnection, - }); - - await this.connectionRepository.save(connectionRecord); - return connectionRecord; - } - - public getConnections() { - return this.connectionRepository.findAll(); - } - - /** - * Retrieve a connection record by id - * - * @param connectionId The connection record id - * @throws {Error} If no record is found - * @return The connection record - * - */ - public async getById(connectionId: string): Promise { - return this.connectionRepository.find(connectionId); - } - - public async find(connectionId: string): Promise { - try { - const connection = await this.connectionRepository.find(connectionId); - - return connection; - } catch { - // connection not found. - return null; - } - } - - public async findByVerkey(verkey: Verkey): Promise { - const connectionRecords = await this.connectionRepository.findByQuery({ verkey }); - - if (connectionRecords.length > 1) { - throw new Error(`There is more than one connection for given verkey ${verkey}`); - } - - if (connectionRecords.length < 1) { - return null; - } - - return connectionRecords[0]; - } - - public async findByTheirKey(verkey: Verkey): Promise { - const connectionRecords = await this.connectionRepository.findByQuery({ theirKey: verkey }); - - if (connectionRecords.length > 1) { - throw new Error(`There is more than one connection for given verkey ${verkey}`); - } - - if (connectionRecords.length < 1) { - return null; - } - - return connectionRecords[0]; - } -} diff --git a/src/lib/modules/connections/services/TrustPingService.ts b/src/lib/modules/connections/services/TrustPingService.ts deleted file mode 100644 index 6170ea279d..0000000000 --- a/src/lib/modules/connections/services/TrustPingService.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createOutboundMessage } from '../../../agent/helpers'; -import { InboundMessageContext } from '../../../agent/models/InboundMessageContext'; -import { ConnectionRecord } from '../repository/ConnectionRecord'; -import { TrustPingMessage, TrustPingResponseMessage } from '../messages'; - -/** - * @todo use connection from message context - */ -export class TrustPingService { - public processPing({ message }: InboundMessageContext, connection: ConnectionRecord) { - if (message.responseRequested) { - const response = new TrustPingResponseMessage({ - threadId: message.id, - }); - - return createOutboundMessage(connection, response); - } - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public processPingResponse(inboundMessage: InboundMessageContext) { - // TODO: handle ping response message - } -} diff --git a/src/lib/modules/connections/services/index.ts b/src/lib/modules/connections/services/index.ts deleted file mode 100644 index 0334644614..0000000000 --- a/src/lib/modules/connections/services/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ConnectionService'; -export * from './TrustPingService'; diff --git a/src/lib/modules/credentials/CredentialUtils.ts b/src/lib/modules/credentials/CredentialUtils.ts deleted file mode 100644 index 1ced3d42d8..0000000000 --- a/src/lib/modules/credentials/CredentialUtils.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { CredValues } from 'indy-sdk'; -import { sha256 } from 'js-sha256'; -import BigNumber from 'bn.js'; - -import { CredentialPreview } from './messages/CredentialPreview'; - -export class CredentialUtils { - /** - * Converts int value to string - * Converts string value: - * - hash with sha256, - * - convert to byte array and reverse it - * - convert it to BigInteger and return as a string - * @param credentialPreview - * - * @returns CredValues - */ - public static convertPreviewToValues(credentialPreview: CredentialPreview): CredValues { - return credentialPreview.attributes.reduce((credentialValues, attribute) => { - return { - [attribute.name]: { - raw: attribute.value, - encoded: CredentialUtils.encode(attribute.value), - }, - ...credentialValues, - }; - }, {}); - } - - /** - * Check whether the raw value matches the encoded version according to the encoding format described in Aries RFC 0037 - * Use this method to ensure the received proof (over the encoded) value is the same as the raw value of the data. - * - * @param raw - * @param encoded - * @returns Whether raw and encoded value match - * - * @see https://github.com/hyperledger/aries-framework-dotnet/blob/a18bef91e5b9e4a1892818df7408e2383c642dfa/src/Hyperledger.Aries/Utils/CredentialUtils.cs#L78-L89 - * @see https://github.com/hyperledger/aries-rfcs/blob/be4ad0a6fb2823bb1fc109364c96f077d5d8dffa/features/0037-present-proof/README.md#verifying-claims-of-indy-based-verifiable-credentials - */ - public static checkValidEncoding(raw: any, encoded: string) { - return encoded === CredentialUtils.encode(raw); - } - - /** - * Encode value according to the encoding format described in Aries RFC 0036/0037 - * - * @param value - * @returns Encoded version of value - * - * @see https://github.com/hyperledger/aries-cloudagent-python/blob/0000f924a50b6ac5e6342bff90e64864672ee935/aries_cloudagent/messaging/util.py#L106-L136 - * @see https://github.com/hyperledger/aries-rfcs/blob/be4ad0a6fb2823bb1fc109364c96f077d5d8dffa/features/0037-present-proof/README.md#verifying-claims-of-indy-based-verifiable-credentials - * @see https://github.com/hyperledger/aries-rfcs/blob/be4ad0a6fb2823bb1fc109364c96f077d5d8dffa/features/0036-issue-credential/README.md#encoding-claims-for-indy-based-verifiable-credentials - */ - public static encode(value: any) { - const isString = typeof value === 'string'; - const isEmpty = isString && value === ''; - const isNumber = typeof value === 'number'; - const isBoolean = typeof value === 'boolean'; - - // If bool return bool as number string - if (isBoolean) { - return Number(value).toString(); - } - - // If value is int32 return as number string - if (isNumber && this.isInt32(value)) { - return value.toString(); - } - - // If value is an int32 number string return as number string - if (isString && !isEmpty && !isNaN(Number(value)) && this.isInt32(Number(value))) { - return value; - } - - if (isNumber) { - value = value.toString(); - } - - // If value is null we must use the string value 'None' - if (value === null || value === undefined) { - value = 'None'; - } - - return new BigNumber(sha256.array(value)).toString(); - } - - private static isInt32(number: number) { - const minI32 = -2147483648; - const maxI32 = 2147483647; - - // Check if number is integer and in range of int32 - return Number.isInteger(number) && number >= minI32 && number <= maxI32; - } -} diff --git a/src/lib/modules/credentials/CredentialsModule.ts b/src/lib/modules/credentials/CredentialsModule.ts deleted file mode 100644 index de3a5fade9..0000000000 --- a/src/lib/modules/credentials/CredentialsModule.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { CredentialRecord } from './repository/CredentialRecord'; -import { createOutboundMessage } from '../../agent/helpers'; -import { MessageSender } from '../../agent/MessageSender'; -import { ConnectionService } from '../connections'; -import { EventEmitter } from 'events'; -import { CredentialOfferTemplate, CredentialService } from './services'; -import { ProposeCredentialMessage, ProposeCredentialMessageOptions } from './messages'; -import { JsonTransformer } from '../../utils/JsonTransformer'; -import { CredentialInfo } from './models'; -import { Dispatcher } from '../../agent/Dispatcher'; -import { - ProposeCredentialHandler, - OfferCredentialHandler, - RequestCredentialHandler, - IssueCredentialHandler, - CredentialAckHandler, -} from './handlers'; - -export class CredentialsModule { - private connectionService: ConnectionService; - private credentialService: CredentialService; - private messageSender: MessageSender; - - public constructor( - dispatcher: Dispatcher, - connectionService: ConnectionService, - credentialService: CredentialService, - messageSender: MessageSender - ) { - this.connectionService = connectionService; - this.credentialService = credentialService; - this.messageSender = messageSender; - this.registerHandlers(dispatcher); - } - - /** - * Get the event emitter for the credential service. Will emit state changed events - * when the state of credential records changes. - * - * @returns event emitter for credential related state changes - */ - public get events(): EventEmitter { - return this.credentialService; - } - - /** - * Initiate a new credential exchange as holder by sending a credential proposal message - * to the connection with the specified connection id. - * - * @param connectionId The connection to send the credential proposal to - * @param config Additional configuration to use for the proposal - * @returns Credential record associated with the sent proposal message - */ - public async proposeCredential(connectionId: string, config?: Omit) { - const connection = await this.connectionService.getById(connectionId); - - const { message, credentialRecord } = await this.credentialService.createProposal(connection, config); - - const outbound = createOutboundMessage(connection, message); - await this.messageSender.sendMessage(outbound); - - return credentialRecord; - } - - /** - * Accept a credential proposal as issuer (by sending a credential offer message) to the connection - * associated with the credential record. - * - * @param credentialRecordId The id of the credential record for which to accept the proposal - * @param config Additional configuration to use for the offer - * @returns Credential record associated with the credential offer - * - */ - public async acceptProposal( - credentialRecordId: string, - config?: { - comment?: string; - credentialDefinitionId?: string; - } - ) { - const credentialRecord = await this.credentialService.getById(credentialRecordId); - const connection = await this.connectionService.getById(credentialRecord.connectionId); - - // FIXME: transformation should be handled by record class - const credentialProposalMessage = JsonTransformer.fromJSON( - credentialRecord.proposalMessage, - ProposeCredentialMessage - ); - - if (!credentialProposalMessage.credentialProposal) { - throw new Error(`Credential record with id ${credentialRecordId} is missing required credential proposal`); - } - - const credentialDefinitionId = config?.credentialDefinitionId ?? credentialProposalMessage.credentialDefinitionId; - - if (!credentialDefinitionId) { - throw new Error( - 'Missing required credential definition id. If credential proposal message contains no credential definition id it must be passed to config.' - ); - } - - // TODO: check if it is possible to issue credential based on proposal filters - const { message } = await this.credentialService.createOfferAsResponse(credentialRecord, { - preview: credentialProposalMessage.credentialProposal, - credentialDefinitionId, - comment: config?.comment, - }); - - const outboundMessage = createOutboundMessage(connection, message); - await this.messageSender.sendMessage(outboundMessage); - - return credentialRecord; - } - - /** - * Initiate a new credential exchange as issuer by sending a credential offer message - * to the connection with the specified connection id. - * - * @param connectionId The connection to send the credential offer to - * @param credentialTemplate The credential template to use for the offer - * @returns Credential record associated with the sent credential offer message - */ - public async offerCredential( - connectionId: string, - credentialTemplate: CredentialOfferTemplate - ): Promise { - const connection = await this.connectionService.getById(connectionId); - - const { message, credentialRecord } = await this.credentialService.createOffer(connection, credentialTemplate); - - const outboundMessage = createOutboundMessage(connection, message); - await this.messageSender.sendMessage(outboundMessage); - - return credentialRecord; - } - - /** - * Accept a credential offer as holder (by sending a credential request message) to the connection - * associated with the credential record. - * - * @param credentialRecordId The id of the credential record for which to accept the offer - * @param config Additional configuration to use for the request - * @returns Credential record associated with the sent credential request message - * - */ - public async acceptOffer(credentialRecordId: string, config?: { comment?: string }) { - const credentialRecord = await this.credentialService.getById(credentialRecordId); - const connection = await this.connectionService.getById(credentialRecord.connectionId); - - const { message } = await this.credentialService.createRequest(credentialRecord, config); - - const outboundMessage = createOutboundMessage(connection, message); - await this.messageSender.sendMessage(outboundMessage); - - return credentialRecord; - } - - /** - * Accept a credential request as issuer (by sending a credential message) to the connection - * associated with the credential record. - * - * @param credentialRecordId The id of the credential record for which to accept the request - * @param config Additional configuration to use for the credential - * @returns Credential record associated with the sent presentation message - * - */ - public async acceptRequest(credentialRecordId: string, config?: { comment?: string }) { - const credentialRecord = await this.credentialService.getById(credentialRecordId); - const connection = await this.connectionService.getById(credentialRecord.connectionId); - - const { message } = await this.credentialService.createCredential(credentialRecord, config); - const outboundMessage = createOutboundMessage(connection, message); - await this.messageSender.sendMessage(outboundMessage); - - return credentialRecord; - } - - /** - * Accept a credential as holder (by sending a credential acknowledgement message) to the connection - * associated with the credential record. - * - * @param credentialRecordId The id of the credential record for which to accept the credential - * @returns credential record associated with the sent credential acknowledgement message - * - */ - public async acceptCredential(credentialRecordId: string) { - const credentialRecord = await this.credentialService.getById(credentialRecordId); - const connection = await this.connectionService.getById(credentialRecord.connectionId); - - const { message } = await this.credentialService.createAck(credentialRecord); - const outboundMessage = createOutboundMessage(connection, message); - await this.messageSender.sendMessage(outboundMessage); - - return credentialRecord; - } - - /** - * Retrieve all credential records - * - * @returns List containing all credential records - */ - public async getAll(): Promise { - return this.credentialService.getAll(); - } - - /** - * Retrieve a credential record by id - * - * @param credentialRecordId The credential record id - * @throws {Error} If no record is found - * @return The credential record - * - */ - public async getById(credentialRecordId: string) { - return this.credentialService.getById(credentialRecordId); - } - - /** - * Retrieve a credential record by thread id - * - * @param threadId The thread id - * @throws {Error} If no record is found - * @throws {Error} If multiple records are found - * @returns The credential record - */ - public async getByThreadId(threadId: string): Promise { - return this.credentialService.getByThreadId(threadId); - } - - /** - * Retrieve an indy credential by credential id (referent) - * - * @param credentialId the id (referent) of the indy credential - * @returns Indy credential info object - */ - public async getIndyCredential(credentialId: string): Promise { - return this.credentialService.getIndyCredential(credentialId); - } - - private registerHandlers(dispatcher: Dispatcher) { - dispatcher.registerHandler(new ProposeCredentialHandler(this.credentialService)); - dispatcher.registerHandler(new OfferCredentialHandler(this.credentialService)); - dispatcher.registerHandler(new RequestCredentialHandler(this.credentialService)); - dispatcher.registerHandler(new IssueCredentialHandler(this.credentialService)); - dispatcher.registerHandler(new CredentialAckHandler(this.credentialService)); - } -} diff --git a/src/lib/modules/credentials/__tests__/CredentialService.test.ts b/src/lib/modules/credentials/__tests__/CredentialService.test.ts deleted file mode 100644 index a279481e5a..0000000000 --- a/src/lib/modules/credentials/__tests__/CredentialService.test.ts +++ /dev/null @@ -1,838 +0,0 @@ -/* eslint-disable no-console */ -import type Indy from 'indy-sdk'; -import type { CredReqMetadata, WalletQuery, CredDef } from 'indy-sdk'; -import { Wallet } from '../../../wallet/Wallet'; -import { Repository } from '../../../storage/Repository'; -import { CredentialOfferTemplate, CredentialService, CredentialEventType } from '../services'; -import { CredentialRecord } from '../repository/CredentialRecord'; -import { InboundMessageContext } from '../../../agent/models/InboundMessageContext'; -import { CredentialState } from '../CredentialState'; -import { StubWallet } from './StubWallet'; -import { - OfferCredentialMessage, - CredentialPreview, - CredentialPreviewAttribute, - RequestCredentialMessage, - IssueCredentialMessage, - CredentialAckMessage, - INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, - INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, - INDY_CREDENTIAL_ATTACHMENT_ID, -} from '../messages'; -import { AckStatus } from '../../common'; -import { JsonEncoder } from '../../../utils/JsonEncoder'; -import { credDef, credOffer, credReq } from './fixtures'; -import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment'; -import { LedgerService as LedgerServiceImpl } from '../../ledger/services'; -import { ConnectionState } from '../../connections'; -import { getMockConnection } from '../../connections/__tests__/ConnectionService.test'; -import { AgentConfig } from '../../../agent/AgentConfig'; - -jest.mock('./../../../storage/Repository'); -jest.mock('./../../../modules/ledger/services/LedgerService'); - -const indy = {} as typeof Indy; - -const CredentialRepository = >>(Repository); -// const ConnectionService = >(ConnectionServiceImpl); -const LedgerService = >(LedgerServiceImpl); - -const connection = getMockConnection({ - id: '123', - state: ConnectionState.Complete, -}); - -const credentialPreview = new CredentialPreview({ - attributes: [ - new CredentialPreviewAttribute({ - name: 'name', - mimeType: 'text/plain', - value: 'John', - }), - new CredentialPreviewAttribute({ - name: 'age', - mimeType: 'text/plain', - value: '99', - }), - ], -}); - -const offerAttachment = new Attachment({ - id: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: - 'eyJzY2hlbWFfaWQiOiJhYWEiLCJjcmVkX2RlZl9pZCI6IlRoN01wVGFSWlZSWW5QaWFiZHM4MVk6MzpDTDoxNzpUQUciLCJub25jZSI6Im5vbmNlIiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7fX0', - }), -}); - -const requestAttachment = new Attachment({ - id: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(credReq), - }), -}); - -// TODO: replace attachment with credential fixture -const credentialAttachment = new Attachment({ - id: INDY_CREDENTIAL_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(credReq), - }), -}); - -// A record is deserialized to JSON when it's stored into the storage. We want to simulate this behaviour for `offer` -// object to test our service would behave correctly. We use type assertion for `offer` attribute to `any`. -const mockCredentialRecord = ({ - state, - requestMessage, - requestMetadata, - tags, - id, -}: { - state: CredentialState; - requestMessage?: RequestCredentialMessage; - requestMetadata?: CredReqMetadata; - tags?: Record; - id?: string; -}) => - new CredentialRecord({ - offerMessage: new OfferCredentialMessage({ - comment: 'some comment', - credentialPreview: credentialPreview, - attachments: [offerAttachment], - }).toJSON(), - id, - requestMessage, - requestMetadata: requestMetadata, - state: state || CredentialState.OfferSent, - tags: tags || {}, - connectionId: '123', - } as any); - -describe('CredentialService', () => { - let wallet: Wallet; - let credentialRepository: Repository; - let credentialService: CredentialService; - let ledgerService: LedgerServiceImpl; - let repositoryFindMock: jest.Mock, [string]>; - let repositoryFindByQueryMock: jest.Mock, [WalletQuery]>; - let ledgerServiceGetCredDef: jest.Mock, [string]>; - - beforeAll(async () => { - wallet = new StubWallet(); - await wallet.init(); - }); - - afterAll(async () => { - await wallet.close(); - await wallet.delete(); - }); - - beforeEach(() => { - credentialRepository = new CredentialRepository(); - // connectionService = new ConnectionService(); - ledgerService = new LedgerService(); - - credentialService = new CredentialService( - wallet, - credentialRepository, - { getById: () => Promise.resolve(connection) } as any, - ledgerService, - new AgentConfig({ - walletConfig: { id: 'test' }, - walletCredentials: { key: 'test' }, - indy, - label: 'test', - }) - ); - - // make separate repositoryFindMock variable to get the correct jest mock typing - repositoryFindMock = credentialRepository.find as jest.Mock, [string]>; - - // make separate repositoryFindByQueryMock variable to get the correct jest mock typing - repositoryFindByQueryMock = credentialRepository.findByQuery as jest.Mock< - Promise, - [WalletQuery] - >; - - ledgerServiceGetCredDef = ledgerService.getCredentialDefinition as jest.Mock, [string]>; - ledgerServiceGetCredDef.mockReturnValue(Promise.resolve(credDef)); - }); - - describe('createCredentialOffer', () => { - let credentialTemplate: CredentialOfferTemplate; - - beforeEach(() => { - credentialTemplate = { - credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', - comment: 'some comment', - preview: credentialPreview, - }; - }); - - test(`creates credential record in ${CredentialState.OfferSent} state with offer, thread ID`, async () => { - // given - const repositorySaveSpy = jest.spyOn(credentialRepository, 'save'); - - // when - const { message: credentialOffer } = await credentialService.createOffer(connection, credentialTemplate); - - // then - expect(repositorySaveSpy).toHaveBeenCalledTimes(1); - const [[createdCredentialRecord]] = repositorySaveSpy.mock.calls; - expect(createdCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Number), - offerMessage: credentialOffer, - tags: { threadId: createdCredentialRecord.offerMessage?.id }, - state: CredentialState.OfferSent, - }); - }); - - test(`emits stateChange event with a new credential in ${CredentialState.OfferSent} state`, async () => { - const eventListenerMock = jest.fn(); - credentialService.on(CredentialEventType.StateChanged, eventListenerMock); - - await credentialService.createOffer(connection, credentialTemplate); - - expect(eventListenerMock).toHaveBeenCalledTimes(1); - const [[event]] = eventListenerMock.mock.calls; - expect(event).toMatchObject({ - previousState: null, - credentialRecord: { - state: CredentialState.OfferSent, - }, - }); - }); - - test('returns credential offer message', async () => { - const { message: credentialOffer } = await credentialService.createOffer(connection, credentialTemplate); - - expect(credentialOffer.toJSON()).toMatchObject({ - '@id': expect.any(String), - '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/offer-credential', - comment: 'some comment', - credential_preview: { - '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/credential-preview', - attributes: [ - { - name: 'name', - 'mime-type': 'text/plain', - value: 'John', - }, - { - name: 'age', - 'mime-type': 'text/plain', - value: '99', - }, - ], - }, - 'offers~attach': [ - { - '@id': expect.any(String), - 'mime-type': 'application/json', - data: { - base64: expect.any(String), - }, - }, - ], - }); - }); - }); - - describe('processCredentialOffer', () => { - let messageContext: InboundMessageContext; - let credentialOfferMessage: OfferCredentialMessage; - - beforeEach(() => { - credentialOfferMessage = new OfferCredentialMessage({ - comment: 'some comment', - credentialPreview: credentialPreview, - attachments: [offerAttachment], - }); - messageContext = new InboundMessageContext(credentialOfferMessage, { connection }); - messageContext.connection = connection; - }); - - test(`creates and return credential record in ${CredentialState.OfferReceived} state with offer, thread ID`, async () => { - const repositorySaveSpy = jest.spyOn(credentialRepository, 'save'); - - // when - const returnedCredentialRecord = await credentialService.processOffer(messageContext); - - // then - const expectedCredentialRecord = { - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Number), - offerMessage: credentialOfferMessage, - tags: { threadId: credentialOfferMessage.id }, - state: CredentialState.OfferReceived, - }; - expect(repositorySaveSpy).toHaveBeenCalledTimes(1); - const [[createdCredentialRecord]] = repositorySaveSpy.mock.calls; - expect(createdCredentialRecord).toMatchObject(expectedCredentialRecord); - expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord); - }); - - test(`emits stateChange event with ${CredentialState.OfferReceived}`, async () => { - const eventListenerMock = jest.fn(); - credentialService.on(CredentialEventType.StateChanged, eventListenerMock); - - // when - await credentialService.processOffer(messageContext); - - // then - expect(eventListenerMock).toHaveBeenCalledTimes(1); - const [[event]] = eventListenerMock.mock.calls; - expect(event).toMatchObject({ - previousState: null, - credentialRecord: { - state: CredentialState.OfferReceived, - }, - }); - }); - }); - - describe('createCredentialRequest', () => { - let credentialRecord: CredentialRecord; - - beforeEach(() => { - credentialRecord = mockCredentialRecord({ - state: CredentialState.OfferReceived, - tags: { threadId: 'fd9c5ddb-ec11-4acd-bc32-540736249746' }, - }); - }); - - test(`updates state to ${CredentialState.RequestSent}, set request metadata`, async () => { - const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update'); - - // when - await credentialService.createRequest(credentialRecord); - - // then - expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1); - const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls; - expect(updatedCredentialRecord).toMatchObject({ - requestMetadata: { cred_req: 'meta-data' }, - state: CredentialState.RequestSent, - }); - }); - - test(`emits stateChange event with ${CredentialState.RequestSent}`, async () => { - const eventListenerMock = jest.fn(); - credentialService.on(CredentialEventType.StateChanged, eventListenerMock); - - // when - await credentialService.createRequest(credentialRecord); - - // then - expect(eventListenerMock).toHaveBeenCalledTimes(1); - const [[event]] = eventListenerMock.mock.calls; - expect(event).toMatchObject({ - previousState: CredentialState.OfferReceived, - credentialRecord: { - state: CredentialState.RequestSent, - }, - }); - }); - - test('returns credential request message base on existing credential offer message', async () => { - // given - const comment = 'credential request comment'; - - // when - const { message: credentialRequest } = await credentialService.createRequest(credentialRecord, { - comment, - }); - - // then - expect(credentialRequest.toJSON()).toMatchObject({ - '@id': expect.any(String), - '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/request-credential', - '~thread': { - thid: 'fd9c5ddb-ec11-4acd-bc32-540736249746', - }, - comment, - 'requests~attach': [ - { - '@id': expect.any(String), - 'mime-type': 'application/json', - data: { - base64: expect.any(String), - }, - }, - ], - }); - }); - - const validState = CredentialState.OfferReceived; - const invalidCredentialStates = Object.values(CredentialState).filter(state => state !== validState); - test(`throws an error when state transition is invalid`, async () => { - await Promise.all( - invalidCredentialStates.map(async state => { - await expect(credentialService.createRequest(mockCredentialRecord({ state }))).rejects.toThrowError( - `Credential record is in invalid state ${state}. Valid states are: ${validState}.` - ); - }) - ); - }); - }); - - describe('processCredentialRequest', () => { - let credential: CredentialRecord; - let messageContext: InboundMessageContext; - - beforeEach(() => { - credential = mockCredentialRecord({ state: CredentialState.OfferSent }); - - const credentialRequest = new RequestCredentialMessage({ comment: 'abcd', attachments: [requestAttachment] }); - credentialRequest.setThread({ threadId: 'somethreadid' }); - messageContext = new InboundMessageContext(credentialRequest, { connection }); - }); - - test(`updates state to ${CredentialState.RequestReceived}, set request and returns credential record`, async () => { - const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update'); - - // given - repositoryFindByQueryMock.mockReturnValue(Promise.resolve([credential])); - - // when - const returnedCredentialRecord = await credentialService.processRequest(messageContext); - - // then - expect(repositoryFindByQueryMock).toHaveBeenCalledTimes(1); - const [[findByQueryArg]] = repositoryFindByQueryMock.mock.calls; - expect(findByQueryArg).toEqual({ threadId: 'somethreadid' }); - - const expectedCredentialRecord = { - state: CredentialState.RequestReceived, - requestMessage: messageContext.message, - }; - expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1); - const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls; - expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord); - expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord); - }); - - test(`emits stateChange event from ${CredentialState.OfferSent} to ${CredentialState.RequestReceived}`, async () => { - const eventListenerMock = jest.fn(); - credentialService.on(CredentialEventType.StateChanged, eventListenerMock); - repositoryFindByQueryMock.mockReturnValue(Promise.resolve([credential])); - - await credentialService.processRequest(messageContext); - - expect(eventListenerMock).toHaveBeenCalledTimes(1); - const [[event]] = eventListenerMock.mock.calls; - expect(event).toMatchObject({ - previousState: CredentialState.OfferSent, - credentialRecord: { - state: CredentialState.RequestReceived, - }, - }); - }); - - const validState = CredentialState.OfferSent; - const invalidCredentialStates = Object.values(CredentialState).filter(state => state !== validState); - test(`throws an error when state transition is invalid`, async () => { - await Promise.all( - invalidCredentialStates.map(async state => { - repositoryFindByQueryMock.mockReturnValue(Promise.resolve([mockCredentialRecord({ state })])); - await expect(credentialService.processRequest(messageContext)).rejects.toThrowError( - `Credential record is in invalid state ${state}. Valid states are: ${validState}.` - ); - }) - ); - }); - }); - - describe('createCredential', () => { - const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746'; - let credential: CredentialRecord; - - beforeEach(() => { - credential = mockCredentialRecord({ - state: CredentialState.RequestReceived, - requestMessage: new RequestCredentialMessage({ - comment: 'abcd', - attachments: [requestAttachment], - }), - tags: { threadId }, - }); - }); - - test(`updates state to ${CredentialState.CredentialIssued}`, async () => { - const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update'); - - // when - await credentialService.createCredential(credential); - - // then - expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1); - const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls; - expect(updatedCredentialRecord).toMatchObject({ - state: CredentialState.CredentialIssued, - }); - }); - - test(`emits stateChange event from ${CredentialState.RequestReceived} to ${CredentialState.CredentialIssued}`, async () => { - const eventListenerMock = jest.fn(); - credentialService.on(CredentialEventType.StateChanged, eventListenerMock); - - // given - repositoryFindMock.mockReturnValue(Promise.resolve(credential)); - - // when - await credentialService.createCredential(credential); - - // then - expect(eventListenerMock).toHaveBeenCalledTimes(1); - const [[event]] = eventListenerMock.mock.calls; - expect(event).toMatchObject({ - previousState: CredentialState.RequestReceived, - credentialRecord: { - state: CredentialState.CredentialIssued, - }, - }); - }); - - test('returns credential response message base on credential request message', async () => { - // given - repositoryFindMock.mockReturnValue(Promise.resolve(credential)); - const comment = 'credential response comment'; - - // when - const { message: credentialResponse } = await credentialService.createCredential(credential, { comment }); - - // then - expect(credentialResponse.toJSON()).toMatchObject({ - '@id': expect.any(String), - '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/issue-credential', - '~thread': { - thid: 'fd9c5ddb-ec11-4acd-bc32-540736249746', - }, - comment, - 'credentials~attach': [ - { - '@id': expect.any(String), - 'mime-type': 'application/json', - data: { - base64: expect.any(String), - }, - }, - ], - '~please_ack': expect.any(Object), - }); - - // We're using instance of `StubWallet`. Value of `cred` should be as same as in the credential response message. - const [cred] = await wallet.createCredential(credOffer, credReq, {}); - const [responseAttachment] = credentialResponse.attachments; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(JsonEncoder.fromBase64(responseAttachment.data.base64!)).toEqual(cred); - }); - - test('throws error when credential record has no request', async () => { - // when, then - await expect( - credentialService.createCredential( - mockCredentialRecord({ state: CredentialState.RequestReceived, tags: { threadId } }) - ) - ).rejects.toThrowError( - `Missing required base64 encoded attachment data for credential request with thread id ${threadId}` - ); - }); - - const validState = CredentialState.RequestReceived; - const invalidCredentialStates = Object.values(CredentialState).filter(state => state !== validState); - test(`throws an error when state transition is invalid`, async () => { - await Promise.all( - invalidCredentialStates.map(async state => { - await expect( - credentialService.createCredential( - mockCredentialRecord({ - state, - tags: { threadId }, - requestMessage: new RequestCredentialMessage({ - attachments: [requestAttachment], - }), - }) - ) - ).rejects.toThrowError(`Credential record is in invalid state ${state}. Valid states are: ${validState}.`); - }) - ); - }); - }); - - describe('processCredential', () => { - let credential: CredentialRecord; - let messageContext: InboundMessageContext; - - beforeEach(() => { - credential = mockCredentialRecord({ - state: CredentialState.RequestSent, - requestMessage: new RequestCredentialMessage({ attachments: [requestAttachment] }), - requestMetadata: { cred_req: 'meta-data' }, - }); - - const credentialResponse = new IssueCredentialMessage({ comment: 'abcd', attachments: [credentialAttachment] }); - credentialResponse.setThread({ threadId: 'somethreadid' }); - messageContext = new InboundMessageContext(credentialResponse, { connection }); - }); - - test('finds credential record by thread ID and saves credential attachment into the wallet', async () => { - const walletSaveSpy = jest.spyOn(wallet, 'storeCredential'); - - // given - repositoryFindByQueryMock.mockReturnValue(Promise.resolve([credential])); - - // when - await credentialService.processCredential(messageContext); - - // then - expect(repositoryFindByQueryMock).toHaveBeenCalledTimes(1); - const [[findByQueryArg]] = repositoryFindByQueryMock.mock.calls; - expect(findByQueryArg).toEqual({ threadId: 'somethreadid' }); - - expect(walletSaveSpy).toHaveBeenCalledTimes(1); - const [[...walletSaveArgs]] = walletSaveSpy.mock.calls; - expect(walletSaveArgs).toEqual( - expect.arrayContaining([ - expect.any(String), - { cred_req: 'meta-data' }, - messageContext.message.indyCredential, - credDef, - ]) - ); - }); - - test(`updates state to ${CredentialState.CredentialReceived}, set credentialId and returns credential record`, async () => { - const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update'); - - // given - repositoryFindByQueryMock.mockReturnValue(Promise.resolve([credential])); - - // when - const updatedCredential = await credentialService.processCredential(messageContext); - - // then - const expectedCredentialRecord = { - credentialId: expect.any(String), - state: CredentialState.CredentialReceived, - }; - expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1); - const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls; - expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord); - expect(updatedCredential).toMatchObject(expectedCredentialRecord); - }); - - test(`emits stateChange event from ${CredentialState.RequestSent} to ${CredentialState.CredentialReceived}`, async () => { - const eventListenerMock = jest.fn(); - credentialService.on(CredentialEventType.StateChanged, eventListenerMock); - - // given - repositoryFindByQueryMock.mockReturnValue(Promise.resolve([credential])); - - // when - await credentialService.processCredential(messageContext); - - // then - expect(eventListenerMock).toHaveBeenCalledTimes(1); - const [[event]] = eventListenerMock.mock.calls; - expect(event).toMatchObject({ - previousState: CredentialState.RequestSent, - credentialRecord: { - state: CredentialState.CredentialReceived, - }, - }); - }); - - test('throws error when credential record has no request metadata', async () => { - // given - repositoryFindByQueryMock.mockReturnValue( - Promise.resolve([mockCredentialRecord({ state: CredentialState.RequestSent, id: 'id' })]) - ); - - // when, then - await expect(credentialService.processCredential(messageContext)).rejects.toThrowError( - `Missing required request metadata for credential with id id` - ); - }); - - const validState = CredentialState.RequestSent; - const invalidCredentialStates = Object.values(CredentialState).filter(state => state !== validState); - test(`throws an error when state transition is invalid`, async () => { - await Promise.all( - invalidCredentialStates.map(async state => { - repositoryFindByQueryMock.mockReturnValue( - Promise.resolve([mockCredentialRecord({ state, requestMetadata: { cred_req: 'meta-data' } })]) - ); - await expect(credentialService.processCredential(messageContext)).rejects.toThrowError( - `Credential record is in invalid state ${state}. Valid states are: ${validState}.` - ); - }) - ); - }); - }); - - describe('createAck', () => { - const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746'; - let credential: CredentialRecord; - - beforeEach(() => { - credential = mockCredentialRecord({ - state: CredentialState.CredentialReceived, - tags: { threadId }, - }); - }); - - test(`updates state to ${CredentialState.Done}`, async () => { - // given - const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update'); - - // when - await credentialService.createAck(credential); - - // then - expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1); - const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls; - expect(updatedCredentialRecord).toMatchObject({ - state: CredentialState.Done, - }); - }); - - test(`emits stateChange event from ${CredentialState.CredentialReceived} to ${CredentialState.Done}`, async () => { - const eventListenerMock = jest.fn(); - credentialService.on(CredentialEventType.StateChanged, eventListenerMock); - - // given - repositoryFindMock.mockReturnValue(Promise.resolve(credential)); - - // when - await credentialService.createAck(credential); - - // then - expect(eventListenerMock).toHaveBeenCalledTimes(1); - const [[event]] = eventListenerMock.mock.calls; - expect(event).toMatchObject({ - previousState: CredentialState.CredentialReceived, - credentialRecord: { - state: CredentialState.Done, - }, - }); - }); - - test('returns credential response message base on credential request message', async () => { - // given - repositoryFindMock.mockReturnValue(Promise.resolve(credential)); - - // when - const { message: ackMessage } = await credentialService.createAck(credential); - - // then - expect(ackMessage.toJSON()).toMatchObject({ - '@id': expect.any(String), - '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/ack', - '~thread': { - thid: 'fd9c5ddb-ec11-4acd-bc32-540736249746', - }, - }); - }); - - const validState = CredentialState.CredentialReceived; - const invalidCredentialStates = Object.values(CredentialState).filter(state => state !== validState); - test(`throws an error when state transition is invalid`, async () => { - await Promise.all( - invalidCredentialStates.map(async state => { - await expect( - credentialService.createAck(mockCredentialRecord({ state, tags: { threadId } })) - ).rejects.toThrowError(`Credential record is in invalid state ${state}. Valid states are: ${validState}.`); - }) - ); - }); - }); - - describe('processAck', () => { - let credential: CredentialRecord; - let messageContext: InboundMessageContext; - - beforeEach(() => { - credential = mockCredentialRecord({ state: CredentialState.CredentialIssued }); - - const credentialRequest = new CredentialAckMessage({ - status: AckStatus.OK, - threadId: 'somethreadid', - }); - messageContext = new InboundMessageContext(credentialRequest, { - connection, - }); - }); - - test(`updates state to ${CredentialState.Done} and returns credential record`, async () => { - const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update'); - - // given - repositoryFindByQueryMock.mockReturnValue(Promise.resolve([credential])); - - // when - const returnedCredentialRecord = await credentialService.processAck(messageContext); - - // then - const expectedCredentialRecord = { - state: CredentialState.Done, - }; - expect(repositoryFindByQueryMock).toHaveBeenCalledTimes(1); - expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1); - const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls; - expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord); - expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord); - }); - - test(`emits stateChange event from ${CredentialState.CredentialIssued} to ${CredentialState.Done}`, async () => { - const eventListenerMock = jest.fn(); - credentialService.on(CredentialEventType.StateChanged, eventListenerMock); - - // given - repositoryFindByQueryMock.mockReturnValue(Promise.resolve([credential])); - - // when - await credentialService.processAck(messageContext); - - // then - expect(eventListenerMock).toHaveBeenCalledTimes(1); - const [[event]] = eventListenerMock.mock.calls; - expect(event).toMatchObject({ - previousState: CredentialState.CredentialIssued, - credentialRecord: { - state: CredentialState.Done, - }, - }); - }); - - test('throws error when there is no credential found by thread ID', async () => { - // given - repositoryFindByQueryMock.mockReturnValue(Promise.resolve([])); - - // when, then - await expect(credentialService.processAck(messageContext)).rejects.toThrowError( - 'Credential record not found by thread id somethreadid' - ); - }); - - const validState = CredentialState.CredentialIssued; - const invalidCredentialStates = Object.values(CredentialState).filter(state => state !== validState); - test(`throws an error when state transition is invalid`, async () => { - await Promise.all( - invalidCredentialStates.map(async state => { - repositoryFindByQueryMock.mockReturnValue(Promise.resolve([mockCredentialRecord({ state })])); - await expect(credentialService.processAck(messageContext)).rejects.toThrowError( - `Credential record is in invalid state ${state}. Valid states are: ${validState}.` - ); - }) - ); - }); - }); -}); diff --git a/src/lib/modules/credentials/__tests__/CredentialState.test.ts b/src/lib/modules/credentials/__tests__/CredentialState.test.ts deleted file mode 100644 index 1448bbe40d..0000000000 --- a/src/lib/modules/credentials/__tests__/CredentialState.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CredentialState } from '../CredentialState'; - -describe('CredentialState', () => { - test('state matches Issue Credential 1.0 (RFC 0036) state value', () => { - expect(CredentialState.ProposalSent).toBe('proposal-sent'); - expect(CredentialState.ProposalReceived).toBe('proposal-received'); - expect(CredentialState.OfferSent).toBe('offer-sent'); - expect(CredentialState.OfferReceived).toBe('offer-received'); - expect(CredentialState.RequestSent).toBe('request-sent'); - expect(CredentialState.RequestReceived).toBe('request-received'); - expect(CredentialState.CredentialIssued).toBe('credential-issued'); - expect(CredentialState.CredentialReceived).toBe('credential-received'); - expect(CredentialState.Done).toBe('done'); - }); -}); diff --git a/src/lib/modules/credentials/__tests__/CredentialUtils.test.ts b/src/lib/modules/credentials/__tests__/CredentialUtils.test.ts deleted file mode 100644 index 740c038585..0000000000 --- a/src/lib/modules/credentials/__tests__/CredentialUtils.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { CredentialUtils } from '../CredentialUtils'; -import { CredentialPreview, CredentialPreviewAttribute } from '../messages/CredentialPreview'; - -/** - * Sample test cases for encoding/decoding of verifiable credential claims - Aries RFCs 0036 and 0037 - * @see https://gist.github.com/swcurran/78e5a9e8d11236f003f6a6263c6619a6 - */ -const testEncodings: { [key: string]: { raw: any; encoded: string } } = { - address2: { - raw: '101 Wilson Lane', - encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', - }, - zip: { - raw: '87121', - encoded: '87121', - }, - city: { - raw: 'SLC', - encoded: '101327353979588246869873249766058188995681113722618593621043638294296500696424', - }, - address1: { - raw: '101 Tela Lane', - encoded: '63690509275174663089934667471948380740244018358024875547775652380902762701972', - }, - state: { - raw: 'UT', - encoded: '93856629670657830351991220989031130499313559332549427637940645777813964461231', - }, - Empty: { - raw: '', - encoded: '102987336249554097029535212322581322789799900648198034993379397001115665086549', - }, - Null: { - raw: null, - encoded: '99769404535520360775991420569103450442789945655240760487761322098828903685777', - }, - 'bool True': { - raw: true, - encoded: '1', - }, - 'bool False': { - raw: false, - encoded: '0', - }, - 'str True': { - raw: 'True', - encoded: '27471875274925838976481193902417661171675582237244292940724984695988062543640', - }, - 'str False': { - raw: 'False', - encoded: '43710460381310391454089928988014746602980337898724813422905404670995938820350', - }, - 'max i32': { - raw: 2147483647, - encoded: '2147483647', - }, - 'max i32 + 1': { - raw: 2147483648, - encoded: '26221484005389514539852548961319751347124425277437769688639924217837557266135', - }, - 'min i32': { - raw: -2147483648, - encoded: '-2147483648', - }, - 'min i32 - 1': { - raw: -2147483649, - encoded: '68956915425095939579909400566452872085353864667122112803508671228696852865689', - }, - 'float 0.1': { - raw: 0.1, - encoded: '9382477430624249591204401974786823110077201914483282671737639310288175260432', - }, - 'str 0.1': { - raw: '0.1', - encoded: '9382477430624249591204401974786823110077201914483282671737639310288175260432', - }, - 'chr 0': { - raw: String.fromCharCode(0), - encoded: '49846369543417741186729467304575255505141344055555831574636310663216789168157', - }, - 'chr 1': { - raw: String.fromCharCode(1), - encoded: '34356466678672179216206944866734405838331831190171667647615530531663699592602', - }, - 'chr 2': { - raw: String.fromCharCode(2), - encoded: '99398763056634537812744552006896172984671876672520535998211840060697129507206', - }, -}; - -describe('CredentialUtils', () => { - describe('convertPreviewToValues', () => { - test('returns object with raw and encoded attributes', () => { - const credentialPreview = new CredentialPreview({ - attributes: [ - new CredentialPreviewAttribute({ - name: 'name', - mimeType: 'text/plain', - value: '101 Wilson Lane', - }), - new CredentialPreviewAttribute({ - name: 'age', - mimeType: 'text/plain', - value: '1234', - }), - ], - }); - - expect(CredentialUtils.convertPreviewToValues(credentialPreview)).toEqual({ - name: { - raw: '101 Wilson Lane', - encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', - }, - age: { raw: '1234', encoded: '1234' }, - }); - }); - }); - - describe('checkValidEncoding', () => { - // Formatted for test.each - const testEntries = Object.entries(testEncodings).map(([name, { raw, encoded }]) => [name, raw, encoded]); - - test.each(testEntries)('returns true for valid encoding %s', (_, raw, encoded) => { - expect(CredentialUtils.checkValidEncoding(raw, encoded)).toEqual(true); - }); - }); -}); diff --git a/src/lib/modules/credentials/__tests__/StubWallet.ts b/src/lib/modules/credentials/__tests__/StubWallet.ts deleted file mode 100644 index 67cd5c91eb..0000000000 --- a/src/lib/modules/credentials/__tests__/StubWallet.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { - IndyProofRequest, - IndyRequestedCredentials, - Schemas, - CredentialDefs, - RevStates, - IndyProof, - IndyCredentialInfo, - DidConfig, - Schema, - CredDefConfig, - CredDef, - CredOffer, - ProofCred, - CredReq, - CredReqMetadata, - CredValues, - Cred, - CredRevocId, - RevocRegDelta, - CredentialId, - WalletRecordOptions, - WalletRecord, - WalletQuery, - LedgerRequest, - IndyCredential, - RevRegsDefs, -} from 'indy-sdk'; -import { Wallet } from '../../../wallet/Wallet'; -import { UnpackedMessageContext } from '../../../types'; - -export class StubWallet implements Wallet { - public get walletHandle() { - return 0; - } - public get publicDid() { - return undefined; - } - public init(): Promise { - return Promise.resolve(); - } - public close(): Promise { - throw new Error('Method not implemented.'); - } - public createProof( - proofRequest: IndyProofRequest, - requestedCredentials: IndyRequestedCredentials, - schemas: Schemas, - credentialDefs: CredentialDefs, - revStates: RevStates - ): Promise { - throw new Error('Method not implemented.'); - } - public getCredentialsForProofRequest( - proofRequest: IndyProofRequest, - attributeReferent: string - ): Promise { - throw new Error('Method not implemented.'); - } - public verifyProof( - proofRequest: IndyProofRequest, - proof: IndyProof, - schemas: Schemas, - credentialDefs: CredentialDefs, - revRegsDefs: RevRegsDefs, - revRegs: RevStates - ): Promise { - throw new Error('Method not implemented.'); - } - public searchCredentialsForProofRequest(proofRequest: IndyProofRequest): Promise { - throw new Error('Method not implemented.'); - } - public getCredential(credentialId: string): Promise { - throw new Error('Method not implemented.'); - } - public delete(): Promise { - throw new Error('Method not implemented.'); - } - public initPublicDid(didConfig: DidConfig): Promise { - throw new Error('Method not implemented.'); - } - public createDid(didConfig?: DidConfig | undefined): Promise<[string, string]> { - throw new Error('Method not implemented.'); - } - public createCredentialDefinition( - issuerDid: string, - schema: Schema, - tag: string, - signatureType: string, - config: CredDefConfig - ): Promise<[string, CredDef]> { - throw new Error('Method not implemented.'); - } - public createCredentialOffer(credDefId: string): Promise { - return Promise.resolve({ - schema_id: 'aaa', - cred_def_id: credDefId, - // Fields below can depend on Cred Def type - nonce: 'nonce', - key_correctness_proof: {}, - }); - } - public getCredentialsForProofReq(proof: string): Promise { - throw new Error('Method not implemented'); - } - public createCredentialRequest( - proverDid: string, - offer: CredOffer, - credDef: CredDef - ): Promise<[CredReq, CredReqMetadata]> { - return Promise.resolve([ - { - prover_did: proverDid, - cred_def_id: credDef.id, - blinded_ms: {}, - blinded_ms_correctness_proof: {}, - nonce: 'nonce', - }, - { cred_req: 'meta-data' }, - ]); - } - public createCredential( - credOffer: CredOffer, - credReq: CredReq, - credValues: CredValues - ): Promise<[Cred, CredRevocId, RevocRegDelta]> { - return Promise.resolve([ - { - schema_id: 'schema_id', - cred_def_id: 'cred_def_id', - rev_reg_def_id: 'rev_reg_def_id', - values: {}, - signature: 'signature', - signature_correctness_proof: 'signature_correctness_proof', - }, - '1', - {}, - ]); - } - public storeCredential(credentialId: CredentialId): Promise { - return Promise.resolve(credentialId); - } - public pack(payload: Record, recipientKeys: string[], senderVk: string | null): Promise { - throw new Error('Method not implemented.'); - } - public unpack(messagePackage: JsonWebKey): Promise { - throw new Error('Method not implemented.'); - } - public sign(data: Buffer, verkey: string): Promise { - throw new Error('Method not implemented.'); - } - public verify(signerVerkey: string, data: Buffer, signature: Buffer): Promise { - throw new Error('Method not implemented.'); - } - public addWalletRecord(type: string, id: string, value: string, tags: Record): Promise { - throw new Error('Method not implemented.'); - } - public updateWalletRecordValue(type: string, id: string, value: string): Promise { - throw new Error('Method not implemented.'); - } - public updateWalletRecordTags(type: string, id: string, tags: Record): Promise { - throw new Error('Method not implemented.'); - } - public deleteWalletRecord(type: string, id: string): Promise { - throw new Error('Method not implemented.'); - } - public getWalletRecord(type: string, id: string, options: WalletRecordOptions): Promise { - throw new Error('Method not implemented.'); - } - public search(type: string, query: WalletQuery, options: WalletRecordOptions): Promise> { - throw new Error('Method not implemented.'); - } - public signRequest(myDid: string, request: LedgerRequest): Promise { - throw new Error('Method not implemented.'); - } - - public async generateNonce(): Promise { - throw new Error('Method not implemented'); - } -} diff --git a/src/lib/modules/credentials/__tests__/fixtures.ts b/src/lib/modules/credentials/__tests__/fixtures.ts deleted file mode 100644 index 34a5b9c9d3..0000000000 --- a/src/lib/modules/credentials/__tests__/fixtures.ts +++ /dev/null @@ -1,55 +0,0 @@ -export const credDef = { - ver: '1.0', - id: 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:16:TAG', - schemaId: '16', - type: 'CL', - tag: 'TAG', - value: { - primary: { - n: - '92498022445845202032348897620554299694896009176315493627722439892023558526259875239808280186111059586069456394012963552956574651629517633396592827947162983189649269173220440607665417484696688946624963596710652063849006738050417440697782608643095591808084344059908523401576738321329706597491345875134180790935098782801918369980296355919072827164363500681884641551147645504164254206270541724042784184712124576190438261715948768681331862924634233043594086219221089373455065715714369325926959533971768008691000560918594972006312159600845441063618991760512232714992293187779673708252226326233136573974603552763615191259713', - s: - '10526250116244590830801226936689232818708299684432892622156345407187391699799320507237066062806731083222465421809988887959680863378202697458984451550048737847231343182195679453915452156726746705017249911605739136361885518044604626564286545453132948801604882107628140153824106426249153436206037648809856342458324897885659120708767794055147846459394129610878181859361616754832462886951623882371283575513182530118220334228417923423365966593298195040550255217053655606887026300020680355874881473255854564974899509540795154002250551880061649183753819902391970912501350100175974791776321455551753882483918632271326727061054', - r: [Object], - rctxt: - '46370806529776888197599056685386177334629311939451963919411093310852010284763705864375085256873240323432329015015526097014834809926159013231804170844321552080493355339505872140068998254185756917091385820365193200970156007391350745837300010513687490459142965515562285631984769068796922482977754955668569724352923519618227464510753980134744424528043503232724934196990461197793822566137436901258663918660818511283047475389958180983391173176526879694302021471636017119966755980327241734084462963412467297412455580500138233383229217300797768907396564522366006433982511590491966618857814545264741708965590546773466047139517', - z: - '84153935869396527029518633753040092509512111365149323230260584738724940130382637900926220255597132853379358675015222072417404334537543844616589463419189203852221375511010886284448841979468767444910003114007224993233448170299654815710399828255375084265247114471334540928216537567325499206413940771681156686116516158907421215752364889506967984343660576422672840921988126699885304325384925457260272972771547695861942114712679509318179363715259460727275178310181122162544785290813713205047589943947592273130618286905125194410421355167030389500160371886870735704712739886223342214864760968555566496288314800410716250791012', - }, - }, -}; - -export const credOffer = { - schema_id: 'TL1EaPFCZ8Si5aUrqScBDt:2:test-schema-1599055118161:1.0', - cred_def_id: 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:49:TAG', - key_correctness_proof: { - c: '50047550092211803100898435599448498249230644214602846259465380105187911562981', - xz_cap: - '903377919969858361861015636539761203188657065139923565169527138921408162179186528356880386741834936511828233627399006489728775544195659624738894378139967421189010372215352983118513580084886680005590351907106638703178655817619548698392274394080197104513101326422946899502782963819178061725651195158952405559244837834363357514238035344644245428381747318500206935512140018411279271654056625228252895211750431161165113594675112781707690650346028518711572046490157895995321932792559036799731075010805676081761818738662133557673397343395090042309895292970880031625026873886199268438633391631171327618951514526941153292890331525143330509967786605076984412387036942171388655140446222693051734534012842', - xr_cap: [[], [], []], - }, - nonce: '947121108704767252195123', -}; - -export const credReq = { - prover_did: 'did:sov:Y8iyDrCHfUpBY2jkd7Utfx', - cred_def_id: 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:51:TAG', - blinded_ms: { - u: - '110610123432332476473375007487247709218419524765032439076208019871743569018252586850427838771931221771227203551775289761586009084292284314207436640231052129266015503401118322009304919643287710408379757802540667358968471419257863330969561198349637578063688118910240720917456714103872180385172499545967921817473077820161374967377407759331556210823439440478684915287345759439215952485377081630435110911287494666818169608863639467996786227107447757434904894305851282532340335379056077475867151483520074334113239997171746478579695337411744772387197598863836759115206573022265599781958164663366458791934494773405738216913411', - ur: null, - hidden_attributes: ['master_secret'], - committed_attributes: {}, - }, - blinded_ms_correctness_proof: { - c: '74166567145664716669042749172899862913175746842119925863709522367997555162535', - v_dash_cap: - '1891661791592401364793544973569850112519453874155294114300886230795255714579603892516573573155105241417827172655027285062713792077137917614458690245067502490043126222829248919183676387904671567784621260696991226361605344734978904242726352512061421137336169348863177667958333777571812458318894495425085637370715152338807798447174855274779220884193480392221426666786386198680359381546692118689959879385498358879593493608080913336396532253364578927496868954362997951935977034507467171417802640352406191044080192001188762610962085274270807255753335099171457405366335155255038768918649029766176047384127483587155470131765852176320591348954350985301805080951657475246349277435569952922829946940821962356900415616036024524136', - m_caps: { - master_secret: - '32296179824587808657350024608644011637567680645343910724911461554002267640642014452361757388185386803499726200537448417105380225841945137943648126052207146380258164316458003146028', - }, - r_caps: {}, - }, - nonce: '784158051402761459123237', -}; diff --git a/src/lib/modules/credentials/handlers/CredentialAckHandler.ts b/src/lib/modules/credentials/handlers/CredentialAckHandler.ts deleted file mode 100644 index 6d0edb6ddc..0000000000 --- a/src/lib/modules/credentials/handlers/CredentialAckHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { CredentialService } from '../services'; -import { CredentialAckMessage } from '../messages'; - -export class CredentialAckHandler implements Handler { - private credentialService: CredentialService; - public supportedMessages = [CredentialAckMessage]; - - public constructor(credentialService: CredentialService) { - this.credentialService = credentialService; - } - - public async handle(messageContext: HandlerInboundMessage) { - await this.credentialService.processAck(messageContext); - } -} diff --git a/src/lib/modules/credentials/handlers/IssueCredentialHandler.ts b/src/lib/modules/credentials/handlers/IssueCredentialHandler.ts deleted file mode 100644 index b1eaa5ac04..0000000000 --- a/src/lib/modules/credentials/handlers/IssueCredentialHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { CredentialService } from '../services'; -import { IssueCredentialMessage } from '../messages'; - -export class IssueCredentialHandler implements Handler { - private credentialService: CredentialService; - public supportedMessages = [IssueCredentialMessage]; - - public constructor(credentialService: CredentialService) { - this.credentialService = credentialService; - } - - public async handle(messageContext: HandlerInboundMessage) { - await this.credentialService.processCredential(messageContext); - } -} diff --git a/src/lib/modules/credentials/handlers/OfferCredentialHandler.ts b/src/lib/modules/credentials/handlers/OfferCredentialHandler.ts deleted file mode 100644 index a524c58ac6..0000000000 --- a/src/lib/modules/credentials/handlers/OfferCredentialHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { CredentialService } from '../services'; -import { OfferCredentialMessage } from '../messages'; - -export class OfferCredentialHandler implements Handler { - private credentialService: CredentialService; - public supportedMessages = [OfferCredentialMessage]; - - public constructor(credentialService: CredentialService) { - this.credentialService = credentialService; - } - - public async handle(messageContext: HandlerInboundMessage) { - await this.credentialService.processOffer(messageContext); - } -} diff --git a/src/lib/modules/credentials/handlers/ProposeCredentialHandler.ts b/src/lib/modules/credentials/handlers/ProposeCredentialHandler.ts deleted file mode 100644 index db21056b7e..0000000000 --- a/src/lib/modules/credentials/handlers/ProposeCredentialHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { CredentialService } from '../services'; -import { ProposeCredentialMessage } from '../messages'; - -export class ProposeCredentialHandler implements Handler { - private credentialService: CredentialService; - public supportedMessages = [ProposeCredentialMessage]; - - public constructor(credentialService: CredentialService) { - this.credentialService = credentialService; - } - - public async handle(messageContext: HandlerInboundMessage) { - await this.credentialService.processProposal(messageContext); - } -} diff --git a/src/lib/modules/credentials/handlers/RequestCredentialHandler.ts b/src/lib/modules/credentials/handlers/RequestCredentialHandler.ts deleted file mode 100644 index 8cc222020a..0000000000 --- a/src/lib/modules/credentials/handlers/RequestCredentialHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { CredentialService } from '../services'; -import { RequestCredentialMessage } from '../messages'; - -export class RequestCredentialHandler implements Handler { - private credentialService: CredentialService; - public supportedMessages = [RequestCredentialMessage]; - - public constructor(credentialService: CredentialService) { - this.credentialService = credentialService; - } - - public async handle(messageContext: HandlerInboundMessage) { - this.credentialService.processRequest(messageContext); - } -} diff --git a/src/lib/modules/credentials/handlers/index.ts b/src/lib/modules/credentials/handlers/index.ts deleted file mode 100644 index 7cc065f5a7..0000000000 --- a/src/lib/modules/credentials/handlers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './CredentialAckHandler'; -export * from './IssueCredentialHandler'; -export * from './OfferCredentialHandler'; -export * from './ProposeCredentialHandler'; -export * from './RequestCredentialHandler'; diff --git a/src/lib/modules/credentials/index.ts b/src/lib/modules/credentials/index.ts deleted file mode 100644 index 0fabb514f4..0000000000 --- a/src/lib/modules/credentials/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './messages'; -export * from './services'; -export * from './CredentialUtils'; -export * from './models'; -export * from './repository/CredentialRecord'; -export * from './CredentialState'; diff --git a/src/lib/modules/credentials/messages/CredentialAckMessage.ts b/src/lib/modules/credentials/messages/CredentialAckMessage.ts deleted file mode 100644 index dba40cdced..0000000000 --- a/src/lib/modules/credentials/messages/CredentialAckMessage.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Equals } from 'class-validator'; -import { IssueCredentialMessageType } from './IssueCredentialMessageType'; - -import { AckMessage, AckMessageOptions } from '../../../modules/common'; - -export type CredentialAckMessageOptions = AckMessageOptions; - -/** - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0015-acks/README.md#explicit-acks - */ -export class CredentialAckMessage extends AckMessage { - /** - * Create new CredentialAckMessage instance. - * @param options - */ - public constructor(options: CredentialAckMessageOptions) { - super(options); - } - - @Equals(CredentialAckMessage.type) - public readonly type = CredentialAckMessage.type; - public static readonly type = IssueCredentialMessageType.CredentialAck; -} diff --git a/src/lib/modules/credentials/messages/CredentialPreview.ts b/src/lib/modules/credentials/messages/CredentialPreview.ts deleted file mode 100644 index 46922299ef..0000000000 --- a/src/lib/modules/credentials/messages/CredentialPreview.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Expose, Type } from 'class-transformer'; -import { Equals, ValidateNested } from 'class-validator'; - -import { JsonTransformer } from '../../../utils/JsonTransformer'; -import { IssueCredentialMessageType } from './IssueCredentialMessageType'; - -export interface CredentialPreviewOptions { - attributes: CredentialPreviewAttribute[]; -} - -/** - * Credential preview inner message class. - * - * This is not a message but an inner object for other messages in this protocol. It is used construct a preview of the data for the credential. - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0036-issue-credential/README.md#preview-credential - */ -export class CredentialPreview { - public constructor(options: CredentialPreviewOptions) { - if (options) { - this.attributes = options.attributes; - } - } - - @Expose({ name: '@type' }) - @Equals(CredentialPreview.type) - public readonly type = CredentialPreview.type; - public static readonly type = IssueCredentialMessageType.CredentialPreview; - - @Type(() => CredentialPreviewAttribute) - @ValidateNested({ each: true }) - public attributes!: CredentialPreviewAttribute[]; - - public toJSON(): Record { - return JsonTransformer.toJSON(this); - } -} - -interface CredentialPreviewAttributeOptions { - name: string; - mimeType?: string; - value: string; -} - -export class CredentialPreviewAttribute { - public constructor(options: CredentialPreviewAttributeOptions) { - if (options) { - this.name = options.name; - this.mimeType = options.mimeType; - this.value = options.value; - } - } - - public name!: string; - - @Expose({ name: 'mime-type' }) - public mimeType?: string; - - public value!: string; - - public toJSON(): Record { - return JsonTransformer.toJSON(this); - } -} diff --git a/src/lib/modules/credentials/messages/IssueCredentialMessage.ts b/src/lib/modules/credentials/messages/IssueCredentialMessage.ts deleted file mode 100644 index 57fa1e615b..0000000000 --- a/src/lib/modules/credentials/messages/IssueCredentialMessage.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { Cred } from 'indy-sdk'; -import { Expose, Type } from 'class-transformer'; -import { Equals, IsArray, IsString, ValidateNested } from 'class-validator'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { Attachment } from '../../../decorators/attachment/Attachment'; -import { JsonEncoder } from '../../../utils/JsonEncoder'; -import { IssueCredentialMessageType } from './IssueCredentialMessageType'; - -export const INDY_CREDENTIAL_ATTACHMENT_ID = 'libindy-cred-0'; - -interface IssueCredentialMessageOptions { - id?: string; - comment?: string; - attachments: Attachment[]; -} - -export class IssueCredentialMessage extends AgentMessage { - public constructor(options: IssueCredentialMessageOptions) { - super(); - - if (options) { - this.id = options.id ?? this.generateId(); - this.comment = options.comment; - this.attachments = options.attachments; - } - } - - @Equals(IssueCredentialMessage.type) - public readonly type = IssueCredentialMessage.type; - public static readonly type = IssueCredentialMessageType.IssueCredential; - - @IsString() - public comment?: string; - - @Expose({ name: 'credentials~attach' }) - @Type(() => Attachment) - @IsArray() - @ValidateNested({ - each: true, - }) - public attachments!: Attachment[]; - - public get indyCredential(): Cred | null { - const attachment = this.attachments.find(attachment => attachment.id === INDY_CREDENTIAL_ATTACHMENT_ID); - - // Return null if attachment is not found - if (!attachment?.data?.base64) { - return null; - } - - // Extract credential from attachment - const credentialJson = JsonEncoder.fromBase64(attachment.data.base64); - - return credentialJson; - } -} diff --git a/src/lib/modules/credentials/messages/IssueCredentialMessageType.ts b/src/lib/modules/credentials/messages/IssueCredentialMessageType.ts deleted file mode 100644 index 0f8dcea2cd..0000000000 --- a/src/lib/modules/credentials/messages/IssueCredentialMessageType.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum IssueCredentialMessageType { - ProposeCredential = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/propose-credential', - OfferCredential = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/offer-credential', - CredentialPreview = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/credential-preview', - RequestCredential = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/request-credential', - IssueCredential = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/issue-credential', - CredentialAck = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/ack', -} diff --git a/src/lib/modules/credentials/messages/OfferCredentialMessage.ts b/src/lib/modules/credentials/messages/OfferCredentialMessage.ts deleted file mode 100644 index e68a2bcbbe..0000000000 --- a/src/lib/modules/credentials/messages/OfferCredentialMessage.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { CredOffer } from 'indy-sdk'; -import { Equals, IsArray, IsString, ValidateNested } from 'class-validator'; -import { Expose, Type } from 'class-transformer'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { IssueCredentialMessageType } from './IssueCredentialMessageType'; -import { Attachment } from '../../../decorators/attachment/Attachment'; -import { CredentialPreview } from './CredentialPreview'; -import { JsonEncoder } from '../../../utils/JsonEncoder'; - -export const INDY_CREDENTIAL_OFFER_ATTACHMENT_ID = 'libindy-cred-offer-0'; - -export interface OfferCredentialMessageOptions { - id?: string; - comment?: string; - attachments: Attachment[]; - credentialPreview: CredentialPreview; -} - -/** - * Message part of Issue Credential Protocol used to continue or initiate credential exchange by issuer. - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0036-issue-credential/README.md#offer-credential - */ -export class OfferCredentialMessage extends AgentMessage { - public constructor(options: OfferCredentialMessageOptions) { - super(); - - if (options) { - this.id = options.id || this.generateId(); - this.comment = options.comment; - this.credentialPreview = options.credentialPreview; - this.attachments = options.attachments; - } - } - - @Equals(OfferCredentialMessage.type) - public readonly type = OfferCredentialMessage.type; - public static readonly type = IssueCredentialMessageType.OfferCredential; - - @IsString() - public comment?: string; - - @Expose({ name: 'credential_preview' }) - @Type(() => CredentialPreview) - @ValidateNested() - public credentialPreview!: CredentialPreview; - - @Expose({ name: 'offers~attach' }) - @Type(() => Attachment) - @IsArray() - @ValidateNested({ - each: true, - }) - public attachments!: Attachment[]; - - public get indyCredentialOffer(): CredOffer | null { - const attachment = this.attachments.find(attachment => attachment.id === INDY_CREDENTIAL_OFFER_ATTACHMENT_ID); - - // Return null if attachment is not found - if (!attachment?.data?.base64) { - return null; - } - - // Extract credential offer from attachment - const credentialOfferJson = JsonEncoder.fromBase64(attachment.data.base64); - - return credentialOfferJson; - } -} diff --git a/src/lib/modules/credentials/messages/ProposeCredentialMessage.ts b/src/lib/modules/credentials/messages/ProposeCredentialMessage.ts deleted file mode 100644 index f0315ba4ac..0000000000 --- a/src/lib/modules/credentials/messages/ProposeCredentialMessage.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Expose, Type } from 'class-transformer'; -import { Equals, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { CredentialPreview } from './CredentialPreview'; -import { IssueCredentialMessageType } from './IssueCredentialMessageType'; - -export interface ProposeCredentialMessageOptions { - id?: string; - comment?: string; - credentialProposal?: CredentialPreview; - schemaIssuerDid?: string; - schemaId?: string; - schemaName?: string; - schemaVersion?: string; - credentialDefinitionId?: string; - issuerDid?: string; -} - -/** - * Message part of Issue Credential Protocol used to initiate credential exchange by prover. - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0036-issue-credential/README.md#propose-credential - */ -export class ProposeCredentialMessage extends AgentMessage { - public constructor(options: ProposeCredentialMessageOptions) { - super(); - - if (options) { - this.id = options.id ?? this.generateId(); - this.comment = options.comment; - this.credentialProposal = options.credentialProposal; - this.schemaIssuerDid = options.schemaIssuerDid; - this.schemaId = options.schemaId; - this.schemaName = options.schemaName; - this.schemaVersion = options.schemaVersion; - this.credentialDefinitionId = options.credentialDefinitionId; - this.issuerDid = options.issuerDid; - } - } - - @Equals(ProposeCredentialMessage.type) - public readonly type = ProposeCredentialMessage.type; - public static readonly type = IssueCredentialMessageType.ProposeCredential; - - /** - * Human readable information about this Credential Proposal, - * so the proposal can be evaluated by human judgment. - */ - @IsOptional() - @IsString() - public comment?: string; - - /** - * Represents the credential data that Prover wants to receive. - */ - @Expose({ name: 'credential_proposal' }) - @Type(() => CredentialPreview) - @ValidateNested() - public credentialProposal?: CredentialPreview; - - /** - * Filter to request credential based on a particular Schema issuer DID. - */ - @Expose({ name: 'schema_issuer_did' }) - @IsString() - @IsOptional() - public schemaIssuerDid?: string; - - /** - * Filter to request credential based on a particular Schema. - */ - @Expose({ name: 'schema_id' }) - @IsString() - @IsOptional() - public schemaId?: string; - - /** - * Filter to request credential based on a schema name. - */ - @Expose({ name: 'schema_name' }) - @IsString() - @IsOptional() - public schemaName?: string; - - /** - * Filter to request credential based on a schema version. - */ - @Expose({ name: 'schema_version' }) - @IsString() - @IsOptional() - public schemaVersion?: string; - - /** - * Filter to request credential based on a particular Credential Definition. - */ - @Expose({ name: 'cred_def_id' }) - @IsString() - @IsOptional() - public credentialDefinitionId?: string; - - /** - * Filter to request a credential issued by the owner of a particular DID. - */ - @Expose({ name: 'issuer_did' }) - @IsString() - @IsOptional() - public issuerDid?: string; -} diff --git a/src/lib/modules/credentials/messages/RequestCredentialMessage.ts b/src/lib/modules/credentials/messages/RequestCredentialMessage.ts deleted file mode 100644 index ee187ce58d..0000000000 --- a/src/lib/modules/credentials/messages/RequestCredentialMessage.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { CredReq } from 'indy-sdk'; -import { Equals, IsArray, IsString, ValidateNested } from 'class-validator'; -import { AgentMessage } from '../../../agent/AgentMessage'; -import { IssueCredentialMessageType } from './IssueCredentialMessageType'; -import { Expose, Type } from 'class-transformer'; -import { Attachment } from '../../../decorators/attachment/Attachment'; -import { JsonEncoder } from '../../../utils/JsonEncoder'; - -export const INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID = 'libindy-cred-request-0'; - -interface RequestCredentialMessageOptions { - id?: string; - comment?: string; - attachments: Attachment[]; -} - -export class RequestCredentialMessage extends AgentMessage { - public constructor(options: RequestCredentialMessageOptions) { - super(); - - if (options) { - this.id = options.id || this.generateId(); - this.comment = options.comment; - this.attachments = options.attachments; - } - } - - @Equals(RequestCredentialMessage.type) - public readonly type = RequestCredentialMessage.type; - public static readonly type = IssueCredentialMessageType.RequestCredential; - - @IsString() - public comment?: string; - - @Expose({ name: 'requests~attach' }) - @Type(() => Attachment) - @IsArray() - @ValidateNested({ - each: true, - }) - public attachments!: Attachment[]; - - public get indyCredentialRequest(): CredReq | null { - const attachment = this.attachments.find(attachment => attachment.id === INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID); - - // Return null if attachment is not found - if (!attachment?.data?.base64) { - return null; - } - - // Extract proof request from attachment - const credentialReqJson = JsonEncoder.fromBase64(attachment.data.base64); - - return credentialReqJson; - } -} diff --git a/src/lib/modules/credentials/messages/index.ts b/src/lib/modules/credentials/messages/index.ts deleted file mode 100644 index 495d3ab562..0000000000 --- a/src/lib/modules/credentials/messages/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './CredentialAckMessage'; -export * from './CredentialPreview'; -export * from './RequestCredentialMessage'; -export * from './IssueCredentialMessage'; -export * from './IssueCredentialMessageType'; -export * from './OfferCredentialMessage'; -export * from './ProposeCredentialMessage'; diff --git a/src/lib/modules/credentials/models/Credential.ts b/src/lib/modules/credentials/models/Credential.ts deleted file mode 100644 index a632381eb3..0000000000 --- a/src/lib/modules/credentials/models/Credential.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { IndyCredential } from 'indy-sdk'; -import { Expose, Type } from 'class-transformer'; -import { IsOptional, ValidateNested } from 'class-validator'; -import { JsonTransformer } from '../../../utils/JsonTransformer'; - -import { CredentialInfo } from './CredentialInfo'; -import { RevocationInterval } from './RevocationInterval'; - -export class Credential { - public constructor(options: Credential) { - if (options) { - this.credentialInfo = options.credentialInfo; - this.interval = options.interval; - } - } - - @Expose({ name: 'cred_info' }) - @Type(() => CredentialInfo) - @ValidateNested() - public credentialInfo!: CredentialInfo; - - @IsOptional() - @Type(() => RevocationInterval) - @ValidateNested() - public interval?: RevocationInterval; - - public toJSON(): IndyCredential { - return (JsonTransformer.toJSON(this) as unknown) as IndyCredential; - } -} diff --git a/src/lib/modules/credentials/models/CredentialInfo.ts b/src/lib/modules/credentials/models/CredentialInfo.ts deleted file mode 100644 index e1a939f74e..0000000000 --- a/src/lib/modules/credentials/models/CredentialInfo.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { IndyCredentialInfo } from 'indy-sdk'; -import { Expose } from 'class-transformer'; -import { IsOptional, IsString } from 'class-validator'; - -import { JsonTransformer } from '../../../utils/JsonTransformer'; - -export class CredentialInfo { - public constructor(options: CredentialInfo) { - if (options) { - this.referent = options.referent; - this.attributes = options.attributes; - this.schemaId = options.schemaId; - this.credentialDefinitionId = options.credentialDefinitionId; - this.revocationRegistryId = options.revocationRegistryId; - this.credentialRevocationId = options.credentialRevocationId; - } - } - - /** - * Credential ID in the wallet - */ - @IsString() - public referent!: string; - - @Expose({ name: 'attrs' }) - @IsString({ each: true }) - public attributes!: Record; - - @Expose({ name: 'schema_id' }) - @IsString() - public schemaId!: string; - - @Expose({ name: 'cred_def_id' }) - @IsString() - public credentialDefinitionId!: string; - - @Expose({ name: 'rev_reg_id' }) - @IsString() - @IsOptional() - public revocationRegistryId?: string; - - @Expose({ name: 'cred_rev_id' }) - @IsString() - @IsOptional() - public credentialRevocationId?: string; - - public toJSON(): IndyCredentialInfo { - return (JsonTransformer.toJSON(this) as unknown) as IndyCredentialInfo; - } -} diff --git a/src/lib/modules/credentials/models/RevocationInterval.ts b/src/lib/modules/credentials/models/RevocationInterval.ts deleted file mode 100644 index c5d685a57e..0000000000 --- a/src/lib/modules/credentials/models/RevocationInterval.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IsInt, IsOptional } from 'class-validator'; - -export class RevocationInterval { - public constructor(options: { from?: number; to?: number }) { - if (options) { - this.from = options.from; - this.to = options.to; - } - } - - @IsInt() - @IsOptional() - public from?: number; - - @IsInt() - @IsOptional() - public to?: number; -} diff --git a/src/lib/modules/credentials/models/index.ts b/src/lib/modules/credentials/models/index.ts deleted file mode 100644 index c51fef3134..0000000000 --- a/src/lib/modules/credentials/models/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Credential'; -export * from './CredentialInfo'; -export * from './RevocationInterval'; diff --git a/src/lib/modules/credentials/repository/CredentialRecord.ts b/src/lib/modules/credentials/repository/CredentialRecord.ts deleted file mode 100644 index d2e3c2169d..0000000000 --- a/src/lib/modules/credentials/repository/CredentialRecord.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { CredentialId } from 'indy-sdk'; -import { v4 as uuid } from 'uuid'; -import { BaseRecord, RecordType, Tags } from '../../../storage/BaseRecord'; -import { - ProposeCredentialMessage, - IssueCredentialMessage, - RequestCredentialMessage, - OfferCredentialMessage, -} from '../messages'; -import { CredentialState } from '../CredentialState'; - -export interface CredentialStorageProps { - id?: string; - createdAt?: number; - state: CredentialState; - connectionId: string; - requestMetadata?: Record; - credentialId?: CredentialId; - tags: CredentialRecordTags; - proposalMessage?: ProposeCredentialMessage; - offerMessage?: OfferCredentialMessage; - requestMessage?: RequestCredentialMessage; - credentialMessage?: IssueCredentialMessage; -} - -export interface CredentialRecordTags extends Tags { - threadId?: string; -} - -export class CredentialRecord extends BaseRecord implements CredentialStorageProps { - public connectionId: string; - public credentialId?: CredentialId; - public requestMetadata?: Record; - public tags: CredentialRecordTags; - public state: CredentialState; - - // message data - public proposalMessage?: ProposeCredentialMessage; - public offerMessage?: OfferCredentialMessage; - public requestMessage?: RequestCredentialMessage; - public credentialMessage?: IssueCredentialMessage; - - public type = RecordType.CredentialRecord; - public static type: RecordType = RecordType.CredentialRecord; - - public constructor(props: CredentialStorageProps) { - super(props.id ?? uuid(), props.createdAt ?? Date.now()); - this.state = props.state; - this.connectionId = props.connectionId; - this.requestMetadata = props.requestMetadata; - this.credentialId = props.credentialId; - this.tags = props.tags as { [keys: string]: string }; - - this.proposalMessage = props.proposalMessage; - this.offerMessage = props.offerMessage; - this.requestMessage = props.requestMessage; - this.credentialMessage = props.credentialMessage; - } - - public assertState(expectedStates: CredentialState | CredentialState[]) { - if (!Array.isArray(expectedStates)) { - expectedStates = [expectedStates]; - } - - if (!expectedStates.includes(this.state)) { - throw new Error( - `Credential record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.` - ); - } - } - - public assertConnection(currentConnectionId: string) { - if (this.connectionId !== currentConnectionId) { - throw new Error( - `Credential record is associated with connection '${this.connectionId}'. Current connection is '${currentConnectionId}'` - ); - } - } -} diff --git a/src/lib/modules/credentials/services/CredentialService.ts b/src/lib/modules/credentials/services/CredentialService.ts deleted file mode 100644 index 9add23e225..0000000000 --- a/src/lib/modules/credentials/services/CredentialService.ts +++ /dev/null @@ -1,690 +0,0 @@ -import type { CredDefId } from 'indy-sdk'; -import { v4 as uuid } from 'uuid'; -import { EventEmitter } from 'events'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { LedgerService } from '../../ledger/services/LedgerService'; -import { InboundMessageContext } from '../../../agent/models/InboundMessageContext'; -import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment'; -import { ConnectionService, ConnectionRecord } from '../../connections'; -import { CredentialRecord } from '../repository/CredentialRecord'; -import { Repository } from '../../../storage/Repository'; -import { JsonEncoder } from '../../../utils/JsonEncoder'; -import { Wallet } from '../../../wallet/Wallet'; -import { JsonTransformer } from '../../../utils/JsonTransformer'; - -import { CredentialState } from '../CredentialState'; -import { CredentialUtils } from '../CredentialUtils'; -import { CredentialInfo } from '../models'; -import { - OfferCredentialMessage, - CredentialPreview, - INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, - RequestCredentialMessage, - IssueCredentialMessage, - CredentialAckMessage, - INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, - INDY_CREDENTIAL_ATTACHMENT_ID, - ProposeCredentialMessage, - ProposeCredentialMessageOptions, -} from '../messages'; -import { AckStatus } from '../../common'; -import { Logger } from '../../../logger'; -import { AgentConfig } from '../../../agent/AgentConfig'; - -export enum CredentialEventType { - StateChanged = 'stateChanged', -} - -export interface CredentialStateChangedEvent { - credentialRecord: CredentialRecord; - previousState: CredentialState; -} - -export interface CredentialProtocolMsgReturnType { - message: MessageType; - credentialRecord: CredentialRecord; -} - -export class CredentialService extends EventEmitter { - private wallet: Wallet; - private credentialRepository: Repository; - private connectionService: ConnectionService; - private ledgerService: LedgerService; - private logger: Logger; - - public constructor( - wallet: Wallet, - credentialRepository: Repository, - connectionService: ConnectionService, - ledgerService: LedgerService, - agentConfig: AgentConfig - ) { - super(); - this.wallet = wallet; - this.credentialRepository = credentialRepository; - this.connectionService = connectionService; - this.ledgerService = ledgerService; - this.logger = agentConfig.logger; - } - - /** - * Create a {@link ProposeCredentialMessage} not bound to an existing credential exchange. - * To create a proposal as response to an existing credential exchange, use {@link CredentialService#createProposalAsResponse}. - * - * @param connectionRecord The connection for which to create the credential proposal - * @param config Additional configuration to use for the proposal - * @returns Object containing proposal message and associated credential record - * - */ - public async createProposal( - connectionRecord: ConnectionRecord, - config?: Omit - ): Promise> { - // Assert - connectionRecord.assertReady(); - - // Create message - const proposalMessage = new ProposeCredentialMessage(config ?? {}); - - // Create record - const credentialRecord = new CredentialRecord({ - connectionId: connectionRecord.id, - state: CredentialState.ProposalSent, - proposalMessage, - tags: { threadId: proposalMessage.threadId }, - }); - await this.credentialRepository.save(credentialRecord); - this.emit(CredentialEventType.StateChanged, { credentialRecord, previousState: null }); - - return { message: proposalMessage, credentialRecord }; - } - - /** - * Create a {@link ProposePresentationMessage} as response to a received credential offer. - * To create a proposal not bound to an existing credential exchange, use {@link CredentialService#createProposal}. - * - * @param credentialRecord The credential record for which to create the credential proposal - * @param config Additional configuration to use for the proposal - * @returns Object containing proposal message and associated credential record - * - */ - public async createProposalAsResponse( - credentialRecord: CredentialRecord, - config?: Omit - ): Promise> { - // Assert - credentialRecord.assertState(CredentialState.OfferReceived); - - // Create message - const proposalMessage = new ProposeCredentialMessage(config ?? {}); - proposalMessage.setThread({ threadId: credentialRecord.tags.threadId }); - - // Update record - credentialRecord.proposalMessage = proposalMessage; - this.updateState(credentialRecord, CredentialState.ProposalSent); - - return { message: proposalMessage, credentialRecord }; - } - - /** - * Process a received {@link ProposeCredentialMessage}. This will not accept the credential proposal - * or send a credential offer. It will only create a new, or update the existing credential record with - * the information from the credential proposal message. Use {@link CredentialService#createOfferAsResponse} - * after calling this method to create a credential offer. - * - * @param messageContext The message context containing a credential proposal message - * @returns credential record associated with the credential proposal message - * - */ - public async processProposal( - messageContext: InboundMessageContext - ): Promise { - let credentialRecord: CredentialRecord; - const { message: proposalMessage, connection } = messageContext; - - // Assert connection - connection?.assertReady(); - if (!connection) { - throw new Error( - `No connection associated with incoming credential proposal message with thread id ${proposalMessage.threadId}` - ); - } - - try { - // Credential record already exists - credentialRecord = await this.getByThreadId(proposalMessage.threadId); - - // Assert - credentialRecord.assertState(CredentialState.OfferSent); - credentialRecord.assertConnection(connection.id); - - // Update record - credentialRecord.proposalMessage = proposalMessage; - await this.updateState(credentialRecord, CredentialState.ProposalReceived); - } catch { - // No credential record exists with thread id - credentialRecord = new CredentialRecord({ - connectionId: connection.id, - proposalMessage, - state: CredentialState.ProposalReceived, - tags: { threadId: proposalMessage.threadId }, - }); - - // Save record - await this.credentialRepository.save(credentialRecord); - this.emit(CredentialEventType.StateChanged, { credentialRecord, previousState: null }); - } - - return credentialRecord; - } - - /** - * Create a {@link OfferCredentialMessage} as response to a received credential proposal. - * To create an offer not bound to an existing credential exchange, use {@link CredentialService#createOffer}. - * - * @param credentialRecord The credential record for which to create the credential offer - * @param credentialTemplate The credential template to use for the offer - * @returns Object containing offer message and associated credential record - * - */ - public async createOfferAsResponse( - credentialRecord: CredentialRecord, - credentialTemplate: CredentialOfferTemplate - ): Promise> { - // Assert - credentialRecord.assertState(CredentialState.ProposalReceived); - - // Create message - const { credentialDefinitionId, comment, preview } = credentialTemplate; - const credOffer = await this.wallet.createCredentialOffer(credentialDefinitionId); - const attachment = new Attachment({ - id: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(credOffer), - }), - }); - const credentialOfferMessage = new OfferCredentialMessage({ - comment, - attachments: [attachment], - credentialPreview: preview, - }); - credentialOfferMessage.setThread({ threadId: credentialRecord.tags.threadId }); - - credentialRecord.offerMessage = credentialOfferMessage; - await this.updateState(credentialRecord, CredentialState.OfferSent); - - return { message: credentialOfferMessage, credentialRecord }; - } - - /** - * Create a {@link OfferCredentialMessage} not bound to an existing credential exchange. - * To create an offer as response to an existing credential exchange, use {@link ProofService#createOfferAsResponse}. - * - * @param connectionRecord The connection for which to create the credential offer - * @param credentialTemplate The credential template to use for the offer - * @returns Object containing offer message and associated credential record - * - */ - public async createOffer( - connectionRecord: ConnectionRecord, - credentialTemplate: CredentialOfferTemplate - ): Promise> { - // Assert - connectionRecord.assertReady(); - - // Create message - const { credentialDefinitionId, comment, preview } = credentialTemplate; - const credOffer = await this.wallet.createCredentialOffer(credentialDefinitionId); - const attachment = new Attachment({ - id: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(credOffer), - }), - }); - const credentialOfferMessage = new OfferCredentialMessage({ - comment, - attachments: [attachment], - credentialPreview: preview, - }); - - // Create record - const credentialRecord = new CredentialRecord({ - connectionId: connectionRecord.id, - offerMessage: credentialOfferMessage, - state: CredentialState.OfferSent, - tags: { threadId: credentialOfferMessage.id }, - }); - - await this.credentialRepository.save(credentialRecord); - this.emit(CredentialEventType.StateChanged, { credentialRecord, previousState: null }); - - return { message: credentialOfferMessage, credentialRecord }; - } - - /** - * Process a received {@link OfferCredentialMessage}. This will not accept the credential offer - * or send a credential request. It will only create a new credential record with - * the information from the credential offer message. Use {@link CredentialService#createRequest} - * after calling this method to create a credential request. - * - * @param messageContext The message context containing a credential request message - * @returns credential record associated with the credential offer message - * - */ - public async processOffer(messageContext: InboundMessageContext): Promise { - let credentialRecord: CredentialRecord; - const { message: credentialOfferMessage, connection } = messageContext; - - // Assert connection - connection?.assertReady(); - if (!connection) { - throw new Error( - `No connection associated with incoming credential offer message with thread id ${credentialOfferMessage.threadId}` - ); - } - - const indyCredentialOffer = credentialOfferMessage.indyCredentialOffer; - - if (!indyCredentialOffer) { - throw new Error( - `Missing required base64 encoded attachment data for credential offer with thread id ${credentialOfferMessage.threadId}` - ); - } - - try { - // Credential record already exists - credentialRecord = await this.getByThreadId(credentialOfferMessage.threadId); - - // Assert - credentialRecord.assertState(CredentialState.ProposalSent); - credentialRecord.assertConnection(connection.id); - - credentialRecord.offerMessage = credentialOfferMessage; - await this.updateState(credentialRecord, CredentialState.OfferReceived); - } catch { - // No credential record exists with thread id - credentialRecord = new CredentialRecord({ - connectionId: connection.id, - offerMessage: credentialOfferMessage, - state: CredentialState.OfferReceived, - tags: { threadId: credentialOfferMessage.id }, - }); - - // Save in repository - await this.credentialRepository.save(credentialRecord); - this.emit(CredentialEventType.StateChanged, { credentialRecord, previousState: null }); - } - - return credentialRecord; - } - - /** - * Create a {@link RequestCredentialMessage} as response to a received credential offer. - * - * @param credentialRecord The credential record for which to create the credential request - * @param options Additional configuration to use for the credential request - * @returns Object containing request message and associated credential record - * - */ - public async createRequest( - credentialRecord: CredentialRecord, - options: CredentialRequestOptions = {} - ): Promise> { - // Assert credential - credentialRecord.assertState(CredentialState.OfferReceived); - - const connection = await this.connectionService.getById(credentialRecord.connectionId); - const proverDid = connection.did; - - // FIXME: transformation should be handled by credential record - const offer = - credentialRecord.offerMessage instanceof OfferCredentialMessage - ? credentialRecord.offerMessage - : JsonTransformer.fromJSON(credentialRecord.offerMessage, OfferCredentialMessage); - - const credOffer = offer?.indyCredentialOffer; - - if (!credOffer) { - throw new Error( - `Missing required base64 encoded attachment data for credential offer with thread id ${credentialRecord.tags.threadId}` - ); - } - - const credentialDefinition = await this.ledgerService.getCredentialDefinition(credOffer.cred_def_id); - - const [credReq, credReqMetadata] = await this.wallet.createCredentialRequest( - proverDid, - credOffer, - credentialDefinition - ); - const attachment = new Attachment({ - id: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(credReq), - }), - }); - - const { comment } = options; - const credentialRequest = new RequestCredentialMessage({ comment, attachments: [attachment] }); - credentialRequest.setThread({ threadId: credentialRecord.tags.threadId }); - - credentialRecord.requestMetadata = credReqMetadata; - credentialRecord.requestMessage = credentialRequest; - await this.updateState(credentialRecord, CredentialState.RequestSent); - - return { message: credentialRequest, credentialRecord }; - } - - /** - * Process a received {@link RequestCredentialMessage}. This will not accept the credential request - * or send a credential. It will only update the existing credential record with - * the information from the credential request message. Use {@link CredentialService#createCredential} - * after calling this method to create a credential. - * - * @param messageContext The message context containing a credential request message - * @returns credential record associated with the credential request message - * - */ - public async processRequest( - messageContext: InboundMessageContext - ): Promise { - const { message: credentialRequestMessage, connection } = messageContext; - - // Assert connection - connection?.assertReady(); - if (!connection) { - throw new Error( - `No connection associated with incoming credential request message with thread id ${credentialRequestMessage.threadId}` - ); - } - - const indyCredentialRequest = credentialRequestMessage?.indyCredentialRequest; - - if (!indyCredentialRequest) { - throw new Error( - `Missing required base64 encoded attachment data for credential request with thread id ${credentialRequestMessage.threadId}` - ); - } - - const credentialRecord = await this.getByThreadId(credentialRequestMessage.threadId); - credentialRecord.assertState(CredentialState.OfferSent); - credentialRecord.assertConnection(connection.id); - - this.logger.debug('Credential record found when processing credential request', credentialRecord); - - credentialRecord.requestMessage = credentialRequestMessage; - await this.updateState(credentialRecord, CredentialState.RequestReceived); - - return credentialRecord; - } - - /** - * Create a {@link IssueCredentialMessage} as response to a received credential request. - * - * @param credentialRecord The credential record for which to create the credential - * @param options Additional configuration to use for the credential - * @returns Object containing issue credential message and associated credential record - * - */ - public async createCredential( - credentialRecord: CredentialRecord, - options: CredentialResponseOptions = {} - ): Promise> { - // Assert - credentialRecord.assertState(CredentialState.RequestReceived); - - // Transform credential request to class instance if this is not already the case - // FIXME: credential record should handle transformation - const requestMessage = - credentialRecord.requestMessage instanceof RequestCredentialMessage - ? credentialRecord.requestMessage - : JsonTransformer.fromJSON(credentialRecord.requestMessage, RequestCredentialMessage); - - // FIXME: transformation should be handled by credential record - const offerMessage = - credentialRecord.offerMessage instanceof OfferCredentialMessage - ? credentialRecord.offerMessage - : JsonTransformer.fromJSON(credentialRecord.offerMessage, OfferCredentialMessage); - - const indyCredentialOffer = offerMessage?.indyCredentialOffer; - const indyCredentialRequest = requestMessage?.indyCredentialRequest; - const indyCredentialValues = CredentialUtils.convertPreviewToValues(offerMessage.credentialPreview); - - if (!indyCredentialOffer) { - throw new Error( - `Missing required base64 encoded attachment data for credential offer with thread id ${credentialRecord.tags.threadId}` - ); - } - - if (!indyCredentialRequest) { - throw new Error( - `Missing required base64 encoded attachment data for credential request with thread id ${credentialRecord.tags.threadId}` - ); - } - - const [credential] = await this.wallet.createCredential( - indyCredentialOffer, - indyCredentialRequest, - indyCredentialValues - ); - - const credentialAttachment = new Attachment({ - id: INDY_CREDENTIAL_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(credential), - }), - }); - - const { comment } = options; - const issueCredentialMessage = new IssueCredentialMessage({ comment, attachments: [credentialAttachment] }); - issueCredentialMessage.setThread({ threadId: credentialRecord.tags.threadId }); - issueCredentialMessage.setPleaseAck(); - - credentialRecord.credentialMessage = issueCredentialMessage; - - await this.updateState(credentialRecord, CredentialState.CredentialIssued); - - return { message: issueCredentialMessage, credentialRecord }; - } - - /** - * Process a received {@link IssueCredentialMessage}. This will not accept the credential - * or send a credential acknowledgement. It will only update the existing credential record with - * the information from the issue credential message. Use {@link CredentialService#createAck} - * after calling this method to create a credential acknowledgement. - * - * @param messageContext The message context containing an issue credential message - * - * @returns credential record associated with the issue credential message - * - */ - public async processCredential( - messageContext: InboundMessageContext - ): Promise { - const { message: issueCredentialMessage, connection } = messageContext; - - // Assert connection - connection?.assertReady(); - if (!connection) { - throw new Error( - `No connection associated with incoming presentation message with thread id ${issueCredentialMessage.threadId}` - ); - } - - // Assert credential record - const credentialRecord = await this.getByThreadId(issueCredentialMessage.threadId); - credentialRecord.assertState(CredentialState.RequestSent); - - if (!credentialRecord.requestMetadata) { - throw new Error(`Missing required request metadata for credential with id ${credentialRecord.id}`); - } - - const indyCredential = issueCredentialMessage.indyCredential; - if (!indyCredential) { - throw new Error( - `Missing required base64 encoded attachment data for credential with thread id ${issueCredentialMessage.threadId}` - ); - } - - const credentialDefinition = await this.ledgerService.getCredentialDefinition(indyCredential.cred_def_id); - - const credentialId = await this.wallet.storeCredential( - uuid(), - credentialRecord.requestMetadata, - indyCredential, - credentialDefinition - ); - - credentialRecord.credentialId = credentialId; - credentialRecord.credentialMessage = issueCredentialMessage; - await this.updateState(credentialRecord, CredentialState.CredentialReceived); - - return credentialRecord; - } - - /** - * Create a {@link CredentialAckMessage} as response to a received credential. - * - * @param credentialRecord The credential record for which to create the credential acknowledgement - * @returns Object containing credential acknowledgement message and associated credential record - * - */ - public async createAck( - credentialRecord: CredentialRecord - ): Promise> { - credentialRecord.assertState(CredentialState.CredentialReceived); - - // Create message - const ackMessage = new CredentialAckMessage({ - status: AckStatus.OK, - threadId: credentialRecord.tags.threadId!, - }); - - await this.updateState(credentialRecord, CredentialState.Done); - - return { message: ackMessage, credentialRecord }; - } - - /** - * Process a received {@link CredentialAckMessage}. - * - * @param messageContext The message context containing a credential acknowledgement message - * @returns credential record associated with the credential acknowledgement message - * - */ - public async processAck(messageContext: InboundMessageContext): Promise { - const { message: credentialAckMessage, connection } = messageContext; - - // Assert connection - connection?.assertReady(); - if (!connection) { - throw new Error( - `No connection associated with incoming presentation acknowledgement message with thread id ${credentialAckMessage.threadId}` - ); - } - - // Assert credential record - const credentialRecord = await this.getByThreadId(credentialAckMessage.threadId); - credentialRecord.assertState(CredentialState.CredentialIssued); - - // Update record - await this.updateState(credentialRecord, CredentialState.Done); - - return credentialRecord; - } - - /** - * Retrieve all credential records - * - * @returns List containing all credential records - */ - public async getAll(): Promise { - return this.credentialRepository.findAll(); - } - - /** - * Retrieve a credential record by id - * - * @param credentialRecordId The credential record id - * @throws {Error} If no record is found - * @return The credential record - * - */ - public async getById(credentialRecordId: string): Promise { - return this.credentialRepository.find(credentialRecordId); - } - - /** - * Retrieve a credential record by thread id - * - * @param threadId The thread id - * @throws {Error} If no record is found - * @throws {Error} If multiple records are found - * @returns The credential record - */ - public async getByThreadId(threadId: string): Promise { - const credentialRecords = await this.credentialRepository.findByQuery({ threadId }); - - if (credentialRecords.length === 0) { - throw new Error(`Credential record not found by thread id ${threadId}`); - } - - if (credentialRecords.length > 1) { - throw new Error(`Multiple credential records found by thread id ${threadId}`); - } - - return credentialRecords[0]; - } - - /** - * Retrieve an indy credential by credential id (referent) - * - * @param credentialId the id (referent) of the indy credential - * @returns Indy credential info object - */ - public async getIndyCredential(credentialId: string): Promise { - const indyCredential = await this.wallet.getCredential(credentialId); - - return JsonTransformer.fromJSON(indyCredential, CredentialInfo); - } - - /** - * Update the record to a new state and emit an state changed event. Also updates the record - * in storage. - * - * @param credentialRecord The credential record to update the state for - * @param newState The state to update to - * - */ - private async updateState(credentialRecord: CredentialRecord, newState: CredentialState) { - const previousState = credentialRecord.state; - credentialRecord.state = newState; - await this.credentialRepository.update(credentialRecord); - - const event: CredentialStateChangedEvent = { - credentialRecord, - previousState: previousState, - }; - - this.emit(CredentialEventType.StateChanged, event); - } -} - -export interface CredentialOfferTemplate { - credentialDefinitionId: CredDefId; - comment?: string; - preview: CredentialPreview; -} - -interface CredentialRequestOptions { - comment?: string; -} - -interface CredentialResponseOptions { - comment?: string; -} diff --git a/src/lib/modules/credentials/services/index.ts b/src/lib/modules/credentials/services/index.ts deleted file mode 100644 index 24f25c2a45..0000000000 --- a/src/lib/modules/credentials/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './CredentialService'; diff --git a/src/lib/modules/ledger/LedgerModule.ts b/src/lib/modules/ledger/LedgerModule.ts deleted file mode 100644 index 278b603d00..0000000000 --- a/src/lib/modules/ledger/LedgerModule.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { CredDefId, Did, PoolConfig, SchemaId } from 'indy-sdk'; -import { LedgerService, SchemaTemplate, CredDefTemplate } from './services'; -import { Wallet } from '../../wallet/Wallet'; - -export class LedgerModule { - private ledgerService: LedgerService; - private wallet: Wallet; - - public constructor(wallet: Wallet, ledgerService: LedgerService) { - this.ledgerService = ledgerService; - this.wallet = wallet; - } - - public async connect(poolName: string, poolConfig: PoolConfig) { - return this.ledgerService.connect(poolName, poolConfig); - } - - public async registerPublicDid() { - throw new Error('registerPublicDid not implemented.'); - } - - public async getPublicDid(did: Did) { - return this.ledgerService.getPublicDid(did); - } - - public async registerSchema(schema: SchemaTemplate) { - const did = this.wallet.publicDid?.did; - - if (!did) { - throw new Error('Agent has no public DID.'); - } - - return this.ledgerService.registerSchema(did, schema); - } - - public async getSchema(id: SchemaId) { - return this.ledgerService.getSchema(id); - } - - public async registerCredentialDefinition(credentialDefinitionTemplate: CredDefTemplate) { - const did = this.wallet.publicDid?.did; - - if (!did) { - throw new Error('Agent has no public DID.'); - } - - return this.ledgerService.registerCredentialDefinition(did, credentialDefinitionTemplate); - } - - public async getCredentialDefinition(id: CredDefId) { - return this.ledgerService.getCredentialDefinition(id); - } -} diff --git a/src/lib/modules/ledger/index.ts b/src/lib/modules/ledger/index.ts deleted file mode 100644 index e371345e62..0000000000 --- a/src/lib/modules/ledger/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './services'; diff --git a/src/lib/modules/ledger/services/LedgerService.ts b/src/lib/modules/ledger/services/LedgerService.ts deleted file mode 100644 index 9837d3fac9..0000000000 --- a/src/lib/modules/ledger/services/LedgerService.ts +++ /dev/null @@ -1,291 +0,0 @@ -import type Indy from 'indy-sdk'; -import type { - CredDef, - CredDefId, - Did, - LedgerRequest, - PoolConfig, - PoolHandle, - Schema, - SchemaId, - LedgerReadReplyResponse, - LedgerWriteReplyResponse, -} from 'indy-sdk'; -import { AgentConfig } from '../../../agent/AgentConfig'; -import { Logger } from '../../../logger'; -import { isIndyError } from '../../../utils/indyError'; -import { Wallet } from '../../../wallet/Wallet'; - -export class LedgerService { - private wallet: Wallet; - private indy: typeof Indy; - private logger: Logger; - private _poolHandle?: PoolHandle; - private authorAgreement?: AuthorAgreement | null; - - public constructor(wallet: Wallet, agentConfig: AgentConfig) { - this.wallet = wallet; - this.indy = agentConfig.indy; - this.logger = agentConfig.logger; - } - - private get poolHandle() { - if (!this._poolHandle) { - throw new Error('Pool has not been initialized yet.'); - } - - return this._poolHandle; - } - - public async connect(poolName: string, poolConfig: PoolConfig) { - this.logger.debug(`Connecting to ledger pool '${poolName}'`, poolConfig); - try { - this.logger.debug(`Creating pool '${poolName}'`); - await this.indy.createPoolLedgerConfig(poolName, poolConfig); - } catch (error) { - if (isIndyError(error, 'PoolLedgerConfigAlreadyExistsError')) { - this.logger.debug(`Pool '${poolName}' already exists`, { indyError: 'PoolLedgerConfigAlreadyExistsError' }); - } else { - throw error; - } - } - - this.logger.debug('Setting ledger protocol version to 2'); - await this.indy.setProtocolVersion(2); - - this.logger.debug(`Opening pool ${poolName}`); - this._poolHandle = await this.indy.openPoolLedger(poolName); - } - - public async getPublicDid(did: Did) { - this.logger.debug(`Get public did '${did}' from ledger`); - const request = await this.indy.buildGetNymRequest(null, did); - - this.logger.debug(`Submitting get did request for did '${did}' to ledger`); - const response = await this.indy.submitRequest(this.poolHandle, request); - - const result = await this.indy.parseGetNymResponse(response); - this.logger.debug(`Retrieved did '${did}' from ledger`, result); - - return result; - } - - public async registerSchema(did: Did, schemaTemplate: SchemaTemplate): Promise<[SchemaId, Schema]> { - try { - this.logger.debug(`Register schema on ledger with did '${did}'`, schemaTemplate); - const { name, attributes, version } = schemaTemplate; - const [schemaId, schema] = await this.indy.issuerCreateSchema(did, name, version, attributes); - - const request = await this.indy.buildSchemaRequest(did, schema); - - const response = await this.submitWriteRequest(request, did); - this.logger.debug(`Registered schema '${schemaId}' on ledger`, { response, schema }); - - schema.seqNo = response.result.txnMetadata.seqNo; - - return [schemaId, schema]; - } catch (error) { - this.logger.error(`Error registering schema for did '${did}' on ledger`, { - error, - did, - poolHandle: this.poolHandle, - schemaTemplate, - }); - - throw error; - } - } - - public async getSchema(schemaId: SchemaId) { - try { - this.logger.debug(`Get schema '${schemaId}' from ledger`); - - const request = await this.indy.buildGetSchemaRequest(null, schemaId); - - this.logger.debug(`Submitting get schema request for schema '${schemaId}' to ledger`); - const response = await this.submitReadRequest(request); - - const [, schema] = await this.indy.parseGetSchemaResponse(response); - this.logger.debug(`Got schema '${schemaId}' from ledger`, { response, schema }); - - return schema; - } catch (error) { - this.logger.error(`Error retrieving schema '${schemaId}' from ledger`, { - error, - schemaId, - poolHandle: this.poolHandle, - }); - - throw error; - } - } - - public async registerCredentialDefinition( - did: Did, - credentialDefinitionTemplate: CredDefTemplate - ): Promise<[CredDefId, CredDef]> { - try { - this.logger.debug(`Register credential definition on ledger with did '${did}'`, credentialDefinitionTemplate); - const { schema, tag, signatureType, config } = credentialDefinitionTemplate; - - const [credDefId, credDef] = await this.wallet.createCredentialDefinition(did, schema, tag, signatureType, { - support_revocation: config.supportRevocation, - }); - - const request = await this.indy.buildCredDefRequest(did, credDef); - - const response = await this.submitWriteRequest(request, did); - - this.logger.debug(`Registered credential definition '${credDefId}' on ledger`, { - response, - credentialDefinition: credDef, - }); - - return [credDefId, credDef]; - } catch (error) { - this.logger.error( - `Error registering credential definition for schema '${credentialDefinitionTemplate.schema.id}' on ledger`, - { - error, - did, - poolHandle: this.poolHandle, - credentialDefinitionTemplate, - } - ); - - throw error; - } - } - - public async getCredentialDefinition(credentialDefinitionId: CredDefId) { - try { - this.logger.debug(`Get credential definition '${credentialDefinitionId}' from ledger`); - - const request = await this.indy.buildGetCredDefRequest(null, credentialDefinitionId); - - this.logger.debug( - `Submitting get credential definition request for credential definition '${credentialDefinitionId}' to ledger` - ); - const response = await this.submitReadRequest(request); - - const [, credentialDefinition] = await this.indy.parseGetCredDefResponse(response); - this.logger.debug(`Got credential definition '${credentialDefinitionId}' from ledger`, { - response, - credentialDefinition, - }); - - return credentialDefinition; - } catch (error) { - this.logger.error(`Error retrieving credential definition '${credentialDefinitionId}' from ledger`, { - error, - credentialDefinitionId: credentialDefinitionId, - poolHandle: this.poolHandle, - }); - throw error; - } - } - - private async submitWriteRequest(request: LedgerRequest, signDid: string): Promise { - const requestWithTaa = await this.appendTaa(request); - const signedRequestWithTaa = await this.wallet.signRequest(signDid, requestWithTaa); - - const response = await this.indy.submitRequest(this.poolHandle, signedRequestWithTaa); - - if (response.op === 'REJECT') { - throw Error(`Ledger rejected transaction request: ${response.reason}`); - } - - return response as LedgerWriteReplyResponse; - } - - private async submitReadRequest(request: LedgerRequest): Promise { - const response = await this.indy.submitRequest(this.poolHandle, request); - - if (response.op === 'REJECT') { - throw Error(`Ledger rejected transaction request: ${response.reason}`); - } - - return response as LedgerReadReplyResponse; - } - - private async appendTaa(request: LedgerRequest) { - const authorAgreement = await this.getTransactionAuthorAgreement(); - - // If ledger does not have TAA, we can just send request - if (authorAgreement == null) { - return request; - } - - const requestWithTaa = await this.indy.appendTxnAuthorAgreementAcceptanceToRequest( - request, - authorAgreement.text, - authorAgreement.version, - authorAgreement.digest, - this.getFirstAcceptanceMechanism(authorAgreement), - // Current time since epoch - // We can't use ratification_ts, as it must be greater than 1499906902 - Math.floor(new Date().getTime() / 1000) - ); - - return requestWithTaa; - } - - private async getTransactionAuthorAgreement(): Promise { - // TODO Replace this condition with memoization - if (this.authorAgreement !== undefined) { - return this.authorAgreement; - } - - const taaRequest = await this.indy.buildGetTxnAuthorAgreementRequest(null); - const taaResponse = await this.submitReadRequest(taaRequest); - const acceptanceMechanismRequest = await this.indy.buildGetAcceptanceMechanismsRequest(null); - const acceptanceMechanismResponse = await this.submitReadRequest(acceptanceMechanismRequest); - - // TAA can be null - if (taaResponse.result.data == null) { - this.authorAgreement = null; - return null; - } - - // If TAA is not null, we can be sure AcceptanceMechanisms is also not null - const authorAgreement = taaResponse.result.data as AuthorAgreement; - const acceptanceMechanisms = acceptanceMechanismResponse.result.data as AcceptanceMechanisms; - this.authorAgreement = { - ...authorAgreement, - acceptanceMechanisms, - }; - return this.authorAgreement; - } - - private getFirstAcceptanceMechanism(authorAgreement: AuthorAgreement) { - const [firstMechanism] = Object.keys(authorAgreement.acceptanceMechanisms.aml); - return firstMechanism; - } -} - -export interface SchemaTemplate { - name: string; - version: string; - attributes: string[]; -} - -export interface CredDefTemplate { - schema: Schema; - tag: string; - signatureType: string; - config: { supportRevocation: boolean }; -} - -interface AuthorAgreement { - digest: string; - version: string; - text: string; - ratification_ts: number; - acceptanceMechanisms: AcceptanceMechanisms; -} - -interface AcceptanceMechanisms { - aml: Record; - amlContext: string; - version: string; -} diff --git a/src/lib/modules/ledger/services/index.ts b/src/lib/modules/ledger/services/index.ts deleted file mode 100644 index 1d3b68ac82..0000000000 --- a/src/lib/modules/ledger/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './LedgerService'; diff --git a/src/lib/modules/proofs/ProofsModule.ts b/src/lib/modules/proofs/ProofsModule.ts deleted file mode 100644 index 45778405b1..0000000000 --- a/src/lib/modules/proofs/ProofsModule.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { createOutboundMessage } from '../../agent/helpers'; -import { MessageSender } from '../../agent/MessageSender'; -import { ConnectionService } from '../connections'; -import { ProofService } from './services'; -import { ProofRecord } from './repository/ProofRecord'; -import { ProofRequest } from './models/ProofRequest'; -import { JsonTransformer } from '../../utils/JsonTransformer'; -import { EventEmitter } from 'events'; -import { PresentationPreview, ProposePresentationMessage } from './messages'; -import { RequestedCredentials } from './models'; -import { Dispatcher } from '../../agent/Dispatcher'; -import { - ProposePresentationHandler, - RequestPresentationHandler, - PresentationAckHandler, - PresentationHandler, -} from './handlers'; - -export class ProofsModule { - private proofService: ProofService; - private connectionService: ConnectionService; - private messageSender: MessageSender; - - public constructor( - dispatcher: Dispatcher, - proofService: ProofService, - connectionService: ConnectionService, - messageSender: MessageSender - ) { - this.proofService = proofService; - this.connectionService = connectionService; - this.messageSender = messageSender; - this.registerHandlers(dispatcher); - } - - /** - * Get the event emitter for the proof service. Will emit state changed events - * when the state of proof records changes. - * - * @returns event emitter for proof related actions - */ - public get events(): EventEmitter { - return this.proofService; - } - - /** - * Initiate a new presentation exchange as prover by sending a presentation proposal message - * to the connection with the specified connection id. - * - * @param connectionId The connection to send the proof proposal to - * @param presentationProposal The presentation proposal to include in the message - * @param config Additional configuration to use for the proposal - * @returns Proof record associated with the sent proposal message - * - */ - public async proposeProof( - connectionId: string, - presentationProposal: PresentationPreview, - config?: { - comment?: string; - } - ): Promise { - const connection = await this.connectionService.getById(connectionId); - - const { message, proofRecord } = await this.proofService.createProposal(connection, presentationProposal, config); - - const outbound = createOutboundMessage(connection, message); - await this.messageSender.sendMessage(outbound); - - return proofRecord; - } - - /** - * Accept a presentation proposal as verifier (by sending a presentation request message) to the connection - * associated with the proof record. - * - * @param proofRecordId The id of the proof record for which to accept the proposal - * @param config Additional configuration to use for the request - * @returns Proof record associated with the presentation request - * - */ - public async acceptProposal( - proofRecordId: string, - config?: { - request?: { - name?: string; - version?: string; - nonce?: string; - }; - comment?: string; - } - ): Promise { - const proofRecord = await this.proofService.getById(proofRecordId); - const connection = await this.connectionService.getById(proofRecord.connectionId); - - // FIXME: transformation should be handled by record class - const presentationProposal = JsonTransformer.fromJSON(proofRecord.proposalMessage, ProposePresentationMessage) - .presentationProposal; - - if (!presentationProposal) { - throw new Error(`Proof record with id ${proofRecordId} is missing required presentation proposal`); - } - - const proofRequest = await this.proofService.createProofRequestFromProposal(presentationProposal, { - name: config?.request?.name ?? 'proof-request', - version: config?.request?.version ?? '1.0', - nonce: config?.request?.nonce, - }); - - const { message } = await this.proofService.createRequestAsResponse(proofRecord, proofRequest, { - comment: config?.comment, - }); - - const outboundMessage = createOutboundMessage(connection, message); - await this.messageSender.sendMessage(outboundMessage); - - return proofRecord; - } - - /** - * Initiate a new presentation exchange as verifier by sending a presentation request message - * to the connection with the specified connection id - * - * @param connectionId The connection to send the proof request to - * @param proofRequestOptions Options to build the proof request - * @param config Additional configuration to use for the request - * @returns Proof record associated with the sent request message - * - */ - public async requestProof( - connectionId: string, - proofRequestOptions: Partial>, - config?: { - comment?: string; - } - ): Promise { - const connection = await this.connectionService.getById(connectionId); - - const nonce = proofRequestOptions.nonce ?? (await this.proofService.generateProofRequestNonce()); - - const proofRequest = new ProofRequest({ - name: proofRequestOptions.name ?? 'proof-request', - version: proofRequestOptions.name ?? '1.0', - nonce, - requestedAttributes: proofRequestOptions.requestedAttributes, - requestedPredicates: proofRequestOptions.requestedPredicates, - }); - - const { message, proofRecord } = await this.proofService.createRequest(connection, proofRequest, config); - - const outboundMessage = createOutboundMessage(connection, message); - await this.messageSender.sendMessage(outboundMessage); - - return proofRecord; - } - - /** - * Accept a presentation request as prover (by sending a presentation message) to the connection - * associated with the proof record. - * - * @param proofRecordId The id of the proof record for which to accept the request - * @param requestedCredentials The requested credentials object specifying which credentials to use for the proof - * @param config Additional configuration to use for the presentation - * @returns Proof record associated with the sent presentation message - * - */ - public async acceptRequest( - proofRecordId: string, - requestedCredentials: RequestedCredentials, - config?: { - comment?: string; - } - ): Promise { - const proofRecord = await this.proofService.getById(proofRecordId); - const connection = await this.connectionService.getById(proofRecord.connectionId); - - const { message } = await this.proofService.createPresentation(proofRecord, requestedCredentials, config); - - const outboundMessage = createOutboundMessage(connection, message); - await this.messageSender.sendMessage(outboundMessage); - - return proofRecord; - } - - /** - * Accept a presentation as prover (by sending a presentation acknowledgement message) to the connection - * associated with the proof record. - * - * @param proofRecordId The id of the proof record for which to accept the presentation - * @returns Proof record associated with the sent presentation acknowledgement message - * - */ - public async acceptPresentation(proofRecordId: string): Promise { - const proofRecord = await this.proofService.getById(proofRecordId); - const connection = await this.connectionService.getById(proofRecord.connectionId); - - const { message } = await this.proofService.createAck(proofRecord); - const outboundMessage = createOutboundMessage(connection, message); - await this.messageSender.sendMessage(outboundMessage); - - return proofRecord; - } - - /** - * Create a RequestedCredentials object. Given input proof request and presentation proposal, - * use credentials in the wallet to build indy requested credentials object for input to proof creation. - * If restrictions allow, self attested attributes will be used. - * - * Use the return value of this method as input to {@link ProofService.createPresentation} to automatically - * accept a received presentation request. - * - * @param proofRequest The proof request to build the requested credentials object from - * @param presentationProposal Optional presentation proposal to improve credential selection algorithm - * @returns Requested credentials object for use in proof creation - */ - public async getRequestedCredentialsForProofRequest( - proofRequest: ProofRequest, - presentationProposal?: PresentationPreview - ) { - return this.proofService.getRequestedCredentialsForProofRequest(proofRequest, presentationProposal); - } - - /** - * Retrieve all proof records - * - * @returns List containing all proof records - */ - public async getAll(): Promise { - return this.proofService.getAll(); - } - - /** - * Retrieve a proof record by id - * - * @param proofRecordId The proof record id - * @throws {Error} If no record is found - * @return The proof record - * - */ - public async getById(proofRecordId: string): Promise { - return this.proofService.getById(proofRecordId); - } - - /** - * Retrieve a proof record by thread id - * - * @param threadId The thread id - * @throws {Error} If no record is found - * @throws {Error} If multiple records are found - * @returns The proof record - */ - public async getByThreadId(threadId: string): Promise { - return this.proofService.getByThreadId(threadId); - } - - private registerHandlers(dispatcher: Dispatcher) { - dispatcher.registerHandler(new ProposePresentationHandler(this.proofService)); - dispatcher.registerHandler(new RequestPresentationHandler(this.proofService)); - dispatcher.registerHandler(new PresentationHandler(this.proofService)); - dispatcher.registerHandler(new PresentationAckHandler(this.proofService)); - } -} diff --git a/src/lib/modules/proofs/__tests__/ProofState.test.ts b/src/lib/modules/proofs/__tests__/ProofState.test.ts deleted file mode 100644 index b372991d71..0000000000 --- a/src/lib/modules/proofs/__tests__/ProofState.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ProofState } from '../ProofState'; - -describe('ProofState', () => { - test('state matches Present Proof 1.0 (RFC 0037) state value', () => { - expect(ProofState.ProposalSent).toBe('proposal-sent'); - expect(ProofState.ProposalReceived).toBe('proposal-received'); - expect(ProofState.RequestSent).toBe('request-sent'); - expect(ProofState.RequestReceived).toBe('request-received'); - expect(ProofState.PresentationSent).toBe('presentation-sent'); - expect(ProofState.PresentationReceived).toBe('presentation-received'); - expect(ProofState.Done).toBe('done'); - }); -}); diff --git a/src/lib/modules/proofs/handlers/PresentationAckHandler.ts b/src/lib/modules/proofs/handlers/PresentationAckHandler.ts deleted file mode 100644 index 2347ffca10..0000000000 --- a/src/lib/modules/proofs/handlers/PresentationAckHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { ProofService } from '../services'; -import { PresentationAckMessage } from '../messages'; - -export class PresentationAckHandler implements Handler { - private proofService: ProofService; - public supportedMessages = [PresentationAckMessage]; - - public constructor(proofService: ProofService) { - this.proofService = proofService; - } - - public async handle(messageContext: HandlerInboundMessage) { - await this.proofService.processAck(messageContext); - } -} diff --git a/src/lib/modules/proofs/handlers/PresentationHandler.ts b/src/lib/modules/proofs/handlers/PresentationHandler.ts deleted file mode 100644 index 2cb16cdfc3..0000000000 --- a/src/lib/modules/proofs/handlers/PresentationHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { PresentationMessage } from '../messages'; -import { ProofService } from '../services'; - -export class PresentationHandler implements Handler { - private proofService: ProofService; - public supportedMessages = [PresentationMessage]; - - public constructor(proofService: ProofService) { - this.proofService = proofService; - } - - public async handle(messageContext: HandlerInboundMessage) { - await this.proofService.processPresentation(messageContext); - } -} diff --git a/src/lib/modules/proofs/handlers/ProposePresentationHandler.ts b/src/lib/modules/proofs/handlers/ProposePresentationHandler.ts deleted file mode 100644 index 0d7e6cfd30..0000000000 --- a/src/lib/modules/proofs/handlers/ProposePresentationHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { ProposePresentationMessage } from '../messages'; -import { ProofService } from '../services'; - -export class ProposePresentationHandler implements Handler { - private proofService: ProofService; - public supportedMessages = [ProposePresentationMessage]; - - public constructor(proofService: ProofService) { - this.proofService = proofService; - } - - public async handle(messageContext: HandlerInboundMessage) { - await this.proofService.processProposal(messageContext); - } -} diff --git a/src/lib/modules/proofs/handlers/RequestPresentationHandler.ts b/src/lib/modules/proofs/handlers/RequestPresentationHandler.ts deleted file mode 100644 index cd3ae9bcaf..0000000000 --- a/src/lib/modules/proofs/handlers/RequestPresentationHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { RequestPresentationMessage } from '../messages'; -import { ProofService } from '../services'; - -export class RequestPresentationHandler implements Handler { - private proofService: ProofService; - public supportedMessages = [RequestPresentationMessage]; - - public constructor(proofService: ProofService) { - this.proofService = proofService; - } - - public async handle(messageContext: HandlerInboundMessage) { - await this.proofService.processRequest(messageContext); - } -} diff --git a/src/lib/modules/proofs/handlers/index.ts b/src/lib/modules/proofs/handlers/index.ts deleted file mode 100644 index d46492cefe..0000000000 --- a/src/lib/modules/proofs/handlers/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './PresentationAckHandler'; -export * from './PresentationHandler'; -export * from './ProposePresentationHandler'; -export * from './RequestPresentationHandler'; diff --git a/src/lib/modules/proofs/index.ts b/src/lib/modules/proofs/index.ts deleted file mode 100644 index 239c0a2b82..0000000000 --- a/src/lib/modules/proofs/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './messages'; -export * from './models'; -export * from './services'; -export * from './ProofState'; -export * from './repository/ProofRecord'; diff --git a/src/lib/modules/proofs/messages/PresentProofMessageType.ts b/src/lib/modules/proofs/messages/PresentProofMessageType.ts deleted file mode 100644 index 165a101372..0000000000 --- a/src/lib/modules/proofs/messages/PresentProofMessageType.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum PresentProofMessageType { - ProposePresentation = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/1.0/propose-presentation', - RequestPresentation = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/1.0/request-presentation', - Presentation = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/1.0/presentation', - PresentationPreview = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/1.0/presentation-preview', - PresentationAck = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/1.0/ack', -} diff --git a/src/lib/modules/proofs/messages/PresentationAckMessage.ts b/src/lib/modules/proofs/messages/PresentationAckMessage.ts deleted file mode 100644 index b9e325e382..0000000000 --- a/src/lib/modules/proofs/messages/PresentationAckMessage.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Equals } from 'class-validator'; - -import { AckMessage, AckMessageOptions } from '../../../modules/common'; -import { PresentProofMessageType } from './PresentProofMessageType'; - -export type PresentationAckMessageOptions = AckMessageOptions; - -/** - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0015-acks/README.md#explicit-acks - */ -export class PresentationAckMessage extends AckMessage { - public constructor(options: PresentationAckMessageOptions) { - super(options); - } - - @Equals(PresentationAckMessage.type) - public readonly type = PresentationAckMessage.type; - public static readonly type = PresentProofMessageType.PresentationAck; -} diff --git a/src/lib/modules/proofs/messages/PresentationMessage.ts b/src/lib/modules/proofs/messages/PresentationMessage.ts deleted file mode 100644 index 3f7bddd8f7..0000000000 --- a/src/lib/modules/proofs/messages/PresentationMessage.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { IndyProof } from 'indy-sdk'; -import { Equals, IsArray, IsString, ValidateNested, IsOptional } from 'class-validator'; -import { Expose, Type } from 'class-transformer'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { Attachment } from '../../../decorators/attachment/Attachment'; -import { JsonEncoder } from '../../../utils/JsonEncoder'; -import { PresentProofMessageType } from './PresentProofMessageType'; - -export const INDY_PROOF_ATTACHMENT_ID = 'libindy-presentation-0'; - -export interface PresentationOptions { - id?: string; - comment?: string; - attachments: Attachment[]; -} - -/** - * Presentation Message part of Present Proof Protocol used as a response to a {@link PresentationRequestMessage | Presentation Request Message} from prover to verifier. - * Contains signed presentations. - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#presentation - */ -export class PresentationMessage extends AgentMessage { - public constructor(options: PresentationOptions) { - super(); - - if (options) { - this.id = options.id ?? this.generateId(); - this.comment = options.comment; - this.attachments = options.attachments; - } - } - - @Equals(PresentationMessage.type) - public readonly type = PresentationMessage.type; - public static readonly type = PresentProofMessageType.Presentation; - - /** - * Provides some human readable information about this request for a presentation. - */ - @IsOptional() - @IsString() - public comment?: string; - - /** - * An array of attachments containing the presentation in the requested format(s). - */ - @Expose({ name: 'presentations~attach' }) - @Type(() => Attachment) - @IsArray() - @ValidateNested({ - each: true, - }) - public attachments!: Attachment[]; - - public get indyProof(): IndyProof | null { - const attachment = this.attachments.find(attachment => attachment.id === INDY_PROOF_ATTACHMENT_ID); - - // Return null if attachment is not found - if (!attachment?.data?.base64) { - return null; - } - - const proofJson = JsonEncoder.fromBase64(attachment.data.base64); - - return proofJson; - } -} diff --git a/src/lib/modules/proofs/messages/PresentationPreview.ts b/src/lib/modules/proofs/messages/PresentationPreview.ts deleted file mode 100644 index 08e9ad6a41..0000000000 --- a/src/lib/modules/proofs/messages/PresentationPreview.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Equals, IsEnum, IsInt, IsString, ValidateIf, ValidateNested } from 'class-validator'; -import { Expose, Type } from 'class-transformer'; - -import { PresentProofMessageType } from './PresentProofMessageType'; -import { JsonTransformer } from '../../../utils/JsonTransformer'; -import { PredicateType } from '../models/PredicateType'; - -export interface PresentationPreviewOptions { - attributes?: PresentationPreviewAttribute[]; - predicates?: PresentationPreviewPredicate[]; -} - -/** - * Presentation preview inner message class. - * - * This is not a message but an inner object for other messages in this protocol. It is used to construct a preview of the data for the presentation. - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#presentation-preview - */ -export class PresentationPreview { - public constructor(options: PresentationPreviewOptions) { - if (options) { - this.attributes = options.attributes ?? []; - this.predicates = options.predicates ?? []; - } - } - - @Expose({ name: '@type' }) - @Equals(PresentationPreview.type) - public readonly type = PresentationPreview.type; - public static readonly type = PresentProofMessageType.PresentationPreview; - - @Type(() => PresentationPreviewAttribute) - @ValidateNested({ each: true }) - public attributes!: PresentationPreviewAttribute[]; - - @Type(() => PresentationPreviewPredicate) - @ValidateNested({ each: true }) - public predicates!: PresentationPreviewPredicate[]; - - public toJSON(): Record { - return JsonTransformer.toJSON(this); - } -} - -export interface PresentationPreviewAttributeOptions { - name: string; - credentialDefinitionId?: string; - mimeType?: string; - value?: string; - referent?: string; -} - -export class PresentationPreviewAttribute { - public constructor(options: PresentationPreviewAttributeOptions) { - if (options) { - this.name = options.name; - this.credentialDefinitionId = options.credentialDefinitionId; - this.mimeType = options.mimeType; - this.value = options.value; - this.referent = options.referent; - } - } - - public name!: string; - - @Expose({ name: 'cred_def_id' }) - @IsString() - @ValidateIf((o: PresentationPreviewAttribute) => o.referent !== undefined) - public credentialDefinitionId?: string; - - @Expose({ name: 'mime-type' }) - public mimeType?: string; - - public value?: string; - - public referent?: string; - - public toJSON(): Record { - return JsonTransformer.toJSON(this); - } -} - -export interface PresentationPreviewPredicateOptions { - name: string; - credentialDefinitionId: string; - predicate: PredicateType; - threshold: number; -} - -export class PresentationPreviewPredicate { - public constructor(options: PresentationPreviewPredicateOptions) { - if (options) { - this.name = options.name; - this.credentialDefinitionId = options.credentialDefinitionId; - this.predicate = options.predicate; - this.threshold = options.threshold; - } - } - - public name!: string; - - @Expose({ name: 'cred_def_id' }) - @IsString() - public credentialDefinitionId!: string; - - @IsEnum(PredicateType) - public predicate!: PredicateType; - - @IsInt() - public threshold!: number; - - public toJSON(): Record { - return JsonTransformer.toJSON(this); - } -} diff --git a/src/lib/modules/proofs/messages/ProposePresentationMessage.ts b/src/lib/modules/proofs/messages/ProposePresentationMessage.ts deleted file mode 100644 index bd68618e6c..0000000000 --- a/src/lib/modules/proofs/messages/ProposePresentationMessage.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Equals, IsOptional, IsString, ValidateNested } from 'class-validator'; -import { Expose, Type } from 'class-transformer'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { PresentProofMessageType } from './PresentProofMessageType'; -import { PresentationPreview } from './PresentationPreview'; - -export interface ProposePresentationMessageOptions { - id?: string; - comment?: string; - presentationProposal: PresentationPreview; -} - -/** - * Propose Presentation Message part of Present Proof Protocol used to initiate presentation exchange by holder. - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#propose-presentation - */ -export class ProposePresentationMessage extends AgentMessage { - public constructor(options: ProposePresentationMessageOptions) { - super(); - - if (options) { - this.id = options.id ?? this.generateId(); - this.comment = options.comment; - this.presentationProposal = options.presentationProposal; - } - } - - @Equals(ProposePresentationMessage.type) - public readonly type = ProposePresentationMessage.type; - public static readonly type = PresentProofMessageType.ProposePresentation; - - /** - * Provides some human readable information about the proposed presentation. - */ - @IsString() - @IsOptional() - public comment?: string; - - /** - * Represents the presentation example that prover wants to provide. - */ - @Expose({ name: 'presentation_proposal' }) - @Type(() => PresentationPreview) - @ValidateNested() - public presentationProposal!: PresentationPreview; -} diff --git a/src/lib/modules/proofs/messages/RequestPresentationMessage.ts b/src/lib/modules/proofs/messages/RequestPresentationMessage.ts deleted file mode 100644 index 412733ebd2..0000000000 --- a/src/lib/modules/proofs/messages/RequestPresentationMessage.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Equals, IsArray, IsString, ValidateNested, IsOptional } from 'class-validator'; -import { Expose, Type } from 'class-transformer'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { Attachment } from '../../../decorators/attachment/Attachment'; -import { JsonEncoder } from '../../../utils/JsonEncoder'; -import { JsonTransformer } from '../../../utils/JsonTransformer'; -import { ProofRequest } from '../models'; -import { PresentProofMessageType } from './PresentProofMessageType'; - -export interface RequestPresentationOptions { - id?: string; - comment?: string; - attachments: Attachment[]; -} - -export const INDY_PROOF_REQUEST_ATTACHMENT_ID = 'libindy-request-presentation-0'; - -/** - * Request Presentation Message part of Present Proof Protocol used to initiate request from verifier to prover. - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#request-presentation - */ -export class RequestPresentationMessage extends AgentMessage { - public constructor(options: RequestPresentationOptions) { - super(); - - if (options) { - this.id = options.id ?? this.generateId(); - this.comment = options.comment; - this.attachments = options.attachments; - } - } - - @Equals(RequestPresentationMessage.type) - public readonly type = RequestPresentationMessage.type; - public static readonly type = PresentProofMessageType.RequestPresentation; - - /** - * Provides some human readable information about this request for a presentation. - */ - @IsOptional() - @IsString() - public comment?: string; - - /** - * An array of attachments defining the acceptable formats for the presentation. - */ - @Expose({ name: 'request_presentations~attach' }) - @Type(() => Attachment) - @IsArray() - @ValidateNested({ - each: true, - }) - public attachments!: Attachment[]; - - public get indyProofRequest(): ProofRequest | null { - const attachment = this.attachments.find(attachment => attachment.id === INDY_PROOF_REQUEST_ATTACHMENT_ID); - - // Return null if attachment is not found - if (!attachment?.data?.base64) { - return null; - } - - // Extract proof request from attachment - const proofRequestJson = JsonEncoder.fromBase64(attachment.data.base64); - const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest); - - return proofRequest; - } -} diff --git a/src/lib/modules/proofs/messages/index.ts b/src/lib/modules/proofs/messages/index.ts deleted file mode 100644 index 9d2d0e310b..0000000000 --- a/src/lib/modules/proofs/messages/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './PresentProofMessageType'; -export * from './ProposePresentationMessage'; -export * from './RequestPresentationMessage'; -export * from './PresentationMessage'; -export * from './PresentationPreview'; -export * from './PresentationAckMessage'; diff --git a/src/lib/modules/proofs/models/AttributeFilter.ts b/src/lib/modules/proofs/models/AttributeFilter.ts deleted file mode 100644 index 85382520fa..0000000000 --- a/src/lib/modules/proofs/models/AttributeFilter.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Expose, Transform, TransformationType, Type } from 'class-transformer'; -import { IsOptional, IsString, ValidateNested } from 'class-validator'; - -export class AttributeValue { - public constructor(options: AttributeValue) { - this.name = options.name; - this.value = options.value; - } - - @IsString() - public name: string; - - @IsString() - public value: string; -} - -export class AttributeFilter { - public constructor(options: AttributeFilter) { - if (options) { - this.schemaId = options.schemaId; - this.schemaIssuerDid = options.schemaIssuerDid; - this.schemaName = options.schemaName; - this.schemaVersion = options.schemaVersion; - this.issuerDid = options.issuerDid; - this.credentialDefinitionId = options.credentialDefinitionId; - this.attributeValue = options.attributeValue; - } - } - - @Expose({ name: 'schema_id' }) - @IsOptional() - @IsString() - public schemaId?: string; - - @Expose({ name: 'schema_issuer_did' }) - @IsOptional() - @IsString() - public schemaIssuerDid?: string; - - @Expose({ name: 'schema_name' }) - @IsOptional() - @IsString() - public schemaName?: string; - - @Expose({ name: 'schema_version' }) - @IsOptional() - @IsString() - public schemaVersion?: string; - - @Expose({ name: 'issuer_did' }) - @IsOptional() - @IsString() - public issuerDid?: string; - - @Expose({ name: 'cred_def_id' }) - @IsOptional() - @IsString() - public credentialDefinitionId?: string; - - @IsOptional() - @Type(() => AttributeValue) - @ValidateNested() - public attributeValue?: AttributeValue; -} - -/** - * Decorator that transforms attribute filter to corresonding class instances. - * Needed for transformation of attribute value filter. - * - * Transforms attribute value between these formats: - * - * JSON: - * ```json - * { - * "attr::test_prop::value": "test_value" - * } - * ``` - * - * Class: - * ```json - * { - * "attributeValue": { - * "name": "test_props", - * "value": "test_value" - * } - * } - * ``` - * - * @example - * class Example { - * AttributeFilterTransformer() - * public attributeFilter?: AttributeFilter; - * } - * - * @see https://github.com/hyperledger/aries-framework-dotnet/blob/a18bef91e5b9e4a1892818df7408e2383c642dfa/src/Hyperledger.Aries/Features/PresentProof/Models/AttributeFilterConverter.cs - */ -export function AttributeFilterTransformer() { - return Transform(({ value: attributeFilter, type: transformationType }) => { - switch (transformationType) { - case TransformationType.CLASS_TO_PLAIN: - const attributeValue = attributeFilter.attributeValue; - if (attributeValue) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - attributeFilter[`attr::${attributeValue.name}::value`] = attributeValue.value; - delete attributeFilter.attributeValue; - } - - return attributeFilter; - - case TransformationType.PLAIN_TO_CLASS: - const regex = new RegExp('^attr::([^:]+)::(value)$'); - - for (const [key, value] of Object.entries(attributeFilter)) { - const match = regex.exec(key); - - if (match) { - const attributeValue = new AttributeValue({ - name: match[1], - value: value as string, - }); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - delete attributeFilter[key]; - attributeFilter.attributeValue = attributeValue; - - return attributeFilter; - } - } - return attributeFilter; - default: - return attributeFilter; - } - }); -} diff --git a/src/lib/modules/proofs/models/PartialProof.ts b/src/lib/modules/proofs/models/PartialProof.ts deleted file mode 100644 index 891c0675f9..0000000000 --- a/src/lib/modules/proofs/models/PartialProof.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Expose, Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { ProofIdentifier } from './ProofIdentifier'; -import { RequestedProof } from './RequestedProof'; - -export class PartialProof { - public constructor(options: PartialProof) { - if (options) { - this.identifiers = options.identifiers; - } - } - - @Type(() => ProofIdentifier) - @ValidateNested({ each: true }) - public identifiers!: ProofIdentifier[]; - - @Expose({ name: 'requested_proof' }) - @Type(() => RequestedProof) - @ValidateNested() - public requestedProof!: RequestedProof; -} diff --git a/src/lib/modules/proofs/models/PredicateType.ts b/src/lib/modules/proofs/models/PredicateType.ts deleted file mode 100644 index f5dda2fc14..0000000000 --- a/src/lib/modules/proofs/models/PredicateType.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum PredicateType { - LessThan = '<', - LessThanOrEqualTo = '<=', - GreaterThan = '>', - GreaterThanOrEqualTo = '>=', -} diff --git a/src/lib/modules/proofs/models/ProofAttribute.ts b/src/lib/modules/proofs/models/ProofAttribute.ts deleted file mode 100644 index 477a435bf9..0000000000 --- a/src/lib/modules/proofs/models/ProofAttribute.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IsInt, IsPositive, IsString } from 'class-validator'; -import { Expose } from 'class-transformer'; - -export class ProofAttribute { - public constructor(options: ProofAttribute) { - if (options) { - this.subProofIndex = options.subProofIndex; - this.raw = options.raw; - this.encoded = options.encoded; - } - } - - @Expose({ name: 'sub_proof_index' }) - @IsInt() - @IsPositive() - public subProofIndex!: number; - - @IsString() - public raw!: string; - - @IsString() - public encoded!: string; -} diff --git a/src/lib/modules/proofs/models/ProofAttributeInfo.ts b/src/lib/modules/proofs/models/ProofAttributeInfo.ts deleted file mode 100644 index 51c8d37ded..0000000000 --- a/src/lib/modules/proofs/models/ProofAttributeInfo.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Expose, Type } from 'class-transformer'; -import { IsString, IsOptional, IsArray, ValidateNested } from 'class-validator'; - -import { AttributeFilter } from './AttributeFilter'; -import { RevocationInterval } from '../../credentials'; - -export class ProofAttributeInfo { - public constructor(options: ProofAttributeInfo) { - if (options) { - this.name = options.name; - this.names = options.names; - this.nonRevoked = options.nonRevoked; - this.restrictions = options.restrictions; - } - } - - @IsString() - @IsOptional() - public name?: string; - - @IsArray() - @IsString({ each: true }) - @IsOptional() - public names?: string[]; - - @Expose({ name: 'non_revoked' }) - @ValidateNested() - @Type(() => RevocationInterval) - @IsOptional() - public nonRevoked?: RevocationInterval; - - @ValidateNested({ each: true }) - @Type(() => AttributeFilter) - @IsOptional() - @IsArray() - public restrictions?: AttributeFilter[]; -} diff --git a/src/lib/modules/proofs/models/ProofIdentifier.ts b/src/lib/modules/proofs/models/ProofIdentifier.ts deleted file mode 100644 index 561103ba1b..0000000000 --- a/src/lib/modules/proofs/models/ProofIdentifier.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Expose } from 'class-transformer'; -import { IsNumber, IsOptional, IsString } from 'class-validator'; - -export class ProofIdentifier { - public constructor(options: ProofIdentifier) { - if (options) { - this.schemaId = options.schemaId; - this.credentialDefinitionId = options.credentialDefinitionId; - this.revocationRegistryId = options.revocationRegistryId; - this.timestamp = options.timestamp; - } - } - - @Expose({ name: 'schema_id' }) - @IsString() - public schemaId!: string; - - @Expose({ name: 'cred_def_id' }) - @IsString() - public credentialDefinitionId!: string; - - @Expose({ name: 'rev_reg_id' }) - @IsOptional() - @IsString() - public revocationRegistryId?: string; - - @IsOptional() - @IsNumber() - public timestamp?: number; -} diff --git a/src/lib/modules/proofs/models/ProofPredicateInfo.ts b/src/lib/modules/proofs/models/ProofPredicateInfo.ts deleted file mode 100644 index 2d076840f1..0000000000 --- a/src/lib/modules/proofs/models/ProofPredicateInfo.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Expose, Type } from 'class-transformer'; -import { IsArray, IsEnum, IsInt, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { AttributeFilter } from './AttributeFilter'; -import { PredicateType } from './PredicateType'; -import { RevocationInterval } from '../../credentials'; - -export class ProofPredicateInfo { - public constructor(options: ProofPredicateInfo) { - if (options) { - this.name = options.name; - this.nonRevoked = options.nonRevoked; - this.restrictions = options.restrictions; - this.predicateType = options.predicateType; - this.predicateValue = options.predicateValue; - } - } - - @IsString() - public name!: string; - - @Expose({ name: 'p_type' }) - @IsEnum(PredicateType) - public predicateType!: PredicateType; - - @Expose({ name: 'p_value' }) - @IsInt() - public predicateValue!: number; - - @Expose({ name: 'non_revoked' }) - @ValidateNested() - @Type(() => RevocationInterval) - @IsOptional() - public nonRevoked?: RevocationInterval; - - @ValidateNested({ each: true }) - @Type(() => AttributeFilter) - @IsOptional() - @IsArray() - public restrictions?: AttributeFilter[]; -} diff --git a/src/lib/modules/proofs/models/ProofRequest.ts b/src/lib/modules/proofs/models/ProofRequest.ts deleted file mode 100644 index a2d961f30f..0000000000 --- a/src/lib/modules/proofs/models/ProofRequest.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { IndyProofRequest } from 'indy-sdk'; -import { IsString, ValidateNested, IsOptional, IsIn } from 'class-validator'; -import { Expose, Type } from 'class-transformer'; - -import { RevocationInterval } from '../../credentials'; -import { ProofAttributeInfo } from './ProofAttributeInfo'; -import { ProofPredicateInfo } from './ProofPredicateInfo'; - -import { JsonTransformer } from '../../../utils/JsonTransformer'; -import { Optional } from '../../../utils/type'; -import { RecordTransformer } from '../../../utils/transformers'; - -/** - * Proof Request for Indy based proof format - * - * @see https://github.com/hyperledger/indy-sdk/blob/57dcdae74164d1c7aa06f2cccecaae121cefac25/libindy/src/api/anoncreds.rs#L1222-L1239 - */ -export class ProofRequest { - public constructor(options: Optional, 'requestedAttributes' | 'requestedPredicates'>) { - if (options) { - this.name = options.name; - this.version = options.version; - this.nonce = options.nonce; - this.requestedAttributes = options.requestedAttributes ?? {}; - this.requestedPredicates = options.requestedPredicates ?? {}; - this.nonRevoked = options.nonRevoked; - this.ver = options.ver; - } - } - - @IsString() - public name!: string; - - @IsString() - public version!: string; - - @IsString() - public nonce!: string; - - @Expose({ name: 'requested_attributes' }) - @ValidateNested({ each: true }) - @RecordTransformer(ProofAttributeInfo) - public requestedAttributes!: Record; - - @Expose({ name: 'requested_predicates' }) - @ValidateNested({ each: true }) - @RecordTransformer(ProofPredicateInfo) - public requestedPredicates!: Record; - - @Expose({ name: 'non_revoked' }) - @ValidateNested() - @Type(() => RevocationInterval) - @IsOptional() - public nonRevoked?: RevocationInterval; - - @IsIn(['1.0', '2.0']) - @IsOptional() - public ver?: '1.0' | '2.0'; - - public toJSON() { - // IndyProofRequest is indy-sdk json type - return (JsonTransformer.toJSON(this) as unknown) as IndyProofRequest; - } -} diff --git a/src/lib/modules/proofs/models/RequestedAttribute.ts b/src/lib/modules/proofs/models/RequestedAttribute.ts deleted file mode 100644 index fcd47490db..0000000000 --- a/src/lib/modules/proofs/models/RequestedAttribute.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { IsBoolean, IsInt, IsOptional, IsPositive, IsString } from 'class-validator'; -import { Expose } from 'class-transformer'; - -/** - * Requested Attribute for Indy proof creation - */ -export class RequestedAttribute { - public constructor(options: RequestedAttribute) { - if (options) { - this.credentialId = options.credentialId; - this.timestamp = options.timestamp; - this.revealed = options.revealed; - } - } - - @Expose({ name: 'cred_id' }) - @IsString() - public credentialId!: string; - - @Expose({ name: 'timestamp' }) - @IsPositive() - @IsInt() - @IsOptional() - public timestamp?: number; - - @IsBoolean() - public revealed!: boolean; -} diff --git a/src/lib/modules/proofs/models/RequestedCredentials.ts b/src/lib/modules/proofs/models/RequestedCredentials.ts deleted file mode 100644 index a562c117de..0000000000 --- a/src/lib/modules/proofs/models/RequestedCredentials.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { IndyRequestedCredentials } from 'indy-sdk'; -import { ValidateNested } from 'class-validator'; -import { Expose, Type } from 'class-transformer'; - -import { RequestedAttribute } from './RequestedAttribute'; -import { RequestedPredicate } from './RequestedPredicate'; -import { JsonTransformer } from '../../../utils/JsonTransformer'; -import { RecordTransformer } from '../../../utils/transformers'; - -interface RequestedCredentialsOptions { - requestedAttributes?: Record; - requestedPredicates?: Record; - selfAttestedAttributes?: Record; -} - -/** - * Requested Credentials for Indy proof creation - * - * @see https://github.com/hyperledger/indy-sdk/blob/57dcdae74164d1c7aa06f2cccecaae121cefac25/libindy/src/api/anoncreds.rs#L1433-L1445 - */ -export class RequestedCredentials { - public constructor(options: RequestedCredentialsOptions) { - if (options) { - this.requestedAttributes = options.requestedAttributes ?? {}; - this.requestedPredicates = options.requestedPredicates ?? {}; - this.selfAttestedAttributes = options.selfAttestedAttributes ?? {}; - } - } - - @Expose({ name: 'requested_attributes' }) - @ValidateNested({ each: true }) - @RecordTransformer(RequestedAttribute) - public requestedAttributes!: Record; - - @Expose({ name: 'requested_predicates' }) - @ValidateNested({ each: true }) - @Type(() => RequestedPredicate) - @RecordTransformer(RequestedPredicate) - public requestedPredicates!: Record; - - @Expose({ name: 'self_attested_attributes' }) - public selfAttestedAttributes!: Record; - - public toJSON() { - // IndyRequestedCredentials is indy-sdk json type - return (JsonTransformer.toJSON(this) as unknown) as IndyRequestedCredentials; - } - - public getCredentialIdentifiers(): string[] { - const credIds = new Set(); - - Object.values(this.requestedAttributes).forEach(attr => { - credIds.add(attr.credentialId); - }); - - Object.values(this.requestedPredicates).forEach(pred => { - credIds.add(pred.credentialId); - }); - - return Array.from(credIds); - } -} diff --git a/src/lib/modules/proofs/models/RequestedPredicate.ts b/src/lib/modules/proofs/models/RequestedPredicate.ts deleted file mode 100644 index e415414fff..0000000000 --- a/src/lib/modules/proofs/models/RequestedPredicate.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IsInt, IsOptional, IsPositive, IsString } from 'class-validator'; -import { Expose } from 'class-transformer'; - -/** - * Requested Predicate for Indy proof creation - */ -export class RequestedPredicate { - public constructor(options: RequestedPredicate) { - if (options) { - this.credentialId = options.credentialId; - this.timestamp = options.timestamp; - } - } - - @Expose({ name: 'cred_id' }) - @IsString() - public credentialId!: string; - - @Expose({ name: 'timestamp' }) - @IsPositive() - @IsInt() - @IsOptional() - public timestamp?: number; -} diff --git a/src/lib/modules/proofs/models/RequestedProof.ts b/src/lib/modules/proofs/models/RequestedProof.ts deleted file mode 100644 index a1116edb90..0000000000 --- a/src/lib/modules/proofs/models/RequestedProof.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { IsString, ValidateNested } from 'class-validator'; -import { Expose, Type } from 'class-transformer'; - -import { ProofAttribute } from './ProofAttribute'; - -export class RequestedProof { - public constructor(options: RequestedProof) { - if (options) { - this.revealedAttributes = options.revealedAttributes; - this.selfAttestedAttributes = options.selfAttestedAttributes; - } - } - - @Expose({ name: 'revealed_attrs' }) - @ValidateNested({ each: true }) - @Type(() => ProofAttribute) - public revealedAttributes!: Map; - - @Expose({ name: 'self_attested_attrs' }) - @IsString({ each: true }) - public selfAttestedAttributes!: Map; -} diff --git a/src/lib/modules/proofs/models/index.ts b/src/lib/modules/proofs/models/index.ts deleted file mode 100644 index d897a76ab5..0000000000 --- a/src/lib/modules/proofs/models/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export * from './AttributeFilter'; -export * from './PartialProof'; -export * from './PredicateType'; -export * from './ProofAttribute'; -export * from './ProofAttributeInfo'; -export * from './ProofIdentifier'; -export * from './ProofPredicateInfo'; -export * from './ProofRequest'; -export * from './RequestedAttribute'; -export * from './RequestedCredentials'; -export * from './RequestedPredicate'; -export * from './RequestedProof'; diff --git a/src/lib/modules/proofs/repository/ProofRecord.ts b/src/lib/modules/proofs/repository/ProofRecord.ts deleted file mode 100644 index 9344d5ba86..0000000000 --- a/src/lib/modules/proofs/repository/ProofRecord.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { v4 as uuid } from 'uuid'; -import { BaseRecord, RecordType, Tags } from '../../../storage/BaseRecord'; -import { ProposePresentationMessage, RequestPresentationMessage, PresentationMessage } from '../messages'; -import { ProofState } from '../ProofState'; - -export interface ProofRecordProps { - id?: string; - createdAt?: number; - - isVerified?: boolean; - state: ProofState; - connectionId: string; - presentationId?: string; - tags: ProofRecordTags; - - // message data - proposalMessage?: ProposePresentationMessage; - requestMessage?: RequestPresentationMessage; - presentationMessage?: PresentationMessage; -} -export interface ProofRecordTags extends Tags { - threadId?: string; -} - -export class ProofRecord extends BaseRecord implements ProofRecordProps { - public connectionId: string; - public isVerified?: boolean; - public presentationId?: string; - public state: ProofState; - public tags: ProofRecordTags; - - // message data - public proposalMessage?: ProposePresentationMessage; - public requestMessage?: RequestPresentationMessage; - public presentationMessage?: PresentationMessage; - - public static readonly type: RecordType = RecordType.ProofRecord; - public readonly type = RecordType.ProofRecord; - - public constructor(props: ProofRecordProps) { - super(props.id ?? uuid(), props.createdAt ?? Date.now()); - this.proposalMessage = props.proposalMessage; - this.requestMessage = props.requestMessage; - this.presentationMessage = props.presentationMessage; - this.isVerified = props.isVerified; - this.state = props.state; - this.connectionId = props.connectionId; - this.presentationId = props.presentationId; - this.tags = props.tags as { [keys: string]: string }; - } - - public assertState(expectedStates: ProofState | ProofState[]) { - if (!Array.isArray(expectedStates)) { - expectedStates = [expectedStates]; - } - - if (!expectedStates.includes(this.state)) { - throw new Error( - `Proof record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.` - ); - } - } - - public assertConnection(currentConnectionId: string) { - if (this.connectionId !== currentConnectionId) { - throw new Error( - `Proof record is associated with connection '${this.connectionId}'. Current connection is '${currentConnectionId}'` - ); - } - } -} diff --git a/src/lib/modules/proofs/services/ProofService.ts b/src/lib/modules/proofs/services/ProofService.ts deleted file mode 100644 index 0df0a93b61..0000000000 --- a/src/lib/modules/proofs/services/ProofService.ts +++ /dev/null @@ -1,898 +0,0 @@ -import type { IndyProof, Schema, CredDef } from 'indy-sdk'; - -import { EventEmitter } from 'events'; -import { validateOrReject } from 'class-validator'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { LedgerService } from '../../ledger/services/LedgerService'; -import { InboundMessageContext } from '../../../agent/models/InboundMessageContext'; -import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment'; -import { ConnectionRecord } from '../../connections'; -import { ProofRecord } from '../repository/ProofRecord'; -import { Repository } from '../../../storage/Repository'; -import { JsonEncoder } from '../../../utils/JsonEncoder'; -import { JsonTransformer } from '../../../utils/JsonTransformer'; -import { uuid } from '../../../utils/uuid'; -import { Wallet } from '../../../wallet/Wallet'; -import { CredentialUtils, Credential, CredentialInfo } from '../../credentials'; - -import { - PresentationMessage, - PresentationPreview, - PresentationPreviewAttribute, - ProposePresentationMessage, - RequestPresentationMessage, - PresentationAckMessage, - INDY_PROOF_REQUEST_ATTACHMENT_ID, - INDY_PROOF_ATTACHMENT_ID, -} from '../messages'; -import { AckStatus } from '../../common'; -import { - PartialProof, - ProofAttributeInfo, - AttributeFilter, - ProofPredicateInfo, - ProofRequest, - RequestedCredentials, - RequestedAttribute, - RequestedPredicate, -} from '../models'; -import { ProofState } from '../ProofState'; -import { AgentConfig } from '../../../agent/AgentConfig'; -import { Logger } from '../../../logger'; - -export enum ProofEventType { - StateChanged = 'stateChanged', -} - -export interface ProofStateChangedEvent { - proofRecord: ProofRecord; - previousState: ProofState; -} - -export interface ProofProtocolMsgReturnType { - message: MessageType; - proofRecord: ProofRecord; -} - -/** - * @todo add method to check if request matches proposal. Useful to see if a request I received is the same as the proposal I sent. - * @todo add method to reject / revoke messages - * @todo validate attachments / messages - */ -export class ProofService extends EventEmitter { - private proofRepository: Repository; - private ledgerService: LedgerService; - private wallet: Wallet; - private logger: Logger; - - public constructor( - proofRepository: Repository, - ledgerService: LedgerService, - wallet: Wallet, - agentConfig: AgentConfig - ) { - super(); - - this.proofRepository = proofRepository; - this.ledgerService = ledgerService; - this.wallet = wallet; - this.logger = agentConfig.logger; - } - - /** - * Create a {@link ProposePresentationMessage} not bound to an existing presentation exchange. - * To create a proposal as response to an existing presentation exchange, use {@link ProofService#createProposalAsResponse}. - * - * @param connectionRecord The connection for which to create the presentation proposal - * @param presentationProposal The presentation proposal to include in the message - * @param config Additional configuration to use for the proposal - * @returns Object containing proposal message and associated proof record - * - */ - public async createProposal( - connectionRecord: ConnectionRecord, - presentationProposal: PresentationPreview, - config?: { - comment?: string; - } - ): Promise> { - // Assert - connectionRecord.assertReady(); - - // Create message - const proposalMessage = new ProposePresentationMessage({ - comment: config?.comment, - presentationProposal, - }); - - // Create record - const proofRecord = new ProofRecord({ - connectionId: connectionRecord.id, - state: ProofState.ProposalSent, - proposalMessage, - tags: { threadId: proposalMessage.threadId }, - }); - await this.proofRepository.save(proofRecord); - this.emit(ProofEventType.StateChanged, { proofRecord, previousState: null }); - - return { message: proposalMessage, proofRecord }; - } - - /** - * Create a {@link ProposePresentationMessage} as response to a received presentation request. - * To create a proposal not bound to an existing presentation exchange, use {@link ProofService#createProposal}. - * - * @param proofRecord The proof record for which to create the presentation proposal - * @param presentationProposal The presentation proposal to include in the message - * @param config Additional configuration to use for the proposal - * @returns Object containing proposal message and associated proof record - * - */ - public async createProposalAsResponse( - proofRecord: ProofRecord, - presentationProposal: PresentationPreview, - config?: { - comment?: string; - } - ): Promise> { - // Assert - proofRecord.assertState(ProofState.RequestReceived); - - // Create message - const proposalMessage = new ProposePresentationMessage({ - comment: config?.comment, - presentationProposal, - }); - proposalMessage.setThread({ threadId: proofRecord.tags.threadId }); - - // Update record - proofRecord.proposalMessage = proposalMessage; - this.updateState(proofRecord, ProofState.ProposalSent); - - return { message: proposalMessage, proofRecord }; - } - - /** - * Process a received {@link ProposePresentationMessage}. This will not accept the presentation proposal - * or send a presentation request. It will only create a new, or update the existing proof record with - * the information from the presentation proposal message. Use {@link ProofService#createRequestAsResponse} - * after calling this method to create a presentation request. - * - * @param messageContext The message context containing a presentation proposal message - * @returns proof record associated with the presentation proposal message - * - */ - public async processProposal( - messageContext: InboundMessageContext - ): Promise { - let proofRecord: ProofRecord; - const { message: proposalMessage, connection } = messageContext; - - // Assert connection - connection?.assertReady(); - if (!connection) { - throw new Error( - `No connection associated with incoming presentation proposal message with thread id ${proposalMessage.threadId}` - ); - } - - try { - // Proof record already exists - proofRecord = await this.getByThreadId(proposalMessage.threadId); - - // Assert - proofRecord.assertState(ProofState.RequestSent); - proofRecord.assertConnection(connection.id); - - // Update record - proofRecord.proposalMessage = proposalMessage; - await this.updateState(proofRecord, ProofState.ProposalReceived); - } catch { - // No proof record exists with thread id - proofRecord = new ProofRecord({ - connectionId: connection.id, - proposalMessage, - state: ProofState.ProposalReceived, - tags: { threadId: proposalMessage.threadId }, - }); - - // Save record - await this.proofRepository.save(proofRecord); - this.emit(ProofEventType.StateChanged, { proofRecord, previousState: null }); - } - - return proofRecord; - } - - /** - * Create a {@link RequestPresentationMessage} as response to a received presentation proposal. - * To create a request not bound to an existing presentation exchange, use {@link ProofService#createRequest}. - * - * @param proofRecord The proof record for which to create the presentation request - * @param proofRequest The proof request to include in the message - * @param config Additional configuration to use for the request - * @returns Object containing request message and associated proof record - * - */ - public async createRequestAsResponse( - proofRecord: ProofRecord, - proofRequest: ProofRequest, - config?: { - comment?: string; - } - ): Promise> { - // Assert - proofRecord.assertState(ProofState.ProposalReceived); - - // Create message - const attachment = new Attachment({ - id: INDY_PROOF_REQUEST_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(proofRequest), - }), - }); - const requestPresentationMessage = new RequestPresentationMessage({ - comment: config?.comment, - attachments: [attachment], - }); - requestPresentationMessage.setThread({ threadId: proofRecord.tags.threadId }); - - // Update record - proofRecord.requestMessage = requestPresentationMessage; - await this.updateState(proofRecord, ProofState.RequestSent); - - return { message: requestPresentationMessage, proofRecord }; - } - - /** - * Create a {@link RequestPresentationMessage} not bound to an existing presentation exchange. - * To create a request as response to an existing presentation exchange, use {@link ProofService#createRequestAsResponse}. - * - * @param connectionRecord The connection for which to create the presentation request - * @param proofRequest The proof request to include in the message - * @param config Additional configuration to use for the request - * @returns Object containing request message and associated proof record - * - */ - public async createRequest( - connectionRecord: ConnectionRecord, - proofRequest: ProofRequest, - config?: { - comment?: string; - } - ): Promise> { - // Assert - connectionRecord.assertReady(); - - // Create message - const attachment = new Attachment({ - id: INDY_PROOF_REQUEST_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(proofRequest), - }), - }); - const requestPresentationMessage = new RequestPresentationMessage({ - comment: config?.comment, - attachments: [attachment], - }); - - // Create record - const proofRecord = new ProofRecord({ - connectionId: connectionRecord.id, - requestMessage: requestPresentationMessage, - state: ProofState.RequestSent, - tags: { threadId: requestPresentationMessage.threadId }, - }); - - await this.proofRepository.save(proofRecord); - this.emit(ProofEventType.StateChanged, { proofRecord, previousState: null }); - - return { message: requestPresentationMessage, proofRecord }; - } - - /** - * Process a received {@link RequestPresentationMessage}. This will not accept the presentation request - * or send a presentation. It will only create a new, or update the existing proof record with - * the information from the presentation request message. Use {@link ProofService#createPresentation} - * after calling this method to create a presentation. - * - * @param messageContext The message context containing a presentation request message - * @returns proof record associated with the presentation request message - * - */ - public async processRequest(messageContext: InboundMessageContext): Promise { - let proofRecord: ProofRecord; - const { message: proofRequestMessage, connection } = messageContext; - - // Assert connection - connection?.assertReady(); - if (!connection) { - throw new Error( - `No connection associated with incoming presentation request message with thread id ${proofRequestMessage.threadId}` - ); - } - - const proofRequest = proofRequestMessage.indyProofRequest; - - // Assert attachment - if (!proofRequest) { - throw new Error( - `Missing required base64 encoded attachment data for presentation request with thread id ${proofRequestMessage.threadId}` - ); - } - await validateOrReject(proofRequest); - - this.logger.debug('received proof request', proofRequest); - - try { - // Proof record already exists - proofRecord = await this.getByThreadId(proofRequestMessage.threadId); - - // Assert - proofRecord.assertState(ProofState.ProposalSent); - proofRecord.assertConnection(connection.id); - - // Update record - proofRecord.requestMessage = proofRequestMessage; - await this.updateState(proofRecord, ProofState.RequestReceived); - } catch { - // No proof record exists with thread id - proofRecord = new ProofRecord({ - connectionId: connection.id, - requestMessage: proofRequestMessage, - state: ProofState.RequestReceived, - tags: { threadId: proofRequestMessage.threadId }, - }); - - // Save in repository - await this.proofRepository.save(proofRecord); - this.emit(ProofEventType.StateChanged, { proofRecord, previousState: null }); - } - - return proofRecord; - } - - /** - * Create a {@link PresentationMessage} as response to a received presentation request. - * - * @param proofRecord The proof record for which to create the presentation - * @param requestedCredentials The requested credentials object specifying which credentials to use for the proof - * @param config Additional configuration to use for the presentation - * @returns Object containing presentation message and associated proof record - * - */ - public async createPresentation( - proofRecord: ProofRecord, - requestedCredentials: RequestedCredentials, - config?: { - comment?: string; - } - ): Promise> { - // Assert - proofRecord.assertState(ProofState.RequestReceived); - - // Transform proof request to class instance if this is not already the case - // FIXME: proof record should handle transformation - const requestMessage = - proofRecord.requestMessage instanceof RequestPresentationMessage - ? proofRecord.requestMessage - : JsonTransformer.fromJSON(proofRecord.requestMessage, RequestPresentationMessage); - - const indyProofRequest = requestMessage.indyProofRequest; - if (!indyProofRequest) { - throw new Error( - `Missing required base64 encoded attachment data for presentation with thread id ${proofRecord.tags.threadId}` - ); - } - - // Create proof - const proof = await this.createProof(indyProofRequest, requestedCredentials); - - // Create message - const attachment = new Attachment({ - id: INDY_PROOF_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(proof), - }), - }); - const presentationMessage = new PresentationMessage({ - comment: config?.comment, - attachments: [attachment], - }); - presentationMessage.setThread({ threadId: proofRecord.tags.threadId }); - - // Update record - proofRecord.presentationMessage = presentationMessage; - await this.updateState(proofRecord, ProofState.PresentationSent); - - return { message: presentationMessage, proofRecord }; - } - - /** - * Process a received {@link PresentationMessage}. This will not accept the presentation - * or send a presentation acknowledgement. It will only update the existing proof record with - * the information from the presentation message. Use {@link ProofService#createAck} - * after calling this method to create a presentation acknowledgement. - * - * @param messageContext The message context containing a presentation message - * @returns proof record associated with the presentation message - * - */ - public async processPresentation(messageContext: InboundMessageContext): Promise { - const { message: presentationMessage, connection } = messageContext; - - // Assert connection - connection?.assertReady(); - if (!connection) { - throw new Error( - `No connection associated with incoming presentation message with thread id ${presentationMessage.threadId}` - ); - } - - // Assert proof record - const proofRecord = await this.getByThreadId(presentationMessage.threadId); - proofRecord.assertState(ProofState.RequestSent); - - // TODO: add proof class with validator - const indyProofJson = presentationMessage.indyProof; - // FIXME: Transformation should be handled by record class - const indyProofRequest = JsonTransformer.fromJSON(proofRecord.requestMessage, RequestPresentationMessage) - .indyProofRequest; - - if (!indyProofJson) { - throw new Error( - `Missing required base64 encoded attachment data for presentation with thread id ${presentationMessage.threadId}` - ); - } - - if (!indyProofRequest) { - throw new Error( - `Missing required base64 encoded attachment data for presentation request with thread id ${presentationMessage.threadId}` - ); - } - - const isValid = await this.verifyProof(indyProofJson, indyProofRequest); - - // Update record - proofRecord.isVerified = isValid; - proofRecord.presentationMessage = presentationMessage; - await this.updateState(proofRecord, ProofState.PresentationReceived); - - return proofRecord; - } - - /** - * Create a {@link PresentationAckMessage} as response to a received presentation. - * - * @param proofRecord The proof record for which to create the presentation acknowledgement - * @returns Object containing presentation acknowledgement message and associated proof record - * - */ - public async createAck(proofRecord: ProofRecord): Promise> { - // Assert - proofRecord.assertState(ProofState.PresentationReceived); - - // Create message - const ackMessage = new PresentationAckMessage({ - status: AckStatus.OK, - threadId: proofRecord.tags.threadId!, - }); - - // Update record - await this.updateState(proofRecord, ProofState.Done); - - return { message: ackMessage, proofRecord }; - } - - /** - * Process a received {@link PresentationAckMessage}. - * - * @param messageContext The message context containing a presentation acknowledgement message - * @returns proof record associated with the presentation acknowledgement message - * - */ - public async processAck(messageContext: InboundMessageContext): Promise { - const { message: presentationAckMessage, connection } = messageContext; - - // Assert connection - connection?.assertReady(); - if (!connection) { - throw new Error( - `No connection associated with incoming presentation acknowledgement message with thread id ${presentationAckMessage.threadId}` - ); - } - - // Assert proof record - const proofRecord = await this.getByThreadId(presentationAckMessage.threadId); - proofRecord.assertState(ProofState.PresentationSent); - - // Update record - await this.updateState(proofRecord, ProofState.Done); - - return proofRecord; - } - - public async generateProofRequestNonce() { - return this.wallet.generateNonce(); - } - - /** - * Create a {@link ProofRequest} from a presentation proposal. This method can be used to create the - * proof request from a received proposal for use in {@link ProofService#createRequestAsResponse} - * - * @param presentationProposal The presentation proposal to create a proof request from - * @param config Additional configuration to use for the proof request - * @returns proof request object - * - */ - public async createProofRequestFromProposal( - presentationProposal: PresentationPreview, - config: { name: string; version: string; nonce?: string } - ): Promise { - const nonce = config.nonce ?? (await this.generateProofRequestNonce()); - - const proofRequest = new ProofRequest({ - name: config.name, - version: config.version, - nonce, - }); - - /** - * Create mapping of attributes by referent. This required the - * attributes to come from the same credential. - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#referent - * - * { - * "referent1": [Attribute1, Attribute2], - * "referent2": [Attribute3] - * } - */ - const attributesByReferent: Record = {}; - for (const proposedAttributes of presentationProposal.attributes) { - if (!proposedAttributes.referent) proposedAttributes.referent = uuid(); - - const referentAttributes = attributesByReferent[proposedAttributes.referent]; - - // Referent key already exist, add to list - if (referentAttributes) { - referentAttributes.push(proposedAttributes); - } - // Referent key does not exist yet, create new entry - else { - attributesByReferent[proposedAttributes.referent] = [proposedAttributes]; - } - } - - // Transform attributes by referent to requested attributes - for (const [referent, proposedAttributes] of Object.entries(attributesByReferent)) { - // Either attributeName or attributeNames will be undefined - const attributeName = proposedAttributes.length == 1 ? proposedAttributes[0].name : undefined; - const attributeNames = proposedAttributes.length > 1 ? proposedAttributes.map(a => a.name) : undefined; - - const requestedAttribute = new ProofAttributeInfo({ - name: attributeName, - names: attributeNames, - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: proposedAttributes[0].credentialDefinitionId, - }), - ], - }); - - proofRequest.requestedAttributes[referent] = requestedAttribute; - } - - this.logger.debug('proposal predicates', presentationProposal.predicates); - // Transform proposed predicates to requested predicates - for (const proposedPredicate of presentationProposal.predicates) { - const requestedPredicate = new ProofPredicateInfo({ - name: proposedPredicate.name, - predicateType: proposedPredicate.predicate, - predicateValue: proposedPredicate.threshold, - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: proposedPredicate.credentialDefinitionId, - }), - ], - }); - - proofRequest.requestedPredicates[uuid()] = requestedPredicate; - } - - return proofRequest; - } - - /** - * Create a {@link RequestedCredentials} object. Given input proof request and presentation proposal, - * use credentials in the wallet to build indy requested credentials object for input to proof creation. - * If restrictions allow, self attested attributes will be used. - * - * Use the return value of this method as input to {@link ProofService.createPresentation} to automatically - * accept a received presentation request. - * - * @param proofRequest The proof request to build the requested credentials object from - * @param presentationProposal Optional presentation proposal to improve credential selection algorithm - * @returns Requested credentials object for use in proof creation - */ - public async getRequestedCredentialsForProofRequest( - proofRequest: ProofRequest, - presentationProposal?: PresentationPreview - ): Promise { - const requestedCredentials = new RequestedCredentials({}); - - for (const [referent, requestedAttribute] of Object.entries(proofRequest.requestedAttributes)) { - let credentialMatch: Credential | null = null; - const credentials = await this.getCredentialsForProofRequest(proofRequest, referent); - - // Can't construct without matching credentials - if (credentials.length === 0) { - throw new Error( - `Could not automatically construct requested credentials for proof request '${proofRequest.name}'` - ); - } - // If we have exactly one credential, or no proposal to pick preferences - // on the credential to use, we will use the first one - else if (credentials.length === 1 || !presentationProposal) { - credentialMatch = credentials[0]; - } - // If we have a proposal we will use that to determine the credential to use - else { - const names = requestedAttribute.names ?? [requestedAttribute.name]; - - // Find credential that matches all parameters from the proposal - for (const credential of credentials) { - const { attributes, credentialDefinitionId } = credential.credentialInfo; - - // Check if credential matches all parameters from proposal - const isMatch = names.every(name => - presentationProposal.attributes.find( - a => - a.name === name && - a.credentialDefinitionId === credentialDefinitionId && - (!a.value || a.value === attributes[name]) - ) - ); - - if (isMatch) { - credentialMatch = credential; - break; - } - } - - if (!credentialMatch) { - throw new Error( - `Could not automatically construct requested credentials for proof request '${proofRequest.name}'` - ); - } - } - - if (requestedAttribute.restrictions) { - requestedCredentials.requestedAttributes[referent] = new RequestedAttribute({ - credentialId: credentialMatch.credentialInfo.referent, - revealed: true, - }); - } - // If there are no restrictions we can self attest the attribute - else { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const value = credentialMatch.credentialInfo.attributes[requestedAttribute.name!]; - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - requestedCredentials.selfAttestedAttributes[referent] = value!; - } - } - - for (const [referent, requestedPredicate] of Object.entries(proofRequest.requestedPredicates)) { - const credentials = await this.getCredentialsForProofRequest(proofRequest, referent); - - // Can't create requestedPredicates without matching credentials - if (credentials.length === 0) { - throw new Error( - `Could not automatically construct requested credentials for proof request '${proofRequest.name}'` - ); - } - - const credentialMatch = credentials[0]; - if (requestedPredicate.restrictions) { - requestedCredentials.requestedPredicates[referent] = new RequestedPredicate({ - credentialId: credentialMatch.credentialInfo.referent, - }); - } - // If there are no restrictions we can self attest the attribute - else { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const value = credentialMatch.credentialInfo.attributes[requestedPredicate.name!]; - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - requestedCredentials.selfAttestedAttributes[referent] = value!; - } - } - - return requestedCredentials; - } - - /** - * Verify an indy proof object. Will also verify raw values against encodings. - * - * @param proofRequest The proof request to use for proof verification - * @param proofJson The proof object to verify - * @throws {Error} If the raw values do not match the encoded values - * @returns Boolean whether the proof is valid - * - */ - public async verifyProof(proofJson: IndyProof, proofRequest: ProofRequest): Promise { - const proof = JsonTransformer.fromJSON(proofJson, PartialProof); - - for (const [referent, attribute] of proof.requestedProof.revealedAttributes.entries()) { - if (!CredentialUtils.checkValidEncoding(attribute.raw, attribute.encoded)) { - throw new Error( - `The encoded value for '${referent}' is invalid. ` + - `Expected '${CredentialUtils.encode(attribute.raw)}'. ` + - `Actual '${attribute.encoded}'` - ); - } - } - - // TODO: pre verify proof json - // I'm not 100% sure how much indy does. Also if it checks whether the proof requests matches the proof - // @see https://github.com/hyperledger/aries-cloudagent-python/blob/master/aries_cloudagent/indy/sdk/verifier.py#L79-L164 - - const schemas = await this.getSchemas(new Set(proof.identifiers.map(i => i.schemaId))); - const credentialDefinitions = await this.getCredentialDefinitions( - new Set(proof.identifiers.map(i => i.credentialDefinitionId)) - ); - - return await this.wallet.verifyProof(proofRequest.toJSON(), proofJson, schemas, credentialDefinitions, {}, {}); - } - - /** - * Retrieve all proof records - * - * @returns List containing all proof records - */ - public async getAll(): Promise { - return this.proofRepository.findAll(); - } - - /** - * Retrieve a proof record by id - * - * @param proofRecordId The proof record id - * @throws {Error} If no record is found - * @return The proof record - * - */ - public async getById(proofRecordId: string): Promise { - return this.proofRepository.find(proofRecordId); - } - - /** - * Retrieve a proof record by thread id - * - * @param threadId The thread id - * @throws {Error} If no record is found - * @throws {Error} If multiple records are found - * @returns The proof record - */ - public async getByThreadId(threadId: string): Promise { - const proofRecords = await this.proofRepository.findByQuery({ threadId }); - - if (proofRecords.length === 0) { - throw new Error(`Proof record not found by thread id ${threadId}`); - } - - if (proofRecords.length > 1) { - throw new Error(`Multiple proof records found by thread id ${threadId}`); - } - - return proofRecords[0]; - } - - /** - * Create indy proof from a given proof request and requested credential object. - * - * @param proofRequest The proof request to create the proof for - * @param requestedCredentials The requested credentials object specifying which credentials to use for the proof - * @returns indy proof object - */ - private async createProof( - proofRequest: ProofRequest, - requestedCredentials: RequestedCredentials - ): Promise { - const credentialObjects: CredentialInfo[] = []; - - for (const credentialId of requestedCredentials.getCredentialIdentifiers()) { - const credentialInfo = JsonTransformer.fromJSON(await this.wallet.getCredential(credentialId), CredentialInfo); - - credentialObjects.push(credentialInfo); - } - - const schemas = await this.getSchemas(new Set(credentialObjects.map(c => c.schemaId))); - const credentialDefinitions = await this.getCredentialDefinitions( - new Set(credentialObjects.map(c => c.credentialDefinitionId)) - ); - - const proof = await this.wallet.createProof( - proofRequest.toJSON(), - requestedCredentials.toJSON(), - schemas, - credentialDefinitions, - {} - ); - - return proof; - } - - private async getCredentialsForProofRequest( - proofRequest: ProofRequest, - attributeReferent: string - ): Promise { - const credentialsJson = await this.wallet.getCredentialsForProofRequest(proofRequest.toJSON(), attributeReferent); - return (JsonTransformer.fromJSON(credentialsJson, Credential) as unknown) as Credential[]; - } - - /** - * Update the record to a new state and emit an state changed event. Also updates the record - * in storage. - * - * @param proofRecord The proof record to update the state for - * @param newState The state to update to - * - */ - private async updateState(proofRecord: ProofRecord, newState: ProofState) { - const previousState = proofRecord.state; - proofRecord.state = newState; - await this.proofRepository.update(proofRecord); - - const event: ProofStateChangedEvent = { - proofRecord, - previousState: previousState, - }; - - this.emit(ProofEventType.StateChanged, event); - } - - /** - * Build schemas object needed to create and verify proof objects. - * - * Creates object with `{ schemaId: Schema }` mapping - * - * @param schemaIds List of schema ids - * @returns Object containing schemas for specified schema ids - * - */ - private async getSchemas(schemaIds: Set) { - const schemas: { [key: string]: Schema } = {}; - - for (const schemaId of schemaIds) { - const schema = await this.ledgerService.getSchema(schemaId); - schemas[schemaId] = schema; - } - - return schemas; - } - - /** - * Build credential definitions object needed to create and verify proof objects. - * - * Creates object with `{ credentialDefinitionId: CredentialDefinition }` mapping - * - * @param credentialDefinitionIds List of credential definition ids - * @returns Object containing credential definitions for specified credential definition ids - * - */ - private async getCredentialDefinitions(credentialDefinitionIds: Set) { - const credentialDefinitions: { [key: string]: CredDef } = {}; - - for (const credDefId of credentialDefinitionIds) { - const credDef = await this.ledgerService.getCredentialDefinition(credDefId); - credentialDefinitions[credDefId] = credDef; - } - - return credentialDefinitions; - } -} diff --git a/src/lib/modules/proofs/services/index.ts b/src/lib/modules/proofs/services/index.ts deleted file mode 100644 index 5aa72fc12d..0000000000 --- a/src/lib/modules/proofs/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ProofService'; diff --git a/src/lib/modules/routing/RoutingModule.ts b/src/lib/modules/routing/RoutingModule.ts deleted file mode 100644 index f7dbc648a0..0000000000 --- a/src/lib/modules/routing/RoutingModule.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { AgentConfig } from '../../agent/AgentConfig'; -import { ProviderRoutingService, MessagePickupService, ProvisioningService } from './services'; -import { MessageSender } from '../../agent/MessageSender'; -import { createOutboundMessage } from '../../agent/helpers'; -import { - ConnectionService, - ConnectionState, - ConnectionInvitationMessage, - ConnectionResponseMessage, -} from '../connections'; -import { BatchMessage } from './messages'; -import type { Verkey } from 'indy-sdk'; -import { Dispatcher } from '../../agent/Dispatcher'; -import { MessagePickupHandler, ForwardHandler, KeylistUpdateHandler } from './handlers'; -import { Logger } from '../../logger'; -export class RoutingModule { - private agentConfig: AgentConfig; - private providerRoutingService: ProviderRoutingService; - private provisioningService: ProvisioningService; - private messagePickupService: MessagePickupService; - private connectionService: ConnectionService; - private messageSender: MessageSender; - private logger: Logger; - - public constructor( - dispatcher: Dispatcher, - agentConfig: AgentConfig, - providerRoutingService: ProviderRoutingService, - provisioningService: ProvisioningService, - messagePickupService: MessagePickupService, - connectionService: ConnectionService, - messageSender: MessageSender - ) { - this.agentConfig = agentConfig; - this.providerRoutingService = providerRoutingService; - this.provisioningService = provisioningService; - this.messagePickupService = messagePickupService; - this.connectionService = connectionService; - this.messageSender = messageSender; - this.logger = agentConfig.logger; - this.registerHandlers(dispatcher); - } - - public async provision(mediatorConfiguration: MediatorConfiguration) { - let provisioningRecord = await this.provisioningService.find(); - - if (!provisioningRecord) { - this.logger.info('No provision record found. Creating connection with mediator.'); - const { verkey, invitationUrl, alias = 'Mediator' } = mediatorConfiguration; - const mediatorInvitation = await ConnectionInvitationMessage.fromUrl(invitationUrl); - - const connection = await this.connectionService.processInvitation(mediatorInvitation, { alias }); - const { - message: connectionRequest, - connectionRecord: connectionRecord, - } = await this.connectionService.createRequest(connection.id); - const connectionResponse = await this.messageSender.sendAndReceiveMessage( - createOutboundMessage(connectionRecord, connectionRequest, connectionRecord.invitation), - ConnectionResponseMessage - ); - await this.connectionService.processResponse(connectionResponse); - const { message: trustPing } = await this.connectionService.createTrustPing(connectionRecord.id); - await this.messageSender.sendMessage(createOutboundMessage(connectionRecord, trustPing)); - - const provisioningProps = { - mediatorConnectionId: connectionRecord.id, - mediatorPublicVerkey: verkey, - }; - provisioningRecord = await this.provisioningService.create(provisioningProps); - this.logger.debug('Provisioning record has been saved.'); - } - - this.logger.debug('Provisioning record:', provisioningRecord); - - const agentConnectionAtMediator = await this.connectionService.find(provisioningRecord.mediatorConnectionId); - - if (!agentConnectionAtMediator) { - throw new Error('Connection not found!'); - } - this.logger.debug('agentConnectionAtMediator', agentConnectionAtMediator); - - agentConnectionAtMediator.assertState(ConnectionState.Complete); - - this.agentConfig.establishInbound({ - verkey: provisioningRecord.mediatorPublicVerkey, - connection: agentConnectionAtMediator, - }); - - return agentConnectionAtMediator; - } - - public async downloadMessages() { - const inboundConnection = this.getInboundConnection(); - if (inboundConnection) { - const outboundMessage = await this.messagePickupService.batchPickup(inboundConnection); - const batchResponse = await this.messageSender.sendAndReceiveMessage(outboundMessage, BatchMessage); - - // TODO: do something about the different types of message variable all having a different purpose - return batchResponse.message.messages.map(msg => msg.message); - } - return []; - } - - public getInboundConnection() { - return this.agentConfig.inboundConnection; - } - - public getRoutingTable() { - return this.providerRoutingService.getRoutes(); - } - - private registerHandlers(dispatcher: Dispatcher) { - dispatcher.registerHandler(new KeylistUpdateHandler(this.providerRoutingService)); - dispatcher.registerHandler(new ForwardHandler(this.providerRoutingService)); - dispatcher.registerHandler(new MessagePickupHandler(this.messagePickupService)); - } -} - -interface MediatorConfiguration { - verkey: Verkey; - invitationUrl: string; - alias?: string; -} diff --git a/src/lib/modules/routing/handlers/ForwardHandler.ts b/src/lib/modules/routing/handlers/ForwardHandler.ts deleted file mode 100644 index bd642f0295..0000000000 --- a/src/lib/modules/routing/handlers/ForwardHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { ProviderRoutingService } from '../services'; -import { ForwardMessage } from '../messages'; - -export class ForwardHandler implements Handler { - private routingService: ProviderRoutingService; - public supportedMessages = [ForwardMessage]; - - public constructor(routingService: ProviderRoutingService) { - this.routingService = routingService; - } - - public async handle(messageContext: HandlerInboundMessage) { - return this.routingService.forward(messageContext); - } -} diff --git a/src/lib/modules/routing/handlers/KeylistUpdateHandler.ts b/src/lib/modules/routing/handlers/KeylistUpdateHandler.ts deleted file mode 100644 index 49090f4808..0000000000 --- a/src/lib/modules/routing/handlers/KeylistUpdateHandler.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { ProviderRoutingService } from '../services'; -import { KeylistUpdateMessage } from '../messages'; - -export class KeylistUpdateHandler implements Handler { - private routingService: ProviderRoutingService; - public supportedMessages = [KeylistUpdateMessage]; - - public constructor(routingService: ProviderRoutingService) { - this.routingService = routingService; - } - - public async handle(messageContext: HandlerInboundMessage) { - if (!messageContext.connection) { - throw new Error(`Connection for verkey ${messageContext.recipientVerkey} not found!`); - } - - this.routingService.updateRoutes(messageContext, messageContext.connection); - } -} diff --git a/src/lib/modules/routing/handlers/MessagePickupHandler.ts b/src/lib/modules/routing/handlers/MessagePickupHandler.ts deleted file mode 100644 index 9ba7bba314..0000000000 --- a/src/lib/modules/routing/handlers/MessagePickupHandler.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Handler, HandlerInboundMessage } from '../../../agent/Handler'; -import { MessagePickupService } from '../services'; -import { BatchPickupMessage } from '../messages'; - -export class MessagePickupHandler implements Handler { - private messagePickupService: MessagePickupService; - public supportedMessages = [BatchPickupMessage]; - - public constructor(messagePickupService: MessagePickupService) { - this.messagePickupService = messagePickupService; - } - - public async handle(messageContext: HandlerInboundMessage) { - if (!messageContext.connection) { - throw new Error(`Connection for verkey ${messageContext.recipientVerkey} not found!`); - } - - return this.messagePickupService.batch(messageContext.connection); - } -} diff --git a/src/lib/modules/routing/handlers/index.ts b/src/lib/modules/routing/handlers/index.ts deleted file mode 100644 index 77832f1b1d..0000000000 --- a/src/lib/modules/routing/handlers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './ForwardHandler'; -export * from './KeylistUpdateHandler'; -export * from './MessagePickupHandler'; diff --git a/src/lib/modules/routing/index.ts b/src/lib/modules/routing/index.ts deleted file mode 100644 index e1f9601c62..0000000000 --- a/src/lib/modules/routing/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './messages'; -export * from './services'; -export * from './repository/ProvisioningRecord'; diff --git a/src/lib/modules/routing/messages/BatchMessage.ts b/src/lib/modules/routing/messages/BatchMessage.ts deleted file mode 100644 index a066f753f5..0000000000 --- a/src/lib/modules/routing/messages/BatchMessage.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Equals, Matches, IsArray, ValidateNested } from 'class-validator'; -import { Type, Expose } from 'class-transformer'; -import { v4 as uuid } from 'uuid'; - -import { MessageIdRegExp } from '../../../agent/BaseMessage'; -import { AgentMessage } from '../../../agent/AgentMessage'; -import { RoutingMessageType as MessageType } from './RoutingMessageType'; -import { WireMessage } from '../../../types'; - -export interface BatchMessageOptions { - id?: string; - messages: BatchMessageMessage[]; -} - -/** - * A message that contains multiple waiting messages. - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0212-pickup/README.md#batch - */ -export class BatchMessage extends AgentMessage { - public constructor(options: BatchMessageOptions) { - super(); - - if (options) { - this.id = options.id || this.generateId(); - this.messages = options.messages; - } - } - - @Equals(BatchMessage.type) - public readonly type = BatchMessage.type; - public static readonly type = MessageType.Batch; - - @Type(() => BatchMessageMessage) - @IsArray() - @ValidateNested() - // TODO: Update to attachment decorator - // However i think the usage of the attachment decorator - // as specified in the Pickup Protocol is incorrect - @Expose({ name: 'messages~attach' }) - public messages!: BatchMessageMessage[]; -} - -export class BatchMessageMessage { - public constructor(options: { id?: string; message: WireMessage }) { - if (options) { - this.id = options.id || uuid(); - this.message = options.message; - } - } - - @Matches(MessageIdRegExp) - public id!: string; - - public message!: WireMessage; -} diff --git a/src/lib/modules/routing/messages/BatchPickupMessage.ts b/src/lib/modules/routing/messages/BatchPickupMessage.ts deleted file mode 100644 index 9094b0e277..0000000000 --- a/src/lib/modules/routing/messages/BatchPickupMessage.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Equals, IsNumber } from 'class-validator'; -import { Expose } from 'class-transformer'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { RoutingMessageType as MessageType } from './RoutingMessageType'; - -export interface BatchPickupMessageOptions { - id?: string; - batchSize: number; -} - -/** - * A message to request to have multiple waiting messages sent inside a `batch` message. - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0212-pickup/README.md#batch-pickup - */ -export class BatchPickupMessage extends AgentMessage { - /** - * Create new BatchPickupMessage instance. - * - * @param options - */ - public constructor(options: BatchPickupMessageOptions) { - super(); - - if (options) { - this.id = options.id || this.generateId(); - this.batchSize = options.batchSize; - } - } - - @Equals(BatchPickupMessage.type) - public readonly type = BatchPickupMessage.type; - public static readonly type = MessageType.BatchPickup; - - @IsNumber() - @Expose({ name: 'batch_size' }) - public batchSize!: number; -} diff --git a/src/lib/modules/routing/messages/ForwardMessage.ts b/src/lib/modules/routing/messages/ForwardMessage.ts deleted file mode 100644 index dcbb5732b2..0000000000 --- a/src/lib/modules/routing/messages/ForwardMessage.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Equals, IsString } from 'class-validator'; -import { Expose } from 'class-transformer'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { RoutingMessageType as MessageType } from './RoutingMessageType'; - -export interface ForwardMessageOptions { - id?: string; - to: string; - message: JsonWebKey; -} - -/** - * @see https://github.com/hyperledger/aries-rfcs/blob/master/concepts/0094-cross-domain-messaging/README.md#corerouting10forward - */ -export class ForwardMessage extends AgentMessage { - /** - * Create new ForwardMessage instance. - * - * @param options - */ - public constructor(options: ForwardMessageOptions) { - super(); - - if (options) { - this.id = options.id || this.generateId(); - this.to = options.to; - this.message = options.message; - } - } - - @Equals(ForwardMessage.type) - public readonly type = ForwardMessage.type; - public static readonly type = MessageType.ForwardMessage; - - @IsString() - public to!: string; - - @Expose({ name: 'msg' }) - public message!: JsonWebKey; -} diff --git a/src/lib/modules/routing/messages/KeylistUpdateMessage.ts b/src/lib/modules/routing/messages/KeylistUpdateMessage.ts deleted file mode 100644 index 221a529b95..0000000000 --- a/src/lib/modules/routing/messages/KeylistUpdateMessage.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { Verkey } from 'indy-sdk'; -import { Equals, IsArray, ValidateNested, IsString, IsEnum } from 'class-validator'; -import { Type } from 'class-transformer'; - -import { AgentMessage } from '../../../agent/AgentMessage'; -import { RoutingMessageType as MessageType } from './RoutingMessageType'; - -export interface KeylistUpdateMessageOptions { - id?: string; - updates: KeylistUpdate[]; -} - -/** - * Used to notify the mediator of keys in use by the recipient. - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0211-route-coordination/README.md#keylist-update - */ -export class KeylistUpdateMessage extends AgentMessage { - public constructor(options: KeylistUpdateMessageOptions) { - super(); - - if (options) { - this.id = options.id || this.generateId(); - this.updates = options.updates; - } - } - - @Equals(KeylistUpdateMessage.type) - public readonly type = KeylistUpdateMessage.type; - public static readonly type = MessageType.KeylistUpdate; - - @Type(() => KeylistUpdate) - @IsArray() - @ValidateNested() - public updates!: KeylistUpdate[]; -} - -export enum KeylistUpdateAction { - add = 'add', - remove = 'remove', -} - -export class KeylistUpdate { - public constructor(options: { recipientKey: Verkey; action: KeylistUpdateAction }) { - if (options) { - this.recipientKey = options.recipientKey; - this.action = options.action; - } - } - - @IsString() - public recipientKey!: Verkey; - - @IsEnum(KeylistUpdateAction) - public action!: KeylistUpdateAction; -} diff --git a/src/lib/modules/routing/messages/RoutingMessageType.ts b/src/lib/modules/routing/messages/RoutingMessageType.ts deleted file mode 100644 index e9c91c8650..0000000000 --- a/src/lib/modules/routing/messages/RoutingMessageType.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum RoutingMessageType { - // TODO: add other messages from mediator coordination protocol - KeylistUpdate = 'https://didcomm.org/coordinatemediation/1.0/keylist_update', - BatchPickup = 'https://didcomm.org/messagepickup/1.0/batch-pickup', - Batch = 'https://didcomm.org/messagepickup/1.0/batch', - ForwardMessage = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/routing/1.0/forward', -} diff --git a/src/lib/modules/routing/messages/index.ts b/src/lib/modules/routing/messages/index.ts deleted file mode 100644 index 486ea7ced5..0000000000 --- a/src/lib/modules/routing/messages/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './BatchMessage'; -export * from './BatchPickupMessage'; -export * from './ForwardMessage'; -export * from './KeylistUpdateMessage'; -export * from './RoutingMessageType'; diff --git a/src/lib/modules/routing/repository/ProvisioningRecord.ts b/src/lib/modules/routing/repository/ProvisioningRecord.ts deleted file mode 100644 index c71d743e51..0000000000 --- a/src/lib/modules/routing/repository/ProvisioningRecord.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Verkey } from 'indy-sdk'; -import { v4 as uuid } from 'uuid'; -import { BaseRecord, RecordType } from '../../../storage/BaseRecord'; - -interface ProvisioningRecordProps { - id: string; - createdAt?: number; - tags?: { [keys: string]: string }; - mediatorConnectionId: string; - mediatorPublicVerkey: Verkey; -} - -export class ProvisioningRecord extends BaseRecord { - public mediatorConnectionId: string; - public mediatorPublicVerkey: Verkey; - - public static readonly type: RecordType = RecordType.ProvisioningRecord; - public readonly type = ProvisioningRecord.type; - - public constructor(props: ProvisioningRecordProps) { - super(props.id ?? uuid(), props.createdAt ?? Date.now()); - this.mediatorConnectionId = props.mediatorConnectionId; - this.mediatorPublicVerkey = props.mediatorPublicVerkey; - this.tags = props.tags || {}; - } -} diff --git a/src/lib/modules/routing/services/ConsumerRoutingService.ts b/src/lib/modules/routing/services/ConsumerRoutingService.ts deleted file mode 100644 index 9cfda996e1..0000000000 --- a/src/lib/modules/routing/services/ConsumerRoutingService.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Verkey } from 'indy-sdk'; -import { createOutboundMessage } from '../../../agent/helpers'; -import { AgentConfig } from '../../../agent/AgentConfig'; -import { MessageSender } from '../../../agent/MessageSender'; -import { KeylistUpdateMessage, KeylistUpdate, KeylistUpdateAction } from '../messages'; -import { Logger } from '../../../logger'; - -class ConsumerRoutingService { - private messageSender: MessageSender; - private logger: Logger; - private agentConfig: AgentConfig; - - public constructor(messageSender: MessageSender, agentConfig: AgentConfig) { - this.messageSender = messageSender; - this.agentConfig = agentConfig; - this.logger = agentConfig.logger; - } - - public async createRoute(verkey: Verkey) { - this.logger.debug(`Registering route for verkey '${verkey}' at mediator`); - - if (!this.agentConfig.inboundConnection) { - this.logger.debug(`There is no mediator. Creating route for verkey '${verkey}' skipped.`); - } else { - const routingConnection = this.agentConfig.inboundConnection.connection; - - const keylistUpdateMessage = new KeylistUpdateMessage({ - updates: [ - new KeylistUpdate({ - action: KeylistUpdateAction.add, - recipientKey: verkey, - }), - ], - }); - - const outboundMessage = createOutboundMessage(routingConnection, keylistUpdateMessage); - await this.messageSender.sendMessage(outboundMessage); - } - } -} - -export { ConsumerRoutingService }; diff --git a/src/lib/modules/routing/services/MessagePickupService.ts b/src/lib/modules/routing/services/MessagePickupService.ts deleted file mode 100644 index 3849526d3a..0000000000 --- a/src/lib/modules/routing/services/MessagePickupService.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { InboundConnection } from '../../../types'; -import { createOutboundMessage } from '../../../agent/helpers'; -import { MessageRepository } from '../../../storage/MessageRepository'; -import { ConnectionRecord } from '../../connections'; -import { BatchMessage, BatchMessageMessage, BatchPickupMessage } from '../messages'; - -export class MessagePickupService { - private messageRepository?: MessageRepository; - - public constructor(messageRepository?: MessageRepository) { - this.messageRepository = messageRepository; - } - - public async batchPickup(inboundConnection: InboundConnection) { - const batchPickupMessage = new BatchPickupMessage({ - batchSize: 10, - }); - - return createOutboundMessage(inboundConnection.connection, batchPickupMessage); - } - - // TODO: add support for batchSize property - public async batch(connection: ConnectionRecord) { - if (!this.messageRepository) { - throw new Error('There is no message repository.'); - } - if (!connection.theirKey) { - throw new Error('Trying to find messages to connection without theirKey!'); - } - - const messages = this.messageRepository.findByVerkey(connection.theirKey); - // TODO: each message should be stored with an id. to be able to conform to the id property - // of batch message - const batchMessages = messages.map( - msg => - new BatchMessageMessage({ - message: msg, - }) - ); - - const batchMessage = new BatchMessage({ - messages: batchMessages, - }); - - await this.messageRepository.deleteAllByVerkey(connection.theirKey); // TODO Maybe, don't delete, but just marked them as read - return createOutboundMessage(connection, batchMessage); - } -} diff --git a/src/lib/modules/routing/services/ProviderRoutingService.ts b/src/lib/modules/routing/services/ProviderRoutingService.ts deleted file mode 100644 index 31dafaaa91..0000000000 --- a/src/lib/modules/routing/services/ProviderRoutingService.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { Verkey } from 'indy-sdk'; -import { OutboundMessage } from '../../../types'; -import { createOutboundMessage } from '../../../agent/helpers'; -import { InboundMessageContext } from '../../../agent/models/InboundMessageContext'; -import { ConnectionRecord } from '../../connections'; -import { KeylistUpdateMessage, KeylistUpdateAction, ForwardMessage } from '../messages'; - -export interface RoutingTable { - [recipientKey: string]: ConnectionRecord | undefined; -} - -class ProviderRoutingService { - private routingTable: RoutingTable = {}; - - /** - * @todo use connection from message context - */ - public updateRoutes(messageContext: InboundMessageContext, connection: ConnectionRecord) { - const { message } = messageContext; - - for (const update of message.updates) { - switch (update.action) { - case KeylistUpdateAction.add: - this.saveRoute(update.recipientKey, connection); - break; - case KeylistUpdateAction.remove: - this.removeRoute(update.recipientKey, connection); - break; - } - } - } - - public forward(messageContext: InboundMessageContext): OutboundMessage { - const { message, recipientVerkey } = messageContext; - - // TODO: update to class-validator validation - if (!message.to) { - throw new Error('Invalid Message: Missing required attribute "to"'); - } - - const connection = this.findRecipient(message.to); - - if (!connection) { - throw new Error(`Connection for verkey ${recipientVerkey} not found!`); - } - - if (!connection.theirKey) { - throw new Error(`Connection with verkey ${connection.verkey} has no recipient keys.`); - } - - return createOutboundMessage(connection, message); - } - - public getRoutes() { - return this.routingTable; - } - - public findRecipient(recipientKey: Verkey) { - const connection = this.routingTable[recipientKey]; - - // TODO: function with find in name should now throw error when not found. - // It should either be called getRecipient and throw error - // or findRecipient and return null - if (!connection) { - throw new Error(`Routing entry for recipientKey ${recipientKey} does not exists.`); - } - - return connection; - } - - public saveRoute(recipientKey: Verkey, connection: ConnectionRecord) { - if (this.routingTable[recipientKey]) { - throw new Error(`Routing entry for recipientKey ${recipientKey} already exists.`); - } - - this.routingTable[recipientKey] = connection; - } - - public removeRoute(recipientKey: Verkey, connection: ConnectionRecord) { - const storedConnection = this.routingTable[recipientKey]; - - if (!storedConnection) { - throw new Error('Cannot remove non-existing routing entry'); - } - - if (storedConnection.id !== connection.id) { - throw new Error('Cannot remove routing entry for another connection'); - } - - delete this.routingTable[recipientKey]; - } -} - -export { ProviderRoutingService }; diff --git a/src/lib/modules/routing/services/ProvisioningService.ts b/src/lib/modules/routing/services/ProvisioningService.ts deleted file mode 100644 index 5647834731..0000000000 --- a/src/lib/modules/routing/services/ProvisioningService.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Verkey } from 'indy-sdk'; -import { Repository } from '../../../storage/Repository'; -import { ProvisioningRecord } from '../repository/ProvisioningRecord'; -import { isIndyError } from '../../../utils/indyError'; -import { AgentConfig } from '../../../agent/AgentConfig'; -import { Logger } from '../../../logger'; - -const UNIQUE_PROVISIONING_ID = 'UNIQUE_PROVISIONING_ID'; - -export class ProvisioningService { - private provisioningRepository: Repository; - private logger: Logger; - - public constructor(provisioningRepository: Repository, agentConfig: AgentConfig) { - this.provisioningRepository = provisioningRepository; - this.logger = agentConfig.logger; - } - - public async find(): Promise { - try { - const provisioningRecord = await this.provisioningRepository.find(UNIQUE_PROVISIONING_ID); - return provisioningRecord; - } catch (error) { - if (isIndyError(error, 'WalletItemNotFound')) { - this.logger.debug(`Provision record with id '${UNIQUE_PROVISIONING_ID}' not found.`, { - indyError: 'WalletItemNotFound', - }); - return null; - } else { - throw error; - } - } - } - - public async create({ mediatorConnectionId, mediatorPublicVerkey }: ProvisioningProps): Promise { - const provisioningRecord = new ProvisioningRecord({ - id: UNIQUE_PROVISIONING_ID, - mediatorConnectionId, - mediatorPublicVerkey, - }); - await this.provisioningRepository.save(provisioningRecord); - return provisioningRecord; - } -} - -interface ProvisioningProps { - mediatorConnectionId: string; - mediatorPublicVerkey: Verkey; -} diff --git a/src/lib/modules/routing/services/index.ts b/src/lib/modules/routing/services/index.ts deleted file mode 100644 index 7b89a0b3a6..0000000000 --- a/src/lib/modules/routing/services/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './ConsumerRoutingService'; -export * from './MessagePickupService'; -export * from './ProviderRoutingService'; -export * from './ProvisioningService'; diff --git a/src/lib/storage/BaseRecord.ts b/src/lib/storage/BaseRecord.ts deleted file mode 100644 index c01f9c6ea0..0000000000 --- a/src/lib/storage/BaseRecord.ts +++ /dev/null @@ -1,42 +0,0 @@ -export enum RecordType { - BaseRecord = 'BaseRecord', - ConnectionRecord = 'ConnectionRecord', - BasicMessageRecord = 'BasicMessageRecord', - ProvisioningRecord = 'ProvisioningRecord', - CredentialRecord = 'CredentialRecord', - ProofRecord = 'PresentationRecord', -} - -export type Tags = Record; - -export abstract class BaseRecord { - public createdAt: number; - public updatedAt?: number; - public id: string; - public tags: Tags; - - // Required because Javascript doesn't allow accessing static types - // like instance.static_member - public static readonly type: RecordType = RecordType.BaseRecord; - public readonly type = BaseRecord.type; - - public constructor(id: string, createdAt: number) { - this.id = id; - this.createdAt = createdAt; - this.tags = {}; - } - - public getValue(): string { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { id, tags, ...value } = this; - return JSON.stringify(value); - } - - public static fromPersistence(typeClass: { new (...args: unknown[]): T }, props: Record): T { - // eslint-disable-next-line - // @ts-ignore - const { value, ...rest } = props; - - return new typeClass({ ...JSON.parse(value), ...rest }); - } -} diff --git a/src/lib/storage/InMemoryMessageRepository.ts b/src/lib/storage/InMemoryMessageRepository.ts deleted file mode 100644 index bffab99f02..0000000000 --- a/src/lib/storage/InMemoryMessageRepository.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Verkey } from 'indy-sdk'; -import { MessageRepository } from './MessageRepository'; -import { WireMessage } from '../types'; - -export class InMemoryMessageRepository implements MessageRepository { - private messages: { [key: string]: WireMessage } = {}; - - public findByVerkey(theirKey: Verkey): WireMessage[] { - return this.messages[theirKey] ?? []; - } - - public deleteAllByVerkey(theirKey: Verkey): void { - this.messages[theirKey] = []; - } - - public save(theirKey: Verkey, payload: WireMessage) { - if (!this.messages[theirKey]) { - this.messages[theirKey] = []; - } - this.messages[theirKey].push(payload); - } -} diff --git a/src/lib/storage/IndyStorageService.test.ts b/src/lib/storage/IndyStorageService.test.ts deleted file mode 100644 index 5a1caba054..0000000000 --- a/src/lib/storage/IndyStorageService.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Repository } from './Repository'; -import { IndyStorageService } from './IndyStorageService'; -import { IndyWallet } from '../wallet/IndyWallet'; -import { BaseRecord, RecordType } from './BaseRecord'; -import { v4 as uuid } from 'uuid'; -import indy from 'indy-sdk'; -import { AgentConfig } from '../agent/AgentConfig'; - -interface TestRecordProps { - id?: string; - createdAt?: number; - tags: { [keys: string]: string }; - foo: string; -} - -class TestRecord extends BaseRecord { - public foo: string; - - public static readonly type: RecordType = RecordType.BaseRecord; - public readonly type = TestRecord.type; - - public constructor(props: TestRecordProps) { - super(props.id ?? uuid(), props.createdAt ?? Date.now()); - this.foo = props.foo; - this.tags = props.tags; - } -} - -describe('IndyStorageService', () => { - let wallet: IndyWallet; - let testRepository: Repository; - - beforeEach(async () => { - wallet = new IndyWallet( - new AgentConfig({ - indy, - label: 'test', - walletConfig: { id: 'testWallet' }, - walletCredentials: { key: 'asbdabsd' }, - }) - ); - const storageService = new IndyStorageService(wallet); - testRepository = new Repository(TestRecord, storageService); - await wallet.init(); - }); - - afterEach(async () => { - await wallet.close(); - await wallet.delete(); - }); - - const insertRecord = async () => { - const props = { - foo: 'bar', - tags: { myTag: 'foobar' }, - }; - const record = new TestRecord(props); - await testRepository.save(record); - return record; - }; - - test('it is able to save messages', async () => { - await insertRecord(); - }); - - test('does not change id, createdAt attributes', async () => { - const record = await insertRecord(); - const found = await testRepository.find(record.id); - expect(found.id).toEqual(record.id); - expect(found.createdAt).toEqual(record.createdAt); - }); - - test('it is able to get the record', async () => { - const record = await insertRecord(); - const found = await testRepository.find(record.id); - expect(found.id).toStrictEqual(record.id); - }); - - test('it is able to find all records', async () => { - for (let i = 0; i < 10; i++) { - const props = { - foo: `123123_${i}`, - tags: {}, - }; - const rec = new TestRecord(props); - await testRepository.save(rec); - } - - const records = await testRepository.findAll(); - expect(records.length).toStrictEqual(10); - }); - - test('it is able to update records', async () => { - const record = await insertRecord(); - record.tags = { ...record.tags, foo: 'bar' }; - record.foo = 'foobaz'; - await testRepository.update(record); - const got = await testRepository.find(record.id); - expect(got.foo).toStrictEqual(record.foo); - expect(got.tags).toStrictEqual(record.tags); - }); - - test('it is able to delete a record', async () => { - const record = await insertRecord(); - await testRepository.delete(record); - expect(async () => { - await testRepository.find(record.id); - }).rejects; - }); - - test('it is able to query a record', async () => { - await insertRecord(); - const result = await testRepository.findByQuery({ myTag: 'foobar' }); - expect(result.length).toBe(1); - }); -}); diff --git a/src/lib/storage/IndyStorageService.ts b/src/lib/storage/IndyStorageService.ts deleted file mode 100644 index 9a0e862272..0000000000 --- a/src/lib/storage/IndyStorageService.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { WalletQuery } from 'indy-sdk'; - -import { StorageService } from './StorageService'; -import { BaseRecord } from './BaseRecord'; -import { Wallet } from '../wallet/Wallet'; - -export class IndyStorageService implements StorageService { - private wallet: Wallet; - private static DEFAULT_QUERY_OPTIONS = { retrieveType: true, retrieveTags: true }; - - public constructor(wallet: Wallet) { - this.wallet = wallet; - } - - public async save(record: T) { - const { type, id, tags } = record; - const value = record.getValue(); - return this.wallet.addWalletRecord(type, id, value, tags); - } - - public async update(record: T): Promise { - const { type, id, tags } = record; - const value = record.getValue(); - await this.wallet.updateWalletRecordValue(type, id, value); - await this.wallet.updateWalletRecordTags(type, id, tags); - } - - public async delete(record: T) { - const { id, type } = record; - return this.wallet.deleteWalletRecord(type, id); - } - - public async find(typeClass: { new (...args: unknown[]): T }, id: string, type: string): Promise { - const record = await this.wallet.getWalletRecord(type, id, IndyStorageService.DEFAULT_QUERY_OPTIONS); - return BaseRecord.fromPersistence(typeClass, record); - } - - public async findAll(typeClass: { new (...args: unknown[]): T }, type: string): Promise { - const recordIterator = await this.wallet.search(type, {}, IndyStorageService.DEFAULT_QUERY_OPTIONS); - const records = []; - for await (const record of recordIterator) { - records.push(BaseRecord.fromPersistence(typeClass, record)); - } - return records; - } - - public async findByQuery( - typeClass: { new (...args: unknown[]): T }, - type: string, - query: WalletQuery - ): Promise { - const recordIterator = await this.wallet.search(type, query, IndyStorageService.DEFAULT_QUERY_OPTIONS); - const records = []; - for await (const record of recordIterator) { - records.push(BaseRecord.fromPersistence(typeClass, record)); - } - return records; - } -} diff --git a/src/lib/storage/MessageRepository.ts b/src/lib/storage/MessageRepository.ts deleted file mode 100644 index 4d36ec2603..0000000000 --- a/src/lib/storage/MessageRepository.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Verkey } from 'indy-sdk'; -import { WireMessage } from '../types'; - -export interface MessageRepository { - findByVerkey(verkey: Verkey): WireMessage[]; - deleteAllByVerkey(verkey: Verkey): void; - save(key: Verkey, payload: WireMessage): void; -} diff --git a/src/lib/storage/Repository.ts b/src/lib/storage/Repository.ts deleted file mode 100644 index 216cdaa31c..0000000000 --- a/src/lib/storage/Repository.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { WalletQuery } from 'indy-sdk'; - -import { BaseRecord, RecordType } from './BaseRecord'; -import { StorageService } from './StorageService'; - -export class Repository { - private storageService: StorageService; - private recordType: { new (...args: unknown[]): T; type: RecordType }; - - public constructor(recordType: { new (...args: any[]): T; type: RecordType }, storageService: StorageService) { - this.storageService = storageService; - this.recordType = recordType; - } - - public async save(record: T): Promise { - this.storageService.save(record); - } - - public async update(record: T): Promise { - return this.storageService.update(record); - } - - public async delete(record: T): Promise { - return this.storageService.delete(record); - } - - public async find(id: string): Promise { - return this.storageService.find(this.recordType, id, this.recordType.type); - } - - public async findAll(): Promise { - return this.storageService.findAll(this.recordType, this.recordType.type); - } - - public async findByQuery(query: WalletQuery): Promise { - return this.storageService.findByQuery(this.recordType, this.recordType.type, query); - } -} diff --git a/src/lib/storage/StorageService.ts b/src/lib/storage/StorageService.ts deleted file mode 100644 index 9b580f5f11..0000000000 --- a/src/lib/storage/StorageService.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { WalletQuery } from 'indy-sdk'; - -import { BaseRecord } from './BaseRecord'; - -export interface StorageService { - save(record: T): Promise; - - update(record: T): Promise; - - delete(record: T): Promise; - - find(typeClass: { new (...args: unknown[]): T }, id: string, type: string): Promise; - - findAll(typeClass: { new (...args: unknown[]): T }, type: string): Promise; - - findByQuery(typeClass: { new (...args: unknown[]): T }, type: string, query: WalletQuery): Promise; -} diff --git a/src/lib/transport/InboundTransporter.ts b/src/lib/transport/InboundTransporter.ts deleted file mode 100644 index 684198bd14..0000000000 --- a/src/lib/transport/InboundTransporter.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Agent } from '../agent/Agent'; - -export interface InboundTransporter { - start(agent: Agent): void; -} diff --git a/src/lib/transport/OutboundTransporter.ts b/src/lib/transport/OutboundTransporter.ts deleted file mode 100644 index 79784ed54b..0000000000 --- a/src/lib/transport/OutboundTransporter.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { OutboundPackage } from '../types'; - -export interface OutboundTransporter { - sendMessage(outboundPackage: OutboundPackage, receiveReply: boolean): Promise; -} diff --git a/src/lib/types.ts b/src/lib/types.ts deleted file mode 100644 index cd353bd394..0000000000 --- a/src/lib/types.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type Indy from 'indy-sdk'; -import type { Did, WalletConfig, WalletCredentials, Verkey } from 'indy-sdk'; -import { ConnectionRecord } from './modules/connections'; -import { AgentMessage } from './agent/AgentMessage'; -import { Logger } from './logger'; - -type $FixMe = any; - -export type WireMessage = $FixMe; - -export interface InitConfig { - host?: string; - port?: string | number; - endpoint?: string; - label: string; - publicDid?: Did; - publicDidSeed?: string; - mediatorUrl?: string; - walletConfig: WalletConfig; - walletCredentials: WalletCredentials; - autoAcceptConnections?: boolean; - genesisPath?: string; - poolName?: string; - logger?: Logger; - indy: typeof Indy; -} - -export interface UnpackedMessage { - '@type': string; - [key: string]: unknown; -} - -export interface UnpackedMessageContext { - message: UnpackedMessage; - sender_verkey?: Verkey; - recipient_verkey?: Verkey; -} - -export interface OutboundMessage { - connection: ConnectionRecord; - endpoint?: string; - payload: T; - recipientKeys: Verkey[]; - routingKeys: Verkey[]; - senderVk: Verkey | null; -} - -export interface OutboundPackage { - connection: ConnectionRecord; - payload: WireMessage; - endpoint?: string; -} - -export interface InboundConnection { - verkey: Verkey; - connection: ConnectionRecord; -} diff --git a/src/lib/utils/BufferEncoder.ts b/src/lib/utils/BufferEncoder.ts deleted file mode 100644 index 32afe8895b..0000000000 --- a/src/lib/utils/BufferEncoder.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { base64ToBase64URL } from './base64'; -import { Buffer } from './buffer'; - -export class BufferEncoder { - /** - * Encode buffer into base64 string. - * - * @param buffer the buffer to encode into base64 string - */ - public static toBase64(buffer: Buffer) { - return buffer.toString('base64'); - } - - /** - * Encode buffer into base64url string. - * - * @param buffer the buffer to encode into base64url string - */ - public static toBase64URL(buffer: Buffer) { - return base64ToBase64URL(BufferEncoder.toBase64(buffer)); - } - - /** - * Decode base64 string into buffer. Also supports base64url - * - * @param base64 the base64 or base64url string to decode into buffer format - */ - public static fromBase64(base64: string) { - return Buffer.from(base64, 'base64'); - } -} diff --git a/src/lib/utils/JsonEncoder.ts b/src/lib/utils/JsonEncoder.ts deleted file mode 100644 index 2cd238b646..0000000000 --- a/src/lib/utils/JsonEncoder.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { base64ToBase64URL } from './base64'; -import { Buffer } from './buffer'; - -export class JsonEncoder { - /** - * Encode json object into base64 string. - * - * @param json the json object to encode into base64 string - */ - public static toBase64(json: unknown) { - return JsonEncoder.toBuffer(json).toString('base64'); - } - - /** - * Encode json object into base64url string. - * - * @param json the json object to encode into base64url string - */ - public static toBase64URL(json: unknown) { - return base64ToBase64URL(JsonEncoder.toBase64(json)); - } - - /** - * Decode base64 string into json object. Also supports base64url - * - * @param base64 the base64 or base64url string to decode into json - */ - public static fromBase64(base64: string) { - return JsonEncoder.fromBuffer(Buffer.from(base64, 'base64')); - } - - /** - * Encode json object into string - * - * @param json the json object to encode into string - */ - public static toString(json: unknown) { - return JSON.stringify(json); - } - - /** - * Decode string into json object - * - * @param string the string to decode into json - */ - public static fromString(string: string) { - return JSON.parse(string); - } - - /** - * Encode json object into buffer - * - * @param json the json object to encode into buffer format - */ - public static toBuffer(json: unknown) { - return Buffer.from(JsonEncoder.toString(json)); - } - - /** - * Decode buffer into json object - * - * @param buffer the buffer to decode into json - */ - public static fromBuffer(buffer: Buffer) { - return JsonEncoder.fromString(buffer.toString('utf-8')); - } -} diff --git a/src/lib/utils/JsonTransformer.ts b/src/lib/utils/JsonTransformer.ts deleted file mode 100644 index 6a1086600c..0000000000 --- a/src/lib/utils/JsonTransformer.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { classToPlain, deserialize, plainToClass, serialize } from 'class-transformer'; - -export class JsonTransformer { - public static toJSON(classInstance: T) { - return classToPlain(classInstance, { - exposeDefaultValues: true, - }); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public static fromJSON(json: any, Class: { new (...args: any[]): T }): T { - return plainToClass(Class, json, { exposeDefaultValues: true }); - } - - public static serialize(classInstance: T): string { - return serialize(classInstance, { - exposeDefaultValues: true, - }); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public static deserialize(jsonString: string, Class: { new (...args: any[]): T }): T { - return deserialize(Class, jsonString, { exposeDefaultValues: true }); - } -} diff --git a/src/lib/utils/__tests__/BufferEncoder.test.ts b/src/lib/utils/__tests__/BufferEncoder.test.ts deleted file mode 100644 index d74931a38e..0000000000 --- a/src/lib/utils/__tests__/BufferEncoder.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { BufferEncoder } from '../BufferEncoder'; - -describe('BufferEncoder', () => { - const mockCredentialRequestBuffer = Buffer.from( - JSON.stringify({ - prover_did: 'did:sov:4xRwQoKEBcLMR3ni1uEVxo', - cred_def_id: 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:132:TAG', - blinded_ms: { - u: - '29923358933378594884016949116015048362197910313115517615886712002247549877203754161040662113193681067971700889005059954680562568543707874455462734614770719857476517583732118175472117258262243524371047244612027670795341517678404538774311254888125372395478117859977957781545203614422057872425388379038174161933077364160507888216751250173474993459002409808169413785534283242566104648944508528553665015907898791106766199966107619949762668272339931067050394002255637771035855365582098862561250576470504742036107864292062117797625825433248517924504550308968312780301031964645548333248088593015937359889688860141757860414963', - ur: null, - hidden_attributes: ['master_secret'], - committed_attributes: {}, - }, - blinded_ms_correctness_proof: { - c: '75472844799889714957212252604198654959564254049476575366093619008804723782477', - v_dash_cap: - '241658605435359439784797922191819649424225260343012968028448862434367856520024882808883591559121551472624638941147168471260306900890395947200746462759366134826652687587855065931934069125722728256802714393333799201395529881991611738749782316620019450872378759017790239991970563038638122599532027158669448906627074717578703201192531300159510207969537644782742047266895171574655304024820059682220314444862709893029720461916722147413329108444569762285168314593698192502335045295322386283022390355151395113112616277162501380456415321942055143111557003766356007622317275215637810918710682103087743057471347095588547791117729784209946546040216520440891760010822467637114796927573279512910382694631674566232457443287483928629', - m_caps: { - master_secret: - '10958352359087386063400811574253208318437641929870855399316173807950198557972017190892697250360879224208078813463868425727031560613450102005398401915135973584419891172590586358799', - }, - r_caps: {}, - }, - nonce: '1050445344368089902090762', - }) - ); - - describe('toBase64', () => { - test('encodes buffer to Base64 string', () => { - expect(BufferEncoder.toBase64(mockCredentialRequestBuffer)).toEqual( - 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0=' - ); - }); - }); - - describe('toBase64URL', () => { - test('encodes buffer to Base64URL string', () => { - expect(BufferEncoder.toBase64URL(mockCredentialRequestBuffer)).toEqual( - 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0' - ); - }); - }); - - describe('fromBase64', () => { - test('decodes Base64 string to buffer object', () => { - expect( - BufferEncoder.fromBase64( - 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0=' - ).equals(mockCredentialRequestBuffer) - ).toEqual(true); - }); - - test('decodes Base64URL string to buffer object', () => { - expect( - BufferEncoder.fromBase64( - 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0' - ).equals(mockCredentialRequestBuffer) - ).toEqual(true); - }); - }); -}); diff --git a/src/lib/utils/__tests__/JsonTransformer.test.ts b/src/lib/utils/__tests__/JsonTransformer.test.ts deleted file mode 100644 index 7c22f816c2..0000000000 --- a/src/lib/utils/__tests__/JsonTransformer.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { ConnectionInvitationMessage } from '../../modules/connections'; -import { JsonTransformer } from '../JsonTransformer'; - -describe('JsonTransformer', () => { - describe('toJSON', () => { - it('transforms class instance to JSON object', () => { - const invitation = new ConnectionInvitationMessage({ - did: 'did:sov:test1234', - id: 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', - label: 'test-label', - }); - - const json = { - '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation', - '@id': 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', - label: 'test-label', - did: 'did:sov:test1234', - }; - - expect(JsonTransformer.toJSON(invitation)).toEqual(json); - }); - }); - - describe('fromJSON', () => { - it('transforms JSON object to class instance', () => { - const json = { - '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation', - '@id': 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', - label: 'test-label', - did: 'did:sov:test1234', - }; - - const invitation = new ConnectionInvitationMessage({ - did: 'did:sov:test1234', - id: 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', - label: 'test-label', - }); - - expect(JsonTransformer.fromJSON(json, ConnectionInvitationMessage)).toEqual(invitation); - }); - }); - - describe('serialize', () => { - it('transforms class instance to JSON string', () => { - const invitation = new ConnectionInvitationMessage({ - did: 'did:sov:test1234', - id: 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', - label: 'test-label', - }); - - const jsonString = - '{"@type":"did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation","@id":"afe2867e-58c3-4a8d-85b2-23370dd9c9f0","label":"test-label","did":"did:sov:test1234"}'; - - expect(JsonTransformer.serialize(invitation)).toEqual(jsonString); - }); - }); - - describe('deserialize', () => { - it('transforms JSON string to class instance', () => { - const jsonString = - '{"@type":"did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation","@id":"afe2867e-58c3-4a8d-85b2-23370dd9c9f0","label":"test-label","did":"did:sov:test1234"}'; - - const invitation = new ConnectionInvitationMessage({ - did: 'did:sov:test1234', - id: 'afe2867e-58c3-4a8d-85b2-23370dd9c9f0', - label: 'test-label', - }); - - expect(JsonTransformer.deserialize(jsonString, ConnectionInvitationMessage)).toEqual(invitation); - }); - }); -}); diff --git a/src/lib/utils/__tests__/did.test.ts b/src/lib/utils/__tests__/did.test.ts deleted file mode 100644 index 56e15bc97d..0000000000 --- a/src/lib/utils/__tests__/did.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { isAbbreviatedVerkey, isDid, isDidIdentifier, isFullVerkey, isVerkey } from '../did'; - -const validAbbreviatedVerkeys = [ - '~PKAYz8Ev4yoQgr2LaMAWFx', - '~Soy1augaQrQYtNZRRHsikB', - '~BUF7uxYTxZ6qYdZ4G9e1Gi', - '~DbZ4gkBqhFRVsT5P7BJqyZ', - '~4zmNTdG78iYyMAQdEQLrf8', -]; - -const invalidAbbreviatedVerkeys = [ - '6YnVN5Qdb6mqimTRQcQmSXrHXKdTEdRn5YHZReezUTvt', - '8jG2Bim1HNSybCTdKBRppP4PCQSSijx1pBnreqsdo8JG', - 'ABUF7uxYTxZ6qYdZ4G9e1Gi', - '~Db3IgkBqhFRVsT5P7BJqyZ', - '~4zmNTlG78iYyMAQdEQLrf8', -]; - -const validFullVerkeys = [ - '6YnVN5Qdb6mqimTRQcQmSXrHXKdTEdRn5YHZReezUTvt', - '8jG2Bim1HNSybCTdKBRppP4PCQSSijx1pBnreqsdo8JG', - '9wMLhw9SSxtTUyosrndMbvWY4TtDbVvRnMtzG2NysniP', - '6m2XT39vivJ7tLSxNPM8siMnhYCZcdMxbkTcJDSzAQTu', - 'CAgL85iEecPNQMmxQ1hgbqczwq7SAerQ8RbWTRtC7SoK', - 'MqXmB7cTsTXqyxDPBbrgu5EPqw61kouK1qjMvnoPa96', -]; - -const invalidFullVerkeys = [ - '~PKAYz8Ev4yoQgr2LaMAWFx', - '~Soy1augaQrQYtNZRRHsikB', - '6YnVN5Qdb6mqimTRQcQmSXrHXKdTEdRn5YHZReezUTvta', - '6m2XT39vIvJ7tLSxNPM8siMnhYCZcdMxbkTcJDSzAQTu', - 'CAgL85iEecPNQMlxQ1hgbqczwq7SAerQ8RbWTRtC7SoK', -]; - -const invalidVerkeys = [ - '6YnVN5Qdb6mqimTIQcQmSXrHXKdTEdRn5YHZReezUTvta', - '6m2XT39vIvJ7tlSxNPM8siMnhYCZcdMxbkTcJDSzAQTu', - 'CAgL85iEecPNQMlxQ1hgbqczwq7SAerQ8RbWTRtC7SoK', - '6YnVN5Qdb6mqilTRQcQmSXrHXKdTEdRn5YHZReezUTvt', - '8jG2Bim1HNIybCTdKBRppP4PCQSSijx1pBnreqsdo8JG', - 'ABUF7uxYTxZ6qYdZ4G9e1Gi', - '~Db3IgkBqhFRVsT5P7BJqyZ', - '~4zmNTlG78IYyMAQdEQLrf8', - 'randomverkey', -]; - -const validDids = [ - 'did:indy:BBPoJqRKatdcfLEAFL7exC', - 'did:sov:N8NQHLtCKfPmWMgCSdfa7h', - 'did:random:FBSegXg6AsF8J73kx22gjk', - 'did:sov:8u2b8ZH6sHeWfvphyQuHCL', - 'did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a', - 'did:btcr:xyv2-xzpq-q9wa-p7t', -]; - -const invalidDids = [ - '6YnVN5Qdb6mqimTIQcQmSXrHXKdTEdRn5YHZReezUTvta', - 'did:BBPoJqRKatdcfLEAFL7exC', - 'sov:N8NQHLtCKfPmWMgCSdfa7h', - '8kyt-fzzq-qpqq-ljsc-5l', - 'did:test1:N8NQHLtCKfPmWMgCSdfa7h', - 'deid:ethr:9noxi4nL4SiJAsFcMLp2U4', -]; - -const validDidIdentifiers = [ - '8kyt-fzzq-qpqq-ljsc-5l', - 'fEMDp21GvaafC5hXLaLHf', - '9noxi4nL4SiJAsFcMLp2U4', - 'QdAJFDpbVoHYrUpNAMe3An', - 'B9Y3e8PUKrM1ShumWU36xW', - '0xf3beac30c498d9e26865f34fcaa57dbb935b0d74', -]; - -const invalidDidIdentifiers = [ - '6YnVN5Qdb6mqimTIQcQmSXrHXKdTEdRn5YHZReezUTvt/a', - 'did:BBPoJqRKatdcfLEAFL7exC', - 'sov:N8NQHLtCKfPmWMgCSdfa7h', - 'did:test1:N8NQHLtCKfPmWMgCSdfa7h', - 'deid:ethr:9noxi4nL4SiJAsFcMLp2U4', -]; - -describe('Utils | Did', () => { - describe('isAbbreviatedVerkey()', () => { - test.each(validAbbreviatedVerkeys)('returns true when valid abbreviated verkey "%s" is passed in', verkey => { - expect(isAbbreviatedVerkey(verkey)).toBe(true); - }); - - test.each(invalidAbbreviatedVerkeys)('returns false when invalid abbreviated verkey "%s" is passed in', verkey => { - expect(isAbbreviatedVerkey(verkey)).toBe(false); - }); - }); - - describe('isFullVerkey()', () => { - test.each(validFullVerkeys)('returns true when valid full verkey "%s" is passed in', verkey => { - expect(isFullVerkey(verkey)).toBe(true); - }); - - test.each(invalidFullVerkeys)('returns false when invalid full verkey "%s" is passed in', verkey => { - expect(isFullVerkey(verkey)).toBe(false); - }); - }); - - describe('isVerkey()', () => { - const validVerkeys = [...validAbbreviatedVerkeys, ...validFullVerkeys]; - - test.each(validVerkeys)('returns true when valid verkey "%s" is passed in', verkey => { - expect(isVerkey(verkey)).toBe(true); - }); - - test.each(invalidVerkeys)('returns false when invalid verkey "%s" is passed in', verkey => { - expect(isVerkey(verkey)).toBe(false); - }); - }); - - describe('isDid()', () => { - test.each(validDids)('returns true when valid did "%s" is passed in', did => { - expect(isDid(did)).toBe(true); - }); - - test.each(invalidDids)('returns false when invalid did "%s" is passed in', did => { - expect(isDid(did)).toBe(false); - }); - }); - - describe('isDidIdentifier()', () => { - test.each(validDidIdentifiers)('returns true when valid did identifier "%s" is passed in', didIdentifier => { - expect(isDidIdentifier(didIdentifier)).toBe(true); - }); - - test.each(invalidDidIdentifiers)('returns false when invalid did identifier "%s" is passed in', didIdentifier => { - expect(isDidIdentifier(didIdentifier)).toBe(false); - }); - }); -}); diff --git a/src/lib/utils/__tests__/indyError.test.ts b/src/lib/utils/__tests__/indyError.test.ts deleted file mode 100644 index 4ced1968f6..0000000000 --- a/src/lib/utils/__tests__/indyError.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { isIndyError } from '../indyError'; - -describe('isIndyError()', () => { - it('should return true when the name is "IndyError" and no errorName is passed', () => { - const error = { name: 'IndyError' }; - - expect(isIndyError(error)).toBe(true); - }); - - it('should return false when the name is not "IndyError"', () => { - const error = { name: 'IndyError2' }; - - expect(isIndyError(error)).toBe(false); - expect(isIndyError(error, 'WalletAlreadyExistsError')).toBe(false); - }); - - it('should return true when indyName matches the passed errorName', () => { - const error = { name: 'IndyError', indyName: 'WalletAlreadyExistsError' }; - - expect(isIndyError(error, 'WalletAlreadyExistsError')).toBe(true); - }); - - it('should return false when the indyName does not match the passes errorName', () => { - const error = { name: 'IndyError', indyName: 'WalletAlreadyExistsError' }; - - expect(isIndyError(error, 'DoesNotMatchError')).toBe(false); - }); - - // Below here are temporary until indy-sdk releases new version - it('should return true when the indyName is missing but the message contains a matching error code', () => { - const error = { name: 'IndyError', message: '212' }; - - expect(isIndyError(error, 'WalletItemNotFound')).toBe(true); - }); - - it('should return false when the indyName is missing and the message contains a valid but not matching error code', () => { - const error = { name: 'IndyError', message: '212' }; - - expect(isIndyError(error, 'DoesNotMatchError')).toBe(false); - }); - - it('should throw an error when the indyName is missing and the message contains an invalid error code', () => { - const error = { name: 'IndyError', message: '832882' }; - - expect(() => isIndyError(error, 'SomeNewErrorWeDoNotHave')).toThrowError( - 'Could not determine errorName of indyError 832882' - ); - }); -}); diff --git a/src/lib/utils/buffer.ts b/src/lib/utils/buffer.ts deleted file mode 100644 index 917e86f59a..0000000000 --- a/src/lib/utils/buffer.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Import and re-export buffer. In NodeJS native buffer -// library will be used. In RN buffer npm package will be used -import { Buffer } from 'buffer'; - -export { Buffer }; diff --git a/src/lib/utils/did.ts b/src/lib/utils/did.ts deleted file mode 100644 index aeef205c56..0000000000 --- a/src/lib/utils/did.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Based on DidUtils implementation in Aries Framework .NET - * @see: https://github.com/hyperledger/aries-framework-dotnet/blob/f90eaf9db8548f6fc831abea917e906201755763/src/Hyperledger.Aries/Utils/DidUtils.cs - * - * Some context about full verkeys versus abbreviated verkeys: - * A standard verkey is 32 bytes, and by default in Indy the DID is chosen as the first 16 bytes of that key, before base58 encoding. - * An abbreviated verkey replaces the first 16 bytes of the verkey with ~ when it matches the DID. - * - * When a full verkey is used to register on the ledger, this is stored as a full verkey on the ledger and also returned from the ledger as a full verkey. - * The same applies to an abbreviated verkey. If an abbreviated verkey is used to register on the ledger, this is stored as an abbreviated verkey on the ledger and also returned from the ledger as an abbreviated verkey. - * - * For this reason we need some methods to check whether verkeys are full or abbreviated, so we can align this with `indy.abbreviateVerkey` - * - * Aries Framework .NET also abbreviates verkey before sending to ledger: - * https://github.com/hyperledger/aries-framework-dotnet/blob/f90eaf9db8548f6fc831abea917e906201755763/src/Hyperledger.Aries/Ledger/DefaultLedgerService.cs#L139-L147 - */ - -export const FULL_VERKEY_REGEX = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/; -export const ABBREVIATED_VERKEY_REGEX = /^~[1-9A-HJ-NP-Za-km-z]{21,22}$/; -export const VERKEY_REGEX = new RegExp(`${FULL_VERKEY_REGEX.source}|${ABBREVIATED_VERKEY_REGEX.source}`); -export const DID_REGEX = /^did:([a-z]+):([a-zA-z\d]+)/; -export const DID_IDENTIFIER_REGEX = /^[a-zA-z\d-]+$/; - -/** - * Check a base58 encoded string against a regex expression to determine if it is a full valid verkey - * @param verkey Base58 encoded string representation of a verkey - * @return Boolean indicating if the string is a valid verkey - */ -export function isFullVerkey(verkey: string): boolean { - return FULL_VERKEY_REGEX.test(verkey); -} - -/** - * Check a base58 encoded string against a regex expression to determine if it is a valid abbreviated verkey - * @param verkey Base58 encoded string representation of an abbreviated verkey - * @returns Boolean indicating if the string is a valid abbreviated verkey - */ -export function isAbbreviatedVerkey(verkey: string): boolean { - return ABBREVIATED_VERKEY_REGEX.test(verkey); -} - -/** - * Check a base58 encoded string to determine if it is a valid verkey - * @param verkey Base58 encoded string representation of a verkey - * @returns Boolean indicating if the string is a valid verkey - */ -export function isVerkey(verkey: string): boolean { - return VERKEY_REGEX.test(verkey); -} - -/** - * Check a string to determine if it is a valid did - * @param did - * @return Boolean indicating if the string is a valid did - */ -export function isDid(did: string): boolean { - return DID_REGEX.test(did); -} - -/** - * Check a string to determine if it is a valid did identifier. - * @param identifier Did identifier. This is a did without the did:method part - * @return Boolean indicating if the string is a valid did identifier - */ -export function isDidIdentifier(identifier: string): boolean { - return DID_IDENTIFIER_REGEX.test(identifier); -} diff --git a/src/lib/utils/indyError.ts b/src/lib/utils/indyError.ts deleted file mode 100644 index 5c43efd9ba..0000000000 --- a/src/lib/utils/indyError.ts +++ /dev/null @@ -1,84 +0,0 @@ -export const indyErrors: { [key: number]: string } = { - 100: 'CommonInvalidParam1', - 101: 'CommonInvalidParam2', - 102: 'CommonInvalidParam3', - 103: 'CommonInvalidParam4', - 104: 'CommonInvalidParam5', - 105: 'CommonInvalidParam6', - 106: 'CommonInvalidParam7', - 107: 'CommonInvalidParam8', - 108: 'CommonInvalidParam9', - 109: 'CommonInvalidParam10', - 110: 'CommonInvalidParam11', - 111: 'CommonInvalidParam12', - 112: 'CommonInvalidState', - 113: 'CommonInvalidStructure', - 114: 'CommonIOError', - 115: 'CommonInvalidParam13', - 116: 'CommonInvalidParam14', - 200: 'WalletInvalidHandle', - 201: 'WalletUnknownTypeError', - 202: 'WalletTypeAlreadyRegisteredError', - 203: 'WalletAlreadyExistsError', - 204: 'WalletNotFoundError', - 205: 'WalletIncompatiblePoolError', - 206: 'WalletAlreadyOpenedError', - 207: 'WalletAccessFailed', - 208: 'WalletInputError', - 209: 'WalletDecodingError', - 210: 'WalletStorageError', - 211: 'WalletEncryptionError', - 212: 'WalletItemNotFound', - 213: 'WalletItemAlreadyExists', - 214: 'WalletQueryError', - 300: 'PoolLedgerNotCreatedError', - 301: 'PoolLedgerInvalidPoolHandle', - 302: 'PoolLedgerTerminated', - 303: 'LedgerNoConsensusError', - 304: 'LedgerInvalidTransaction', - 305: 'LedgerSecurityError', - 306: 'PoolLedgerConfigAlreadyExistsError', - 307: 'PoolLedgerTimeout', - 308: 'PoolIncompatibleProtocolVersion', - 309: 'LedgerNotFound', - 400: 'AnoncredsRevocationRegistryFullError', - 401: 'AnoncredsInvalidUserRevocId', - 404: 'AnoncredsMasterSecretDuplicateNameError', - 405: 'AnoncredsProofRejected', - 406: 'AnoncredsCredentialRevoked', - 407: 'AnoncredsCredDefAlreadyExistsError', - 500: 'UnknownCryptoTypeError', - 600: 'DidAlreadyExistsError', - 700: 'PaymentUnknownMethodError', - 701: 'PaymentIncompatibleMethodsError', - 702: 'PaymentInsufficientFundsError', - 703: 'PaymentSourceDoesNotExistError', - 704: 'PaymentOperationNotSupportedError', - 705: 'PaymentExtraFundsError', - 706: 'TransactionNotAllowedError', -}; - -export function isIndyError(error: any, errorName?: string) { - const indyError = error.name === 'IndyError'; - - // if no specific indy error name is passed - // or the error is no indy error - // we can already return - if (!indyError || !errorName) return indyError; - - // NodeJS Wrapper is missing some type names. When a type is missing it will - // only have the error code as string in the message field - // Until that is fixed we take that into account to make AFJ work with rn-indy-sdk - // See: https://github.com/AbsaOSS/rn-indy-sdk/pull/24 - // See: https://github.com/hyperledger/indy-sdk/pull/2283 - if (!error.indyName) { - const errorCode = Number(error.message); - if (!isNaN(errorCode) && indyErrors.hasOwnProperty(errorCode)) { - return errorName === indyErrors[errorCode]; - } - - throw new Error(`Could not determine errorName of indyError ${error.message}`); - } - - return error.indyName === errorName; -} diff --git a/src/lib/utils/timestamp.ts b/src/lib/utils/timestamp.ts deleted file mode 100644 index 7fd148c74b..0000000000 --- a/src/lib/utils/timestamp.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Question: Spec isn't clear about the endianness. Assumes big-endian here -// since ACA-Py uses big-endian. -export default function timestamp(): Uint8Array { - let time = Date.now(); - const bytes = []; - for (let i = 0; i < 8; i++) { - const byte = time & 0xff; - bytes.push(byte); - time = (time - byte) / 256; // Javascript right shift (>>>) only works on 32 bit integers - } - return Uint8Array.from(bytes).reverse(); -} diff --git a/src/lib/utils/transformers.ts b/src/lib/utils/transformers.ts deleted file mode 100644 index 6f01e2ad76..0000000000 --- a/src/lib/utils/transformers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Transform, TransformationType } from 'class-transformer'; -import { JsonTransformer } from './JsonTransformer'; - -/** - * Decorator that transforms json to and from corresponding record. - * - * @example - * class Example { - * RecordTransformer(Service) - * private services: Record; - * } - */ -export function RecordTransformer(Class: { new (...args: any[]): T }) { - return Transform(({ value, type }) => { - switch (type) { - case TransformationType.CLASS_TO_PLAIN: - return Object.entries(value).reduce( - (accumulator, [key, attribute]) => ({ ...accumulator, [key]: JsonTransformer.toJSON(attribute) }), - {} - ); - - case TransformationType.PLAIN_TO_CLASS: - return Object.entries(value).reduce( - (accumulator, [key, attribute]) => ({ ...accumulator, [key]: JsonTransformer.fromJSON(attribute, Class) }), - {} - ); - - default: - return value; - } - }); -} diff --git a/src/lib/utils/type.ts b/src/lib/utils/type.ts deleted file mode 100644 index 4a4634c2ff..0000000000 --- a/src/lib/utils/type.ts +++ /dev/null @@ -1 +0,0 @@ -export type Optional = Pick, K> & Omit; diff --git a/src/lib/utils/uuid.ts b/src/lib/utils/uuid.ts deleted file mode 100644 index 119ef2941a..0000000000 --- a/src/lib/utils/uuid.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { v4 } from 'uuid'; - -export function uuid() { - return v4(); -} diff --git a/src/lib/wallet/IndyWallet.ts b/src/lib/wallet/IndyWallet.ts deleted file mode 100644 index 4dce2194ee..0000000000 --- a/src/lib/wallet/IndyWallet.ts +++ /dev/null @@ -1,328 +0,0 @@ -import type { - Cred, - CredDef, - CredDefConfig, - CredDefId, - CredentialDefs, - CredentialId, - CredOffer, - CredReq, - CredReqMetadata, - CredRevocId, - CredValues, - Did, - DidConfig, - IndyCredential, - IndyProofRequest, - IndyRequestedCredentials, - LedgerRequest, - ProofCred, - RevocRegDelta, - RevStates, - Schema, - Schemas, - Verkey, - WalletConfig, - WalletCredentials, - WalletQuery, - WalletRecord, - WalletRecordOptions, - WalletSearchOptions, -} from 'indy-sdk'; -import type Indy from 'indy-sdk'; - -import { UnpackedMessageContext } from '../types'; -import { isIndyError } from '../utils/indyError'; -import { Wallet, DidInfo } from './Wallet'; -import { JsonEncoder } from '../utils/JsonEncoder'; -import { AgentConfig } from '../agent/AgentConfig'; -import { Logger } from '../logger'; - -export class IndyWallet implements Wallet { - private _walletHandle?: number; - private _masterSecretId?: string; - private walletConfig: WalletConfig; - private walletCredentials: WalletCredentials; - private logger: Logger; - private publicDidInfo: DidInfo | undefined; - private indy: typeof Indy; - - public constructor(agentConfig: AgentConfig) { - this.walletConfig = agentConfig.walletConfig; - this.walletCredentials = agentConfig.walletCredentials; - this.logger = agentConfig.logger; - this.indy = agentConfig.indy; - } - - public get publicDid() { - return this.publicDidInfo; - } - - private get walletHandle() { - if (!this._walletHandle) { - throw new Error('Wallet has not been initialized yet'); - } - - return this._walletHandle; - } - - private get masterSecretId() { - // In theory this is not possible if the wallet handle is available - if (!this._masterSecretId) { - throw new Error('Master secret has not been initialized yet'); - } - - return this._masterSecretId; - } - - public async init() { - this.logger.info(`Initializing wallet '${this.walletConfig.id}'`, this.walletConfig); - try { - await this.indy.createWallet(this.walletConfig, this.walletCredentials); - } catch (error) { - if (isIndyError(error, 'WalletAlreadyExistsError')) { - this.logger.debug(`Wallet '${this.walletConfig.id} already exists'`, { indyError: 'WalletAlreadyExistsError' }); - } else { - this.logger.error(`Error opening wallet ${this.walletConfig.id}`, { indyError: error.indyName, error }); - throw error; - } - } - - this._walletHandle = await this.indy.openWallet(this.walletConfig, this.walletCredentials); - - try { - this.logger.debug(`Creating master secret`); - this._masterSecretId = await this.indy.proverCreateMasterSecret(this.walletHandle, this.walletConfig.id); - } catch (error) { - if (isIndyError(error, 'AnoncredsMasterSecretDuplicateNameError')) { - // master secret id is the same as the master secret id passed in the create function - // so if it already exists we can just assign it. - this._masterSecretId = this.walletConfig.id; - this.logger.debug(`Master secret with id '${this.masterSecretId}' already exists`, { - indyError: 'AnoncredsMasterSecretDuplicateNameError', - }); - } else { - this.logger.error(`Error creating master secret with id ${this.walletConfig.id}`, { - indyError: error.indyName, - error, - }); - - throw error; - } - } - - this.logger.debug(`Wallet opened with handle: '${this.walletHandle}'`); - } - - public async initPublicDid(didConfig: DidConfig) { - const [did, verkey] = await this.createDid(didConfig); - this.publicDidInfo = { - did, - verkey, - }; - } - - public async createDid(didConfig?: DidConfig): Promise<[Did, Verkey]> { - return this.indy.createAndStoreMyDid(this.walletHandle, didConfig || {}); - } - - public async createCredentialDefinition( - issuerDid: string, - schema: Schema, - tag: string, - signatureType: string, - config?: CredDefConfig - ): Promise<[CredDefId, CredDef]> { - return this.indy.issuerCreateAndStoreCredentialDef( - this.walletHandle, - issuerDid, - schema, - tag, - signatureType, - config - ); - } - - public searchCredentialsForProofRequest(proofRequest: IndyProofRequest): Promise { - return this.indy.proverSearchCredentialsForProofReq(this.walletHandle, proofRequest, {} as any); - } - - public async createCredentialOffer(credDefId: CredDefId) { - return this.indy.issuerCreateCredentialOffer(this.walletHandle, credDefId); - } - - /** - * Retrieve the credentials that are available for an attribute referent in the proof request. - * - * @param proofRequest The proof request to retrieve the credentials for - * @param attributeReferent An attribute referent from the proof request to retrieve the credentials for - * @returns List of credentials that are available for building a proof for the given proof request - * - */ - public async getCredentialsForProofRequest( - proofRequest: IndyProofRequest, - attributeReferent: string - ): Promise { - const searchHandle = await this.indy.proverSearchCredentialsForProofReq(this.walletHandle, proofRequest, {} as any); - const credentialsJson = await this.indy.proverFetchCredentialsForProofReq(searchHandle, attributeReferent, 100); - // TODO: make the count, offset etc more flexible - await this.indy.proverCloseCredentialsSearchForProofReq(searchHandle); - return credentialsJson; - } - - public async createCredentialRequest( - proverDid: string, - offer: CredOffer, - credDef: CredDef - ): Promise<[CredReq, CredReqMetadata]> { - return this.indy.proverCreateCredentialReq(this.walletHandle, proverDid, offer, credDef, this.masterSecretId); - } - - public async createCredential( - credOffer: CredOffer, - credReq: CredReq, - credValues: CredValues - ): Promise<[Cred, CredRevocId, RevocRegDelta]> { - // TODO This is just dummy tails writer config to get dummy blob reader handle because revocations feature - // is not part of the credential issuance task. It needs to be implemented properly together with revocations - // feature implementation. - const tailsWriterConfig = { - base_dir: '', - uri_pattern: '', - }; - const blobReaderHandle = await this.indy.openBlobStorageReader('default', tailsWriterConfig); - - return this.indy.issuerCreateCredential(this.walletHandle, credOffer, credReq, credValues, null, blobReaderHandle); - } - - public async storeCredential( - credentialId: CredentialId, - credReqMetadata: CredReqMetadata, - cred: Cred, - credDef: CredDef - ) { - return this.indy.proverStoreCredential(this.walletHandle, credentialId, credReqMetadata, cred, credDef, null); - } - - public async getCredential(credentialId: CredentialId) { - return this.indy.proverGetCredential(this.walletHandle, credentialId); - } - - public async createProof( - proofRequest: IndyProofRequest, - requestedCredentials: IndyRequestedCredentials, - schemas: Schemas, - credentialDefs: CredentialDefs, - revStates: RevStates - ) { - return this.indy.proverCreateProof( - this.walletHandle, - proofRequest, - requestedCredentials, - this.masterSecretId, - schemas, - credentialDefs, - revStates - ); - } - - public verifyProof( - proofRequest: IndyProofRequest, - proof: Indy.IndyProof, - schemas: Schemas, - credentialDefs: CredentialDefs, - revRegsDefs: Indy.RevRegsDefs, - revRegs: RevStates - ): Promise { - return this.indy.verifierVerifyProof(proofRequest, proof, schemas, credentialDefs, revRegsDefs, revRegs); - } - - public async pack(payload: Record, recipientKeys: Verkey[], senderVk: Verkey): Promise { - const messageRaw = JsonEncoder.toBuffer(payload); - const packedMessage = await this.indy.packMessage(this.walletHandle, messageRaw, recipientKeys, senderVk); - return JsonEncoder.fromBuffer(packedMessage); - } - - public async unpack(messagePackage: JsonWebKey): Promise { - const unpackedMessageBuffer = await this.indy.unpackMessage( - this.walletHandle, - JsonEncoder.toBuffer(messagePackage) - ); - const unpackedMessage = JsonEncoder.fromBuffer(unpackedMessageBuffer); - return { - ...unpackedMessage, - message: JsonEncoder.fromString(unpackedMessage.message), - }; - } - - public async sign(data: Buffer, verkey: Verkey): Promise { - const signatureBuffer = await this.indy.cryptoSign(this.walletHandle, verkey, data); - - return signatureBuffer; - } - - public async verify(signerVerkey: Verkey, data: Buffer, signature: Buffer): Promise { - // check signature - const isValid = await this.indy.cryptoVerify(signerVerkey, data, signature); - - return isValid; - } - - public async close() { - return this.indy.closeWallet(this.walletHandle); - } - - public async delete() { - return this.indy.deleteWallet(this.walletConfig, this.walletCredentials); - } - - public async addWalletRecord(type: string, id: string, value: string, tags: Record) { - return this.indy.addWalletRecord(this.walletHandle, type, id, value, tags); - } - - public async updateWalletRecordValue(type: string, id: string, value: string) { - return this.indy.updateWalletRecordValue(this.walletHandle, type, id, value); - } - - public async updateWalletRecordTags(type: string, id: string, tags: Record) { - return this.indy.addWalletRecordTags(this.walletHandle, type, id, tags); - } - - public async deleteWalletRecord(type: string, id: string) { - return this.indy.deleteWalletRecord(this.walletHandle, type, id); - } - - public async search(type: string, query: WalletQuery, options: WalletSearchOptions) { - const sh: number = await this.indy.openWalletSearch(this.walletHandle, type, query, options); - const generator = async function* (indy: typeof Indy, wh: number) { - try { - while (true) { - // count should probably be exported as a config? - const recordSearch = await indy.fetchWalletSearchNextRecords(wh, sh, 10); - for (const record of recordSearch.records) { - yield record; - } - } - } catch (error) { - // pass - } finally { - await indy.closeWalletSearch(sh); - return; - } - }; - - return generator(this.indy, this.walletHandle); - } - - public getWalletRecord(type: string, id: string, options: WalletRecordOptions): Promise { - return this.indy.getWalletRecord(this.walletHandle, type, id, options); - } - - public signRequest(myDid: Did, request: LedgerRequest) { - return this.indy.signRequest(this.walletHandle, myDid, request); - } - - public async generateNonce() { - return this.indy.generateNonce(); - } -} diff --git a/src/lib/wallet/Wallet.test.ts b/src/lib/wallet/Wallet.test.ts deleted file mode 100644 index 9756b8b6d2..0000000000 --- a/src/lib/wallet/Wallet.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import indy from 'indy-sdk'; -import { IndyWallet } from './IndyWallet'; -import { AgentConfig } from '../agent/AgentConfig'; - -describe('Wallet', () => { - const wallet = new IndyWallet( - new AgentConfig({ - label: 'test', - walletConfig: { id: 'test_wallet' }, - walletCredentials: { key: 'test_key' }, - indy, - }) - ); - - test('initialize public did', async () => { - await wallet.init(); - - await wallet.initPublicDid({ seed: '00000000000000000000000Forward01' }); - - expect(wallet.publicDid).toEqual({ - did: 'DtWRdd6C5dN5vpcN6XRAvu', - verkey: '82RBSn3heLgXzZd74UsMC8Q8YRfEEhQoAM7LUqE6bevJ', - }); - }); - - afterEach(async () => { - await wallet.close(); - await wallet.delete(); - }); -}); diff --git a/src/lib/wallet/Wallet.ts b/src/lib/wallet/Wallet.ts deleted file mode 100644 index a70a1e1a32..0000000000 --- a/src/lib/wallet/Wallet.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { - DidConfig, - Did, - Verkey, - Schema, - CredDefConfig, - CredDefId, - CredDef, - CredOffer, - CredReq, - CredReqMetadata, - CredValues, - Cred, - CredRevocId, - RevocRegDelta, - IndyProofRequest, - IndyRequestedCredentials, - Schemas, - CredentialDefs, - RevStates, - IndyProof, - CredentialId, - IndyCredentialInfo, - WalletRecordOptions, - WalletRecord, - WalletQuery, - WalletSearchOptions, - LedgerRequest, - IndyCredential, - RevRegsDefs, -} from 'indy-sdk'; -import { UnpackedMessageContext } from '../types'; - -export interface Wallet { - publicDid: DidInfo | undefined; - - init(): Promise; - close(): Promise; - delete(): Promise; - initPublicDid(didConfig: DidConfig): Promise; - createDid(didConfig?: DidConfig): Promise<[Did, Verkey]>; - createCredentialDefinition( - issuerDid: Did, - schema: Schema, - tag: string, - signatureType: string, - config?: CredDefConfig - ): Promise<[CredDefId, CredDef]>; - createCredentialOffer(credDefId: CredDefId): Promise; - createCredentialRequest(proverDid: Did, offer: CredOffer, credDef: CredDef): Promise<[CredReq, CredReqMetadata]>; - createCredential( - credOffer: CredOffer, - credReq: CredReq, - credValues: CredValues - ): Promise<[Cred, CredRevocId, RevocRegDelta]>; - createProof( - proofRequest: IndyProofRequest, - requestedCredentials: IndyRequestedCredentials, - schemas: Schemas, - credentialDefs: CredentialDefs, - revStates: RevStates - ): Promise; - getCredentialsForProofRequest(proofRequest: IndyProofRequest, attributeReferent: string): Promise; - // TODO Method `verifyProof` does not have a dependency on `wallet`, we could eventually move it outside to another class. - verifyProof( - proofRequest: IndyProofRequest, - proof: IndyProof, - schemas: Schemas, - credentialDefs: CredentialDefs, - revRegsDefs: RevRegsDefs, - revRegs: RevStates - ): Promise; - storeCredential( - credentialId: CredentialId, - credReqMetadata: CredReqMetadata, - cred: Cred, - credDef: CredDef - ): Promise; - getCredential(credentialId: CredentialId): Promise; - pack(payload: Record, recipientKeys: Verkey[], senderVk: Verkey | null): Promise; - unpack(messagePackage: JsonWebKey): Promise; - sign(data: Buffer, verkey: Verkey): Promise; - verify(signerVerkey: Verkey, data: Buffer, signature: Buffer): Promise; - addWalletRecord(type: string, id: string, value: string, tags: Record): Promise; - updateWalletRecordValue(type: string, id: string, value: string): Promise; - updateWalletRecordTags(type: string, id: string, tags: Record): Promise; - deleteWalletRecord(type: string, id: string): Promise; - getWalletRecord(type: string, id: string, options: WalletRecordOptions): Promise; - search(type: string, query: WalletQuery, options: WalletSearchOptions): Promise>; - signRequest(myDid: Did, request: LedgerRequest): Promise; - generateNonce(): Promise; -} - -export interface DidInfo { - did: Did; - verkey: Verkey; -} diff --git a/src/samples/__tests__/e2e.test.ts b/src/samples/__tests__/e2e.test.ts deleted file mode 100644 index d1cfafaa5c..0000000000 --- a/src/samples/__tests__/e2e.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { Agent, InboundTransporter, OutboundTransporter } from '../../lib'; -import { OutboundPackage, InitConfig } from '../../lib/types'; -import { get, post } from '../http'; -import { sleep, toBeConnectedWith, waitForBasicMessage } from '../../lib/__tests__/helpers'; -import indy from 'indy-sdk'; -import testLogger from '../../lib/__tests__/logger'; - -expect.extend({ toBeConnectedWith }); - -const aliceConfig: InitConfig = { - label: 'e2e Alice', - mediatorUrl: 'http://localhost:3001', - walletConfig: { id: 'e2e-alice' }, - walletCredentials: { key: '00000000000000000000000000000Test01' }, - autoAcceptConnections: true, - logger: testLogger, - indy, -}; - -const bobConfig: InitConfig = { - label: 'e2e Bob', - mediatorUrl: 'http://localhost:3002', - walletConfig: { id: 'e2e-bob' }, - walletCredentials: { key: '00000000000000000000000000000Test02' }, - autoAcceptConnections: true, - logger: testLogger, - indy, -}; - -describe('with mediator', () => { - let aliceAgent: Agent; - let bobAgent: Agent; - let aliceAtAliceBobId: string; - - afterAll(async () => { - (aliceAgent.inboundTransporter as PollingInboundTransporter).stop = true; - (bobAgent.inboundTransporter as PollingInboundTransporter).stop = true; - - // Wait for messages to flush out - await new Promise(r => setTimeout(r, 1000)); - - await aliceAgent.closeAndDeleteWallet(); - await bobAgent.closeAndDeleteWallet(); - }); - - test('Alice and Bob make a connection with mediator', async () => { - const aliceAgentSender = new HttpOutboundTransporter(); - const aliceAgentReceiver = new PollingInboundTransporter(); - const bobAgentSender = new HttpOutboundTransporter(); - const bobAgentReceiver = new PollingInboundTransporter(); - - aliceAgent = new Agent(aliceConfig, aliceAgentReceiver, aliceAgentSender); - await aliceAgent.init(); - - bobAgent = new Agent(bobConfig, bobAgentReceiver, bobAgentSender); - await bobAgent.init(); - - const aliceInbound = aliceAgent.routing.getInboundConnection(); - const aliceInboundConnection = aliceInbound?.connection; - const aliceKeyAtAliceMediator = aliceInboundConnection?.verkey; - testLogger.test('aliceInboundConnection', aliceInboundConnection); - - const bobInbound = bobAgent.routing.getInboundConnection(); - const bobInboundConnection = bobInbound?.connection; - const bobKeyAtBobMediator = bobInboundConnection?.verkey; - testLogger.test('bobInboundConnection', bobInboundConnection); - - // TODO This endpoint currently exists at mediator only for the testing purpose. It returns mediator's part of the pairwise connection. - const mediatorConnectionAtAliceMediator = JSON.parse( - await get(`${aliceAgent.getMediatorUrl()}/api/connections/${aliceKeyAtAliceMediator}`) - ); - const mediatorConnectionAtBobMediator = JSON.parse( - await get(`${bobAgent.getMediatorUrl()}/api/connections/${bobKeyAtBobMediator}`) - ); - - testLogger.test('mediatorConnectionAtAliceMediator', mediatorConnectionAtAliceMediator); - testLogger.test('mediatorConnectionAtBobMediator', mediatorConnectionAtBobMediator); - - expect(aliceInboundConnection).toBeConnectedWith(mediatorConnectionAtAliceMediator); - expect(bobInboundConnection).toBeConnectedWith(mediatorConnectionAtBobMediator); - }); - - test('Alice and Bob make a connection via mediator', async () => { - // eslint-disable-next-line prefer-const - let { invitation, connectionRecord: aliceAgentConnection } = await aliceAgent.connections.createConnection(); - - let bobAgentConnection = await bobAgent.connections.receiveInvitation(invitation); - - aliceAgentConnection = await aliceAgent.connections.returnWhenIsConnected(aliceAgentConnection.id); - - bobAgentConnection = await bobAgent.connections.returnWhenIsConnected(bobAgentConnection.id); - - expect(aliceAgentConnection).toBeConnectedWith(bobAgentConnection); - expect(bobAgentConnection).toBeConnectedWith(aliceAgentConnection); - - // We save this verkey to send message via this connection in the following test - aliceAtAliceBobId = aliceAgentConnection.id; - }); - - test('Send a message from Alice to Bob via mediator', async () => { - // send message from Alice to Bob - const aliceConnectionAtAliceBob = await aliceAgent.connections.find(aliceAtAliceBobId); - if (!aliceConnectionAtAliceBob) { - throw new Error(`There is no connection for id ${aliceAtAliceBobId}`); - } - - testLogger.test('aliceConnectionAtAliceBob\n', aliceConnectionAtAliceBob); - - const message = 'hello, world'; - await aliceAgent.basicMessages.sendMessage(aliceConnectionAtAliceBob, message); - - const basicMessage = await waitForBasicMessage(bobAgent, { - content: message, - }); - - expect(basicMessage.content).toBe(message); - }); -}); - -class PollingInboundTransporter implements InboundTransporter { - public stop: boolean; - - public constructor() { - this.stop = false; - } - public async start(agent: Agent) { - await this.registerMediator(agent); - } - - public async registerMediator(agent: Agent) { - const mediatorUrl = agent.getMediatorUrl() || ''; - const mediatorInvitationUrl = await get(`${mediatorUrl}/invitation`); - const { verkey: mediatorVerkey } = JSON.parse(await get(`${mediatorUrl}/`)); - await agent.routing.provision({ verkey: mediatorVerkey, invitationUrl: mediatorInvitationUrl }); - this.pollDownloadMessages(agent); - } - - private pollDownloadMessages(agent: Agent) { - new Promise(async () => { - while (!this.stop) { - const downloadedMessages = await agent.routing.downloadMessages(); - const messages = [...downloadedMessages]; - testLogger.test('downloaded messages', messages); - while (messages && messages.length > 0) { - const message = messages.shift(); - await agent.receiveMessage(message); - } - - await sleep(1000); - } - }); - } -} - -class HttpOutboundTransporter implements OutboundTransporter { - public async sendMessage(outboundPackage: OutboundPackage, receiveReply: boolean) { - const { payload, endpoint } = outboundPackage; - - if (!endpoint) { - throw new Error(`Missing endpoint. I don't know how and where to send the message.`); - } - - testLogger.test(`Sending outbound message to connection ${outboundPackage.connection.id}`, outboundPackage.payload); - - if (receiveReply) { - const response = await post(`${endpoint}`, JSON.stringify(payload)); - const wireMessage = JSON.parse(response); - testLogger.test('received response', wireMessage); - return wireMessage; - } else { - await post(`${endpoint}`, JSON.stringify(payload)); - } - } -} diff --git a/src/samples/config.ts b/src/samples/config.ts deleted file mode 100644 index 8320acefe7..0000000000 --- a/src/samples/config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import indy from 'indy-sdk'; -import * as dotenv from 'dotenv'; -import { InitConfig } from '../lib/types'; -import { TestLogger } from '../lib/__tests__/logger'; -import { LogLevel } from '../lib/logger'; -dotenv.config(); - -const agentConfig: InitConfig = { - host: process.env.AGENT_HOST, - port: process.env.AGENT_PORT || 3000, - endpoint: process.env.AGENT_ENDPOINT, - label: process.env.AGENT_LABEL || '', - walletConfig: { id: process.env.WALLET_NAME || '' }, - walletCredentials: { key: process.env.WALLET_KEY || '' }, - publicDid: process.env.PUBLIC_DID || '', - publicDidSeed: process.env.PUBLIC_DID_SEED || '', - autoAcceptConnections: true, - logger: new TestLogger(LogLevel.debug), - indy, -}; - -export default agentConfig; diff --git a/src/samples/http.ts b/src/samples/http.ts deleted file mode 100644 index fd7d2aeefe..0000000000 --- a/src/samples/http.ts +++ /dev/null @@ -1,16 +0,0 @@ -import fetch, { BodyInit } from 'node-fetch'; -import testLogger from '../lib/__tests__/logger'; - -export async function get(url: string) { - testLogger.debug(`HTTP GET request: '${url}'`); - const response = await fetch(url); - testLogger.debug(`HTTP GET response status: ${response.status} - ${response.statusText}`); - return response.text(); -} - -export async function post(url: string, body: BodyInit) { - testLogger.debug(`HTTP POST request: '${url}'`); - const response = await fetch(url, { method: 'POST', body }); - testLogger.debug(`HTTP POST response status: ${response.status} - ${response.statusText}`); - return response.text(); -} diff --git a/src/samples/mediator.ts b/src/samples/mediator.ts deleted file mode 100644 index e9bdc3ab61..0000000000 --- a/src/samples/mediator.ts +++ /dev/null @@ -1,112 +0,0 @@ -import express, { Express } from 'express'; -import cors from 'cors'; -import config from './config'; -import testLogger from '../lib/__tests__/logger'; -import { Agent, InboundTransporter, OutboundTransporter } from '../lib'; -import { OutboundPackage } from '../lib/types'; -import { MessageRepository } from '../lib/storage/MessageRepository'; -import { InMemoryMessageRepository } from '../lib/storage/InMemoryMessageRepository'; - -class HttpInboundTransporter implements InboundTransporter { - private app: Express; - - public constructor(app: Express) { - this.app = app; - } - - public start(agent: Agent) { - this.app.post('/msg', async (req, res) => { - const message = req.body; - const packedMessage = JSON.parse(message); - const outboundMessage = await agent.receiveMessage(packedMessage); - if (outboundMessage) { - res.status(200).json(outboundMessage.payload).end(); - } else { - res.status(200).end(); - } - }); - } -} - -class StorageOutboundTransporter implements OutboundTransporter { - public messages: { [key: string]: any } = {}; - private messageRepository: MessageRepository; - - public constructor(messageRepository: MessageRepository) { - this.messageRepository = messageRepository; - } - - public async sendMessage(outboundPackage: OutboundPackage) { - const { connection, payload } = outboundPackage; - - if (!connection) { - throw new Error(`Missing connection. I don't know how and where to send the message.`); - } - - if (!connection.theirKey) { - throw new Error('Trying to save message without theirKey!'); - } - - testLogger.debug('Storing message', { connection, payload }); - - this.messageRepository.save(connection.theirKey, payload); - } -} - -const PORT = config.port; -const app = express(); - -app.use(cors()); -app.use( - express.text({ - type: ['application/ssi-agent-wire', 'text/plain'], - }) -); -app.set('json spaces', 2); - -const messageRepository = new InMemoryMessageRepository(); -const messageSender = new StorageOutboundTransporter(messageRepository); -const messageReceiver = new HttpInboundTransporter(app); -const agent = new Agent(config, messageReceiver, messageSender, messageRepository); - -app.get('/', async (req, res) => { - const agentDid = agent.publicDid; - res.send(agentDid); -}); - -// Create new invitation as inviter to invitee -app.get('/invitation', async (req, res) => { - const { invitation } = await agent.connections.createConnection(); - - res.send(invitation.toUrl()); -}); - -app.get('/api/connections/:verkey', async (req, res) => { - // TODO This endpoint is for testing purpose only. Return mediator connection by their verkey. - const verkey = req.params.verkey; - const connection = await agent.connections.findConnectionByTheirKey(verkey); - res.send(connection); -}); - -app.get('/api/connections', async (req, res) => { - // TODO This endpoint is for testing purpose only. Return mediator connection by their verkey. - const connections = await agent.connections.getAll(); - res.json(connections); -}); - -app.get('/api/routes', async (req, res) => { - // TODO This endpoint is for testing purpose only. Return mediator connection by their verkey. - const routes = agent.routing.getRoutingTable(); - res.send(routes); -}); - -app.get('/api/messages', async (req, res) => { - // TODO This endpoint is for testing purpose only. - res.send(messageSender.messages); -}); - -app.listen(PORT, async () => { - await agent.init(); - messageReceiver.start(agent); - testLogger.info(`Application started on port ${PORT}`); -}); diff --git a/tests/InMemoryStorageService.ts b/tests/InMemoryStorageService.ts new file mode 100644 index 0000000000..5dfe659491 --- /dev/null +++ b/tests/InMemoryStorageService.ts @@ -0,0 +1,224 @@ +import type { AgentContext } from '../packages/core/src/agent' +import type { BaseRecord, TagsBase } from '../packages/core/src/storage/BaseRecord' +import type { + StorageService, + BaseRecordConstructor, + Query, + QueryOptions, +} from '../packages/core/src/storage/StorageService' + +import { InMemoryWallet } from './InMemoryWallet' + +import { RecordNotFoundError, RecordDuplicateError, JsonTransformer, injectable } from '@credo-ts/core' + +interface StorageRecord { + value: Record + tags: Record + type: string + id: string +} + +interface InMemoryRecords { + [id: string]: StorageRecord +} + +interface ContextCorrelationIdToRecords { + [contextCorrelationId: string]: { + records: InMemoryRecords + creationDate: Date + } +} + +@injectable() +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class InMemoryStorageService = BaseRecord> + implements StorageService +{ + public contextCorrelationIdToRecords: ContextCorrelationIdToRecords = {} + + private recordToInstance(record: StorageRecord, recordClass: BaseRecordConstructor): T { + const instance = JsonTransformer.fromJSON(record.value, recordClass) + instance.id = record.id + instance.replaceTags(record.tags as TagsBase) + + return instance + } + + private getRecordsForContext(agentContext: AgentContext): InMemoryRecords { + const contextCorrelationId = agentContext.contextCorrelationId + + if (!this.contextCorrelationIdToRecords[contextCorrelationId]) { + this.contextCorrelationIdToRecords[contextCorrelationId] = { + records: {}, + creationDate: new Date(), + } + } else if (agentContext.wallet instanceof InMemoryWallet && agentContext.wallet.activeWalletId) { + const walletCreationDate = agentContext.wallet.inMemoryWallets[agentContext.wallet.activeWalletId].creationDate + const storageCreationDate = this.contextCorrelationIdToRecords[contextCorrelationId].creationDate + + // If the storage was created before the wallet, it means the wallet has been deleted in the meantime + // and thus we need to recreate the storage as we don't want to serve records from the previous wallet + // FIXME: this is a flaw in our wallet/storage model. I think wallet should be for keys, and storage + // for records and you can create them separately. But that's a bigger change. + if (storageCreationDate < walletCreationDate) { + this.contextCorrelationIdToRecords[contextCorrelationId] = { + records: {}, + creationDate: new Date(), + } + } + } + + return this.contextCorrelationIdToRecords[contextCorrelationId].records + } + + /** @inheritDoc */ + public async save(agentContext: AgentContext, record: T) { + record.updatedAt = new Date() + const value = JsonTransformer.toJSON(record) + + if (this.getRecordsForContext(agentContext)[record.id]) { + throw new RecordDuplicateError(`Record with id ${record.id} already exists`, { recordType: record.type }) + } + + this.getRecordsForContext(agentContext)[record.id] = { + value, + id: record.id, + type: record.type, + tags: record.getTags(), + } + } + + /** @inheritDoc */ + public async update(agentContext: AgentContext, record: T): Promise { + record.updatedAt = new Date() + const value = JsonTransformer.toJSON(record) + delete value._tags + + if (!this.getRecordsForContext(agentContext)[record.id]) { + throw new RecordNotFoundError(`record with id ${record.id} not found.`, { + recordType: record.type, + }) + } + + this.getRecordsForContext(agentContext)[record.id] = { + value, + id: record.id, + type: record.type, + tags: record.getTags(), + } + } + + /** @inheritDoc */ + public async delete(agentContext: AgentContext, record: T) { + if (!this.getRecordsForContext(agentContext)[record.id]) { + throw new RecordNotFoundError(`record with id ${record.id} not found.`, { + recordType: record.type, + }) + } + + delete this.getRecordsForContext(agentContext)[record.id] + } + + /** @inheritDoc */ + public async deleteById( + agentContext: AgentContext, + recordClass: BaseRecordConstructor, + id: string + ): Promise { + if (!this.getRecordsForContext(agentContext)[id]) { + throw new RecordNotFoundError(`record with id ${id} not found.`, { + recordType: recordClass.type, + }) + } + + delete this.getRecordsForContext(agentContext)[id] + } + + /** @inheritDoc */ + public async getById(agentContext: AgentContext, recordClass: BaseRecordConstructor, id: string): Promise { + const record = this.getRecordsForContext(agentContext)[id] + + if (!record) { + throw new RecordNotFoundError(`record with id ${id} not found.`, { + recordType: recordClass.type, + }) + } + + return this.recordToInstance(record, recordClass) + } + + /** @inheritDoc */ + public async getAll(agentContext: AgentContext, recordClass: BaseRecordConstructor): Promise { + const records = Object.values(this.getRecordsForContext(agentContext)) + .filter((record) => record.type === recordClass.type) + .map((record) => this.recordToInstance(record, recordClass)) + + return records + } + + /** @inheritDoc */ + public async findByQuery( + agentContext: AgentContext, + recordClass: BaseRecordConstructor, + query: Query, + queryOptions?: QueryOptions + ): Promise { + const { offset = 0, limit } = queryOptions || {} + + const allRecords = Object.values(this.getRecordsForContext(agentContext)) + .filter((record) => record.type === recordClass.type) + .filter((record) => filterByQuery(record, query)) + + const slicedRecords = limit !== undefined ? allRecords.slice(offset, offset + limit) : allRecords.slice(offset) + + return slicedRecords.map((record) => this.recordToInstance(record, recordClass)) + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function filterByQuery>(record: StorageRecord, query: Query) { + const { $and, $or, $not, ...restQuery } = query + + if ($not) { + throw new Error('$not query not supported in in memory storage') + } + + // Top level query + if (!matchSimpleQuery(record, restQuery)) return false + + // All $and queries MUST match + if ($and) { + const allAndMatch = ($and as Query[]).every((and) => filterByQuery(record, and)) + if (!allAndMatch) return false + } + + // Only one $or queries has to match + if ($or) { + const oneOrMatch = ($or as Query[]).some((or) => filterByQuery(record, or)) + if (!oneOrMatch) return false + } + + return true +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function matchSimpleQuery>(record: StorageRecord, query: Query) { + const tags = record.tags as TagsBase + + for (const [key, value] of Object.entries(query)) { + // We don't query for value undefined, the value should be null in that case + if (value === undefined) continue + + // TODO: support null + if (Array.isArray(value)) { + const tagValue = tags[key] + if (!Array.isArray(tagValue) || !value.every((v) => tagValue.includes(v))) { + return false + } + } else if (tags[key] !== value) { + return false + } + } + + return true +} diff --git a/tests/InMemoryWallet.ts b/tests/InMemoryWallet.ts new file mode 100644 index 0000000000..effe1fa215 --- /dev/null +++ b/tests/InMemoryWallet.ts @@ -0,0 +1,347 @@ +import type { + EncryptedMessage, + WalletConfig, + WalletCreateKeyOptions, + WalletSignOptions, + UnpackedMessageContext, + WalletVerifyOptions, + Wallet, +} from '@credo-ts/core' + +import { CryptoBox, Store, Key as AskarKey, keyAlgFromString } from '@hyperledger/aries-askar-nodejs' +import BigNumber from 'bn.js' + +import { didcommV1Pack, didcommV1Unpack } from '../packages/askar/src/wallet/didcommV1' + +import { + JsonEncoder, + WalletNotFoundError, + injectable, + isValidSeed, + isValidPrivateKey, + KeyType, + Buffer, + CredoError, + WalletError, + Key, + TypedArrayEncoder, +} from '@credo-ts/core' + +const inMemoryWallets: InMemoryWallets = {} + +const isError = (error: unknown): error is Error => error instanceof Error + +interface InMemoryKey { + publicKeyBytes: Uint8Array + secretKeyBytes: Uint8Array + keyType: KeyType +} + +interface InMemoryKeys { + [id: string]: InMemoryKey +} + +interface InMemoryWallets { + [id: string]: { + keys: InMemoryKeys + creationDate: Date + } +} + +@injectable() +export class InMemoryWallet implements Wallet { + // activeWalletId can be set even if wallet is closed. So make sure to also look at + // isInitialized to see if the wallet is actually open + public activeWalletId?: string + + public get inMemoryWallets() { + return inMemoryWallets + } + /** + * Abstract methods that need to be implemented by subclasses + */ + public isInitialized = false + public isProvisioned = false + + public get supportedKeyTypes() { + return [KeyType.Ed25519, KeyType.P256] + } + + private getInMemoryKeys(): InMemoryKeys { + if (!this.activeWalletId || !this.isInitialized) { + throw new WalletError('No active wallet') + } + + if (!this.inMemoryWallets[this.activeWalletId]) { + throw new WalletError('wallet does not exist') + } + + return this.inMemoryWallets[this.activeWalletId].keys + } + + public async create(walletConfig: WalletConfig) { + if (this.inMemoryWallets[walletConfig.id]) { + throw new WalletError('Wallet already exists') + } + + this.inMemoryWallets[walletConfig.id] = { + keys: {}, + creationDate: new Date(), + } + } + + public async createAndOpen(walletConfig: WalletConfig) { + await this.create(walletConfig) + await this.open(walletConfig) + } + + public async open(walletConfig: WalletConfig) { + if (this.isInitialized) { + throw new WalletError('A wallet is already open') + } + + if (!this.inMemoryWallets[walletConfig.id]) { + throw new WalletNotFoundError('Wallet does not exist', { walletType: 'InMemoryWallet' }) + } + + this.activeWalletId = walletConfig.id + this.isProvisioned = true + this.isInitialized = true + } + + public rotateKey(): Promise { + throw new Error('Method not implemented.') + } + + public async close() { + this.isInitialized = false + } + + public async delete() { + if (!this.activeWalletId) { + throw new WalletError('wallet is not provisioned') + } + + delete this.inMemoryWallets[this.activeWalletId] + this.activeWalletId = undefined + this.isProvisioned = false + } + + public async export() { + throw new Error('Method not implemented.') + } + + public async import() { + throw new Error('Method not implemented.') + } + + public async dispose() { + this.isInitialized = false + } + + /** + * Create a key with an optional seed and keyType. + * The keypair is also automatically stored in the wallet afterwards + */ + public async createKey({ seed, privateKey, keyType }: WalletCreateKeyOptions): Promise { + try { + if (seed && privateKey) { + throw new WalletError('Only one of seed and privateKey can be set') + } + + if (seed && !isValidSeed(seed, keyType)) { + throw new WalletError('Invalid seed provided') + } + + if (privateKey && !isValidPrivateKey(privateKey, keyType)) { + throw new WalletError('Invalid private key provided') + } + + if (!this.supportedKeyTypes.includes(keyType)) { + throw new WalletError(`Unsupported key type: '${keyType}'`) + } + + const algorithm = keyAlgFromString(keyType) + + // Create key + let key: AskarKey | undefined + try { + key = privateKey + ? AskarKey.fromSecretBytes({ secretKey: privateKey, algorithm }) + : seed + ? AskarKey.fromSeed({ seed, algorithm }) + : AskarKey.generate(algorithm) + + const keyPublicBytes = key.publicBytes + // Store key + this.getInMemoryKeys()[TypedArrayEncoder.toBase58(keyPublicBytes)] = { + publicKeyBytes: keyPublicBytes, + secretKeyBytes: key.secretBytes, + keyType, + } + + return Key.fromPublicKey(keyPublicBytes, keyType) + } finally { + key?.handle.free() + } + } catch (error) { + // If already instance of `WalletError`, re-throw + if (error instanceof WalletError) throw error + + if (!isError(error)) { + throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError(`Error creating key with key type '${keyType}': ${error.message}`, { cause: error }) + } + } + + /** + * sign a Buffer with an instance of a Key class + * + * @param data Buffer The data that needs to be signed + * @param key Key The key that is used to sign the data + * + * @returns A signature for the data + */ + public async sign({ data, key }: WalletSignOptions): Promise { + const inMemoryKey = this.getInMemoryKeys()[key.publicKeyBase58] + if (!inMemoryKey) { + throw new WalletError(`Key not found in wallet`) + } + + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`Currently not supporting signing of multiple messages`) + } + + let askarKey: AskarKey | undefined + try { + const inMemoryKey = this.getInMemoryKeys()[key.publicKeyBase58] + askarKey = AskarKey.fromSecretBytes({ + algorithm: keyAlgFromString(inMemoryKey.keyType), + secretKey: inMemoryKey.secretKeyBytes, + }) + + const signed = askarKey.signMessage({ message: data as Buffer }) + + return Buffer.from(signed) + } finally { + askarKey?.handle.free() + } + } + + /** + * Verify the signature with the data and the used key + * + * @param data Buffer The data that has to be confirmed to be signed + * @param key Key The key that was used in the signing process + * @param signature Buffer The signature that was created by the signing process + * + * @returns A boolean whether the signature was created with the supplied data and key + * + * @throws {WalletError} When it could not do the verification + * @throws {WalletError} When an unsupported keytype is used + */ + public async verify({ data, key, signature }: WalletVerifyOptions): Promise { + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`Currently not supporting signing of multiple messages`) + } + + let askarKey: AskarKey | undefined + try { + askarKey = AskarKey.fromPublicBytes({ + algorithm: keyAlgFromString(key.keyType), + publicKey: key.publicKey, + }) + return askarKey.verifySignature({ message: data as Buffer, signature }) + } finally { + askarKey?.handle.free() + } + } + + /** + * Pack a message using DIDComm V1 algorithm + * + * @param payload message to send + * @param recipientKeys array containing recipient keys in base58 + * @param senderVerkey sender key in base58 + * @returns JWE Envelope to send + */ + public async pack( + payload: Record, + recipientKeys: string[], + senderVerkey?: string // in base58 + ): Promise { + const senderKey = senderVerkey ? this.getInMemoryKeys()[senderVerkey] : undefined + + if (senderVerkey && !senderKey) { + throw new WalletError(`Sender key not found`) + } + + const askarSenderKey = senderKey + ? AskarKey.fromSecretBytes({ + algorithm: keyAlgFromString(senderKey.keyType), + secretKey: senderKey.secretKeyBytes, + }) + : undefined + + try { + const envelope = didcommV1Pack(payload, recipientKeys, askarSenderKey) + return envelope + } finally { + askarSenderKey?.handle.free() + } + } + + /** + * Unpacks a JWE Envelope coded using DIDComm V1 algorithm + * + * @param messagePackage JWE Envelope + * @returns UnpackedMessageContext with plain text message, sender key and recipient key + */ + public async unpack(messagePackage: EncryptedMessage): Promise { + const protectedJson = JsonEncoder.fromBase64(messagePackage.protected) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recipientKids: string[] = protectedJson.recipients.map((r: any) => r.header.kid) + + for (const recipientKid of recipientKids) { + const recipientKey = this.getInMemoryKeys()[recipientKid] + const recipientAskarKey = recipientKey + ? AskarKey.fromSecretBytes({ + algorithm: keyAlgFromString(recipientKey.keyType), + secretKey: recipientKey.secretKeyBytes, + }) + : undefined + try { + if (recipientAskarKey) { + const unpacked = didcommV1Unpack(messagePackage, recipientAskarKey) + return unpacked + } + } finally { + recipientAskarKey?.handle.free() + } + } + + throw new WalletError('No corresponding recipient key found') + } + + public async generateNonce(): Promise { + try { + // generate an 80-bit nonce suitable for AnonCreds proofs + const nonce = CryptoBox.randomNonce().slice(0, 10) + return new BigNumber(nonce).toString() + } catch (error) { + if (!isError(error)) { + throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError('Error generating nonce', { cause: error }) + } + } + + public async generateWalletKey() { + try { + return Store.generateRawKey() + } catch (error) { + throw new WalletError('Error generating wallet key', { cause: error }) + } + } +} diff --git a/tests/InMemoryWalletModule.ts b/tests/InMemoryWalletModule.ts new file mode 100644 index 0000000000..a43a903dcd --- /dev/null +++ b/tests/InMemoryWalletModule.ts @@ -0,0 +1,22 @@ +import type { DependencyManager, Module } from '@credo-ts/core' + +import { InMemoryStorageService } from './InMemoryStorageService' +import { InMemoryWallet } from './InMemoryWallet' + +import { CredoError, InjectionSymbols } from '@credo-ts/core' + +export class InMemoryWalletModule implements Module { + public register(dependencyManager: DependencyManager) { + if (dependencyManager.isRegistered(InjectionSymbols.Wallet)) { + throw new CredoError('There is an instance of Wallet already registered') + } else { + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, InMemoryWallet) + } + + if (dependencyManager.isRegistered(InjectionSymbols.StorageService)) { + throw new CredoError('There is an instance of StorageService already registered') + } else { + dependencyManager.registerSingleton(InjectionSymbols.StorageService, InMemoryStorageService) + } + } +} diff --git a/tests/e2e-askar-indy-vdr-anoncreds-rs.e2e.test.ts b/tests/e2e-askar-indy-vdr-anoncreds-rs.e2e.test.ts new file mode 100644 index 0000000000..7ea2f8aa5c --- /dev/null +++ b/tests/e2e-askar-indy-vdr-anoncreds-rs.e2e.test.ts @@ -0,0 +1,114 @@ +import type { SubjectMessage } from './transport/SubjectInboundTransport' +import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/anoncredsSetup' + +import { Subject } from 'rxjs' + +import { getAnonCredsModules } from '../packages/anoncreds/tests/anoncredsSetup' +import { askarModule } from '../packages/askar/tests/helpers' +import { getAgentOptions } from '../packages/core/tests/helpers' + +import { e2eTest } from './e2e-test' +import { SubjectInboundTransport } from './transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from './transport/SubjectOutboundTransport' + +import { + Agent, + AutoAcceptCredential, + MediatorModule, + MediatorPickupStrategy, + MediationRecipientModule, +} from '@credo-ts/core' + +const recipientAgentOptions = getAgentOptions( + 'E2E Askar Subject Recipient', + {}, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + askar: askarModule, + } +) +const mediatorAgentOptions = getAgentOptions( + 'E2E Askar Subject Mediator', + { + endpoints: ['rxjs:mediator'], + }, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediator: new MediatorModule({ autoAcceptMediationRequests: true }), + askar: askarModule, + } +) +const senderAgentOptions = getAgentOptions( + 'E2E Askar Subject Sender', + { + endpoints: ['rxjs:sender'], + }, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorPollingInterval: 1000, + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + askar: askarModule, + } +) + +describe('E2E Askar-AnonCredsRS-IndyVDR Subject tests', () => { + let recipientAgent: AnonCredsTestsAgent + let mediatorAgent: AnonCredsTestsAgent + let senderAgent: AnonCredsTestsAgent + + beforeEach(async () => { + recipientAgent = new Agent(recipientAgentOptions) as unknown as AnonCredsTestsAgent + mediatorAgent = new Agent(mediatorAgentOptions) as unknown as AnonCredsTestsAgent + senderAgent = new Agent(senderAgentOptions) as unknown as AnonCredsTestsAgent + }) + + afterEach(async () => { + await recipientAgent.shutdown() + await recipientAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + await senderAgent.shutdown() + await senderAgent.wallet.delete() + }) + + test('Full Subject flow (connect, request mediation, issue, verify)', async () => { + const mediatorMessages = new Subject() + const senderMessages = new Subject() + + const subjectMap = { + 'rxjs:mediator': mediatorMessages, + 'rxjs:sender': senderMessages, + } + + // Recipient Setup + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await recipientAgent.initialize() + + // Mediator Setup + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + await mediatorAgent.initialize() + + // Sender Setup + senderAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + senderAgent.registerInboundTransport(new SubjectInboundTransport(senderMessages)) + await senderAgent.initialize() + + await e2eTest({ + mediatorAgent, + senderAgent, + recipientAgent, + }) + }) +}) diff --git a/tests/e2e-http.e2e.test.ts b/tests/e2e-http.e2e.test.ts new file mode 100644 index 0000000000..8ce820697b --- /dev/null +++ b/tests/e2e-http.e2e.test.ts @@ -0,0 +1,100 @@ +import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/anoncredsSetup' + +import { getAnonCredsModules } from '../packages/anoncreds/tests/anoncredsSetup' +import { getInMemoryAgentOptions } from '../packages/core/tests/helpers' + +import { e2eTest } from './e2e-test' + +import { + HttpOutboundTransport, + Agent, + AutoAcceptCredential, + MediatorPickupStrategy, + MediationRecipientModule, + MediatorModule, +} from '@credo-ts/core' +import { HttpInboundTransport } from '@credo-ts/node' + +const recipientAgentOptions = getInMemoryAgentOptions( + 'E2E HTTP Recipient', + {}, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorPollingInterval: 500, + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + } +) + +const mediatorPort = 3000 +const mediatorAgentOptions = getInMemoryAgentOptions( + 'E2E HTTP Mediator', + { + endpoints: [`http://localhost:${mediatorPort}`], + }, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediator: new MediatorModule({ + autoAcceptMediationRequests: true, + }), + } +) + +const senderPort = 3001 +const senderAgentOptions = getInMemoryAgentOptions( + 'E2E HTTP Sender', + { + endpoints: [`http://localhost:${senderPort}`], + }, + getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }) +) + +describe('E2E HTTP tests', () => { + let recipientAgent: AnonCredsTestsAgent + let mediatorAgent: AnonCredsTestsAgent + let senderAgent: AnonCredsTestsAgent + + beforeEach(async () => { + recipientAgent = new Agent(recipientAgentOptions) as AnonCredsTestsAgent + mediatorAgent = new Agent(mediatorAgentOptions) as AnonCredsTestsAgent + senderAgent = new Agent(senderAgentOptions) as AnonCredsTestsAgent + }) + + afterEach(async () => { + await recipientAgent.shutdown() + await recipientAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + await senderAgent.shutdown() + await senderAgent.wallet.delete() + }) + + test('Full HTTP flow (connect, request mediation, issue, verify)', async () => { + // Recipient Setup + recipientAgent.registerOutboundTransport(new HttpOutboundTransport()) + await recipientAgent.initialize() + + // Mediator Setup + mediatorAgent.registerInboundTransport(new HttpInboundTransport({ port: mediatorPort })) + mediatorAgent.registerOutboundTransport(new HttpOutboundTransport()) + await mediatorAgent.initialize() + + // Sender Setup + senderAgent.registerInboundTransport(new HttpInboundTransport({ port: senderPort })) + senderAgent.registerOutboundTransport(new HttpOutboundTransport()) + await senderAgent.initialize() + + await e2eTest({ + mediatorAgent, + senderAgent, + recipientAgent, + }) + }) +}) diff --git a/tests/e2e-subject.e2e.test.ts b/tests/e2e-subject.e2e.test.ts new file mode 100644 index 0000000000..fe04fbe9ff --- /dev/null +++ b/tests/e2e-subject.e2e.test.ts @@ -0,0 +1,110 @@ +import type { SubjectMessage } from './transport/SubjectInboundTransport' +import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/anoncredsSetup' + +import { Subject } from 'rxjs' + +import { getAnonCredsModules } from '../packages/anoncreds/tests/anoncredsSetup' +import { getInMemoryAgentOptions } from '../packages/core/tests/helpers' + +import { e2eTest } from './e2e-test' +import { SubjectInboundTransport } from './transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from './transport/SubjectOutboundTransport' + +import { + Agent, + AutoAcceptCredential, + MediatorModule, + MediatorPickupStrategy, + MediationRecipientModule, +} from '@credo-ts/core' + +const recipientAgentOptions = getInMemoryAgentOptions( + 'E2E Subject Recipient', + {}, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + } +) +const mediatorAgentOptions = getInMemoryAgentOptions( + 'E2E Subject Mediator', + { + endpoints: ['rxjs:mediator'], + }, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediator: new MediatorModule({ autoAcceptMediationRequests: true }), + } +) +const senderAgentOptions = getInMemoryAgentOptions( + 'E2E Subject Sender', + { + endpoints: ['rxjs:sender'], + }, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorPollingInterval: 1000, + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + } +) + +describe('E2E Subject tests', () => { + let recipientAgent: AnonCredsTestsAgent + let mediatorAgent: AnonCredsTestsAgent + let senderAgent: AnonCredsTestsAgent + + beforeEach(async () => { + recipientAgent = new Agent(recipientAgentOptions) as AnonCredsTestsAgent + mediatorAgent = new Agent(mediatorAgentOptions) as AnonCredsTestsAgent + senderAgent = new Agent(senderAgentOptions) as AnonCredsTestsAgent + }) + + afterEach(async () => { + await recipientAgent.shutdown() + await recipientAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + await senderAgent.shutdown() + await senderAgent.wallet.delete() + }) + + test('Full Subject flow (connect, request mediation, issue, verify)', async () => { + const mediatorMessages = new Subject() + const senderMessages = new Subject() + + const subjectMap = { + 'rxjs:mediator': mediatorMessages, + 'rxjs:sender': senderMessages, + } + + // Recipient Setup + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await recipientAgent.initialize() + + // Mediator Setup + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + await mediatorAgent.initialize() + + // Sender Setup + senderAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + senderAgent.registerInboundTransport(new SubjectInboundTransport(senderMessages)) + await senderAgent.initialize() + + await e2eTest({ + mediatorAgent, + senderAgent, + recipientAgent, + }) + }) +}) diff --git a/tests/e2e-test.ts b/tests/e2e-test.ts new file mode 100644 index 0000000000..d65b681a88 --- /dev/null +++ b/tests/e2e-test.ts @@ -0,0 +1,161 @@ +import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/anoncredsSetup' +import type { AgentMessageProcessedEvent, AgentMessageSentEvent } from '@credo-ts/core' + +import { filter, firstValueFrom, map } from 'rxjs' + +import { presentAnonCredsProof, issueAnonCredsCredential } from '../packages/anoncreds/tests/anoncredsSetup' +import { + anoncredsDefinitionFourAttributesNoRevocation, + storePreCreatedAnonCredsDefinition, +} from '../packages/anoncreds/tests/preCreatedAnonCredsDefinition' +import { setupEventReplaySubjects } from '../packages/core/tests' +import { makeConnection } from '../packages/core/tests/helpers' + +import { + V2CredentialPreview, + V1BatchMessage, + V1BatchPickupMessage, + V2DeliveryRequestMessage, + V2MessageDeliveryMessage, + CredentialState, + MediationState, + ProofState, + CredentialEventTypes, + ProofEventTypes, + AgentEventTypes, +} from '@credo-ts/core' + +export async function e2eTest({ + mediatorAgent, + recipientAgent, + senderAgent, +}: { + mediatorAgent: AnonCredsTestsAgent + recipientAgent: AnonCredsTestsAgent + senderAgent: AnonCredsTestsAgent +}) { + const [senderReplay, recipientReplay] = setupEventReplaySubjects( + [senderAgent, recipientAgent, mediatorAgent], + [ + CredentialEventTypes.CredentialStateChanged, + ProofEventTypes.ProofStateChanged, + AgentEventTypes.AgentMessageProcessed, + AgentEventTypes.AgentMessageSent, + ] + ) + + // Make connection between mediator and recipient + const [mediatorRecipientConnection, recipientMediatorConnection] = await makeConnection(mediatorAgent, recipientAgent) + expect(recipientMediatorConnection).toBeConnectedWith(mediatorRecipientConnection) + + // Request mediation from mediator + const mediationRecord = await recipientAgent.mediationRecipient.requestAndAwaitGrant(recipientMediatorConnection) + expect(mediationRecord.state).toBe(MediationState.Granted) + + // Set mediator as default for recipient, start picking up messages + await recipientAgent.mediationRecipient.setDefaultMediator(mediationRecord) + await recipientAgent.mediationRecipient.initiateMessagePickup(mediationRecord) + const defaultMediator = await recipientAgent.mediationRecipient.findDefaultMediator() + expect(defaultMediator?.id).toBe(mediationRecord.id) + + // Make connection between sender and recipient + const [recipientSenderConnection, senderRecipientConnection] = await makeConnection(recipientAgent, senderAgent) + expect(recipientSenderConnection).toBeConnectedWith(senderRecipientConnection) + + const { credentialDefinitionId } = await storePreCreatedAnonCredsDefinition( + senderAgent, + anoncredsDefinitionFourAttributesNoRevocation + ) + + const { holderCredentialExchangeRecord, issuerCredentialExchangeRecord } = await issueAnonCredsCredential({ + issuerAgent: senderAgent, + issuerReplay: senderReplay, + holderAgent: recipientAgent, + holderReplay: recipientReplay, + revocationRegistryDefinitionId: null, + + issuerHolderConnectionId: senderRecipientConnection.id, + offer: { + credentialDefinitionId, + attributes: V2CredentialPreview.fromRecord({ + name: 'John', + age: '25', + 'x-ray': 'not taken', + profile_picture: 'looking good', + }).attributes, + }, + }) + + expect(holderCredentialExchangeRecord.state).toBe(CredentialState.Done) + expect(issuerCredentialExchangeRecord.state).toBe(CredentialState.Done) + + // Present Proof from recipient to sender + const { holderProofExchangeRecord, verifierProofExchangeRecord } = await presentAnonCredsProof({ + verifierAgent: senderAgent, + verifierReplay: senderReplay, + + holderAgent: recipientAgent, + holderReplay: recipientReplay, + + verifierHolderConnectionId: senderRecipientConnection.id, + request: { + attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + predicates: { + olderThan21: { + name: 'age', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + p_type: '<=', + p_value: 20000712, + }, + }, + }, + }) + + expect(holderProofExchangeRecord.state).toBe(ProofState.Done) + expect(verifierProofExchangeRecord.state).toBe(ProofState.Done) + + // We want to stop the mediator polling before the agent is shutdown. + await recipientAgent.mediationRecipient.stopMessagePickup() + + const pickupRequestMessages = [V2DeliveryRequestMessage.type.messageTypeUri, V1BatchPickupMessage.type.messageTypeUri] + const deliveryMessages = [V2MessageDeliveryMessage.type.messageTypeUri, V1BatchMessage.type.messageTypeUri] + + let lastSentPickupMessageThreadId: undefined | string = undefined + recipientReplay + .pipe( + filter((e): e is AgentMessageSentEvent => e.type === AgentEventTypes.AgentMessageSent), + filter((e) => pickupRequestMessages.includes(e.payload.message.message.type)), + map((e) => e.payload.message.message.threadId) + ) + .subscribe((threadId) => (lastSentPickupMessageThreadId = threadId)) + + // Wait for the response to the pickup message to be processed + if (lastSentPickupMessageThreadId) { + await firstValueFrom( + recipientReplay.pipe( + filter((e): e is AgentMessageProcessedEvent => e.type === AgentEventTypes.AgentMessageProcessed), + filter((e) => deliveryMessages.includes(e.payload.message.type)), + filter((e) => e.payload.message.threadId === lastSentPickupMessageThreadId) + ) + ) + } + + // FIXME: we should add some fancy logic here that checks whether the last sent message has been received by the other + // agent and possibly wait for the response. So e.g. if pickup v1 is used, we wait for the delivery message to be returned + // as that is the final message that will be exchange after we've called stopMessagePickup. We can hook into the + // replay subject AgentMessageProcessed and AgentMessageSent events. + // await sleep(5000) +} diff --git a/tests/e2e-ws-pickup-v2.e2e.test.ts b/tests/e2e-ws-pickup-v2.e2e.test.ts new file mode 100644 index 0000000000..833486bb57 --- /dev/null +++ b/tests/e2e-ws-pickup-v2.e2e.test.ts @@ -0,0 +1,153 @@ +import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/anoncredsSetup' + +import { getAnonCredsModules } from '../packages/anoncreds/tests/anoncredsSetup' +import { askarModule } from '../packages/askar/tests/helpers' +import { MessageForwardingStrategy } from '../packages/core/src/modules/routing/MessageForwardingStrategy' +import { getAgentOptions } from '../packages/core/tests/helpers' + +import { e2eTest } from './e2e-test' + +import { + Agent, + WsOutboundTransport, + AutoAcceptCredential, + MediatorPickupStrategy, + MediationRecipientModule, + MediatorModule, +} from '@credo-ts/core' +import { WsInboundTransport } from '@credo-ts/node' + +// FIXME: somehow if we use the in memory wallet and storage service in the WS test it will fail, +// but it succeeds with Askar. We should look into this at some point + +// FIXME: port numbers should not depend on availability from other test suites that use web sockets +const mediatorPort = 4100 +const mediatorOptions = getAgentOptions( + 'E2E WS Pickup V2 Mediator', + { + endpoints: [`ws://localhost:${mediatorPort}`], + }, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediator: new MediatorModule({ + autoAcceptMediationRequests: true, + messageForwardingStrategy: MessageForwardingStrategy.QueueAndLiveModeDelivery, + }), + askar: askarModule, + } +) + +const senderPort = 4101 +const senderOptions = getAgentOptions( + 'E2E WS Pickup V2 Sender', + { + endpoints: [`ws://localhost:${senderPort}`], + }, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + askar: askarModule, + } +) + +describe('E2E WS Pickup V2 tests', () => { + let recipientAgent: AnonCredsTestsAgent + let mediatorAgent: AnonCredsTestsAgent + let senderAgent: AnonCredsTestsAgent + + beforeEach(async () => { + mediatorAgent = new Agent(mediatorOptions) as unknown as AnonCredsTestsAgent + senderAgent = new Agent(senderOptions) as unknown as AnonCredsTestsAgent + }) + + afterEach(async () => { + // NOTE: the order is important here, as the recipient sends pickup messages to the mediator + // so we first want the recipient to fully be finished with the sending of messages + await recipientAgent.shutdown() + await recipientAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + await senderAgent.shutdown() + await senderAgent.wallet.delete() + }) + + test('Full WS flow (connect, request mediation, issue, verify) using Message Pickup V2 polling mode', async () => { + const recipientOptions = getAgentOptions( + 'E2E WS Pickup V2 Recipient polling mode', + {}, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV2, + mediatorPollingInterval: 500, + }), + askar: askarModule, + } + ) + + recipientAgent = new Agent(recipientOptions) as unknown as AnonCredsTestsAgent + + // Recipient Setup + recipientAgent.registerOutboundTransport(new WsOutboundTransport()) + await recipientAgent.initialize() + + // Mediator Setup + mediatorAgent.registerInboundTransport(new WsInboundTransport({ port: mediatorPort })) + mediatorAgent.registerOutboundTransport(new WsOutboundTransport()) + await mediatorAgent.initialize() + + // Sender Setup + senderAgent.registerInboundTransport(new WsInboundTransport({ port: senderPort })) + senderAgent.registerOutboundTransport(new WsOutboundTransport()) + await senderAgent.initialize() + + await e2eTest({ + mediatorAgent, + senderAgent, + recipientAgent, + }) + }) + + test('Full WS flow (connect, request mediation, issue, verify) using Message Pickup V2 live mode', async () => { + const recipientOptions = getAgentOptions( + 'E2E WS Pickup V2 Recipient live mode', + {}, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV2LiveMode, + }), + askar: askarModule, + } + ) + + recipientAgent = new Agent(recipientOptions) as unknown as AnonCredsTestsAgent + + // Recipient Setup + recipientAgent.registerOutboundTransport(new WsOutboundTransport()) + await recipientAgent.initialize() + + // Mediator Setup + mediatorAgent.registerInboundTransport(new WsInboundTransport({ port: mediatorPort })) + mediatorAgent.registerOutboundTransport(new WsOutboundTransport()) + await mediatorAgent.initialize() + + // Sender Setup + senderAgent.registerInboundTransport(new WsInboundTransport({ port: senderPort })) + senderAgent.registerOutboundTransport(new WsOutboundTransport()) + await senderAgent.initialize() + + await e2eTest({ + mediatorAgent, + senderAgent, + recipientAgent, + }) + }) +}) diff --git a/tests/e2e-ws.e2e.test.ts b/tests/e2e-ws.e2e.test.ts new file mode 100644 index 0000000000..0da198e86b --- /dev/null +++ b/tests/e2e-ws.e2e.test.ts @@ -0,0 +1,109 @@ +import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/anoncredsSetup' + +import { getAnonCredsModules } from '../packages/anoncreds/tests/anoncredsSetup' +import { askarModule } from '../packages/askar/tests/helpers' +import { getAgentOptions } from '../packages/core/tests/helpers' + +import { e2eTest } from './e2e-test' + +import { + Agent, + WsOutboundTransport, + AutoAcceptCredential, + MediatorPickupStrategy, + MediationRecipientModule, + MediatorModule, +} from '@credo-ts/core' +import { WsInboundTransport } from '@credo-ts/node' + +// FIXME: somehow if we use the in memory wallet and storage service in the WS test it will fail, +// but it succeeds with Askar. We should look into this at some point +const recipientAgentOptions = getAgentOptions( + 'E2E WS Recipient ', + {}, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + askar: askarModule, + } +) + +const mediatorPort = 4000 +const mediatorAgentOptions = getAgentOptions( + 'E2E WS Mediator', + { + endpoints: [`ws://localhost:${mediatorPort}`], + }, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediator: new MediatorModule({ autoAcceptMediationRequests: true }), + askar: askarModule, + } +) + +const senderPort = 4001 +const senderAgentOptions = getAgentOptions( + 'E2E WS Sender', + { + endpoints: [`ws://localhost:${senderPort}`], + }, + { + ...getAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorPollingInterval: 1000, + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }), + askar: askarModule, + } +) + +describe('E2E WS tests', () => { + let recipientAgent: AnonCredsTestsAgent + let mediatorAgent: AnonCredsTestsAgent + let senderAgent: AnonCredsTestsAgent + + beforeEach(async () => { + recipientAgent = new Agent(recipientAgentOptions) as unknown as AnonCredsTestsAgent + mediatorAgent = new Agent(mediatorAgentOptions) as unknown as AnonCredsTestsAgent + senderAgent = new Agent(senderAgentOptions) as unknown as AnonCredsTestsAgent + }) + + afterEach(async () => { + await recipientAgent.shutdown() + await recipientAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + await senderAgent.shutdown() + await senderAgent.wallet.delete() + }) + + test('Full WS flow (connect, request mediation, issue, verify)', async () => { + // Recipient Setup + recipientAgent.registerOutboundTransport(new WsOutboundTransport()) + await recipientAgent.initialize() + + // Mediator Setup + mediatorAgent.registerInboundTransport(new WsInboundTransport({ port: mediatorPort })) + mediatorAgent.registerOutboundTransport(new WsOutboundTransport()) + await mediatorAgent.initialize() + + // Sender Setup + senderAgent.registerInboundTransport(new WsInboundTransport({ port: senderPort })) + senderAgent.registerOutboundTransport(new WsOutboundTransport()) + await senderAgent.initialize() + + await e2eTest({ + mediatorAgent, + senderAgent, + recipientAgent, + }) + }) +}) diff --git a/tests/jest.config.ts b/tests/jest.config.ts new file mode 100644 index 0000000000..0f965f5f8a --- /dev/null +++ b/tests/jest.config.ts @@ -0,0 +1,11 @@ +import type { Config } from '@jest/types' + +import base from '../jest.config.base' + +const config: Config.InitialOptions = { + ...base, + displayName: '@credo-ts/e2e-test', + setupFilesAfterEnv: ['../packages/core/tests/setup.ts'], +} + +export default config diff --git a/tests/runInVersion.ts b/tests/runInVersion.ts new file mode 100644 index 0000000000..743e8de158 --- /dev/null +++ b/tests/runInVersion.ts @@ -0,0 +1,12 @@ +type NodeVersions = 18 | 20 + +export function describeRunInNodeVersion(versions: NodeVersions[], ...parameters: Parameters) { + const runtimeVersion = process.version + const mappedVersions = versions.map((version) => `v${version}.`) + + if (mappedVersions.some((version) => runtimeVersion.startsWith(version))) { + describe(...parameters) + } else { + describe.skip(...parameters) + } +} diff --git a/tests/transport/SubjectInboundTransport.ts b/tests/transport/SubjectInboundTransport.ts new file mode 100644 index 0000000000..7cf6932dec --- /dev/null +++ b/tests/transport/SubjectInboundTransport.ts @@ -0,0 +1,83 @@ +import type { InboundTransport, Agent, AgentContext } from '../../packages/core/src' +import type { TransportSession } from '../../packages/core/src/agent/TransportService' +import type { EncryptedMessage } from '../../packages/core/src/types' +import type { Subscription } from 'rxjs' + +import { Subject } from 'rxjs' + +import { MessageReceiver } from '../../packages/core/src' +import { TransportService } from '../../packages/core/src/agent/TransportService' +import { uuid } from '../../packages/core/src/utils/uuid' + +export type SubjectMessage = { message: EncryptedMessage; replySubject?: Subject } + +export class SubjectInboundTransport implements InboundTransport { + public readonly ourSubject: Subject + private subscription?: Subscription + + public constructor(ourSubject = new Subject()) { + this.ourSubject = ourSubject + } + + public async start(agent: Agent) { + this.subscribe(agent) + } + + public async stop() { + this.subscription?.unsubscribe() + } + + private subscribe(agent: Agent) { + const logger = agent.config.logger + const transportService = agent.dependencyManager.resolve(TransportService) + const messageReceiver = agent.dependencyManager.resolve(MessageReceiver) + + this.subscription = this.ourSubject.subscribe({ + next: async ({ message, replySubject }: SubjectMessage) => { + logger.test('Received message') + + let session: SubjectTransportSession | undefined + if (replySubject) { + session = new SubjectTransportSession(`subject-session-${uuid()}`, replySubject) + + // When the subject is completed (e.g. when the session is closed), we need to + // remove the session from the transport service so it won't be used for sending messages + // in the future. + replySubject.subscribe({ + complete: () => session && transportService.removeSession(session), + }) + } + + // This emits a custom error in order to easily catch in E2E tests when a message + // reception throws an error. TODO: Remove as soon as agent throws errors automatically + try { + await messageReceiver.receiveMessage(message, { session }) + } catch (error) { + agent.events.emit(agent.context, { + type: 'AgentReceiveMessageError', + payload: error, + }) + } + }, + }) + } +} + +export class SubjectTransportSession implements TransportSession { + public id: string + public readonly type = 'subject' + private replySubject: Subject + + public constructor(id: string, replySubject: Subject) { + this.id = id + this.replySubject = replySubject + } + + public async send(agentContext: AgentContext, encryptedMessage: EncryptedMessage): Promise { + this.replySubject.next({ message: encryptedMessage }) + } + + public async close(): Promise { + this.replySubject.complete() + } +} diff --git a/tests/transport/SubjectOutboundTransport.ts b/tests/transport/SubjectOutboundTransport.ts new file mode 100644 index 0000000000..0291f84948 --- /dev/null +++ b/tests/transport/SubjectOutboundTransport.ts @@ -0,0 +1,63 @@ +import type { SubjectMessage } from './SubjectInboundTransport' +import type { OutboundPackage, OutboundTransport, Agent, Logger } from '@credo-ts/core' + +import { takeUntil, Subject, take } from 'rxjs' + +import { MessageReceiver, InjectionSymbols, CredoError } from '@credo-ts/core' + +export class SubjectOutboundTransport implements OutboundTransport { + private logger!: Logger + private subjectMap: { [key: string]: Subject | undefined } + private agent!: Agent + private stop$!: Subject + + public supportedSchemes = ['rxjs', 'wss'] + + public constructor(subjectMap: { [key: string]: Subject | undefined }) { + this.subjectMap = subjectMap + } + + public async start(agent: Agent): Promise { + this.agent = agent + + this.logger = agent.dependencyManager.resolve(InjectionSymbols.Logger) + this.stop$ = agent.dependencyManager.resolve(InjectionSymbols.Stop$) + } + + public async stop(): Promise { + // No logic needed + } + + public async sendMessage(outboundPackage: OutboundPackage) { + const messageReceiver = this.agent.dependencyManager.resolve(MessageReceiver) + this.logger.debug(`Sending outbound message to endpoint ${outboundPackage.endpoint}`, { + endpoint: outboundPackage.endpoint, + }) + const { payload, endpoint } = outboundPackage + + if (!endpoint) { + throw new CredoError('Cannot send message to subject without endpoint') + } + + const subject = this.subjectMap[endpoint] + + if (!subject) { + throw new CredoError(`No subject found for endpoint ${endpoint}`) + } + + // Create a replySubject just for this session. Both ends will be able to close it, + // mimicking a transport like http or websocket. Close session automatically when agent stops + const replySubject = new Subject() + this.stop$.pipe(take(1)).subscribe(() => !replySubject.closed && replySubject.complete()) + + replySubject.pipe(takeUntil(this.stop$)).subscribe({ + next: async ({ message }: SubjectMessage) => { + this.logger.test('Received message') + + await messageReceiver.receiveMessage(message) + }, + }) + + subject.next({ message: payload, replySubject }) + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000000..25788d4b03 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2017", + "declaration": true, + "sourceMap": true, + "strict": true, + "noEmitOnError": true, + "lib": [], + "types": [], + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + // TODO: we should update code to assume errors are of type 'unknown' + "useUnknownInCatchVariables": false + }, + "exclude": ["node_modules", "build", "**/*.test.ts", "**/__tests__/*.ts", "**/__mocks__/*.ts", "**/build/**"] +} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000000..67c6233f11 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.build.json", + + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@credo-ts/*": ["packages/*/src"] + } + }, + "include": [ + "packages", + "./.eslintrc.js", + "./jest.config.ts", + "./jest.config.base.ts", + "types", + "tests", + "samples", + "demo", + "demo-openid", + "scripts" + ], + "exclude": ["node_modules", "build"] +} diff --git a/tsconfig.json b/tsconfig.json index 7e72a77d36..9b5c56eefa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,64 +1,14 @@ { + "extends": "./tsconfig.build.json", + "ts-node": { + "files": true + }, "compilerOptions": { - /* Basic Options */ - // "incremental": true, /* Enable incremental compilation */ - "target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, - "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, - // "lib": [], /* Specify library files to be included in the compilation. */ - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - "declaration": true /* Generates corresponding '.d.ts' file. */, - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - "sourceMap": true /* Generates corresponding '.map' file. */, - // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./build" /* Redirect output structure to the directory. */, - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - - /* Strict Type-Checking Options */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - - /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - "typeRoots": ["node_modules/@types", "./types"] /* List of folders to include type definitions from. */, - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - "resolveJsonModule": true, - - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, - "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */ - } + "baseUrl": ".", + "paths": { + "@credo-ts/*": ["packages/*/src"] + }, + "types": ["jest", "node"] + }, + "exclude": ["node_modules", "**/build/**"] } diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000000..e24fac40b6 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.build.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@credo-ts/*": ["packages/*/src"] + }, + "types": ["jest", "node"] + }, + "include": ["tests", "samples", "demo", "demo-openid", "packages/core/types/jest.d.ts"], + "exclude": ["node_modules", "build", "**/build/**"] +} diff --git a/types/jest.d.ts b/types/jest.d.ts deleted file mode 100644 index ffb4364c0b..0000000000 --- a/types/jest.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { ConnectionRecord } from '../src/lib/modules/connections/repository/ConnectionRecord'; - -declare global { - namespace jest { - interface Matchers { - toBeConnectedWith(connection: ConnectionRecord): R; - } - } -} diff --git a/yarn.lock b/yarn.lock index 1a447c3f58..32e5ad917d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,163 +2,364 @@ # yarn lockfile v1 -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== +"@2060.io/ffi-napi@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@2060.io/ffi-napi/-/ffi-napi-4.0.9.tgz#194fca2132932ba02e62d716c786d20169b20b8d" + integrity sha512-JfVREbtkJhMXSUpya3JCzDumdjeZDCKv4PemiWK+pts5CYgdoMidxeySVlFeF5pHqbBpox4I0Be7sDwAq4N0VQ== dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/core@^7.1.0", "@babel/core@^7.7.5": - version "7.11.6" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651" - integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.6" - "@babel/helper-module-transforms" "^7.11.0" - "@babel/helpers" "^7.10.4" - "@babel/parser" "^7.11.5" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.11.5" - "@babel/types" "^7.11.5" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.2" - lodash "^4.17.19" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" + "@2060.io/ref-napi" "^3.0.6" + debug "^4.1.1" + get-uv-event-loop-napi-h "^1.0.5" + node-addon-api "^3.0.0" + node-gyp-build "^4.2.1" + ref-struct-di "^1.1.0" -"@babel/generator@^7.11.5", "@babel/generator@^7.11.6": - version "7.11.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" - integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA== +"@2060.io/ref-napi@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@2060.io/ref-napi/-/ref-napi-3.0.6.tgz#32b1a257cada096f95345fd7abae746385ecc5dd" + integrity sha512-8VAIXLdKL85E85jRYpPcZqATBL6fGnC/XjBGNeSgRSMJtrAMSmfRksqIq5AmuZkA2eeJXMWCiN6UQOUdozcymg== dependencies: - "@babel/types" "^7.11.5" - jsesc "^2.5.1" - source-map "^0.5.0" + debug "^4.1.1" + get-symbol-from-current-process-h "^1.0.2" + node-addon-api "^3.0.0" + node-gyp-build "^4.2.1" -"@babel/helper-function-name@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" - integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== dependencies: - "@babel/helper-get-function-arity" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" -"@babel/helper-get-function-arity@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" - integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== +"@astronautlabs/jsonpath@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@astronautlabs/jsonpath/-/jsonpath-1.1.2.tgz#af19bb4a7d13dcfbc60c3c998ee1e73d7c2ddc38" + integrity sha512-FqL/muoreH7iltYC1EB5Tvox5E8NSOOPGkgns4G+qxRKl6k5dxEVljUjB5NcKESzkqwnUqWjSZkL61XGYOuV+A== dependencies: - "@babel/types" "^7.10.4" + static-eval "2.0.2" -"@babel/helper-member-expression-to-functions@^7.10.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" - integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== +"@azure/core-asynciterator-polyfill@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz#0dd3849fb8d97f062a39db0e5cadc9ffaf861fec" + integrity sha512-3rkP4LnnlWawl0LZptJOdXNrT/fHp2eQMadoasa6afspXdpGrtPZuAQc2PD0cpgyuoXtUWyC3tv7xfntjGS5Dw== + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== + dependencies: + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" + +"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.8": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.9.tgz#53eee4e68f1c1d0282aa0eb05ddb02d033fc43a0" + integrity sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.14.6", "@babel/core@^7.20.0", "@babel/core@^7.23.9": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.9.tgz#dc07c9d307162c97fa9484ea997ade65841c7c82" + integrity sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.24.9" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-module-transforms" "^7.24.9" + "@babel/helpers" "^7.24.8" + "@babel/parser" "^7.24.8" + "@babel/template" "^7.24.7" + "@babel/traverse" "^7.24.8" + "@babel/types" "^7.24.9" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.20.0", "@babel/generator@^7.24.8", "@babel/generator@^7.24.9", "@babel/generator@^7.7.2": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.9.tgz#5c2575a1070e661bbbc9df82a853989c9a656f12" + integrity sha512-G8v3jRg+z8IwY1jHFxvCNhOPYPterE4XljNgdGTYfSTtzzwjIswIzIaSPSLs3R7yFuqnqNeay5rjICfqVr+/6A== + dependencies: + "@babel/types" "^7.24.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" + integrity sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.24.7", "@babel/helper-compilation-targets@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz#b607c3161cd9d1744977d4f97139572fe778c271" + integrity sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw== + dependencies: + "@babel/compat-data" "^7.24.8" + "@babel/helper-validator-option" "^7.24.8" + browserslist "^4.23.1" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.8.tgz#47f546408d13c200c0867f9d935184eaa0851b09" + integrity sha512-4f6Oqnmyp2PP3olgUMmOwC3akxSm5aBYraQ6YDdKy7NcAMkDECHWG0DEnV6M2UAkERgIBhYt8S27rURPg7SxWA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-member-expression-to-functions" "^7.24.8" + "@babel/helper-optimise-call-expression" "^7.24.7" + "@babel/helper-replace-supers" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz#be4f435a80dc2b053c76eeb4b7d16dd22cfc89da" + integrity sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + regexpu-core "^5.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.1", "@babel/helper-define-polyfill-provider@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" + integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== dependencies: - "@babel/types" "^7.11.0" + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" -"@babel/helper-module-imports@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" - integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-module-transforms@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" - integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== - dependencies: - "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" - "@babel/helper-simple-access" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/template" "^7.10.4" - "@babel/types" "^7.11.0" - lodash "^4.17.19" +"@babel/helper-environment-visitor@^7.18.9", "@babel/helper-environment-visitor@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" + integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ== + dependencies: + "@babel/types" "^7.24.7" -"@babel/helper-optimise-call-expression@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" - integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== +"@babel/helper-function-name@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz#75f1e1725742f39ac6584ee0b16d94513da38dd2" + integrity sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA== dependencies: - "@babel/types" "^7.10.4" + "@babel/template" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-hoist-variables@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz#b4ede1cde2fd89436397f30dc9376ee06b0f25ee" + integrity sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-member-expression-to-functions@^7.24.7", "@babel/helper-member-expression-to-functions@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz#6155e079c913357d24a4c20480db7c712a5c3fb6" + integrity sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA== + dependencies: + "@babel/traverse" "^7.24.8" + "@babel/types" "^7.24.8" + +"@babel/helper-module-imports@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" + integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-module-transforms@^7.24.8", "@babel/helper-module-transforms@^7.24.9": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz#e13d26306b89eea569180868e652e7f514de9d29" + integrity sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw== + dependencies: + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-simple-access" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + +"@babel/helper-optimise-call-expression@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" + integrity sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878" + integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg== + +"@babel/helper-remap-async-to-generator@^7.18.9", "@babel/helper-remap-async-to-generator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz#b3f0f203628522713849d49403f1a414468be4c7" + integrity sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-wrap-function" "^7.24.7" + +"@babel/helper-replace-supers@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz#f933b7eed81a1c0265740edc91491ce51250f765" + integrity sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg== + dependencies: + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-member-expression-to-functions" "^7.24.7" + "@babel/helper-optimise-call-expression" "^7.24.7" + +"@babel/helper-simple-access@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" + integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-skip-transparent-expression-wrappers@^7.20.0", "@babel/helper-skip-transparent-expression-wrappers@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz#5f8fa83b69ed5c27adc56044f8be2b3ea96669d9" + integrity sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-split-export-declaration@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856" + integrity sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== + +"@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + +"@babel/helper-validator-option@^7.24.7", "@babel/helper-validator-option@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" + integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== + +"@babel/helper-wrap-function@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz#52d893af7e42edca7c6d2c6764549826336aae1f" + integrity sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw== + dependencies: + "@babel/helper-function-name" "^7.24.7" + "@babel/template" "^7.24.7" + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helpers@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.8.tgz#2820d64d5d6686cca8789dd15b074cd862795873" + integrity sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ== + dependencies: + "@babel/template" "^7.24.7" + "@babel/types" "^7.24.8" + +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== + dependencies: + "@babel/helper-validator-identifier" "^7.24.7" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" - integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== +"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.24.7", "@babel/parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.8.tgz#58a4dbbcad7eb1d48930524a3fd93d93e9084c6f" + integrity sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w== -"@babel/helper-plugin-utils@^7.12.13": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz#806526ce125aed03373bc416a828321e3a6a33af" - integrity sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ== +"@babel/plugin-proposal-async-generator-functions@^7.0.0": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" + integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-remap-async-to-generator" "^7.18.9" + "@babel/plugin-syntax-async-generators" "^7.8.4" -"@babel/helper-replace-supers@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" - integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== +"@babel/plugin-proposal-class-properties@^7.0.0", "@babel/plugin-proposal-class-properties@^7.13.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" + integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== dependencies: - "@babel/helper-member-expression-to-functions" "^7.10.4" - "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" -"@babel/helper-simple-access@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" - integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw== +"@babel/plugin-proposal-export-default-from@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.24.7.tgz#0b539c46b8ac804f694e338f803c8354c0f788b6" + integrity sha512-CcmFwUJ3tKhLjPdt4NP+SHMshebytF8ZTYOv5ZDpkzq2sin80Wb5vJrGt8fhPrORQCfoSa0LAxC/DW+GAC5+Hw== dependencies: - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-export-default-from" "^7.24.7" -"@babel/helper-split-export-declaration@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" - integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== +"@babel/plugin-proposal-export-namespace-from@^7.14.5": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" + integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== dependencies: - "@babel/types" "^7.11.0" + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== +"@babel/plugin-proposal-nullish-coalescing-operator@^7.0.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.13.8": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" + integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/helpers@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" - integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== +"@babel/plugin-proposal-object-rest-spread@^7.0.0": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" + integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== dependencies: - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/compat-data" "^7.20.5" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.20.7" -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== +"@babel/plugin-proposal-optional-catch-binding@^7.0.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb" + integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - chalk "^2.0.0" - js-tokens "^4.0.0" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.5": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" - integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== +"@babel/plugin-proposal-optional-chaining@^7.0.0", "@babel/plugin-proposal-optional-chaining@^7.13.12": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz#886f5c8978deb7d30f678b2e24346b287234d3ea" + integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -174,12 +375,40 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c" - integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA== +"@babel/plugin-syntax-class-properties@^7.0.0", "@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-dynamic-import@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-default-from@^7.0.0", "@babel/plugin-syntax-export-default-from@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.24.7.tgz#85dae9098933573aae137fb52141dd3ca52ae7ac" + integrity sha512-bTPz4/635WQ9WhwsyPdxUJDVpsi/X9BMmy/8Rf/UAlOO4jSql4CxUCjWI5PiM+jG+c4LVPTScoTw80geFj9+Bw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.18.0", "@babel/plugin-syntax-flow@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.7.tgz#d1759e84dd4b437cf9fae69b4c06c41d7625bfb7" + integrity sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" @@ -195,6 +424,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.24.7", "@babel/plugin-syntax-jsx@^7.7.2": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz#39a1fa4a7e3d3d7f34e2acc6be585b718d30e02d" + integrity sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -202,7 +438,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": +"@babel/plugin-syntax-nullish-coalescing-operator@^7.0.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== @@ -216,7 +452,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-object-rest-spread@^7.8.3": +"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== @@ -230,7 +466,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-optional-chaining@^7.8.3": +"@babel/plugin-syntax-optional-chaining@^7.0.0", "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== @@ -238,43 +474,322 @@ "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-top-level-await@^7.8.3": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.13.tgz#c5f0fa6e249f5b739727f923540cf7a806130178" - integrity sha512-A81F9pDwyS7yM//KwbCSDqy3Uj4NMIurtplxphWxoYtNPov7cJsDkAFNNyVlIZ3jwGycVsurZ+LtOA8gZ376iQ== + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/template@^7.10.4", "@babel/template@^7.3.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" - integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.11.5": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3" - integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.5" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.11.5" - "@babel/types" "^7.11.5" - debug "^4.1.0" +"@babel/plugin-syntax-typescript@^7.24.7", "@babel/plugin-syntax-typescript@^7.7.2": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz#58d458271b4d3b6bb27ee6ac9525acbb259bad1c" + integrity sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-arrow-functions@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz#4f6886c11e423bd69f3ce51dbf42424a5f275514" + integrity sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-async-to-generator@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz#72a3af6c451d575842a7e9b5a02863414355bdcc" + integrity sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA== + dependencies: + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-remap-async-to-generator" "^7.24.7" + +"@babel/plugin-transform-block-scoped-functions@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz#a4251d98ea0c0f399dafe1a35801eaba455bbf1f" + integrity sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-block-scoping@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz#42063e4deb850c7bd7c55e626bf4e7ab48e6ce02" + integrity sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-classes@^7.0.0": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.8.tgz#ad23301fe5bc153ca4cf7fb572a9bc8b0b711cf7" + integrity sha512-VXy91c47uujj758ud9wx+OMgheXm4qJfyhj1P18YvlrQkNOSrwsteHk+EFS3OMGfhMhpZa0A+81eE7G4QC+3CA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-replace-supers" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" globals "^11.1.0" - lodash "^4.17.19" -"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" - integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q== +"@babel/plugin-transform-computed-properties@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz#4cab3214e80bc71fae3853238d13d097b004c707" + integrity sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ== dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.19" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/template" "^7.24.7" + +"@babel/plugin-transform-destructuring@^7.0.0": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz#c828e814dbe42a2718a838c2a2e16a408e055550" + integrity sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/plugin-transform-flow-strip-types@^7.0.0", "@babel/plugin-transform-flow-strip-types@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.7.tgz#ae454e62219288fbb734541ab00389bfb13c063e" + integrity sha512-cjRKJ7FobOH2eakx7Ja+KpJRj8+y+/SiB3ooYm/n2UJfxu0oEaOoxOinitkJcPqv9KxS0kxTGPUaR7L2XcXDXA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-flow" "^7.24.7" + +"@babel/plugin-transform-for-of@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" + integrity sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + +"@babel/plugin-transform-function-name@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz#6d8601fbffe665c894440ab4470bc721dd9131d6" + integrity sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w== + dependencies: + "@babel/helper-compilation-targets" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-literals@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz#36b505c1e655151a9d7607799a9988fc5467d06c" + integrity sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-member-expression-literals@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz#3b4454fb0e302e18ba4945ba3246acb1248315df" + integrity sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.13.8", "@babel/plugin-transform-modules-commonjs@^7.14.5", "@babel/plugin-transform-modules-commonjs@^7.24.7": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz#ab6421e564b717cb475d6fff70ae7f103536ea3c" + integrity sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA== + dependencies: + "@babel/helper-module-transforms" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-simple-access" "^7.24.7" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz#9042e9b856bc6b3688c0c2e4060e9e10b1460923" + integrity sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-object-super@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz#66eeaff7830bba945dd8989b632a40c04ed625be" + integrity sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-replace-supers" "^7.24.7" + +"@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.20.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz#5881f0ae21018400e320fc7eb817e529d1254b68" + integrity sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-property-literals@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz#f0d2ed8380dfbed949c42d4d790266525d63bbdc" + integrity sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-react-display-name@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz#9caff79836803bc666bcfe210aeb6626230c293b" + integrity sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-react-jsx-self@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz#66bff0248ea0b549972e733516ffad577477bdab" + integrity sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-react-jsx-source@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz#1198aab2548ad19582013815c938d3ebd8291ee3" + integrity sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-react-jsx@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz#17cd06b75a9f0e2bd076503400e7c4b99beedac4" + integrity sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-jsx" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/plugin-transform-runtime@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz#00a5bfaf8c43cf5c8703a8a6e82b59d9c58f38ca" + integrity sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw== + dependencies: + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.1" + babel-plugin-polyfill-regenerator "^0.6.1" + semver "^6.3.1" + +"@babel/plugin-transform-shorthand-properties@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz#85448c6b996e122fa9e289746140aaa99da64e73" + integrity sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-spread@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz#e8a38c0fde7882e0fb8f160378f74bd885cc7bb3" + integrity sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + +"@babel/plugin-transform-sticky-regex@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz#96ae80d7a7e5251f657b5cf18f1ea6bf926f5feb" + integrity sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-template-literals@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz#a05debb4a9072ae8f985bcf77f3f215434c8f8c8" + integrity sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-typescript@^7.24.7", "@babel/plugin-transform-typescript@^7.5.0": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.8.tgz#c104d6286e04bf7e44b8cba1b686d41bad57eb84" + integrity sha512-CgFgtN61BbdOGCP4fLaAMOPkzWUh6yQZNMr5YSt8uz2cZSSiQONCQFWqsE4NeVfOIhqDOlS9CR3WD91FzMeB2Q== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-create-class-features-plugin" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/plugin-syntax-typescript" "^7.24.7" + +"@babel/plugin-transform-unicode-regex@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz#dfc3d4a51127108099b19817c0963be6a2adf19f" + integrity sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/preset-flow@^7.13.13": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.24.7.tgz#eef5cb8e05e97a448fc50c16826f5612fe512c06" + integrity sha512-NL3Lo0NorCU607zU3NwRyJbpaB6E3t0xtd3LfAQKDfkeX4/ggcDXvkmkW42QWT5owUeW/jAe4hn+2qvkV1IbfQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-validator-option" "^7.24.7" + "@babel/plugin-transform-flow-strip-types" "^7.24.7" + +"@babel/preset-typescript@^7.13.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz#66cd86ea8f8c014855671d5ea9a737139cbbfef1" + integrity sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-validator-option" "^7.24.7" + "@babel/plugin-syntax-jsx" "^7.24.7" + "@babel/plugin-transform-modules-commonjs" "^7.24.7" + "@babel/plugin-transform-typescript" "^7.24.7" + +"@babel/register@^7.13.16": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.24.6.tgz#59e21dcc79e1d04eed5377633b0f88029a6bef9e" + integrity sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w== + dependencies: + clone-deep "^4.0.1" + find-cache-dir "^2.0.0" + make-dir "^2.1.0" + pirates "^4.0.6" + source-map-support "^0.5.16" + +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + +"@babel/runtime@^7.0.0", "@babel/runtime@^7.20.1", "@babel/runtime@^7.5.5": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.8.tgz#5d958c3827b13cc6d05e038c07fb2e5e3420d82e" + integrity sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.0.0", "@babel/template@^7.24.7", "@babel/template@^7.3.3": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" + integrity sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/parser" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/traverse@^7.20.0", "@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.8.tgz#6c14ed5232b7549df3371d820fbd9abfcd7dfab7" + integrity sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.24.8" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-hoist-variables" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + "@babel/parser" "^7.24.8" + "@babel/types" "^7.24.8" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.24.9", "@babel/types@^7.3.3": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.9.tgz#228ce953d7b0d16646e755acf204f4cf3d08cc73" + integrity sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ== + dependencies: + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" "@bcoe/v8-coverage@^0.2.3": @@ -282,29 +797,849 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@cnakazawa/watch@^1.0.3": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" - integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ== +"@changesets/apply-release-plan@^7.0.4": + version "7.0.4" + resolved "https://registry.yarnpkg.com/@changesets/apply-release-plan/-/apply-release-plan-7.0.4.tgz#f963e11848efa24c53abd10713662f2012b6082b" + integrity sha512-HLFwhKWayKinWAul0Vj+76jVx1Pc2v55MGPVjZ924Y/ROeSsBMFutv9heHmCUj48lJyRfOTJG5+ar+29FUky/A== + dependencies: + "@babel/runtime" "^7.20.1" + "@changesets/config" "^3.0.2" + "@changesets/get-version-range-type" "^0.4.0" + "@changesets/git" "^3.0.0" + "@changesets/should-skip-package" "^0.1.0" + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + detect-indent "^6.0.0" + fs-extra "^7.0.1" + lodash.startcase "^4.4.0" + outdent "^0.5.0" + prettier "^2.7.1" + resolve-from "^5.0.0" + semver "^7.5.3" + +"@changesets/assemble-release-plan@^6.0.3": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.3.tgz#911ab27d0e4b8e732c7a03a09707b66b38ba48e5" + integrity sha512-bLNh9/Lgl1VwkjWZTq8JmRqH+hj7/Yzfz0jsQ/zJJ+FTmVqmqPj3szeKOri8O/hEM8JmHW019vh2gTO9iq5Cuw== + dependencies: + "@babel/runtime" "^7.20.1" + "@changesets/errors" "^0.2.0" + "@changesets/get-dependents-graph" "^2.1.1" + "@changesets/should-skip-package" "^0.1.0" + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + semver "^7.5.3" + +"@changesets/changelog-git@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@changesets/changelog-git/-/changelog-git-0.2.0.tgz#1f3de11becafff5a38ebe295038a602403c93a86" + integrity sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ== + dependencies: + "@changesets/types" "^6.0.0" + +"@changesets/cli@^2.27.5": + version "2.27.7" + resolved "https://registry.yarnpkg.com/@changesets/cli/-/cli-2.27.7.tgz#275f546fa138799d7fea080a1bd90849b2407207" + integrity sha512-6lr8JltiiXPIjDeYg4iM2MeePP6VN/JkmqBsVA5XRiy01hGS3y629LtSDvKcycj/w/5Eur1rEwby/MjcYS+e2A== + dependencies: + "@babel/runtime" "^7.20.1" + "@changesets/apply-release-plan" "^7.0.4" + "@changesets/assemble-release-plan" "^6.0.3" + "@changesets/changelog-git" "^0.2.0" + "@changesets/config" "^3.0.2" + "@changesets/errors" "^0.2.0" + "@changesets/get-dependents-graph" "^2.1.1" + "@changesets/get-release-plan" "^4.0.3" + "@changesets/git" "^3.0.0" + "@changesets/logger" "^0.1.0" + "@changesets/pre" "^2.0.0" + "@changesets/read" "^0.6.0" + "@changesets/should-skip-package" "^0.1.0" + "@changesets/types" "^6.0.0" + "@changesets/write" "^0.3.1" + "@manypkg/get-packages" "^1.1.3" + "@types/semver" "^7.5.0" + ansi-colors "^4.1.3" + chalk "^2.1.0" + ci-info "^3.7.0" + enquirer "^2.3.0" + external-editor "^3.1.0" + fs-extra "^7.0.1" + human-id "^1.0.2" + mri "^1.2.0" + outdent "^0.5.0" + p-limit "^2.2.0" + preferred-pm "^3.0.0" + resolve-from "^5.0.0" + semver "^7.5.3" + spawndamnit "^2.0.0" + term-size "^2.1.0" + +"@changesets/config@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@changesets/config/-/config-3.0.2.tgz#45b16bf911937afbfa418b87ca4bcebeea1bec47" + integrity sha512-cdEhS4t8woKCX2M8AotcV2BOWnBp09sqICxKapgLHf9m5KdENpWjyrFNMjkLqGJtUys9U+w93OxWT0czorVDfw== + dependencies: + "@changesets/errors" "^0.2.0" + "@changesets/get-dependents-graph" "^2.1.1" + "@changesets/logger" "^0.1.0" + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + fs-extra "^7.0.1" + micromatch "^4.0.2" + +"@changesets/errors@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@changesets/errors/-/errors-0.2.0.tgz#3c545e802b0f053389cadcf0ed54e5636ff9026a" + integrity sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow== dependencies: - exec-sh "^0.3.2" - minimist "^1.2.0" + extendable-error "^0.1.5" -"@eslint/eslintrc@^0.4.0": +"@changesets/get-dependents-graph@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.1.tgz#7d459c1fa453c21bc71e88d58d504fbb32b55750" + integrity sha512-LRFjjvigBSzfnPU2n/AhFsuWR5DK++1x47aq6qZ8dzYsPtS/I5mNhIGAS68IAxh1xjO9BTtz55FwefhANZ+FCA== + dependencies: + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + chalk "^2.1.0" + fs-extra "^7.0.1" + semver "^7.5.3" + +"@changesets/get-release-plan@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@changesets/get-release-plan/-/get-release-plan-4.0.3.tgz#f2ebab1fe59ce9e89be3caf819ac16f24fcf4b8b" + integrity sha512-6PLgvOIwTSdJPTtpdcr3sLtGatT+Jr22+cQwEBJBy6wP0rjB4yJ9lv583J9fVpn1bfQlBkDa8JxbS2g/n9lIyA== + dependencies: + "@babel/runtime" "^7.20.1" + "@changesets/assemble-release-plan" "^6.0.3" + "@changesets/config" "^3.0.2" + "@changesets/pre" "^2.0.0" + "@changesets/read" "^0.6.0" + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + +"@changesets/get-version-range-type@^0.4.0": version "0.4.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.0.tgz#99cc0a0584d72f1df38b900fb062ba995f395547" - integrity sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog== + resolved "https://registry.yarnpkg.com/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz#429a90410eefef4368502c41c63413e291740bf5" + integrity sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ== + +"@changesets/git@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@changesets/git/-/git-3.0.0.tgz#e71d003752a97bc27988db6d410e0038a4a88055" + integrity sha512-vvhnZDHe2eiBNRFHEgMiGd2CT+164dfYyrJDhwwxTVD/OW0FUD6G7+4DIx1dNwkwjHyzisxGAU96q0sVNBns0w== + dependencies: + "@babel/runtime" "^7.20.1" + "@changesets/errors" "^0.2.0" + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + is-subdir "^1.1.1" + micromatch "^4.0.2" + spawndamnit "^2.0.0" + +"@changesets/logger@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@changesets/logger/-/logger-0.1.0.tgz#2d2a58536c5beeeaef52ab464931d99fcf24f17b" + integrity sha512-pBrJm4CQm9VqFVwWnSqKEfsS2ESnwqwH+xR7jETxIErZcfd1u2zBSqrHbRHR7xjhSgep9x2PSKFKY//FAshA3g== + dependencies: + chalk "^2.1.0" + +"@changesets/parse@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@changesets/parse/-/parse-0.4.0.tgz#5cabbd9844b3b213cb83f5edb5768454c70dd2b4" + integrity sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw== + dependencies: + "@changesets/types" "^6.0.0" + js-yaml "^3.13.1" + +"@changesets/pre@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@changesets/pre/-/pre-2.0.0.tgz#ad3edf3d6ac287991d7ef5e26cf280d03c9e3764" + integrity sha512-HLTNYX/A4jZxc+Sq8D1AMBsv+1qD6rmmJtjsCJa/9MSRybdxh0mjbTvE6JYZQ/ZiQ0mMlDOlGPXTm9KLTU3jyw== + dependencies: + "@babel/runtime" "^7.20.1" + "@changesets/errors" "^0.2.0" + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + fs-extra "^7.0.1" + +"@changesets/read@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@changesets/read/-/read-0.6.0.tgz#27e13b58d0b0eb3b0a5cba48a3f4f71f05ef4610" + integrity sha512-ZypqX8+/im1Fm98K4YcZtmLKgjs1kDQ5zHpc2U1qdtNBmZZfo/IBiG162RoP0CUF05tvp2y4IspH11PLnPxuuw== + dependencies: + "@babel/runtime" "^7.20.1" + "@changesets/git" "^3.0.0" + "@changesets/logger" "^0.1.0" + "@changesets/parse" "^0.4.0" + "@changesets/types" "^6.0.0" + chalk "^2.1.0" + fs-extra "^7.0.1" + p-filter "^2.1.0" + +"@changesets/should-skip-package@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@changesets/should-skip-package/-/should-skip-package-0.1.0.tgz#12bb8de00476718e9fbc8a4e783aeea1f632e927" + integrity sha512-FxG6Mhjw7yFStlSM7Z0Gmg3RiyQ98d/9VpQAZ3Fzr59dCOM9G6ZdYbjiSAt0XtFr9JR5U2tBaJWPjrkGGc618g== + dependencies: + "@babel/runtime" "^7.20.1" + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + +"@changesets/types@^4.0.1": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@changesets/types/-/types-4.1.0.tgz#fb8f7ca2324fd54954824e864f9a61a82cb78fe0" + integrity sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw== + +"@changesets/types@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@changesets/types/-/types-6.0.0.tgz#e46abda9890610dd1fbe1617730173d2267544bd" + integrity sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ== + +"@changesets/write@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@changesets/write/-/write-0.3.1.tgz#438ef1dabc790cca35ce9fd36d26643b0f1786c9" + integrity sha512-SyGtMXzH3qFqlHKcvFY2eX+6b0NGiFcNav8AFsYwy5l8hejOeoeTDemu5Yjmke2V5jpzY+pBvM0vCCQ3gdZpfw== + dependencies: + "@babel/runtime" "^7.20.1" + "@changesets/types" "^6.0.0" + fs-extra "^7.0.1" + human-id "^1.0.2" + prettier "^2.7.1" + +"@cheqd/sdk@^2.4.4": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@cheqd/sdk/-/sdk-2.4.4.tgz#80daf50e1ac83da0ec4e471def042c47496015c9" + integrity sha512-ratcHNuKUZH6pmRvyLeiEFODhrlawfiDssaSzANscOTjeDMJzHK0YvEiSXswZAHcsB/DWbGlR+9gKhbLyD5G7w== + dependencies: + "@cheqd/ts-proto" "~2.2.0" + "@cosmjs/amino" "~0.30.0" + "@cosmjs/crypto" "~0.30.0" + "@cosmjs/encoding" "~0.30.0" + "@cosmjs/math" "~0.30.0" + "@cosmjs/proto-signing" "~0.30.0" + "@cosmjs/stargate" "~0.30.0" + "@cosmjs/tendermint-rpc" "~0.30.0" + "@cosmjs/utils" "~0.30.0" + "@stablelib/ed25519" "^1.0.3" + cosmjs-types "^0.7.1" + did-jwt "^6.11.6" + did-resolver "^4.1.0" + file-type "^16.5.4" + long "^4.0.0" + multiformats "^9.9.0" + uuid "^9.0.0" + +"@cheqd/ts-proto@~2.2.0": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@cheqd/ts-proto/-/ts-proto-2.2.2.tgz#c0e808c6d438da7098a225ea24ee94db9822fa06" + integrity sha512-32XCz1tD/T8r9Pw6IWH+XDttnGEguN0/1dWoUnTZ6uIPAA65YYSz2Ba9ZJ69a7YipYzX9C1CRddVZ3u229dfYg== + dependencies: + long "^5.2.3" + protobufjs "^7.2.4" + +"@confio/ics23@^0.6.8": + version "0.6.8" + resolved "https://registry.yarnpkg.com/@confio/ics23/-/ics23-0.6.8.tgz#2a6b4f1f2b7b20a35d9a0745bb5a446e72930b3d" + integrity sha512-wB6uo+3A50m0sW/EWcU64xpV/8wShZ6bMTa7pF8eYsTrSkQA7oLUIJcs/wb8g4y2Oyq701BaGiO6n/ak5WXO1w== + dependencies: + "@noble/hashes" "^1.0.0" + protobufjs "^6.8.8" + +"@cosmjs/amino@^0.30.1", "@cosmjs/amino@~0.30.0": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.30.1.tgz#7c18c14627361ba6c88e3495700ceea1f76baace" + integrity sha512-yNHnzmvAlkETDYIpeCTdVqgvrdt1qgkOXwuRVi8s27UKI5hfqyE9fJ/fuunXE6ZZPnKkjIecDznmuUOMrMvw4w== + dependencies: + "@cosmjs/crypto" "^0.30.1" + "@cosmjs/encoding" "^0.30.1" + "@cosmjs/math" "^0.30.1" + "@cosmjs/utils" "^0.30.1" + +"@cosmjs/crypto@^0.30.1", "@cosmjs/crypto@~0.30.0": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.30.1.tgz#21e94d5ca8f8ded16eee1389d2639cb5c43c3eb5" + integrity sha512-rAljUlake3MSXs9xAm87mu34GfBLN0h/1uPPV6jEwClWjNkAMotzjC0ab9MARy5FFAvYHL3lWb57bhkbt2GtzQ== + dependencies: + "@cosmjs/encoding" "^0.30.1" + "@cosmjs/math" "^0.30.1" + "@cosmjs/utils" "^0.30.1" + "@noble/hashes" "^1" + bn.js "^5.2.0" + elliptic "^6.5.4" + libsodium-wrappers "^0.7.6" + +"@cosmjs/encoding@^0.30.1", "@cosmjs/encoding@~0.30.0": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.30.1.tgz#b5c4e0ef7ceb1f2753688eb96400ed70f35c6058" + integrity sha512-rXmrTbgqwihORwJ3xYhIgQFfMSrwLu1s43RIK9I8EBudPx3KmnmyAKzMOVsRDo9edLFNuZ9GIvysUCwQfq3WlQ== + dependencies: + base64-js "^1.3.0" + bech32 "^1.1.4" + readonly-date "^1.0.0" + +"@cosmjs/json-rpc@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.30.1.tgz#16f21305fc167598c8a23a45549b85106b2372bc" + integrity sha512-pitfC/2YN9t+kXZCbNuyrZ6M8abnCC2n62m+JtU9vQUfaEtVsgy+1Fk4TRQ175+pIWSdBMFi2wT8FWVEE4RhxQ== + dependencies: + "@cosmjs/stream" "^0.30.1" + xstream "^11.14.0" + +"@cosmjs/math@^0.30.1", "@cosmjs/math@~0.30.0": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.30.1.tgz#8b816ef4de5d3afa66cb9fdfb5df2357a7845b8a" + integrity sha512-yaoeI23pin9ZiPHIisa6qqLngfnBR/25tSaWpkTm8Cy10MX70UF5oN4+/t1heLaM6SSmRrhk3psRkV4+7mH51Q== + dependencies: + bn.js "^5.2.0" + +"@cosmjs/proto-signing@^0.30.1", "@cosmjs/proto-signing@~0.30.0": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.30.1.tgz#f0dda372488df9cd2677150b89b3e9c72b3cb713" + integrity sha512-tXh8pPYXV4aiJVhTKHGyeZekjj+K9s2KKojMB93Gcob2DxUjfKapFYBMJSgfKPuWUPEmyr8Q9km2hplI38ILgQ== + dependencies: + "@cosmjs/amino" "^0.30.1" + "@cosmjs/crypto" "^0.30.1" + "@cosmjs/encoding" "^0.30.1" + "@cosmjs/math" "^0.30.1" + "@cosmjs/utils" "^0.30.1" + cosmjs-types "^0.7.1" + long "^4.0.0" + +"@cosmjs/socket@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.30.1.tgz#00b22f4b5e2ab01f4d82ccdb7b2e59536bfe5ce0" + integrity sha512-r6MpDL+9N+qOS/D5VaxnPaMJ3flwQ36G+vPvYJsXArj93BjgyFB7BwWwXCQDzZ+23cfChPUfhbINOenr8N2Kow== + dependencies: + "@cosmjs/stream" "^0.30.1" + isomorphic-ws "^4.0.1" + ws "^7" + xstream "^11.14.0" + +"@cosmjs/stargate@~0.30.0": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.30.1.tgz#e1b22e1226cffc6e93914a410755f1f61057ba04" + integrity sha512-RdbYKZCGOH8gWebO7r6WvNnQMxHrNXInY/gPHPzMjbQF6UatA6fNM2G2tdgS5j5u7FTqlCI10stNXrknaNdzog== + dependencies: + "@confio/ics23" "^0.6.8" + "@cosmjs/amino" "^0.30.1" + "@cosmjs/encoding" "^0.30.1" + "@cosmjs/math" "^0.30.1" + "@cosmjs/proto-signing" "^0.30.1" + "@cosmjs/stream" "^0.30.1" + "@cosmjs/tendermint-rpc" "^0.30.1" + "@cosmjs/utils" "^0.30.1" + cosmjs-types "^0.7.1" + long "^4.0.0" + protobufjs "~6.11.3" + xstream "^11.14.0" + +"@cosmjs/stream@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.30.1.tgz#ba038a2aaf41343696b1e6e759d8e03a9516ec1a" + integrity sha512-Fg0pWz1zXQdoxQZpdHRMGvUH5RqS6tPv+j9Eh7Q953UjMlrwZVo0YFLC8OTf/HKVf10E4i0u6aM8D69Q6cNkgQ== + dependencies: + xstream "^11.14.0" + +"@cosmjs/tendermint-rpc@^0.30.1", "@cosmjs/tendermint-rpc@~0.30.0": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.30.1.tgz#c16378892ba1ac63f72803fdf7567eab9d4f0aa0" + integrity sha512-Z3nCwhXSbPZJ++v85zHObeUggrEHVfm1u18ZRwXxFE9ZMl5mXTybnwYhczuYOl7KRskgwlB+rID0WYACxj4wdQ== + dependencies: + "@cosmjs/crypto" "^0.30.1" + "@cosmjs/encoding" "^0.30.1" + "@cosmjs/json-rpc" "^0.30.1" + "@cosmjs/math" "^0.30.1" + "@cosmjs/socket" "^0.30.1" + "@cosmjs/stream" "^0.30.1" + "@cosmjs/utils" "^0.30.1" + axios "^0.21.2" + readonly-date "^1.0.0" + xstream "^11.14.0" + +"@cosmjs/utils@^0.30.1", "@cosmjs/utils@~0.30.0": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.30.1.tgz#6d92582341be3c2ec8d82090253cfa4b7f959edb" + integrity sha512-KvvX58MGMWh7xA+N+deCfunkA/ZNDvFLw4YbOmX3f/XBIkqrVY7qlotfy2aNb1kgp6h4B6Yc8YawJPDTfvWX7g== + +"@credo-ts/anoncreds@file:./packages/anoncreds": + version "0.5.6" + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@credo-ts/core" "file:../../Library/Caches/Yarn/v6/npm-@credo-ts-anoncreds-0.5.6-f2626540-29ca-43db-b13d-cf64dc4ec27a-1721143792844/node_modules/@credo-ts/core" + "@sphereon/pex-models" "^2.2.4" + big-integer "^1.6.51" + bn.js "^5.2.1" + class-transformer "0.5.1" + class-validator "0.14.1" + reflect-metadata "^0.1.13" + +"@credo-ts/anoncreds@workspace:*": + version "0.5.6" + resolved "https://registry.yarnpkg.com/@credo-ts/anoncreds/-/anoncreds-0.5.6.tgz#c9151c68139b489198f64e4272b38afc837e69a6" + integrity sha512-gB/8oqL2xOtJ9XjXHE2PrSKK/BwlqYjYWxeZ0K5XzJjEko1tatVENQDLNXQUVifRiN1WLHbGZ3ovZSyjRDfolw== + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@credo-ts/core" "0.5.6" + "@sphereon/pex-models" "^2.2.4" + big-integer "^1.6.51" + bn.js "^5.2.1" + class-transformer "0.5.1" + class-validator "0.14.1" + reflect-metadata "^0.1.13" + +"@credo-ts/askar@workspace:*": + version "0.5.6" + resolved "https://registry.yarnpkg.com/@credo-ts/askar/-/askar-0.5.6.tgz#ee4258fbf4a8cc8360e6ccf3d3d2831a6b63d133" + integrity sha512-WmuAPE1Wp57pmVpV2wBD6hj8bJhPCndwKZB4+0UX21vnvSvLYpPXgNgV/DmEprqvyqG9X/I3qrzeV7JXqaBQZw== + dependencies: + "@credo-ts/core" "0.5.6" + bn.js "^5.2.1" + class-transformer "0.5.1" + class-validator "0.14.1" + rxjs "^7.8.0" + tsyringe "^4.8.0" + +"@credo-ts/cheqd@workspace:*": + version "0.5.6" + resolved "https://registry.yarnpkg.com/@credo-ts/cheqd/-/cheqd-0.5.6.tgz#c7aefa8a4b14ee25580ce6092945a76185a96924" + integrity sha512-1sBIIfbP8bNvpTqQYzu6qvyULzuYz1M3un7FYcnvRFm/JxRar1jCbrtEK6nFZ6/8pcnaVPTYKi55V8YKJIyu2Q== + dependencies: + "@cheqd/sdk" "^2.4.4" + "@cheqd/ts-proto" "~2.2.0" + "@cosmjs/crypto" "~0.30.0" + "@cosmjs/proto-signing" "~0.30.0" + "@cosmjs/stargate" "~0.30.0" + "@credo-ts/anoncreds" "0.5.6" + "@credo-ts/core" "0.5.6" + "@stablelib/ed25519" "^1.0.3" + class-transformer "^0.5.1" + class-validator "0.14.1" + rxjs "^7.8.0" + tsyringe "^4.8.0" + +"@credo-ts/core@file:./packages/core": + version "0.5.6" + dependencies: + "@digitalcredentials/jsonld" "^6.0.0" + "@digitalcredentials/jsonld-signatures" "^9.4.0" + "@digitalcredentials/vc" "^6.0.1" + "@multiformats/base-x" "^4.0.1" + "@sd-jwt/core" "^0.7.0" + "@sd-jwt/decode" "^0.7.0" + "@sd-jwt/jwt-status-list" "^0.7.0" + "@sd-jwt/sd-jwt-vc" "^0.7.0" + "@sd-jwt/types" "^0.7.0" + "@sd-jwt/utils" "^0.7.0" + "@sphereon/pex" "^3.3.2" + "@sphereon/pex-models" "^2.2.4" + "@sphereon/ssi-types" "^0.23.0" + "@stablelib/ed25519" "^1.0.2" + "@stablelib/sha256" "^1.0.1" + "@types/ws" "^8.5.4" + abort-controller "^3.0.0" + big-integer "^1.6.51" + borc "^3.0.0" + buffer "^6.0.3" + class-transformer "0.5.1" + class-validator "0.14.1" + did-resolver "^4.1.0" + jsonpath "^1.1.1" + lru_map "^0.4.1" + luxon "^3.3.0" + make-error "^1.3.6" + object-inspect "^1.10.3" + query-string "^7.0.1" + reflect-metadata "^0.1.13" + rxjs "^7.8.0" + tsyringe "^4.8.0" + uuid "^9.0.0" + varint "^6.0.0" + web-did-resolver "^2.0.21" + +"@credo-ts/core@workspace:*": + version "0.5.6" + resolved "https://registry.yarnpkg.com/@credo-ts/core/-/core-0.5.6.tgz#0484d27dd322ca520d2140856e1ed99266fc767d" + integrity sha512-zKNYx9IafKNvHTreU2UCISLJEx/4fBmtEnt5RBuu4WtztwGSFhlo9ILYEWCjU2I0pcZp5Ei7p8V7CuU63oj3BA== + dependencies: + "@digitalcredentials/jsonld" "^6.0.0" + "@digitalcredentials/jsonld-signatures" "^9.4.0" + "@digitalcredentials/vc" "^6.0.1" + "@multiformats/base-x" "^4.0.1" + "@sd-jwt/core" "^0.7.0" + "@sd-jwt/decode" "^0.7.0" + "@sd-jwt/jwt-status-list" "^0.7.0" + "@sd-jwt/sd-jwt-vc" "^0.7.0" + "@sd-jwt/types" "^0.7.0" + "@sd-jwt/utils" "^0.7.0" + "@sphereon/pex" "^3.3.2" + "@sphereon/pex-models" "^2.2.4" + "@sphereon/ssi-types" "^0.23.0" + "@stablelib/ed25519" "^1.0.2" + "@stablelib/sha256" "^1.0.1" + "@types/ws" "^8.5.4" + abort-controller "^3.0.0" + big-integer "^1.6.51" + borc "^3.0.0" + buffer "^6.0.3" + class-transformer "0.5.1" + class-validator "0.14.1" + did-resolver "^4.1.0" + jsonpath "^1.1.1" + lru_map "^0.4.1" + luxon "^3.3.0" + make-error "^1.3.6" + object-inspect "^1.10.3" + query-string "^7.0.1" + reflect-metadata "^0.1.13" + rxjs "^7.8.0" + tsyringe "^4.8.0" + uuid "^9.0.0" + varint "^6.0.0" + web-did-resolver "^2.0.21" + +"@credo-ts/indy-vdr@workspace:*": + version "0.5.6" + resolved "https://registry.yarnpkg.com/@credo-ts/indy-vdr/-/indy-vdr-0.5.6.tgz#04d423d23769625428bbcaa86f7f707f3af78f90" + integrity sha512-cTtYviqsrf+zymEYgHmF+i3isqyOjRL5Yov8vXss1KrYaakEUCCfG/NNZl7guQrRx91TeA6l463hAlN8Apdlvg== + dependencies: + "@credo-ts/anoncreds" "0.5.6" + "@credo-ts/core" "0.5.6" + +"@credo-ts/node@workspace:*": + version "0.5.6" + resolved "https://registry.yarnpkg.com/@credo-ts/node/-/node-0.5.6.tgz#70d71f3ae5a3e9b2bfd95ba46c524645d2ca5e2a" + integrity sha512-yEFmJEjQh7QSKrKCbiN6hoZaHHAsEvX9QEGPeF+pFsDT4j4KLR7Rl+fhRCmra+DuVTv8Y8Xa/+HB50OMGPNWsQ== + dependencies: + "@2060.io/ffi-napi" "^4.0.9" + "@2060.io/ref-napi" "^3.0.6" + "@credo-ts/core" "0.5.6" + "@types/express" "^4.17.15" + express "^4.17.1" + ws "^8.13.0" + +"@credo-ts/openid4vc@workspace:*": + version "0.5.6" + resolved "https://registry.yarnpkg.com/@credo-ts/openid4vc/-/openid4vc-0.5.6.tgz#70ea695bfbd43fdd6df080ee34797c2e6346c7d0" + integrity sha512-R0qGAg+DqdoEvOHns4VBxJkLgolDMtFVJnKF5IcebIIwhL8Ltp0HrCcDXgF7OHJfmbFwpV2mWGoQVS5xf6JXJQ== + dependencies: + "@credo-ts/core" "0.5.6" + "@sphereon/did-auth-siop" "^0.6.4" + "@sphereon/oid4vci-client" "^0.10.3" + "@sphereon/oid4vci-common" "^0.10.3" + "@sphereon/oid4vci-issuer" "^0.10.3" + "@sphereon/ssi-types" "^0.23.0" + class-transformer "^0.5.1" + rxjs "^7.8.0" + +"@credo-ts/tenants@workspace:*": + version "0.5.6" + resolved "https://registry.yarnpkg.com/@credo-ts/tenants/-/tenants-0.5.6.tgz#77f152c641c449f519b5b989a09b537d3c7cfce6" + integrity sha512-Vm2Je7/fsimoIm62ZP+r3rZ5mnEjxBXAkqVzRcqqeAN3xNdPAEz5tuhuE47ejnEs/QB1C0PJdGLadGpptET99A== + dependencies: + "@credo-ts/core" "0.5.6" + async-mutex "^0.4.0" + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@digitalbazaar/bitstring@^3.0.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@digitalbazaar/bitstring/-/bitstring-3.1.0.tgz#bbbacb80eaaa53594723a801879b3a95a0401b11" + integrity sha512-Cii+Sl++qaexOvv3vchhgZFfSmtHPNIPzGegaq4ffPnflVXFu+V2qrJ17aL2+gfLxrlC/zazZFuAltyKTPq7eg== + dependencies: + base64url-universal "^2.0.0" + pako "^2.0.4" + +"@digitalbazaar/http-client@^3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@digitalbazaar/http-client/-/http-client-3.4.1.tgz#5116fc44290d647cfe4b615d1f3fad9d6005e44d" + integrity sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g== + dependencies: + ky "^0.33.3" + ky-universal "^0.11.0" + undici "^5.21.2" + +"@digitalbazaar/security-context@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@digitalbazaar/security-context/-/security-context-1.0.1.tgz#badc4b8da03411a32d4e7321ce7c4b355776b410" + integrity sha512-0WZa6tPiTZZF8leBtQgYAfXQePFQp2z5ivpCEN/iZguYYZ0TB9qRmWtan5XH6mNFuusHtMcyIzAcReyE6rZPhA== + +"@digitalbazaar/vc-status-list-context@^3.0.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@digitalbazaar/vc-status-list-context/-/vc-status-list-context-3.1.1.tgz#cbe570d8d6d39d7b636bf1fce3c5601e2d104696" + integrity sha512-cMVtd+EV+4KN2kUG4/vsV74JVsGE6dcpod6zRoFB/AJA2W/sZbJqR44KL3G6P262+GcAECNhtnSsKsTnQ6y8+w== + +"@digitalbazaar/vc-status-list@^7.0.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@digitalbazaar/vc-status-list/-/vc-status-list-7.1.0.tgz#1d585a1766106e1586e1e2f87092dd0381b3f036" + integrity sha512-p5uxKJlX13N8TcTuv9qFDeej+6bndU+Rh1Cez2MT+bXQE6Jpn5t336FBSHmcECB4yUfZQpkmV/LOcYU4lW8Ojw== + dependencies: + "@digitalbazaar/bitstring" "^3.0.0" + "@digitalbazaar/vc" "^5.0.0" + "@digitalbazaar/vc-status-list-context" "^3.0.1" + credentials-context "^2.0.0" + +"@digitalbazaar/vc@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@digitalbazaar/vc/-/vc-5.0.0.tgz#20180fb492cb755eb2c6b6df9a17f7407d5e4b5a" + integrity sha512-XmLM7Ag5W+XidGnFuxFIyUFSMnHnWEMJlHei602GG94+WzFJ6Ik8txzPQL8T18egSoiTsd1VekymbIlSimhuaQ== + dependencies: + credentials-context "^2.0.0" + jsonld "^8.0.0" + jsonld-signatures "^11.0.0" + +"@digitalcredentials/base58-universal@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@digitalcredentials/base58-universal/-/base58-universal-1.0.1.tgz#41b5a16cdeaac9cf01b23f1e564c560c2599b607" + integrity sha512-1xKdJnfITMvrF/sCgwBx2C4p7qcNAARyIvrAOZGqIHmBaT/hAenpC8bf44qVY+UIMuCYP23kqpIfJQebQDThDQ== + +"@digitalcredentials/base64url-universal@^2.0.2": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@digitalcredentials/base64url-universal/-/base64url-universal-2.0.6.tgz#43c59c62a33b024e7adc3c56403d18dbcb61ec61" + integrity sha512-QJyK6xS8BYNnkKLhEAgQc6Tb9DMe+GkHnBAWJKITCxVRXJAFLhJnr+FsJnCThS3x2Y0UiiDAXoWjwMqtUrp4Kg== + dependencies: + base64url "^3.0.1" + +"@digitalcredentials/bitstring@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@digitalcredentials/bitstring/-/bitstring-2.0.1.tgz#bb887f1d0999980598754e426d831c96a26a3863" + integrity sha512-9priXvsEJGI4LYHPwLqf5jv9HtQGlG0MgeuY8Q4NHN+xWz5rYMylh1TYTVThKa3XI6xF2pR2oEfKZD21eWXveQ== + dependencies: + "@digitalcredentials/base64url-universal" "^2.0.2" + pako "^2.0.4" + +"@digitalcredentials/ed25519-signature-2020@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/ed25519-signature-2020/-/ed25519-signature-2020-3.0.2.tgz#2df8fb6f814a1964b40ebb3402d41630c30120da" + integrity sha512-R8IrR21Dh+75CYriQov3nVHKaOVusbxfk9gyi6eCAwLHKn6fllUt+2LQfuUrL7Ts/sGIJqQcev7YvkX9GvyYRA== + dependencies: + "@digitalcredentials/base58-universal" "^1.0.1" + "@digitalcredentials/ed25519-verification-key-2020" "^3.1.1" + "@digitalcredentials/jsonld-signatures" "^9.3.1" + ed25519-signature-2018-context "^1.1.0" + ed25519-signature-2020-context "^1.0.1" + +"@digitalcredentials/ed25519-verification-key-2020@^3.1.1": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/ed25519-verification-key-2020/-/ed25519-verification-key-2020-3.2.2.tgz#cdf271bf4bb44dd2c417dcde6d7a0436e31d84ca" + integrity sha512-ZfxNFZlA379MZpf+gV2tUYyiZ15eGVgjtCQLWlyu3frWxsumUgv++o0OJlMnrDsWGwzFMRrsXcosd5+752rLOA== + dependencies: + "@digitalcredentials/base58-universal" "^1.0.1" + "@stablelib/ed25519" "^1.0.1" + base64url-universal "^1.1.0" + crypto-ld "^6.0.0" + +"@digitalcredentials/http-client@^1.0.0": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/http-client/-/http-client-1.2.2.tgz#8b09ab6f1e3aa8878d91d3ca51946ca8265cc92e" + integrity sha512-YOwaE+vUDSwiDhZT0BbXSWVg+bvp1HA1eg/gEc8OCwCOj9Bn9FRQdu8P9Y/fnYqyFCioDwwTRzGxgJLl50baEg== + dependencies: + ky "^0.25.1" + ky-universal "^0.8.2" + +"@digitalcredentials/jsonld-signatures@^9.3.1", "@digitalcredentials/jsonld-signatures@^9.3.2", "@digitalcredentials/jsonld-signatures@^9.4.0": + version "9.4.0" + resolved "https://registry.yarnpkg.com/@digitalcredentials/jsonld-signatures/-/jsonld-signatures-9.4.0.tgz#d5881122c4202449b88a7e2384f8e615ae55582c" + integrity sha512-DnR+HDTm7qpcDd0wcD1w6GdlAwfHjQSgu+ahion8REkCkkMRywF+CLunU7t8AZpFB2Gr/+N8naUtiEBNje1Oew== + dependencies: + "@digitalbazaar/security-context" "^1.0.0" + "@digitalcredentials/jsonld" "^6.0.0" + fast-text-encoding "^1.0.3" + isomorphic-webcrypto "^2.3.8" + serialize-error "^8.0.1" + +"@digitalcredentials/jsonld@^5.2.1": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/jsonld/-/jsonld-5.2.2.tgz#d2bdefe25788ece77e900a9491c64c2187e3344c" + integrity sha512-hz7YR3kv6+8UUdgMyTGl1o8NjVKKwnMry/Rh/rWeAvwL+NqgoUHorWzI3rM+PW+MPFyDC0ieXStClt9n9D9SGA== + dependencies: + "@digitalcredentials/http-client" "^1.0.0" + "@digitalcredentials/rdf-canonize" "^1.0.0" + canonicalize "^1.0.1" + lru-cache "^6.0.0" + +"@digitalcredentials/jsonld@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@digitalcredentials/jsonld/-/jsonld-6.0.0.tgz#05d34cb1d81c4bbdfacf61f8958bbaede33be598" + integrity sha512-5tTakj0/GsqAJi8beQFVMQ97wUJZnuxViW9xRuAATL6eOBIefGBwHkVryAgEq2I4J/xKgb/nEyw1ZXX0G8wQJQ== + dependencies: + "@digitalcredentials/http-client" "^1.0.0" + "@digitalcredentials/rdf-canonize" "^1.0.0" + canonicalize "^1.0.1" + lru-cache "^6.0.0" + +"@digitalcredentials/open-badges-context@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@digitalcredentials/open-badges-context/-/open-badges-context-2.1.0.tgz#cefd29af4642adf8feeed5bb7ede663b14913c2f" + integrity sha512-VK7X5u6OoBFxkyIFplNqUPVbo+8vFSAEoam8tSozpj05KPfcGw41Tp5p9fqMnY38oPfwtZR2yDNSctj/slrE0A== + +"@digitalcredentials/rdf-canonize@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@digitalcredentials/rdf-canonize/-/rdf-canonize-1.0.0.tgz#6297d512072004c2be7f280246383a9c4b0877ff" + integrity sha512-z8St0Ex2doecsExCFK1uI4gJC+a5EqYYu1xpRH1pKmqSS9l/nxfuVxexNFyaeEum4dUdg1EetIC2rTwLIFhPRA== + dependencies: + fast-text-encoding "^1.0.3" + isomorphic-webcrypto "^2.3.8" + +"@digitalcredentials/vc-status-list@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/vc-status-list/-/vc-status-list-5.0.2.tgz#9de8b23b6d533668a354ff464a689ecc42f24445" + integrity sha512-PI0N7SM0tXpaNLelbCNsMAi34AjOeuhUzMSYTkHdeqRPX7oT2F3ukyOssgr4koEqDxw9shHtxHu3fSJzrzcPMQ== + dependencies: + "@digitalbazaar/vc-status-list-context" "^3.0.1" + "@digitalcredentials/bitstring" "^2.0.1" + "@digitalcredentials/vc" "^4.1.1" + credentials-context "^2.0.0" + +"@digitalcredentials/vc@^4.1.1": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@digitalcredentials/vc/-/vc-4.2.0.tgz#d2197b26547d670965d5969a9e49437f244b5944" + integrity sha512-8Rxpn77JghJN7noBQdcMuzm/tB8vhDwPoFepr3oGd5w+CyJxOk2RnBlgIGlAAGA+mALFWECPv1rANfXno+hdjA== + dependencies: + "@digitalcredentials/jsonld" "^5.2.1" + "@digitalcredentials/jsonld-signatures" "^9.3.1" + credentials-context "^2.0.0" + +"@digitalcredentials/vc@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@digitalcredentials/vc/-/vc-6.0.1.tgz#e4bdbac37d677c5288f2ad8d9ea59c3b41e0fd78" + integrity sha512-TZgLoi00Jc9uv3b6jStH+G8+bCqpHIqFw9DYODz+fVjNh197ksvcYqSndUDHa2oi0HCcK+soI8j4ba3Sa4Pl4w== + dependencies: + "@digitalbazaar/vc-status-list" "^7.0.0" + "@digitalcredentials/ed25519-signature-2020" "^3.0.2" + "@digitalcredentials/jsonld" "^6.0.0" + "@digitalcredentials/jsonld-signatures" "^9.3.2" + "@digitalcredentials/open-badges-context" "^2.1.0" + "@digitalcredentials/vc-status-list" "^5.0.2" + credentials-context "^2.0.0" + fix-esm "^1.0.1" + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": + version "4.11.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae" + integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^12.1.0" - ignore "^4.0.6" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" + js-yaml "^4.1.0" + minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== + +"@fastify/busboy@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" + integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== + +"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== + dependencies: + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@hyperledger/anoncreds-nodejs@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.2.tgz#5344427c554fc7280efe22f2061e3a26023fdd84" + integrity sha512-qRMSSyERwjAVCPlHjCAY3OJno4DNIJ0uLi+g6ek7HrFVich3X6Kzr0ng/MSiDKmTBXyGiip1zDIBABA8y3yNGg== + dependencies: + "@2060.io/ffi-napi" "^4.0.9" + "@2060.io/ref-napi" "^3.0.6" + "@hyperledger/anoncreds-shared" "0.2.2" + "@mapbox/node-pre-gyp" "^1.0.11" + ref-array-di "1.2.2" + ref-struct-di "1.1.1" + +"@hyperledger/anoncreds-shared@0.2.2", "@hyperledger/anoncreds-shared@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.2.tgz#492995d076448d682a828312bbba58314e943c4a" + integrity sha512-dfYpqbAkqtHJkRkuGmWdJruHfLegLUIbu/dSAWnz5dMRKd8ad8rEnQkwRnockQZ/pc7QDr8kxfG6bT2YDGZeMw== + +"@hyperledger/aries-askar-nodejs@^0.2.1": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-nodejs/-/aries-askar-nodejs-0.2.3.tgz#f939c19047c78b9903b422f992b4f5406ac3aae3" + integrity sha512-2BnGqK08Y96DEB8tDuXy2x+soetChyMGB0+L1yqdHx1Xv5FvRerYrTXdTjJXTW6ANb48k2Np8WlJ4YNePSo6ww== + dependencies: + "@2060.io/ffi-napi" "^4.0.9" + "@2060.io/ref-napi" "^3.0.6" + "@hyperledger/aries-askar-shared" "0.2.3" + "@mapbox/node-pre-gyp" "^1.0.11" + ref-array-di "^1.2.2" + ref-struct-di "^1.1.1" + +"@hyperledger/aries-askar-shared@0.2.3", "@hyperledger/aries-askar-shared@^0.2.1": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-shared/-/aries-askar-shared-0.2.3.tgz#670fea675982e3b0a2f416131b04d0f1e058cd1b" + integrity sha512-g9lao8qa80kPCLqqp02ovNqEfQIrm6cAf4xZVzD5P224VmOhf4zM6AKplQTvQx7USNKoXroe93JrOOSVxPeqrA== + dependencies: + buffer "^6.0.3" + +"@hyperledger/indy-vdr-nodejs@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-nodejs/-/indy-vdr-nodejs-0.2.2.tgz#f1f5ed1b9c34103703882dbc6c10fe480d33b0e6" + integrity sha512-mc0iKuHCtKuOV0sMnGOTVWnQrpfBMS+1tIRyob+CvGCvwC2llGo3Hu5AvgPcR9nqCo/wJ0LoKBo66dYYb0uZbw== + dependencies: + "@2060.io/ffi-napi" "^4.0.9" + "@2060.io/ref-napi" "^3.0.6" + "@hyperledger/indy-vdr-shared" "0.2.2" + "@mapbox/node-pre-gyp" "^1.0.10" + ref-array-di "^1.2.2" + ref-struct-di "^1.1.1" + +"@hyperledger/indy-vdr-shared@0.2.2", "@hyperledger/indy-vdr-shared@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-shared/-/indy-vdr-shared-0.2.2.tgz#9ca8b56cd89ab18792d129a0358b641e211274e3" + integrity sha512-9425MHU3K+/ahccCRjOIX3Z/51gqxvp3Nmyujyqlx9cd7PWG2Rianx7iNWecFBkdAEqS0DfHsb6YqqH39YZp/A== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -316,612 +1651,1581 @@ js-yaml "^3.13.1" resolve-from "^5.0.0" -"@istanbuljs/schema@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" - integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== +"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" - integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^26.6.2" - jest-util "^26.6.2" + jest-message-util "^29.7.0" + jest-util "^29.7.0" slash "^3.0.0" -"@jest/core@^26.6.3": - version "26.6.3" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" - integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== dependencies: - "@jest/console" "^26.6.2" - "@jest/reporters" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" + ci-info "^3.2.0" exit "^0.1.2" - graceful-fs "^4.2.4" - jest-changed-files "^26.6.2" - jest-config "^26.6.3" - jest-haste-map "^26.6.2" - jest-message-util "^26.6.2" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-resolve-dependencies "^26.6.3" - jest-runner "^26.6.3" - jest-runtime "^26.6.3" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" - jest-watcher "^26.6.2" - micromatch "^4.0.2" - p-each-series "^2.1.0" - rimraf "^3.0.0" + graceful-fs "^4.2.9" + jest-changed-files "^29.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/create-cache-key-function@^29.2.1": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz#793be38148fab78e65f40ae30c36785f4ad859f0" + integrity sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA== + dependencies: + "@jest/types" "^29.6.3" + +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== + dependencies: + expect "^29.7.0" + jest-snapshot "^29.7.0" + +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" + +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^6.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" slash "^3.0.0" + string-length "^4.0.1" strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== + dependencies: + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" -"@jest/environment@^26.6.2": +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== + dependencies: + "@jest/test-result" "^29.7.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + slash "^3.0.0" + +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.2.tgz#ba364cc72e221e79cc8f0a99555bf5d7577cf92c" - integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== dependencies: - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" "@types/node" "*" - jest-mock "^26.6.2" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + +"@jest/types@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@manypkg/find-root@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@manypkg/find-root/-/find-root-1.1.0.tgz#a62d8ed1cd7e7d4c11d9d52a8397460b5d4ad29f" + integrity sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA== + dependencies: + "@babel/runtime" "^7.5.5" + "@types/node" "^12.7.1" + find-up "^4.1.0" + fs-extra "^8.1.0" + +"@manypkg/get-packages@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@manypkg/get-packages/-/get-packages-1.1.3.tgz#e184db9bba792fa4693de4658cfb1463ac2c9c47" + integrity sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A== + dependencies: + "@babel/runtime" "^7.5.5" + "@changesets/types" "^4.0.1" + "@manypkg/find-root" "^1.1.0" + fs-extra "^8.1.0" + globby "^11.0.0" + read-yaml-file "^1.1.0" + +"@mapbox/node-pre-gyp@1.0.11", "@mapbox/node-pre-gyp@^1.0.10", "@mapbox/node-pre-gyp@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" + integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + +"@mattrglobal/bbs-signatures@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@mattrglobal/bbs-signatures/-/bbs-signatures-1.3.1.tgz#ed00b9c5bb5ea7fb4ca1dc6316a32d0618acc82e" + integrity sha512-syZGkapPpktD2el4lPTCQRw/LSia6/NwBS83hzCKu4dTlaJRO636qo5NCiiQb+iBYWyZQQEzN0jdRik8N9EUGA== + dependencies: + "@stablelib/random" "1.0.0" + optionalDependencies: + "@mattrglobal/node-bbs-signatures" "0.18.1" + +"@mattrglobal/bbs-signatures@^1.0.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@mattrglobal/bbs-signatures/-/bbs-signatures-1.4.0.tgz#1ee59b81303e2e04925008a1a1dc1a375392aaa0" + integrity sha512-uBK1IWw48fqloO9W/yoDncTs9rfwfbG/53cOrrCQL7XkyZe4DtB40HcLbi3i+yxTYs5wytf1Qr4Z5RpzpW10jw== + dependencies: + "@stablelib/random" "1.0.0" + optionalDependencies: + "@mattrglobal/node-bbs-signatures" "0.18.1" + +"@mattrglobal/bls12381-key-pair@^1.0.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@mattrglobal/bls12381-key-pair/-/bls12381-key-pair-1.2.1.tgz#4c6625bd9375c4bd0702a275d22c7de12c7aba1e" + integrity sha512-Xh63NP1iSGBLW10N5uRpDyoPo2LtNHHh/TRGVJEHRgo+07yxgl8tS06Q2zO9gN9+b+GU5COKvR3lACwrvn+MYw== + dependencies: + "@mattrglobal/bbs-signatures" "1.3.1" + bs58 "4.0.1" + rfc4648 "1.5.2" + +"@mattrglobal/node-bbs-signatures@0.18.1": + version "0.18.1" + resolved "https://registry.yarnpkg.com/@mattrglobal/node-bbs-signatures/-/node-bbs-signatures-0.18.1.tgz#f313da682bdd9b592e7b65549cbbb8f67b0ce583" + integrity sha512-s9ccL/1TTvCP1N//4QR84j/d5D/stx/AI1kPcRgiE4O3KrxyF7ZdL9ca8fmFuN6yh9LAbn/OiGRnOXgvn38Dgg== + dependencies: + "@mapbox/node-pre-gyp" "1.0.11" + neon-cli "0.10.1" + +"@multiformats/base-x@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@multiformats/base-x/-/base-x-4.0.1.tgz#95ff0fa58711789d53aefb2590a8b7a4e715d121" + integrity sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw== + +"@noble/hashes@^1", "@noble/hashes@^1.0.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@peculiar/asn1-schema@^2.3.8": + version "2.3.8" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz#04b38832a814e25731232dd5be883460a156da3b" + integrity sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA== + dependencies: + asn1js "^3.0.5" + pvtsutils "^1.3.5" + tslib "^2.6.2" + +"@peculiar/json-schema@^1.1.12": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.12.tgz#fe61e85259e3b5ba5ad566cb62ca75b3d3cd5339" + integrity sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w== + dependencies: + tslib "^2.0.0" + +"@peculiar/webcrypto@^1.0.22": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.5.0.tgz#9e57174c02c1291051c553600347e12b81469e10" + integrity sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg== + dependencies: + "@peculiar/asn1-schema" "^2.3.8" + "@peculiar/json-schema" "^1.1.12" + pvtsutils "^1.3.5" + tslib "^2.6.2" + webcrypto-core "^1.8.0" + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + +"@react-native-community/cli-clean@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-10.1.1.tgz#4c73ce93a63a24d70c0089d4025daac8184ff504" + integrity sha512-iNsrjzjIRv9yb5y309SWJ8NDHdwYtnCpmxZouQDyOljUdC9MwdZ4ChbtA4rwQyAwgOVfS9F/j56ML3Cslmvrxg== + dependencies: + "@react-native-community/cli-tools" "^10.1.1" + chalk "^4.1.2" + execa "^1.0.0" + prompts "^2.4.0" + +"@react-native-community/cli-config@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-config/-/cli-config-10.1.1.tgz#08dcc5d7ca1915647dc06507ed853fe0c1488395" + integrity sha512-p4mHrjC+s/ayiNVG6T35GdEGdP6TuyBUg5plVGRJfTl8WT6LBfLYLk+fz/iETrEZ/YkhQIsQcEUQC47MqLNHog== + dependencies: + "@react-native-community/cli-tools" "^10.1.1" + chalk "^4.1.2" + cosmiconfig "^5.1.0" + deepmerge "^3.2.0" + glob "^7.1.3" + joi "^17.2.1" + +"@react-native-community/cli-debugger-ui@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-10.0.0.tgz#4bb6d41c7e46449714dc7ba5d9f5b41ef0ea7c57" + integrity sha512-8UKLcvpSNxnUTRy8CkCl27GGLqZunQ9ncGYhSrWyKrU9SWBJJGeZwi2k2KaoJi5FvF2+cD0t8z8cU6lsq2ZZmA== + dependencies: + serve-static "^1.13.1" + +"@react-native-community/cli-doctor@^10.2.7": + version "10.2.7" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-doctor/-/cli-doctor-10.2.7.tgz#1a3ecd18a7b1e685864c9dc95e014a5f8f225f84" + integrity sha512-MejE7m+63DxfKwFSvyZGfq+72jX0RSP9SdSmDbW0Bjz2NIEE3BsE8rNay+ByFbdSLsapRPvaZv2Jof+dK2Y/yg== + dependencies: + "@react-native-community/cli-config" "^10.1.1" + "@react-native-community/cli-platform-ios" "^10.2.5" + "@react-native-community/cli-tools" "^10.1.1" + chalk "^4.1.2" + command-exists "^1.2.8" + envinfo "^7.7.2" + execa "^1.0.0" + hermes-profile-transformer "^0.0.6" + node-stream-zip "^1.9.1" + ora "^5.4.1" + prompts "^2.4.0" + semver "^6.3.0" + strip-ansi "^5.2.0" + sudo-prompt "^9.0.0" + wcwidth "^1.0.1" + +"@react-native-community/cli-hermes@^10.2.7": + version "10.2.7" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-hermes/-/cli-hermes-10.2.7.tgz#bcb28dfe551562a68d13c787550319f9831576b4" + integrity sha512-MULfkgeLx1fietx10pLFLmlfRh0dcfX/HABXB5Tm0BzQDXy7ofFIJ/UxH+IF55NwPKXl6aEpTbPwbgsyJxqPiA== + dependencies: + "@react-native-community/cli-platform-android" "^10.2.0" + "@react-native-community/cli-tools" "^10.1.1" + chalk "^4.1.2" + hermes-profile-transformer "^0.0.6" + +"@react-native-community/cli-platform-android@10.2.0", "@react-native-community/cli-platform-android@^10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-10.2.0.tgz#0bc689270a5f1d9aaf9e723181d43ca4dbfffdef" + integrity sha512-CBenYwGxwFdObZTn1lgxWtMGA5ms2G/ALQhkS+XTAD7KHDrCxFF9yT/fnAjFZKM6vX/1TqGI1RflruXih3kAhw== + dependencies: + "@react-native-community/cli-tools" "^10.1.1" + chalk "^4.1.2" + execa "^1.0.0" + glob "^7.1.3" + logkitty "^0.7.1" + +"@react-native-community/cli-platform-ios@10.2.5", "@react-native-community/cli-platform-ios@^10.2.5": + version "10.2.5" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-10.2.5.tgz#7888c74b83099885bf9e6d52170c6e663ad971ee" + integrity sha512-hq+FZZuSBK9z82GLQfzdNDl8vbFx5UlwCLFCuTtNCROgBoapFtVZQKRP2QBftYNrQZ0dLAb01gkwxagHsQCFyg== + dependencies: + "@react-native-community/cli-tools" "^10.1.1" + chalk "^4.1.2" + execa "^1.0.0" + fast-xml-parser "^4.0.12" + glob "^7.1.3" + ora "^5.4.1" + +"@react-native-community/cli-plugin-metro@^10.2.3": + version "10.2.3" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-10.2.3.tgz#419e0155a50951c3329818fba51cb5021a7294f1" + integrity sha512-jHi2oDuTePmW4NEyVT8JEGNlIYcnFXCSV2ZMp4rnDrUk4TzzyvS3IMvDlESEmG8Kry8rvP0KSUx/hTpy37Sbkw== + dependencies: + "@react-native-community/cli-server-api" "^10.1.1" + "@react-native-community/cli-tools" "^10.1.1" + chalk "^4.1.2" + execa "^1.0.0" + metro "0.73.10" + metro-config "0.73.10" + metro-core "0.73.10" + metro-react-native-babel-transformer "0.73.10" + metro-resolver "0.73.10" + metro-runtime "0.73.10" + readline "^1.3.0" + +"@react-native-community/cli-server-api@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-server-api/-/cli-server-api-10.1.1.tgz#e382269de281bb380c2e685431364fbbb8c1cb3a" + integrity sha512-NZDo/wh4zlm8as31UEBno2bui8+ufzsZV+KN7QjEJWEM0levzBtxaD+4je0OpfhRIIkhaRm2gl/vVf7OYAzg4g== + dependencies: + "@react-native-community/cli-debugger-ui" "^10.0.0" + "@react-native-community/cli-tools" "^10.1.1" + compression "^1.7.1" + connect "^3.6.5" + errorhandler "^1.5.0" + nocache "^3.0.1" + pretty-format "^26.6.2" + serve-static "^1.13.1" + ws "^7.5.1" + +"@react-native-community/cli-tools@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-tools/-/cli-tools-10.1.1.tgz#fa66e509c0d3faa31f7bb87ed7d42ad63f368ddd" + integrity sha512-+FlwOnZBV+ailEzXjcD8afY2ogFEBeHOw/8+XXzMgPaquU2Zly9B+8W089tnnohO3yfiQiZqkQlElP423MY74g== + dependencies: + appdirsjs "^1.2.4" + chalk "^4.1.2" + find-up "^5.0.0" + mime "^2.4.1" + node-fetch "^2.6.0" + open "^6.2.0" + ora "^5.4.1" + semver "^6.3.0" + shell-quote "^1.7.3" + +"@react-native-community/cli-types@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-types/-/cli-types-10.0.0.tgz#046470c75ec18f8b3bd906e54e43a6f678e01a45" + integrity sha512-31oUM6/rFBZQfSmDQsT1DX/5fjqfxg7sf2u8kTPJK7rXVya5SRpAMaCXsPAG0omsmJxXt+J9HxUi3Ic+5Ux5Iw== + dependencies: + joi "^17.2.1" + +"@react-native-community/cli@10.2.7": + version "10.2.7" + resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-10.2.7.tgz#013c47827d138bcc5ac64fb5c5e792ff3350a0e8" + integrity sha512-31GrAP5PjHosXV5bkHWVnYGjAeka2gkTTsPqasJAki5RI1njB1a2WAkYFV0sn+gqc4RU1s96RELBBfT+EGzhAQ== + dependencies: + "@react-native-community/cli-clean" "^10.1.1" + "@react-native-community/cli-config" "^10.1.1" + "@react-native-community/cli-debugger-ui" "^10.0.0" + "@react-native-community/cli-doctor" "^10.2.7" + "@react-native-community/cli-hermes" "^10.2.7" + "@react-native-community/cli-plugin-metro" "^10.2.3" + "@react-native-community/cli-server-api" "^10.1.1" + "@react-native-community/cli-tools" "^10.1.1" + "@react-native-community/cli-types" "^10.0.0" + chalk "^4.1.2" + commander "^9.4.1" + execa "^1.0.0" + find-up "^4.1.0" + fs-extra "^8.1.0" + graceful-fs "^4.1.3" + prompts "^2.4.0" + semver "^6.3.0" + +"@react-native/assets@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@react-native/assets/-/assets-1.0.0.tgz#c6f9bf63d274bafc8e970628de24986b30a55c8e" + integrity sha512-KrwSpS1tKI70wuKl68DwJZYEvXktDHdZMG0k2AXD/rJVSlB23/X2CB2cutVR0HwNMJIal9HOUOBB2rVfa6UGtQ== + +"@react-native/normalize-color@2.1.0", "@react-native/normalize-color@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@react-native/normalize-color/-/normalize-color-2.1.0.tgz#939b87a9849e81687d3640c5efa2a486ac266f91" + integrity sha512-Z1jQI2NpdFJCVgpY+8Dq/Bt3d+YUi1928Q+/CZm/oh66fzM0RUl54vvuXlPJKybH4pdCZey1eDTPaLHkMPNgWA== + +"@react-native/polyfills@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa" + integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ== + +"@sd-jwt/core@0.7.1", "@sd-jwt/core@^0.7.0": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.7.1.tgz#9dd194f3987c2a5a6b2395bc971c6492069bf52a" + integrity sha512-7u7cNeYNYcNNgzDj+mSeHrloY/C44XsewdKzViMp+8jpQSi/TEeudM9CkR5wxx1KulvnGojHZfMygK8Arxey6g== + dependencies: + "@sd-jwt/decode" "0.7.1" + "@sd-jwt/present" "0.7.1" + "@sd-jwt/types" "0.7.1" + "@sd-jwt/utils" "0.7.1" + +"@sd-jwt/decode@0.6.1", "@sd-jwt/decode@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.6.1.tgz#141f7782df53bab7159a75d91ed4711e1c14a7ea" + integrity sha512-QgTIoYd5zyKKLgXB4xEYJTrvumVwtsj5Dog0v0L9UH9ZvHekDaeexS247X7A4iSdzTvmZzUpGskgABOa4D8NmQ== + dependencies: + "@sd-jwt/types" "0.6.1" + "@sd-jwt/utils" "0.6.1" + +"@sd-jwt/decode@0.7.1", "@sd-jwt/decode@^0.7.0": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.7.1.tgz#35dfe9ded911f91b99d68b0e418fe4923c3c4c59" + integrity sha512-jPNjwb9S0PqNULLLl3qR0NPpK0UePpzjB57QJEjEeY9Bdws5N5uANvyr7bF/MG496B+XZE1AugvnBtk4SQguVA== + dependencies: + "@sd-jwt/types" "0.7.1" + "@sd-jwt/utils" "0.7.1" + +"@sd-jwt/jwt-status-list@0.7.1", "@sd-jwt/jwt-status-list@^0.7.0": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/jwt-status-list/-/jwt-status-list-0.7.1.tgz#953dddedcd0682dc6bbe840a319b5796803d28a2" + integrity sha512-HeLluuKrixoAkaHO7buFjPpRuFIjICNGgvT5f4mH06bwrzj7uZ5VNNUWPK9Nb1jq8vHnMpIhpbnSSAmoaVWPEA== + dependencies: + "@sd-jwt/types" "0.7.1" + base64url "^3.0.1" + pako "^2.1.0" + +"@sd-jwt/present@0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/present/-/present-0.7.1.tgz#055774aec807146840c89f8df89863c924346a3a" + integrity sha512-X8ADyHq2DUYRy0snd0KXe9G9vOY8MwsP/1YsmgScEFUXfJM6LFhVNiBGS5uzUr6BkFYz6sFZ6WAHrdhg459J5A== + dependencies: + "@sd-jwt/decode" "0.7.1" + "@sd-jwt/types" "0.7.1" + "@sd-jwt/utils" "0.7.1" + +"@sd-jwt/present@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/present/-/present-0.6.1.tgz#82b9188becb0fa240897c397d84a54d55c7d169e" + integrity sha512-QRD3TUDLj4PqQNZ70bBxh8FLLrOE9mY8V9qiZrJSsaDOLFs2p1CtZG+v9ig62fxFYJZMf4bWKwYjz+qqGAtxCg== + dependencies: + "@sd-jwt/decode" "0.6.1" + "@sd-jwt/types" "0.6.1" + "@sd-jwt/utils" "0.6.1" + +"@sd-jwt/sd-jwt-vc@^0.7.0": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/sd-jwt-vc/-/sd-jwt-vc-0.7.1.tgz#7d33a375e209de49826787103ac63fcdd5edf4a6" + integrity sha512-iwAFoxQJbRAzYlahai3YCUqGzHZea69fJI3ct38iJG7IVKxsgBRj6SdACyS1opDNdZSst7McBl4aWyokzGgRvA== + dependencies: + "@sd-jwt/core" "0.7.1" + "@sd-jwt/jwt-status-list" "0.7.1" + "@sd-jwt/utils" "0.7.1" + +"@sd-jwt/types@0.6.1", "@sd-jwt/types@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.6.1.tgz#fc4235e00cf40d35a21d6bc02e44e12d7162aa9b" + integrity sha512-LKpABZJGT77jNhOLvAHIkNNmGqXzyfwBT+6r+DN9zNzMx1CzuNR0qXk1GMUbast9iCfPkGbnEpUv/jHTBvlIvg== + +"@sd-jwt/types@0.7.1", "@sd-jwt/types@^0.7.0": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.7.1.tgz#fa0ba87fe73190f1c473f230e11660d0ee7ba12a" + integrity sha512-rPXS+kWiDDznWUuRkvAeXTWOhYn2tb5dZLI3deepsXmofjhTGqMP89qNNNBqhnA99kJx9gxnUj/jpQgUm0MjmQ== + +"@sd-jwt/utils@0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.6.1.tgz#33273b20c9eb1954e4eab34118158b646b574ff9" + integrity sha512-1NHZ//+GecGQJb+gSdDicnrHG0DvACUk9jTnXA5yLZhlRjgkjyfJLNsCZesYeCyVp/SiyvIC9B+JwoY4kI0TwQ== + dependencies: + "@sd-jwt/types" "0.6.1" + js-base64 "^3.7.6" + +"@sd-jwt/utils@0.7.1", "@sd-jwt/utils@^0.7.0": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.7.1.tgz#daee353f2a12623dbc75bddc45141bcb9a9c90b5" + integrity sha512-Dx9QxhkBvHD7J52zir2+FNnXlPX55ON0Xc/VFKrBFxC1yHAU6/+pyLXRJMIQLampxqYlreIN9xo7gSipWcY1uQ== + dependencies: + "@sd-jwt/types" "0.7.1" + js-base64 "^3.7.6" + +"@sideway/address@^4.1.5": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" + integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sinonjs/commons@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + +"@sovpro/delimited-stream@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@sovpro/delimited-stream/-/delimited-stream-1.1.0.tgz#4334bba7ee241036e580fdd99c019377630d26b4" + integrity sha512-kQpk267uxB19X3X2T1mvNMjyvIEonpNSHrMlK5ZaBU6aZxw7wPbpgKJOjHN3+/GPVpXgAV9soVT2oyHpLkLtyw== + +"@sphereon/did-auth-siop@^0.6.4": + version "0.6.4" + resolved "https://registry.yarnpkg.com/@sphereon/did-auth-siop/-/did-auth-siop-0.6.4.tgz#7abf0d0e8d2aa0f4108b90c2d7f6186093a23019" + integrity sha512-0hw/lypy7kHpChJc/206XFd1XVhfUEIg2RIuw2u0RE3POqMeuOL5DWiPHh3e7Oo0nzG9gdgJC8Yffv69d9QIrg== + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@sphereon/did-uni-client" "^0.6.2" + "@sphereon/pex" "^3.3.2" + "@sphereon/pex-models" "^2.2.4" + "@sphereon/ssi-types" "0.22.0" + "@sphereon/wellknown-dids-client" "^0.1.3" + cross-fetch "^4.0.0" + did-jwt "6.11.6" + did-resolver "^4.1.0" + events "^3.3.0" + language-tags "^1.0.9" + multiformats "^12.1.3" + qs "^6.11.2" + sha.js "^2.4.11" + uint8arrays "^3.1.1" + uuid "^9.0.0" + +"@sphereon/did-uni-client@^0.6.2": + version "0.6.3" + resolved "https://registry.yarnpkg.com/@sphereon/did-uni-client/-/did-uni-client-0.6.3.tgz#91a96cb233c58bbc226e865874e02214e8f0f82e" + integrity sha512-g7LD7ofbE36slHN7Bhr5dwUrj6t0BuZeXBYJMaVY/pOeL1vJxW1cZHbZqu0NSfOmzyBg4nsYVlgTjyi/Aua2ew== + dependencies: + cross-fetch "^3.1.8" + did-resolver "^4.1.0" + +"@sphereon/oid4vci-client@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-client/-/oid4vci-client-0.10.3.tgz#b8be701f9d2de9daa9e0f81309024a89956d6e09" + integrity sha512-PkIZrwTMrHlgwcDNimWDQaAgi+9ptkV79g/sQJJAe4g8NCt3WyXtsV9l88CdzxDGVGDtzsnYqPXkimxP4eSScw== + dependencies: + "@sphereon/oid4vci-common" "0.10.3" + "@sphereon/ssi-types" "^0.23.0" + cross-fetch "^3.1.8" + debug "^4.3.4" + +"@sphereon/oid4vci-common@0.10.3", "@sphereon/oid4vci-common@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-common/-/oid4vci-common-0.10.3.tgz#a82359e7aae8d40f7833cf1238f018ec57865c52" + integrity sha512-VsUnDKkKm2yQ3lzAt2CY6vL06mZDK9dhwFT6T92aq03ncbUcS6gelwccdsXEMEfi5r4baFemiFM1O5v+mPjuEA== + dependencies: + "@sphereon/ssi-types" "^0.23.0" + cross-fetch "^3.1.8" + jwt-decode "^3.1.2" + sha.js "^2.4.11" + uint8arrays "3.1.1" + +"@sphereon/oid4vci-issuer@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-issuer/-/oid4vci-issuer-0.10.3.tgz#1df841656e56cc4eff7d73f1452a92e2221a55f6" + integrity sha512-qhm8ypkXuYsaG5XmXIFwL9DUJQ0TJScNjvg5w7beAm+zjz0sOkwIjXdS7S+29LfWj0BkYiRZp1d3mj8H/rmdUw== + dependencies: + "@sphereon/oid4vci-common" "0.10.3" + "@sphereon/ssi-types" "^0.23.0" + uuid "^9.0.0" + +"@sphereon/pex-models@^2.2.4": + version "2.2.4" + resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.2.4.tgz#0ce28e9858b38012fe1ff7d9fd12ec503473ee66" + integrity sha512-pGlp+wplneE1+Lk3U48/2htYKTbONMeG5/x7vhO6AnPUOsnOXeJdftPrBYWVSzz/JH5GJptAc6+pAyYE1zMu4Q== + +"@sphereon/pex@^3.3.2": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-3.3.3.tgz#8712ecc3c1a2548bd5e531bb41dd54e8010c1dc5" + integrity sha512-CXwdEcMTUh2z/5AriBn3OuShEG06l2tgiIr7qDJthnkez8DQ3sZo2vr4NEQWKKAL+DeAWAI4FryQGO4KuK7yfg== + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@sd-jwt/decode" "^0.6.1" + "@sd-jwt/present" "^0.6.1" + "@sd-jwt/types" "^0.6.1" + "@sphereon/pex-models" "^2.2.4" + "@sphereon/ssi-types" "0.22.0" + ajv "^8.12.0" + ajv-formats "^2.1.1" + jwt-decode "^3.1.2" + nanoid "^3.3.7" + string.prototype.matchall "^4.0.10" + uint8arrays "^3.1.1" + +"@sphereon/ssi-types@0.22.0": + version "0.22.0" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.22.0.tgz#da2eed7296e8932271af0c72a66eeea20b0b5689" + integrity sha512-YPJAZlKmzNALXK8ohP3ETxj1oVzL4+M9ljj3fD5xrbacvYax1JPCVKc8BWSubGcQckKHPbgbpcS7LYEeghyT9Q== + dependencies: + "@sd-jwt/decode" "^0.6.1" + jwt-decode "^3.1.2" + +"@sphereon/ssi-types@^0.23.0": + version "0.23.4" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.23.4.tgz#8d53e12b51e00376fdc0190c8244b1602f12d5ca" + integrity sha512-1lM2yfOEhpcYYBxm/12KYY4n3ZSahVf5rFqGdterQkMJMthwr20HqTjw3+VK5p7IVf+86DyBoZJyS4V9tSsoCA== + dependencies: + "@sd-jwt/decode" "^0.6.1" + jwt-decode "^3.1.2" + +"@sphereon/ssi-types@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.9.0.tgz#d140eb6abd77381926d0da7ac51b3c4b96a31b4b" + integrity sha512-umCr/syNcmvMMbQ+i/r/mwjI1Qw2aFPp9AwBTvTo1ailAVaaJjJGPkkVz1K9/2NZATNdDiQ3A8yGzdVJoKh9pA== + dependencies: + jwt-decode "^3.1.2" + +"@sphereon/wellknown-dids-client@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@sphereon/wellknown-dids-client/-/wellknown-dids-client-0.1.3.tgz#4711599ed732903e9f45fe051660f925c9b508a4" + integrity sha512-TAT24L3RoXD8ocrkTcsz7HuJmgjNjdoV6IXP1p3DdaI/GqkynytXE3J1+F7vUFMRYwY5nW2RaXSgDQhrFJemaA== + dependencies: + "@sphereon/ssi-types" "^0.9.0" + cross-fetch "^3.1.5" + jwt-decode "^3.1.2" + +"@stablelib/aead@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/aead/-/aead-1.0.1.tgz#c4b1106df9c23d1b867eb9b276d8f42d5fc4c0c3" + integrity sha512-q39ik6sxGHewqtO0nP4BuSe3db5G1fEJE8ukvngS2gLkBXyy6E7pLubhbYgnkDFv6V8cWaxcE4Xn0t6LWcJkyg== -"@jest/fake-timers@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" - integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== +"@stablelib/binary@^1.0.0", "@stablelib/binary@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/binary/-/binary-1.0.1.tgz#c5900b94368baf00f811da5bdb1610963dfddf7f" + integrity sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q== dependencies: - "@jest/types" "^26.6.2" - "@sinonjs/fake-timers" "^6.0.1" - "@types/node" "*" - jest-message-util "^26.6.2" - jest-mock "^26.6.2" - jest-util "^26.6.2" + "@stablelib/int" "^1.0.1" -"@jest/globals@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.6.2.tgz#5b613b78a1aa2655ae908eba638cc96a20df720a" - integrity sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA== +"@stablelib/bytes@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/bytes/-/bytes-1.0.1.tgz#0f4aa7b03df3080b878c7dea927d01f42d6a20d8" + integrity sha512-Kre4Y4kdwuqL8BR2E9hV/R5sOrUj6NanZaZis0V6lX5yzqC3hBuVSDXUIBqQv/sCpmuWRiHLwqiT1pqqjuBXoQ== + +"@stablelib/chacha20poly1305@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/chacha20poly1305/-/chacha20poly1305-1.0.1.tgz#de6b18e283a9cb9b7530d8767f99cde1fec4c2ee" + integrity sha512-MmViqnqHd1ymwjOQfghRKw2R/jMIGT3wySN7cthjXCBdO+qErNPUBnRzqNpnvIwg7JBCg3LdeCZZO4de/yEhVA== dependencies: - "@jest/environment" "^26.6.2" - "@jest/types" "^26.6.2" - expect "^26.6.2" + "@stablelib/aead" "^1.0.1" + "@stablelib/binary" "^1.0.1" + "@stablelib/chacha" "^1.0.1" + "@stablelib/constant-time" "^1.0.1" + "@stablelib/poly1305" "^1.0.1" + "@stablelib/wipe" "^1.0.1" -"@jest/reporters@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" - integrity sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw== +"@stablelib/chacha@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/chacha/-/chacha-1.0.1.tgz#deccfac95083e30600c3f92803a3a1a4fa761371" + integrity sha512-Pmlrswzr0pBzDofdFuVe1q7KdsHKhhU24e8gkEwnTGOmlC7PADzLVxGdn2PoNVBBabdg0l/IfLKg6sHAbTQugg== dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" - chalk "^4.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.2" - graceful-fs "^4.2.4" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^4.0.3" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.2" - jest-haste-map "^26.6.2" - jest-resolve "^26.6.2" - jest-util "^26.6.2" - jest-worker "^26.6.2" - slash "^3.0.0" - source-map "^0.6.0" - string-length "^4.0.1" - terminal-link "^2.0.0" - v8-to-istanbul "^7.0.0" - optionalDependencies: - node-notifier "^8.0.0" + "@stablelib/binary" "^1.0.1" + "@stablelib/wipe" "^1.0.1" -"@jest/source-map@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" - integrity sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA== +"@stablelib/constant-time@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/constant-time/-/constant-time-1.0.1.tgz#bde361465e1cf7b9753061b77e376b0ca4c77e35" + integrity sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg== + +"@stablelib/ed25519@^1.0.1", "@stablelib/ed25519@^1.0.2", "@stablelib/ed25519@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@stablelib/ed25519/-/ed25519-1.0.3.tgz#f8fdeb6f77114897c887bb6a3138d659d3f35996" + integrity sha512-puIMWaX9QlRsbhxfDc5i+mNPMY+0TmQEskunY1rZEBPi1acBCVQAhnsk/1Hk50DGPtVsZtAWQg4NHGlVaO9Hqg== dependencies: - callsites "^3.0.0" - graceful-fs "^4.2.4" - source-map "^0.6.0" + "@stablelib/random" "^1.0.2" + "@stablelib/sha512" "^1.0.1" + "@stablelib/wipe" "^1.0.1" -"@jest/test-result@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" - integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== +"@stablelib/hash@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/hash/-/hash-1.0.1.tgz#3c944403ff2239fad8ebb9015e33e98444058bc5" + integrity sha512-eTPJc/stDkdtOcrNMZ6mcMK1e6yBbqRBaNW55XA1jU8w/7QdnCF0CmMmOD1m7VSkBR44PWrMHU2l6r8YEQHMgg== + +"@stablelib/int@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/int/-/int-1.0.1.tgz#75928cc25d59d73d75ae361f02128588c15fd008" + integrity sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w== + +"@stablelib/keyagreement@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/keyagreement/-/keyagreement-1.0.1.tgz#4612efb0a30989deb437cd352cee637ca41fc50f" + integrity sha512-VKL6xBwgJnI6l1jKrBAfn265cspaWBPAPEc62VBQrWHLqVgNRE09gQ/AnOEyKUWrrqfD+xSQ3u42gJjLDdMDQg== dependencies: - "@jest/console" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/istanbul-lib-coverage" "^2.0.0" - collect-v8-coverage "^1.0.0" + "@stablelib/bytes" "^1.0.1" -"@jest/test-sequencer@^26.6.3": - version "26.6.3" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" - integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== +"@stablelib/poly1305@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/poly1305/-/poly1305-1.0.1.tgz#93bfb836c9384685d33d70080718deae4ddef1dc" + integrity sha512-1HlG3oTSuQDOhSnLwJRKeTRSAdFNVB/1djy2ZbS35rBSJ/PFqx9cf9qatinWghC2UbfOYD8AcrtbUQl8WoxabA== dependencies: - "@jest/test-result" "^26.6.2" - graceful-fs "^4.2.4" - jest-haste-map "^26.6.2" - jest-runner "^26.6.3" - jest-runtime "^26.6.3" + "@stablelib/constant-time" "^1.0.1" + "@stablelib/wipe" "^1.0.1" -"@jest/transform@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" - integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== +"@stablelib/random@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@stablelib/random/-/random-1.0.0.tgz#f441495075cdeaa45de16d7ddcc269c0b8edb16b" + integrity sha512-G9vwwKrNCGMI/uHL6XeWe2Nk4BuxkYyWZagGaDU9wrsuV+9hUwNI1lok2WVo8uJDa2zx7ahNwN7Ij983hOUFEw== dependencies: - "@babel/core" "^7.1.0" - "@jest/types" "^26.6.2" - babel-plugin-istanbul "^6.0.0" - chalk "^4.0.0" - convert-source-map "^1.4.0" - fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.2.4" - jest-haste-map "^26.6.2" - jest-regex-util "^26.0.0" - jest-util "^26.6.2" - micromatch "^4.0.2" - pirates "^4.0.1" - slash "^3.0.0" - source-map "^0.6.1" - write-file-atomic "^3.0.0" + "@stablelib/binary" "^1.0.0" + "@stablelib/wipe" "^1.0.0" -"@jest/types@^26.3.0": - version "26.3.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.3.0.tgz#97627bf4bdb72c55346eef98e3b3f7ddc4941f71" - integrity sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ== +"@stablelib/random@^1.0.1", "@stablelib/random@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@stablelib/random/-/random-1.0.2.tgz#2dece393636489bf7e19c51229dd7900eddf742c" + integrity sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w== dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^15.0.0" - chalk "^4.0.0" + "@stablelib/binary" "^1.0.1" + "@stablelib/wipe" "^1.0.1" -"@jest/types@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" - integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== +"@stablelib/sha256@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/sha256/-/sha256-1.0.1.tgz#77b6675b67f9b0ea081d2e31bda4866297a3ae4f" + integrity sha512-GIIH3e6KH+91FqGV42Kcj71Uefd/QEe7Dy42sBTeqppXV95ggCcxLTk39bEr+lZfJmp+ghsR07J++ORkRELsBQ== dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^15.0.0" - chalk "^4.0.0" + "@stablelib/binary" "^1.0.1" + "@stablelib/hash" "^1.0.1" + "@stablelib/wipe" "^1.0.1" -"@nodelib/fs.scandir@2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" - integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== +"@stablelib/sha512@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/sha512/-/sha512-1.0.1.tgz#6da700c901c2c0ceacbd3ae122a38ac57c72145f" + integrity sha512-13gl/iawHV9zvDKciLo1fQ8Bgn2Pvf7OV6amaRVKiq3pjQ3UmEpXxWiAfV8tYjUpeZroBxtyrwtdooQT/i3hzw== dependencies: - "@nodelib/fs.stat" "2.0.4" - run-parallel "^1.1.9" + "@stablelib/binary" "^1.0.1" + "@stablelib/hash" "^1.0.1" + "@stablelib/wipe" "^1.0.1" -"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" - integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== +"@stablelib/wipe@^1.0.0", "@stablelib/wipe@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/wipe/-/wipe-1.0.1.tgz#d21401f1d59ade56a62e139462a97f104ed19a36" + integrity sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg== -"@nodelib/fs.walk@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" - integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== +"@stablelib/x25519@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@stablelib/x25519/-/x25519-1.0.3.tgz#13c8174f774ea9f3e5e42213cbf9fc68a3c7b7fd" + integrity sha512-KnTbKmUhPhHavzobclVJQG5kuivH+qDLpe84iRqX3CLrKp881cF160JvXJ+hjn1aMyCwYOKeIZefIH/P5cJoRw== dependencies: - "@nodelib/fs.scandir" "2.1.4" - fastq "^1.6.0" + "@stablelib/keyagreement" "^1.0.1" + "@stablelib/random" "^1.0.2" + "@stablelib/wipe" "^1.0.1" -"@sinonjs/commons@^1.7.0": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217" - integrity sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw== +"@stablelib/xchacha20@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/xchacha20/-/xchacha20-1.0.1.tgz#e98808d1f7d8b20e3ff37c71a3062a2a955d9a8c" + integrity sha512-1YkiZnFF4veUwBVhDnDYwo6EHeKzQK4FnLiO7ezCl/zu64uG0bCCAUROJaBkaLH+5BEsO3W7BTXTguMbSLlWSw== dependencies: - type-detect "4.0.8" + "@stablelib/binary" "^1.0.1" + "@stablelib/chacha" "^1.0.1" + "@stablelib/wipe" "^1.0.1" -"@sinonjs/fake-timers@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" - integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== +"@stablelib/xchacha20poly1305@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/xchacha20poly1305/-/xchacha20poly1305-1.0.1.tgz#addcaf30b92dd956f76b3357888e2f91b92e7a61" + integrity sha512-B1Abj0sMJ8h3HNmGnJ7vHBrAvxuNka6cJJoZ1ILN7iuacXp7sUYcgOVEOTLWj+rtQMpspY9tXSCRLPmN1mQNWg== dependencies: - "@sinonjs/commons" "^1.7.0" + "@stablelib/aead" "^1.0.1" + "@stablelib/chacha20poly1305" "^1.0.1" + "@stablelib/constant-time" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + "@stablelib/xchacha20" "^1.0.1" + +"@tokenizer/token@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" + integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== + +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== -"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": - version "7.1.10" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.10.tgz#ca58fc195dd9734e77e57c6f2df565623636ab40" - integrity sha512-x8OM8XzITIMyiwl5Vmo2B1cR1S1Ipkyv4mdlbJjMa1lmuKvKY9FrBbEANIaMlnWn5Rf7uO+rC/VgYabNkE17Hw== +"@types/babel__core@^7.1.14": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" "@types/babel__generator" "*" "@types/babel__template" "*" "@types/babel__traverse" "*" "@types/babel__generator@*": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8" - integrity sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ== + version "7.6.8" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab" + integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== dependencies: "@babel/types" "^7.0.0" "@types/babel__template@*": - version "7.0.3" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.3.tgz#b8aaeba0a45caca7b56a5de9459872dde3727214" - integrity sha512-uCoznIPDmnickEi6D0v11SBpW0OuVqHJCa7syXqQHy5uktSCreIlt0iglsCnmvz8yCb38hGcWeseA8cWJSwv5Q== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.15.tgz#db9e4238931eb69ef8aab0ad6523d4d4caa39d03" - integrity sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A== - dependencies: - "@babel/types" "^7.3.0" - -"@types/babel__traverse@^7.0.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.11.0.tgz#b9a1efa635201ba9bc850323a8793ee2d36c04a0" - integrity sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg== + version "7.20.6" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7" + integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== dependencies: - "@babel/types" "^7.3.0" + "@babel/types" "^7.20.7" -"@types/bn.js@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.0.tgz#32c5d271503a12653c62cf4d2b45e6eab8cebc68" - integrity sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA== +"@types/bn.js@^5.1.0", "@types/bn.js@^5.1.5": + version "5.1.5" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.5.tgz#2e0dacdcce2c0f16b905d20ff87aedbc6f7b4bf0" + integrity sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A== dependencies: "@types/node" "*" "@types/body-parser@*": - version "1.19.0" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" - integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== dependencies: "@types/connect" "*" "@types/node" "*" -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== - "@types/connect@*": - version "3.4.33" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" - integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== dependencies: "@types/node" "*" "@types/cors@^2.8.10": - version "2.8.10" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.10.tgz#61cc8469849e5bcdd0c7044122265c39cec10cf4" - integrity sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ== + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + +"@types/eslint@^8.21.2": + version "8.56.10" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d" + integrity sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/events@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.3.tgz#a8ef894305af28d1fc6d2dfdfc98e899591ea529" + integrity sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g== -"@types/express-serve-static-core@*": - version "4.17.13" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" - integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== +"@types/express-serve-static-core@^4.17.33": + version "4.19.5" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz#218064e321126fcf9048d1ca25dd2465da55d9c6" + integrity sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" + "@types/send" "*" -"@types/express@4.17.8": - version "4.17.8" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" - integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== +"@types/express@*", "@types/express@^4.17.13", "@types/express@^4.17.15", "@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== dependencies: "@types/body-parser" "*" - "@types/express-serve-static-core" "*" + "@types/express-serve-static-core" "^4.17.33" "@types/qs" "*" "@types/serve-static" "*" -"@types/graceful-fs@^4.1.2": - version "4.1.3" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.3.tgz#039af35fe26bec35003e8d86d2ee9c586354348f" - integrity sha512-AiHRaEB50LQg0pZmm659vNBb9f4SJ0qrAnteuzhSeAUcJKxoYgEnprg/83kppCnc2zvtCKbdZry1a5pVY3lOTQ== +"@types/figlet@^1.5.4": + version "1.5.8" + resolved "https://registry.yarnpkg.com/@types/figlet/-/figlet-1.5.8.tgz#96b8186c7e2a388b4f8d09ee3276cba2af88bb0b" + integrity sha512-G22AUvy4Tl95XLE7jmUM8s8mKcoz+Hr+Xm9W90gJsppJq9f9tHvOGkrpn4gRX0q/cLtBdNkWtWCKDg2UDZoZvQ== + +"@types/graceful-fs@^4.1.3": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== dependencies: "@types/node" "*" -"@types/indy-sdk@^1.15.2": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@types/indy-sdk/-/indy-sdk-1.15.2.tgz#533adf24e803601add3152dc4adbaf2a8c25409f" - integrity sha512-xXCWRJve1iR9z1uULshKItEy2C68ChnGuOTvSI3E61a59dr1/3MjhJDqbmy29IL+KHjfB3uA9hFOZE2ElVe1Fg== +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/inquirer@^8.2.6": + version "8.2.10" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-8.2.10.tgz#9444dce2d764c35bc5bb4d742598aaa4acb6561b" + integrity sha512-IdD5NmHyVjWM8SHWo/kPBgtzXatwPkfwzyP3fN1jF2g9BWt5WO+8hL2F4o2GKIYsU40PpqeevuUWvkS/roXJkA== dependencies: - "@types/node" "*" + "@types/through" "*" + rxjs "^7.2.0" "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" - integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw== + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== "@types/istanbul-lib-report@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" - integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== dependencies: "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" - integrity sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^26.0.20": - version "26.0.20" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.20.tgz#cd2f2702ecf69e86b586e1f5223a60e454056307" - integrity sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA== +"@types/jest@^29.5.12": + version "29.5.12" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544" + integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw== dependencies: - jest-diff "^26.0.0" - pretty-format "^26.0.0" + expect "^29.0.0" + pretty-format "^29.0.0" -"@types/json-schema@^7.0.3": - version "7.0.6" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" - integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== +"@types/json-schema@*": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/mime@*": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" - integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/node-fetch@^2.5.8": - version "2.5.8" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb" - integrity sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw== - dependencies: - "@types/node" "*" - form-data "^3.0.0" +"@types/jsonpath@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@types/jsonpath/-/jsonpath-0.2.4.tgz#065be59981c1420832835af656377622271154be" + integrity sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA== -"@types/node@*": - version "14.11.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" - integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== +"@types/long@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== -"@types/normalize-package-data@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" - integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/luxon@^3.2.0": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.4.2.tgz#e4fc7214a420173cea47739c33cdf10874694db7" + integrity sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA== -"@types/prettier@^2.0.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.1.tgz#be148756d5480a84cde100324c03a86ae5739fb5" - integrity sha512-2zs+O+UkDsJ1Vcp667pd3f8xearMdopz/z54i99wtRDI5KLmngk7vlrYZD0ZjKHaROR03EznlBbVY9PfAEyJIQ== +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/multer@^1.4.7": + version "1.4.11" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.11.tgz#c70792670513b4af1159a2b60bf48cc932af55c5" + integrity sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w== + dependencies: + "@types/express" "*" + +"@types/node@*", "@types/node@18.18.8", "@types/node@>=13.7.0", "@types/node@^12.7.1", "@types/node@^18.18.8": + version "18.18.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.8.tgz#2b285361f2357c8c8578ec86b5d097c7f464cfd6" + integrity sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ== + dependencies: + undici-types "~5.26.4" + +"@types/object-inspect@^1.8.0": + version "1.13.0" + resolved "https://registry.yarnpkg.com/@types/object-inspect/-/object-inspect-1.13.0.tgz#f80afdc636247ef4cfa2a16ffcbf124084dae5dd" + integrity sha512-lwGTVESDDV+XsQ1pH4UifpJ1f7OtXzQ6QBOX2Afq2bM/T3oOt8hF6exJMjjIjtEWeAN2YAo25J7HxWh97CCz9w== "@types/qs@*": - version "6.9.5" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" - integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== "@types/range-parser@*": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" - integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/ref-array-di@^1.2.6": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@types/ref-array-di/-/ref-array-di-1.2.8.tgz#2b44567b8eaae72c59db68a482f5d26297e955be" + integrity sha512-+re5xrhRXDUR3sicMvN9N3C+6mklq5kd7FkN3ciRWio3BAvUDh2OEUTTG+619r10dqc6de25LIDtgpHtXCKGbA== + dependencies: + "@types/ref-napi" "*" + +"@types/ref-napi@*": + version "3.0.12" + resolved "https://registry.yarnpkg.com/@types/ref-napi/-/ref-napi-3.0.12.tgz#2ddde995ecf769f1e5da01604e468348949c72c3" + integrity sha512-UZPKghRaLlWx2lPAphpdtYe62TbGBaPeqUM6gF1vI6FPRIu/Tff/WMAzpJRFU3jJIiD8HiXpVt2RjcFHtA6YRg== + dependencies: + "@types/node" "*" + +"@types/ref-struct-di@^1.1.10": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@types/ref-struct-di/-/ref-struct-di-1.1.12.tgz#5d9167488692816754c6d2b9064d9b0313609d59" + integrity sha512-R2RNkGIROGoJTbXYTXrsXybnsQD4iAy26ih/G6HCeCB9luWFQKkr537XGz0uGJ1kH8y8RMkdbQmD/wBulrOPHw== + dependencies: + "@types/ref-napi" "*" + +"@types/semver@^7.5.0": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" "@types/serve-static@*": - version "1.13.5" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.5.tgz#3d25d941a18415d3ab092def846e135a08bbcf53" - integrity sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ== + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== dependencies: - "@types/express-serve-static-core" "*" - "@types/mime" "*" + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" "@types/stack-utils@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" - integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== -"@types/strip-bom@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" - integrity sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I= +"@types/through@*": + version "0.0.33" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.33.tgz#14ebf599320e1c7851e7d598149af183c6b9ea56" + integrity sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ== + dependencies: + "@types/node" "*" -"@types/strip-json-comments@0.0.30": - version "0.0.30" - resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" - integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== +"@types/uuid@^9.0.1": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" + integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== -"@types/uuid@^8.3.0": - version "8.3.0" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" - integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== +"@types/validator@^13.11.8": + version "13.12.0" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.12.0.tgz#1fe4c3ae9de5cf5193ce64717c99ef2fa7d8756f" + integrity sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag== + +"@types/varint@^6.0.0": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/varint/-/varint-6.0.3.tgz#e00b00f94d497d7cbf0e9f391cbc7e8443ae2174" + integrity sha512-DHukoGWdJ2aYkveZJTB2rN2lp6m7APzVsoJQ7j/qy1fQxyamJTPD5xQzCMoJ2Qtgn0mE3wWeNOpbTyBFvF+dyA== + dependencies: + "@types/node" "*" -"@types/validator@^13.1.3": - version "13.1.3" - resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.1.3.tgz#366b394aa3fbeed2392bf0a20ded606fa4a3d35e" - integrity sha512-DaOWN1zf7j+8nHhqXhIgNmS+ltAC53NXqGxYuBhWqWgqolRhddKzfZU814lkHQSTG0IUfQxU7Cg0gb8fFWo2mA== +"@types/ws@^8.5.4": + version "8.5.11" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.11.tgz#90ad17b3df7719ce3e6bc32f83ff954d38656508" + integrity sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w== + dependencies: + "@types/node" "*" "@types/yargs-parser@*": - version "15.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" - integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== "@types/yargs@^15.0.0": - version "15.0.7" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.7.tgz#dad50a7a234a35ef9460737a56024287a3de1d2b" - integrity sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA== + version "15.0.19" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.19.tgz#328fb89e46109ecbdb70c295d96ff2f46dfd01b9" + integrity sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA== dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.17.0.tgz#6f856eca4e6a52ce9cf127dfd349096ad936aa2d" - integrity sha512-/fKFDcoHg8oNan39IKFOb5WmV7oWhQe1K6CDaAVfJaNWEhmfqlA24g+u1lqU5bMH7zuNasfMId4LaYWC5ijRLw== +"@types/yargs@^16.0.0": + version "16.0.9" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.9.tgz#ba506215e45f7707e6cbcaf386981155b7ab956e" + integrity sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA== dependencies: - "@typescript-eslint/experimental-utils" "4.17.0" - "@typescript-eslint/scope-manager" "4.17.0" - debug "^4.1.1" - functional-red-black-tree "^1.0.1" - lodash "^4.17.15" - regexpp "^3.0.0" - semver "^7.3.2" - tsutils "^3.17.1" - -"@typescript-eslint/experimental-utils@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.17.0.tgz#762c44aaa1a6a3c05b6d63a8648fb89b89f84c80" - integrity sha512-ZR2NIUbnIBj+LGqCFGQ9yk2EBQrpVVFOh9/Kd0Lm6gLpSAcCuLLe5lUCibKGCqyH9HPwYC0GIJce2O1i8VYmWA== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.17.0" - "@typescript-eslint/types" "4.17.0" - "@typescript-eslint/typescript-estree" "4.17.0" - eslint-scope "^5.0.0" - eslint-utils "^2.0.0" - -"@typescript-eslint/parser@^4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.17.0.tgz#141b647ffc72ebebcbf9b0fe6087f65b706d3215" - integrity sha512-KYdksiZQ0N1t+6qpnl6JeK9ycCFprS9xBAiIrw4gSphqONt8wydBw4BXJi3C11ywZmyHulvMaLjWsxDjUSDwAw== - dependencies: - "@typescript-eslint/scope-manager" "4.17.0" - "@typescript-eslint/types" "4.17.0" - "@typescript-eslint/typescript-estree" "4.17.0" - debug "^4.1.1" + "@types/yargs-parser" "*" -"@typescript-eslint/scope-manager@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.17.0.tgz#f4edf94eff3b52a863180f7f89581bf963e3d37d" - integrity sha512-OJ+CeTliuW+UZ9qgULrnGpPQ1bhrZNFpfT/Bc0pzNeyZwMik7/ykJ0JHnQ7krHanFN9wcnPK89pwn84cRUmYjw== +"@types/yargs@^17.0.8": + version "17.0.32" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.32.tgz#030774723a2f7faafebf645f4e5a48371dca6229" + integrity sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog== dependencies: - "@typescript-eslint/types" "4.17.0" - "@typescript-eslint/visitor-keys" "4.17.0" + "@types/yargs-parser" "*" -"@typescript-eslint/types@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.17.0.tgz#f57d8fc7f31b348db946498a43050083d25f40ad" - integrity sha512-RN5z8qYpJ+kXwnLlyzZkiJwfW2AY458Bf8WqllkondQIcN2ZxQowAToGSd9BlAUZDB5Ea8I6mqL2quGYCLT+2g== +"@typescript-eslint/eslint-plugin@^7.14.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz#f5f5da52db674b1f2cdb9d5f3644e5b2ec750465" + integrity sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "7.16.1" + "@typescript-eslint/type-utils" "7.16.1" + "@typescript-eslint/utils" "7.16.1" + "@typescript-eslint/visitor-keys" "7.16.1" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/parser@^7.14.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.16.1.tgz#84c581cf86c8b2becd48d33ddc41a6303d57b274" + integrity sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA== + dependencies: + "@typescript-eslint/scope-manager" "7.16.1" + "@typescript-eslint/types" "7.16.1" + "@typescript-eslint/typescript-estree" "7.16.1" + "@typescript-eslint/visitor-keys" "7.16.1" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@7.16.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz#2b43041caabf8ddd74512b8b550b9fc53ca3afa1" + integrity sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw== + dependencies: + "@typescript-eslint/types" "7.16.1" + "@typescript-eslint/visitor-keys" "7.16.1" + +"@typescript-eslint/type-utils@7.16.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz#4d7ae4f3d9e3c8cbdabae91609b1a431de6aa6ca" + integrity sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA== + dependencies: + "@typescript-eslint/typescript-estree" "7.16.1" + "@typescript-eslint/utils" "7.16.1" + debug "^4.3.4" + ts-api-utils "^1.3.0" + +"@typescript-eslint/types@7.16.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.16.1.tgz#bbab066276d18e398bc64067b23f1ce84dfc6d8c" + integrity sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ== + +"@typescript-eslint/typescript-estree@7.16.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz#9b145ba4fd1dde1986697e1ce57dc501a1736dd3" + integrity sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ== + dependencies: + "@typescript-eslint/types" "7.16.1" + "@typescript-eslint/visitor-keys" "7.16.1" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/utils@7.16.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.16.1.tgz#df42dc8ca5a4603016fd102db0346cdab415cdb7" + integrity sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "7.16.1" + "@typescript-eslint/types" "7.16.1" + "@typescript-eslint/typescript-estree" "7.16.1" + +"@typescript-eslint/visitor-keys@7.16.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz#4287bcf44c34df811ff3bb4d269be6cfc7d8c74b" + integrity sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg== + dependencies: + "@typescript-eslint/types" "7.16.1" + eslint-visitor-keys "^3.4.3" + +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@typescript-eslint/typescript-estree@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.17.0.tgz#b835d152804f0972b80dbda92477f9070a72ded1" - integrity sha512-lRhSFIZKUEPPWpWfwuZBH9trYIEJSI0vYsrxbvVvNyIUDoKWaklOAelsSkeh3E2VBSZiNe9BZ4E5tYBZbUczVQ== +"@unimodules/core@*": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@unimodules/core/-/core-7.1.2.tgz#5181b99586476a5d87afd0958f26a04714c47fa1" + integrity sha512-lY+e2TAFuebD3vshHMIRqru3X4+k7Xkba4Wa7QsDBd+ex4c4N2dHAO61E2SrGD9+TRBD8w/o7mzK6ljbqRnbyg== dependencies: - "@typescript-eslint/types" "4.17.0" - "@typescript-eslint/visitor-keys" "4.17.0" - debug "^4.1.1" - globby "^11.0.1" - is-glob "^4.0.1" - semver "^7.3.2" - tsutils "^3.17.1" + compare-versions "^3.4.0" -"@typescript-eslint/visitor-keys@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.17.0.tgz#9c304cfd20287c14a31d573195a709111849b14d" - integrity sha512-WfuMN8mm5SSqXuAr9NM+fItJ0SVVphobWYkWOwQ1odsfC014Vdxk/92t4JwS1Q6fCA/ABfCKpa3AVtpUKTNKGQ== +"@unimodules/react-native-adapter@*": + version "6.3.9" + resolved "https://registry.yarnpkg.com/@unimodules/react-native-adapter/-/react-native-adapter-6.3.9.tgz#2f4bef6b7532dce5bf9f236e69f96403d0243c30" + integrity sha512-i9/9Si4AQ8awls+YGAKkByFbeAsOPgUNeLoYeh2SQ3ddjxJ5ZJDtq/I74clDnpDcn8zS9pYlcDJ9fgVJa39Glw== dependencies: - "@typescript-eslint/types" "4.17.0" - eslint-visitor-keys "^2.0.0" - -abab@^2.0.3, abab@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" - integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + expo-modules-autolinking "^0.0.3" + invariant "^2.2.4" abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" + event-target-shim "^5.0.0" -acorn-globals@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" - integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== +absolute-path@^0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/absolute-path/-/absolute-path-0.0.0.tgz#a78762fbdadfb5297be99b15d35a785b2f095bf7" + integrity sha512-HQiug4c+/s3WOvEnDRxXVmNtSG5s2gJM9r19BTcqjp7BWcE48PB+Y2G6jE65kqI0LpsQeMZygt/b60Gi4KxGyA== + +accepts@^1.3.7, accepts@~1.3.5, accepts@~1.3.7, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== dependencies: - acorn "^7.1.1" - acorn-walk "^7.1.1" + mime-types "~2.1.34" + negotiator "0.6.3" -acorn-jsx@^5.2.0, acorn-jsx@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" - integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^7.1.1: - version "7.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" - integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn-walk@^8.1.1: + version "8.3.3" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e" + integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw== + dependencies: + acorn "^8.11.0" -acorn@^7.1.1, acorn@^7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" - integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== +acorn@^8.11.0, acorn@^8.4.1, acorn@^8.8.2, acorn@^8.9.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== -acorn@^8.0.5: - version "8.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe" - integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA== +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" -ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: - version "6.12.5" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" - integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== 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" -ajv@^7.0.2: - version "7.2.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.2.1.tgz#a5ac226171912447683524fa2f1248fcf8bac83d" - integrity sha512-+nu0HDv7kNSOua9apAVc979qd932rrZeb3WOvoiD31A/p1mIE5/9bN2027pE2rOPYEdS3UHzsvof4hY+lM9/WQ== +ajv@^8.0.0, ajv@^8.12.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" -ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +anser@^1.4.9: + version "1.4.10" + resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" + integrity sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww== + +ansi-colors@^4.1.1, ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== ansi-escapes@^4.2.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" - integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== dependencies: - type-fest "^0.11.0" + type-fest "^0.21.3" -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= +ansi-fragments@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-fragments/-/ansi-fragments-0.2.1.tgz#24409c56c4cc37817c3d7caa99d8969e2de5a05e" + integrity sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w== + dependencies: + colorette "^1.0.7" + slice-ansi "^2.0.0" + strip-ansi "^5.0.0" -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.0, ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^3.2.1: +ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== @@ -929,41 +3233,47 @@ ansi-styles@^3.2.1: color-convert "^1.9.0" ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - "@types/color-name" "^1.1.1" color-convert "^2.0.1" -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -anymatch@^3.0.3, anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== +anymatch@^3.0.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== +appdirsjs@^1.2.4: + version "1.2.7" + resolved "https://registry.yarnpkg.com/appdirsjs/-/appdirsjs-1.2.7.tgz#50b4b7948a26ba6090d4aede2ae2dc2b051be3b3" + integrity sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw== -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== dependencies: delegates "^1.0.0" - readable-stream "^2.0.6" + readable-stream "^3.6.0" arg@^4.1.0: version "4.1.3" @@ -977,118 +3287,259 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -arr-union@^3.1.0: +array-back@^3.0.1, array-back@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= +array-back@^4.0.1, array-back@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" + integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== + +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-includes@^3.1.7: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + is-string "^1.0.7" + +array-index@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-index/-/array-index-1.0.0.tgz#ec56a749ee103e4e08c790b9c353df16055b97f9" + integrity sha512-jesyNbBkLQgGZMSwA1FanaFjalb1mZUGxGeUEkSDidzgrbjBGhvizJkaItdhkt8eIHFOJC7nDsrXk+BaehTdRw== + dependencies: + debug "^2.2.0" + es6-symbol "^3.0.2" array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= +array.prototype.findlastindex@^1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" + integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== +array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== dependencies: - safer-buffer "~2.1.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +asap@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== -assign-symbols@^1.0.0: +asmcrypto.js@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/asmcrypto.js/-/asmcrypto.js-0.22.0.tgz#38fc1440884d802c7bd37d1d23c2b26a5cd5d2d2" + integrity sha512-usgMoyXjMbx/ZPdzTSXExhMPur2FTdz/Vo5PVx2gIaBcdAAJNOFlsdgqveM8Cff7W0v+xrf9BwjOV26JSAF9qA== + +asn1js@^3.0.1, asn1js@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38" + integrity sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ== + dependencies: + pvtsutils "^1.3.2" + pvutils "^1.1.3" + tslib "^2.4.0" + +ast-types@0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.15.2.tgz#39ae4809393c4b16df751ee563411423e85fb49d" + integrity sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg== + dependencies: + tslib "^2.0.1" + +astral-regex@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +async-mutex@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.1.tgz#bccf55b96f2baf8df90ed798cb5544a1f6ee4c2c" + integrity sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA== + dependencies: + tslib "^2.4.0" + +async@^3.2.2, async@^3.2.3: + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +axios@^0.21.2: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= +b64-lite@^1.3.1, b64-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/b64-lite/-/b64-lite-1.4.0.tgz#e62442de11f1f21c60e38b74f111ac0242283d3d" + integrity sha512-aHe97M7DXt+dkpa8fHlCcm1CnskAHrJqEfMI0KN7dwqlzml/aUe1AGt6lk51HzrSfVD67xOso84sOpr+0wIe2w== + dependencies: + base-64 "^0.1.0" + +b64u-lite@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/b64u-lite/-/b64u-lite-1.1.0.tgz#a581b7df94cbd4bed7cbb19feae816654f0b1bf0" + integrity sha512-929qWGDVCRph7gQVTC6koHqQIpF4vtVaSbwLltFQo44B1bYUquALswZdBKFfrJCPEnsCOvWkJsPdQYZ/Ukhw8A== + dependencies: + b64-lite "^1.4.0" -aws4@^1.8.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428" - integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA== +babel-core@^7.0.0-bridge.0: + version "7.0.0-bridge.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" + integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg== -babel-jest@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" - integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== +babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== dependencies: - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/babel__core" "^7.1.7" - babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^26.6.2" + "@jest/transform" "^29.7.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.6.3" chalk "^4.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" slash "^3.0.0" -babel-plugin-istanbul@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765" - integrity sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ== +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@istanbuljs/load-nyc-config" "^1.0.0" "@istanbuljs/schema" "^0.1.2" - istanbul-lib-instrument "^4.0.0" + istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" - integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" - "@types/babel__core" "^7.0.0" + "@types/babel__core" "^7.1.14" "@types/babel__traverse" "^7.0.6" +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.11" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" + integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.6.2" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.10.1: + version "0.10.4" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" + integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.1" + core-js-compat "^3.36.1" + +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz#addc47e240edd1da1058ebda03021f382bba785e" + integrity sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.2" + +babel-plugin-syntax-trailing-function-commas@^7.0.0-beta.0: + version "7.0.0-beta.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz#aa213c1435e2bffeb6fca842287ef534ad05d5cf" + integrity sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ== + babel-preset-current-node-syntax@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" @@ -1107,76 +3558,164 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" - integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== - dependencies: - babel-plugin-jest-hoist "^26.6.2" +babel-preset-fbjs@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/babel-preset-fbjs/-/babel-preset-fbjs-3.4.0.tgz#38a14e5a7a3b285a3f3a86552d650dca5cf6111c" + integrity sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow== + dependencies: + "@babel/plugin-proposal-class-properties" "^7.0.0" + "@babel/plugin-proposal-object-rest-spread" "^7.0.0" + "@babel/plugin-syntax-class-properties" "^7.0.0" + "@babel/plugin-syntax-flow" "^7.0.0" + "@babel/plugin-syntax-jsx" "^7.0.0" + "@babel/plugin-syntax-object-rest-spread" "^7.0.0" + "@babel/plugin-transform-arrow-functions" "^7.0.0" + "@babel/plugin-transform-block-scoped-functions" "^7.0.0" + "@babel/plugin-transform-block-scoping" "^7.0.0" + "@babel/plugin-transform-classes" "^7.0.0" + "@babel/plugin-transform-computed-properties" "^7.0.0" + "@babel/plugin-transform-destructuring" "^7.0.0" + "@babel/plugin-transform-flow-strip-types" "^7.0.0" + "@babel/plugin-transform-for-of" "^7.0.0" + "@babel/plugin-transform-function-name" "^7.0.0" + "@babel/plugin-transform-literals" "^7.0.0" + "@babel/plugin-transform-member-expression-literals" "^7.0.0" + "@babel/plugin-transform-modules-commonjs" "^7.0.0" + "@babel/plugin-transform-object-super" "^7.0.0" + "@babel/plugin-transform-parameters" "^7.0.0" + "@babel/plugin-transform-property-literals" "^7.0.0" + "@babel/plugin-transform-react-display-name" "^7.0.0" + "@babel/plugin-transform-react-jsx" "^7.0.0" + "@babel/plugin-transform-shorthand-properties" "^7.0.0" + "@babel/plugin-transform-spread" "^7.0.0" + "@babel/plugin-transform-template-literals" "^7.0.0" + babel-plugin-syntax-trailing-function-commas "^7.0.0-beta.0" + +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" babel-preset-current-node-syntax "^1.0.0" balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base-64@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" + integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== -base64-js@^1.3.1: +base-x@^3.0.2: + version "3.0.10" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.10.tgz#62de58653f8762b5d6f8d9fe30fa75f7b2585a75" + integrity sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ== + dependencies: + safe-buffer "^5.0.1" + +base64-js@*, base64-js@^1.1.2, base64-js@^1.3.0, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== +base64url-universal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/base64url-universal/-/base64url-universal-1.1.0.tgz#94da6356c1d43ead55b1d91c045c0a5b09ec8181" + integrity sha512-WyftvZqye29YQ10ZnuiBeEj0lk8SN8xHU9hOznkLc85wS1cLTp6RpzlMrHxMPD9nH7S55gsBqMqgGyz93rqmkA== + dependencies: + base64url "^3.0.0" + +base64url-universal@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64url-universal/-/base64url-universal-2.0.0.tgz#6023785c0e349a90de1cf396e8a4519750a4e67b" + integrity sha512-6Hpg7EBf3t148C3+fMzjf+CHnADVDafWzlJUXAqqqbm4MKNXbsoPdOkWeRTjNlkYG7TpyjIpRO1Gk0SnsFD1rw== dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" + base64url "^3.0.1" -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= +base64url@^3.0.0, base64url@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + +bech32@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" + integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== + +bech32@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" + integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== + +better-path-resolve@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/better-path-resolve/-/better-path-resolve-1.0.0.tgz#13a35a1104cdd48a7b74bf8758f96a1ee613f99d" + integrity sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g== dependencies: - tweetnacl "^0.14.3" + is-windows "^1.0.0" -binary-extensions@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" - integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== +big-integer@^1.6.51: + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== -bindings@^1.3.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== +bignumber.js@^9.0.0: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== dependencies: - file-uri-to-path "1.0.0" + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" -bn.js@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" - integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.2.0, bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== -body-parser@1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== dependencies: - bytes "3.1.0" - content-type "~1.0.4" + bytes "3.1.2" + content-type "~1.0.5" debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +borc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/borc/-/borc-3.0.0.tgz#49ada1be84de86f57bb1bb89789f34c186dfa4fe" + integrity sha512-ec4JmVC46kE0+layfnwM3l15O70MlFiEbmQHY/vpqIKiUtPVntv4BY4NVnz3N4vb21edV3mY97XVckFvYHWF9g== + dependencies: + bignumber.js "^9.0.0" + buffer "^6.0.3" + commander "^2.15.0" + ieee754 "^1.1.13" + iso-url "^1.1.5" + json-text-sequence "~0.3.0" + readable-stream "^3.6.0" brace-expansion@^1.1.7: version "1.1.11" @@ -1186,33 +3725,34 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" + balanced-match "^1.0.0" -braces@^3.0.1, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" -browser-process-hrtime@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" - integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + +browserslist@^4.23.0, browserslist@^4.23.1: + version "4.23.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.2.tgz#244fe803641f1c19c28c48c4b6ec9736eb3d32ed" + integrity sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA== + dependencies: + caniuse-lite "^1.0.30001640" + electron-to-chromium "^1.4.820" + node-releases "^2.0.14" + update-browserslist-db "^1.1.0" bs-logger@0.x: version "0.2.6" @@ -1221,6 +3761,13 @@ bs-logger@0.x: dependencies: fast-json-stable-stringify "2.x" +bs58@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== + dependencies: + base-x "^3.0.2" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -1228,10 +3775,18 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -buffer-from@1.x, buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" buffer@^6.0.3: version "6.0.3" @@ -1241,67 +3796,89 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +builtins@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" + integrity sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ== -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" + streamsearch "^1.1.0" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ== + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A== + dependencies: + caller-callsite "^2.0.0" + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ== callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= - camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e" - integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w== +camelcase@^6.0.0, camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -capture-exit@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" - integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== - dependencies: - rsvp "^4.8.4" +caniuse-lite@^1.0.30001640: + version "1.0.30001642" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz#6aa6610eb24067c246d30c57f055a9d0a7f8d05f" + integrity sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA== + +canonicalize@^1.0.1: + version "1.0.8" + resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.8.tgz#24d1f1a00ed202faafd9bf8e63352cd4450c6df1" + integrity sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A== -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +canonicalize@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-2.0.0.tgz#32be2cef4446d67fd5348027a384cae28f17226a" + integrity sha512-ulDEYPv7asdKvqahuAY35c1selLdzDwHqugK92hfkzvlDCwXRRelDkR+Er33md/PtnpqHemgkuDPanZ4fiYZ8w== -chalk@^2.0.0, chalk@^2.4.1: +chalk@^2.1.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1310,10 +3887,10 @@ chalk@^2.0.0, chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" @@ -1323,59 +3900,66 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== -chokidar@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.3.1" +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -cjs-module-lexer@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" - integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== +ci-info@^3.2.0, ci-info@^3.7.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== -class-transformer@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.4.0.tgz#b52144117b423c516afb44cc1c76dbad31c2165b" - integrity sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA== +cjs-module-lexer@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz#c485341ae8fd999ca4ee5af2d7a1c9ae01e0099c" + integrity sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q== -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== +class-transformer@0.5.1, class-transformer@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + +class-validator@0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.1.tgz#ff2411ed8134e9d76acfeb14872884448be98110" + integrity sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ== dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" + "@types/validator" "^13.11.8" + libphonenumber-js "^1.10.53" + validator "^13.9.0" -class-validator@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.13.1.tgz#381b2001ee6b9e05afd133671fbdf760da7dec67" - integrity sha512-zWIeYFhUitvAHBwNhDdCRK09hWx+P0HUwFE8US8/CxFpMVzkUK8RJl7yOIE+BVu2lxyPNgeOaFv78tLE47jBIg== +clear@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/clear/-/clear-0.1.0.tgz#b81b1e03437a716984fd7ac97c87d73bdfe7048a" + integrity sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== dependencies: - "@types/validator" "^13.1.3" - libphonenumber-js "^1.9.7" - validator "^13.5.2" + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== cliui@^6.0.0: version "6.0.0" @@ -1386,28 +3970,38 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== collect-v8-coverage@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" - integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" + version "1.0.2" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" + integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== color-convert@^1.9.0: version "1.9.3" @@ -1426,73 +4020,180 @@ color-convert@^2.0.1: color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +colorette@^1.0.7: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== + +combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== +command-exists@^1.2.8: + version "1.2.9" + resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" + integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== + +command-line-args@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" + integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-commands@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/command-line-commands/-/command-line-commands-3.0.2.tgz#53872a1181db837f21906b1228e260a4eeb42ee4" + integrity sha512-ac6PdCtdR6q7S3HN+JiVLIWGHY30PRYIEl2qPo+FuEuzwAUk0UYyimrngrg7FvF/mCr4Jgoqv5ZnHZgads50rw== + dependencies: + array-back "^4.0.1" + +command-line-usage@^6.1.0: + version "6.1.3" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.3.tgz#428fa5acde6a838779dfa30e44686f4b6761d957" + integrity sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw== + dependencies: + array-back "^4.0.2" + chalk "^2.4.2" + table-layout "^1.0.2" + typical "^5.2.0" + +commander@^2.15.0, commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +commander@^9.4.1: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + +commander@~2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" + integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +compare-versions@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" + integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.1: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +connect@^3.6.5: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" -console-control-strings@^1.0.0, console-control-strings@~1.1.0: +console-control-strings@^1.0.0, console-control-strings@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: - safe-buffer "5.1.2" + safe-buffer "5.2.1" -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - dependencies: - safe-buffer "~5.1.1" +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +core-js-compat@^3.36.1: + version "3.37.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.1.tgz#c844310c7852f4bdf49b8d339730b97e17ff09ee" + integrity sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg== + dependencies: + browserslist "^4.23.0" -core-util-is@1.0.2, core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== cors@^2.8.5: version "2.8.5" @@ -1502,12 +4203,71 @@ cors@^2.8.5: object-assign "^4" vary "^1" +cosmiconfig@^5.0.5, cosmiconfig@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.13.1" + parse-json "^4.0.0" + +cosmjs-types@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cosmjs-types/-/cosmjs-types-0.7.2.tgz#a757371abd340949c5bd5d49c6f8379ae1ffd7e2" + integrity sha512-vf2uLyktjr/XVAgEq0DjMxeAWh1yYREe7AMHDKd7EiHVqxBPCaBS+qEEQUkXbR9ndnckqr1sUG8BQhazh4X5lA== + dependencies: + long "^4.0.0" + protobufjs "~6.11.2" + +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^6.0.0, cross-spawn@^6.0.5: +credentials-context@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/credentials-context/-/credentials-context-2.0.0.tgz#68a9a1a88850c398d3bba4976c8490530af093e8" + integrity sha512-/mFKax6FK26KjgV2KW2D4YqKgoJ5DVJpNt87X2Jc9IxT2HBMy7nEIlc+n7pEi+YFFe721XqrvZPd+jbyyBjsvQ== + +cross-fetch@^3.1.5, cross-fetch@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + +cross-fetch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + +cross-spawn@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + integrity sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A== + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -1518,7 +4278,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2: +cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -1527,151 +4287,218 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" -cssom@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" - integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== - -cssom@~0.3.6: - version "0.3.8" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" - integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== +crypto-ld@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/crypto-ld/-/crypto-ld-6.0.0.tgz#cf8dcf566cb3020bdb27f0279e6cc9b46d031cd7" + integrity sha512-XWL1LslqggNoaCI/m3I7HcvaSt9b2tYzdrXO+jHLUj9G1BvRfvV7ZTFDVY5nifYuIGAPdAGu7unPxLRustw3VA== -cssstyle@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" - integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== dependencies: - cssom "~0.3.6" + es5-ext "^0.10.64" + type "^2.7.2" -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" +data-uri-to-buffer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" + integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== dependencies: - assert-plus "^1.0.0" + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" -data-urls@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" - integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== dependencies: - abab "^2.0.3" - whatwg-mimetype "^2.3.0" - whatwg-url "^8.0.0" + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" -dateformat@~1.0.4-1.2.3: - version "1.0.12" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" - integrity sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk= +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== dependencies: - get-stdin "^4.0.1" - meow "^3.3.0" + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +dayjs@^1.8.15: + version "1.11.11" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" + integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== -debug@2.6.9, debug@^2.2.0, debug@^2.3.3: +debug@2.6.9, debug@^2.2.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" - integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== dependencies: ms "2.1.2" -decamelize@^1.1.2, decamelize@^1.2.0: +debug@^3.1.0, debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +decode-uri-component@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== -decimal.js@^10.2.1: - version "10.2.1" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3" - integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw== +dedent@^1.0.0: + version "1.5.3" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" + integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +deep-extend@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== deep-is@^0.1.3, deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +deepmerge@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.3.0.tgz#d3c47fd6f3a93d517b14426b0628a17b0125f5f7" + integrity sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA== -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== dependencies: - is-descriptor "^0.1.0" + clone "^1.0.2" -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: - is-descriptor "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== +define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +denodeify@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631" + integrity sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg== -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +deprecated-react-native-prop-types@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-3.0.2.tgz#e724a9837e6a7ccb778753c06ae4f79065873493" + integrity sha512-JoZY5iNM+oJlN2Ldpq0KSi0h3Nig4hlNJj5nWzWp8eL3uikMCvHwjSGPitwkEw0arL5JFra5nuGJQpXRbEjApg== + dependencies: + "@react-native/normalize-color" "^2.1.0" + invariant "^2.2.4" + prop-types "^15.8.1" + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-indent@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" + integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== + +detect-libc@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -diff-sequences@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" - integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== +did-jwt@6.11.6, did-jwt@^6.11.6: + version "6.11.6" + resolved "https://registry.yarnpkg.com/did-jwt/-/did-jwt-6.11.6.tgz#3eeb30d6bd01f33bfa17089574915845802a7d44" + integrity sha512-OfbWknRxJuUqH6Lk0x+H1FsuelGugLbBDEwsoJnicFOntIG/A4y19fn0a8RLxaQbWQ5gXg0yDq5E2huSBiiXzw== + dependencies: + "@stablelib/ed25519" "^1.0.2" + "@stablelib/random" "^1.0.1" + "@stablelib/sha256" "^1.0.1" + "@stablelib/x25519" "^1.0.2" + "@stablelib/xchacha20poly1305" "^1.0.1" + bech32 "^2.0.0" + canonicalize "^2.0.0" + did-resolver "^4.0.0" + elliptic "^6.5.4" + js-sha3 "^0.8.0" + multiformats "^9.6.5" + uint8arrays "^3.0.0" + +did-resolver@^4.0.0, did-resolver@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/did-resolver/-/did-resolver-4.1.0.tgz#740852083c4fd5bf9729d528eca5d105aff45eb6" + integrity sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA== + +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== diff@^4.0.1: version "4.0.2" @@ -1685,6 +4512,13 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -1692,42 +4526,50 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -domexception@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" - integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== - dependencies: - webidl-conversions "^5.0.0" - -dotenv@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" - integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== - -dynamic-dedupe@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1" - integrity sha1-BuRMIj9eTpTXjvnbI6ZRXOL5YqE= - dependencies: - xtend "^4.0.0" +ed25519-signature-2018-context@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ed25519-signature-2018-context/-/ed25519-signature-2018-context-1.1.0.tgz#68002ea7497c32e8170667cfd67468dedf7d220e" + integrity sha512-ppDWYMNwwp9bploq0fS4l048vHIq41nWsAbPq6H4mNVx9G/GxW3fwg4Ln0mqctP13MoEpREK7Biz8TbVVdYXqA== -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" +ed25519-signature-2020-context@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ed25519-signature-2020-context/-/ed25519-signature-2020-context-1.1.0.tgz#b2f724f07db154ddf0fd6605410d88736e56fd07" + integrity sha512-dBGSmoUIK6h2vadDctrDnhhTO01PR2hJk0mRNEfrRDPCjaIwrfy4J+eziEQ9Q1m8By4f/CSRgKM1h53ydKfdNg== ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -emittery@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.1.tgz#c02375a927a40948c0345cc903072597f5270451" - integrity sha512-d34LN4L6h18Bzz9xpoku2nPwKxCPlPMr3EEKTkoEBi+1/+b0lcRkRJ1UVyyZaKNeqGR3swcGl6s390DNO4YVgQ== +ejs@^3.0.0: + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== + dependencies: + jake "^10.8.5" + +electron-to-chromium@^1.4.820: + version "1.4.827" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.827.tgz#76068ed1c71dd3963e1befc8ae815004b2da6a02" + integrity sha512-VY+J0e4SFcNfQy19MEoMdaIcZLmDCprqvBtkii1WTCTQHpRvf5N8+3kTYCgL/PcntvwQvmMJWTuDPsq+IlhWKQ== + +elliptic@^6.5.4: + version "6.5.5" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.5.tgz#c715e09f78b6923977610d4c2346d6ce22e6dded" + integrity sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== emoji-regex@^8.0.0: version "8.0.0" @@ -1737,7 +4579,7 @@ emoji-regex@^8.0.0: encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== end-of-stream@^1.1.0: version "1.4.4" @@ -1746,54 +4588,135 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" -enquirer@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== +enhanced-resolve@^5.12.0: + version "5.17.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz#d037603789dd9555b89aaec7eb78845c49089bc5" + integrity sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +enquirer@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" + integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== dependencies: ansi-colors "^4.1.1" + strip-ansi "^6.0.1" + +envinfo@^7.7.2: + version "7.13.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31" + integrity sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q== -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: - version "1.17.7" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" - integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== +error-stack-parser@^2.0.6: + version "2.1.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" + stackframe "^1.3.4" -es-abstract@^1.18.0-next.0: - version "1.18.0-next.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== +errorhandler@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/errorhandler/-/errorhandler-1.5.1.tgz#b9ba5d17cf90744cd1e851357a6e75bf806a9a91" + integrity sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A== dependencies: + accepts "~1.3.7" + escape-html "~1.0.3" + +es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-negative-zero "^2.0.0" - is-regex "^1.1.1" - object-inspect "^1.8.0" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" es-to-primitive@^1.2.1: version "1.2.1" @@ -1804,140 +4727,231 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + esniff "^2.0.1" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.0.2, es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== + dependencies: + d "^1.0.2" + ext "^1.7.0" + +escalade@^3.1.1, escalade@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== escape-string-regexp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -escodegen@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" - integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escodegen@^1.8.1: + version "1.14.3" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== dependencies: esprima "^4.0.1" - estraverse "^5.2.0" + estraverse "^4.2.0" esutils "^2.0.2" optionator "^0.8.1" optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz#4ef1eaf97afe5176e6a75ddfb57c335121abc5a6" - integrity sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw== - -eslint-plugin-prettier@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz#7079cfa2497078905011e6f82e8dd8453d1371b7" - integrity sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ== +eslint-config-prettier@^8.3.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" + integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== + +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-import-resolver-typescript@^3.5.3: + version "3.6.1" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz#7b983680edd3f1c5bce1a5829ae0bc2d57fe9efa" + integrity sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg== + dependencies: + debug "^4.3.4" + enhanced-resolve "^5.12.0" + eslint-module-utils "^2.7.4" + fast-glob "^3.3.1" + get-tsconfig "^4.5.0" + is-core-module "^2.11.0" + is-glob "^4.0.3" + +eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34" + integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.23.4: + version "2.29.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" + integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== + dependencies: + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.8.0" + hasown "^2.0.0" + is-core-module "^2.13.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" + semver "^6.3.1" + tsconfig-paths "^3.15.0" + +eslint-plugin-prettier@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" + integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== dependencies: prettier-linter-helpers "^1.0.0" -eslint-scope@^5.0.0, eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== +eslint-plugin-workspaces@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-workspaces/-/eslint-plugin-workspaces-0.8.0.tgz#e723a1333a8ddddbc416220a9e03578603de0f85" + integrity sha512-8BhKZaGFpl0xAVo7KHaWffaBvvroaOeLuqLkVsMNZvMaN6ZHKYx7QZoaXC/Y299tG3wvN6v7hu27VBHmyg4q4g== dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" + find-workspaces "^0.1.0" -eslint-utils@^2.0.0, eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint-visitor-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" - integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + esrecurse "^4.3.0" + estraverse "^5.2.0" -eslint@^7.21.0: - version "7.21.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.21.0.tgz#4ecd5b8c5b44f5dedc9b8a110b01bbfeb15d1c83" - integrity sha512-W2aJbXpMNofUp0ztQaF40fveSsJBjlSCSWpy//gzfTvwC+USs/nceBrKmlJOiM8r1bLwP2EuYkCqArn/6QTIgg== - dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.0" - ajv "^6.10.0" +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.36.0: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" - debug "^4.0.1" + debug "^4.3.2" doctrine "^3.0.0" - enquirer "^2.3.5" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" - esquery "^1.4.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" esutils "^2.0.2" + fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" - globals "^12.1.0" - ignore "^4.0.6" - import-fresh "^3.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - js-yaml "^3.13.1" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" - lodash "^4.17.20" - minimatch "^3.0.4" + lodash.merge "^4.6.2" + minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" - strip-json-comments "^3.1.0" - table "^6.0.4" + optionator "^0.9.3" + strip-ansi "^6.0.1" text-table "^0.2.0" - v8-compile-cache "^2.0.3" -espree@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348" - integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw== +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== dependencies: - acorn "^7.4.0" - acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.3.0" + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" -espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" -esprima@^4.0.0, esprima@^4.0.1: +esprima@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.2.tgz#76a0fd66fcfe154fd292667dc264019750b1657b" + integrity sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A== + +esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== +esquery@^1.4.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" @@ -1948,15 +4962,15 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1: +estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== estraverse@^5.1.0, estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== esutils@^2.0.2: version "2.0.3" @@ -1966,18 +4980,26 @@ esutils@^2.0.2: etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + +event-target-shim@^5.0.0, event-target-shim@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -exec-sh@^0.3.2: - version "0.3.4" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" - integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== - execa@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" @@ -1991,154 +5013,140 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.3.tgz#0a34dabbad6d66100bd6f2c576c8669403f317f2" - integrity sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A== +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" is-stream "^2.0.0" merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - signal-exit "^3.0.2" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" strip-final-newline "^2.0.0" exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expect@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" - integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== - dependencies: - "@jest/types" "^26.6.2" - ansi-styles "^4.0.0" - jest-get-type "^26.3.0" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-regex-util "^26.0.0" - -express@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== - dependencies: - accepts "~1.3.7" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.0.0, expect@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + +expo-modules-autolinking@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-0.0.3.tgz#45ba8cb1798f9339347ae35e96e9cc70eafb3727" + integrity sha512-azkCRYj/DxbK4udDuDxA9beYzQTwpJ5a9QA0bBgha2jHtWdFGF4ZZWSY+zNA5mtU3KqzYt8jWHfoqgSvKyu1Aw== + dependencies: + chalk "^4.1.0" + commander "^7.2.0" + fast-glob "^3.2.5" + find-up "~5.0.0" + fs-extra "^9.1.0" + +expo-random@*: + version "14.0.1" + resolved "https://registry.yarnpkg.com/expo-random/-/expo-random-14.0.1.tgz#aedc6a3b6ff86c74a85989d77d20e0f1762ed669" + integrity sha512-gX2mtR9o+WelX21YizXUCD/y+a4ZL+RDthDmFkHxaYbdzjSYTn8u/igoje/l3WEO+/RYspmqUFa8w/ckNbt6Vg== + dependencies: + base64-js "^1.3.0" + +express@^4.17.1, express@^4.18.1, express@^4.18.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" + body-parser "1.20.2" + content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.4.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" - depd "~1.1.2" + depd "2.0.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "~1.1.2" + finalhandler "1.2.0" fresh "0.5.2" + http-errors "2.0.0" merge-descriptors "1.0.1" methods "~1.1.2" - on-finished "~2.3.0" + on-finished "2.4.1" parseurl "~1.3.3" path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" + proxy-addr "~2.0.7" + qs "6.11.0" range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" type-is "~1.6.18" utils-merge "1.0.1" vary "~1.1.2" -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= +ext@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" + type "^2.7.2" -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +extendable-error@^0.1.5: + version "0.1.7" + resolved "https://registry.yarnpkg.com/extendable-error/-/extendable-error-0.1.7.tgz#60b9adf206264ac920058a7395685ae4670c2b96" + integrity sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg== -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= +external-editor@^3.0.3, external-editor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +fast-base64-decode@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" + integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-diff@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" - integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== -fast-glob@^3.1.1: - version "3.2.5" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" - integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== +fast-glob@^3.2.12, fast-glob@^3.2.5, fast-glob@^3.2.9, fast-glob@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" + glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" + micromatch "^4.0.4" -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -2146,22 +5154,64 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fast-text-encoding@^1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" + integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== + +fast-uri@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134" + integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw== + +fast-xml-parser@^4.0.12: + version "4.4.0" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz#341cc98de71e9ba9e651a67f41f1752d1441a501" + integrity sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg== + dependencies: + strnum "^1.0.5" fastq@^1.6.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" - integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== dependencies: reusify "^1.0.4" fb-watchman@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" - integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== dependencies: bser "2.1.1" +fetch-blob@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c" + integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow== + +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + +figlet@^1.5.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.7.0.tgz#46903a04603fd19c3e380358418bb2703587a72e" + integrity sha512-gO8l3wvqo0V7wEFLXPbkX83b7MVjRrk1oRLfYlZXol8nEpb/ON9pcKLI4qpBv5YtOTfrINtqb7b40iYY2FTWFg== + +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2169,29 +5219,35 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +file-type@^16.5.4: + version "16.5.4" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.4.tgz#474fb4f704bee427681f98dd390058a172a6c2fd" + integrity sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw== + dependencies: + readable-web-to-node-stream "^3.0.0" + strtok3 "^6.2.4" + token-types "^4.1.1" -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" + minimatch "^5.0.1" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" -finalhandler@~1.1.2: +filter-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== + +finalhandler@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== @@ -2204,13 +5260,41 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-cache-dir@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + dependencies: + commondir "^1.0.1" + make-dir "^2.0.0" + pkg-dir "^3.0.0" + +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" + array-back "^3.0.1" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" @@ -2220,130 +5304,208 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0, find-up@~5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-workspaces@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/find-workspaces/-/find-workspaces-0.1.0.tgz#c01ddc81a1814b2c18927b26adb82afc97b63cea" + integrity sha512-DmHumOdSCtwY6qW6Syx3a/W6ZGYLhGiwqWCiPOsld4sxP9yeRh3LraKeu+G3l5ilgt8jOUAgjDHT4MOFZ8dQ3Q== + dependencies: + fast-glob "^3.2.12" + type-fest "^3.2.0" + yaml "^2.1.3" + +find-yarn-workspace-root2@1.2.16: + version "1.2.16" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz#60287009dd2f324f59646bdb4b7610a6b301c2a9" + integrity sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA== + dependencies: + micromatch "^4.0.2" + pkg-dir "^4.2.0" + +fix-esm@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fix-esm/-/fix-esm-1.0.1.tgz#e0e2199d841e43ff7db9b5f5ba7496bc45130ebb" + integrity sha512-EZtb7wPXZS54GaGxaWxMlhd1DUDCnAg5srlYdu/1ZVeW+7wwR3Tp59nu52dXByFs3MBRq+SByx1wDOJpRvLEXw== + dependencies: + "@babel/core" "^7.14.6" + "@babel/plugin-proposal-export-namespace-from" "^7.14.5" + "@babel/plugin-transform-modules-commonjs" "^7.14.5" + flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: - flatted "^3.1.0" + flatted "^3.2.9" + keyv "^4.5.3" rimraf "^3.0.2" -flatted@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" - integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= +flow-parser@0.*: + version "0.239.1" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.239.1.tgz#45cfc79bbcc54332cffeb13293b82a7c7358cd1c" + integrity sha512-topOrETNxJ6T2gAnQiWqAlzGPj8uI2wtmNOlDIMNB+qyvGJZ6R++STbUOTAYmvPhOMz2gXnXPH0hOvURYmrBow== -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +flow-parser@^0.185.0: + version "0.185.2" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.185.2.tgz#cb7ee57f77377d6c5d69a469e980f6332a15e492" + integrity sha512-2hJ5ACYeJCzNtiVULov6pljKOLygy0zddoqSI1fFetM+XRPpRshFdGEijtqlamA1XwyZ+7rhryI6FQFzvtLWUQ== -form-data@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" - integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== +follow-redirects@^1.14.0: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" + is-callable "^1.1.3" -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== dependencies: asynckit "^0.4.0" - combined-stream "^1.0.6" + combined-stream "^1.0.8" mime-types "^2.1.12" -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== dependencies: - map-cache "^0.2.2" + fetch-blob "^3.1.2" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== -fs-minipass@^1.2.5: - version "1.2.7" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" - integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== +fs-extra@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== dependencies: - minipass "^2.6.0" + minipass "^3.0.0" fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +fsevents@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -fsevents@~2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== dependencies: - aproba "^1.0.3" + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" -gensync@^1.0.0-beta.1: - version "1.0.0-beta.1" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" - integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-caller-file@^2.0.1: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -2351,248 +5513,306 @@ get-stream@^4.0.0: dependencies: pump "^3.0.0" -get-stream@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== dependencies: - pump "^3.0.0" + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= +get-symbol-from-current-process-h@^1.0.1, get-symbol-from-current-process-h@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-from-current-process-h/-/get-symbol-from-current-process-h-1.0.2.tgz#510af52eaef873f7028854c3377f47f7bb200265" + integrity sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw== -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= +get-tsconfig@^4.5.0: + version "4.7.5" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.5.tgz#5e012498579e9a6947511ed0cd403272c7acbbaf" + integrity sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw== dependencies: - assert-plus "^1.0.0" + resolve-pkg-maps "^1.0.0" -glob-parent@^5.0.0, glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== +get-uv-event-loop-napi-h@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/get-uv-event-loop-napi-h/-/get-uv-event-loop-napi-h-1.0.6.tgz#42b0b06b74c3ed21fbac8e7c72845fdb7a200208" + integrity sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg== dependencies: - is-glob "^4.0.1" + get-symbol-from-current-process-h "^1.0.1" + +git-config@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/git-config/-/git-config-0.0.7.tgz#a9c8a3ef07a776c3d72261356d8b727b62202b28" + integrity sha512-LidZlYZXWzVjS+M3TEwhtYBaYwLeOZrXci1tBgqp/vDdZTBMl02atvwb6G35L64ibscYoPnxfbwwUS+VZAISLA== + dependencies: + iniparser "~1.0.5" -glob-parent@^5.1.0: +glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^3.1.1" once "^1.3.0" path-is-absolute "^1.0.0" +glob@^9.2.0: + version "9.3.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" + integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== + dependencies: + fs.realpath "^1.0.0" + minimatch "^8.0.2" + minipass "^4.2.4" + path-scurry "^1.6.1" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^12.1.0: - version "12.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" - integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + +globalthis@^1.0.1, globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== dependencies: - type-fest "^0.8.1" + define-properties "^1.2.1" + gopd "^1.0.1" -globby@^11.0.1: - version "11.0.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83" - integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== +globby@^11.0.0, globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" slash "^3.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" -growly@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" - integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.5, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== +handlebars@^4.7.6: + version "4.7.8" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" + integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" + minimist "^1.2.5" + neo-async "^2.6.2" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" -has-unicode@^2.0.0: +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" + inherits "^2.0.3" + minimalistic-assert "^1.0.1" -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" + function-bind "^1.1.2" -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= +hermes-estree@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.8.0.tgz#530be27243ca49f008381c1f3e8b18fb26bf9ec0" + integrity sha512-W6JDAOLZ5pMPMjEiQGLCXSSV7pIBEgRR5zGkxgmzGSXHOxqV5dC/M1Zevqpbm9TZDE5tu358qZf8Vkzmsc+u7Q== -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= +hermes-parser@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.8.0.tgz#116dceaba32e45b16d6aefb5c4c830eaeba2d257" + integrity sha512-yZKalg1fTYG5eOiToLUaw69rQfZq/fi+/NtEXRU7N87K/XobNRhRWorh80oSge2lWUiZfTgUvRJH+XgZWrhoqA== dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" + hermes-estree "0.8.0" -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== +hermes-profile-transformer@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/hermes-profile-transformer/-/hermes-profile-transformer-0.0.6.tgz#bd0f5ecceda80dd0ddaae443469ab26fb38fc27b" + integrity sha512-cnN7bQUm65UWOy6cbGcCcZ3rpwW8Q/j4OP5aWRhEry4Z2t2aR1cjrbp0BS+KiBN0smvP1caBgAuxutvyvJILzQ== dependencies: - function-bind "^1.1.1" - -hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + source-map "^0.7.3" -html-encoding-sniffer@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" - integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== dependencies: - whatwg-encoding "^1.0.5" + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== dependencies: - depd "~1.1.2" + depd "2.0.0" inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" + agent-base "6" + debug "4" -human-signals@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" - integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +human-id@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/human-id/-/human-id-1.0.2.tgz#e654d4b2b0d8b07e45da9f6020d8af17ec0a5df3" + integrity sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw== -husky@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/husky/-/husky-5.1.3.tgz#1a0645a4fe3ffc006c4d0d8bd0bcb4c98787cc9d" - integrity sha512-fbNJ+Gz5wx2LIBtMweJNY1D7Uc8p1XERi5KNRMccwfQA+rXlxWNSdUxswo0gT8XqxywTIw7Ywm/F4v/O35RdMg== +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" -ieee754@^1.2.1: +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.2.0, ignore@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== -ignore@^5.1.4: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +image-size@^0.6.0: + version "0.6.3" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.6.3.tgz#e7e5c65bb534bd7cdcedd6cb5166272a85f75fb2" + integrity sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA== -import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" - integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg== + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" import-local@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6" - integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA== + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== dependencies: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" @@ -2600,172 +5820,155 @@ import-local@^3.0.2: imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - -indy-sdk@^1.16.0: - version "1.16.0" - resolved "https://registry.yarnpkg.com/indy-sdk/-/indy-sdk-1.16.0.tgz#034385ec5016388b0b52914f49d6334e372bb6c5" - integrity sha512-ATDzxBfHzFvHMiEXZSvmPjdswa3H4/X4npP4SV8yvzR18K/w0u/P4zp9TmEisAWA9of/hy1Fd1ziKMn+phHw+g== - dependencies: - bindings "^1.3.1" - nan "^2.11.1" - node-gyp "^4.0.0" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +iniparser@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/iniparser/-/iniparser-1.0.5.tgz#836d6befe6dfbfcee0bccf1cf9f2acc7027f783d" + integrity sha512-i40MWqgTU6h/70NtMsDVVDLjDYWwcIR1yIEVDPfxZIJno9z9L4s83p/V7vAu2i48Vj0gpByrkGFub7ko9XvPrw== + +inquirer@^7.3.3: + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +inquirer@^8.2.5: + version "8.2.6" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562" + integrity sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + wrap-ansi "^6.0.1" + +internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== dependencies: - kind-of "^6.0.0" + call-bind "^1.0.2" + get-intrinsic "^1.2.1" is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== dependencies: - binary-extensions "^2.0.0" + has-bigints "^1.0.1" -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-callable@^1.1.4, is-callable@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" - integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== - -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== dependencies: - ci-info "^2.0.0" + call-bind "^1.0.2" + has-tostringtag "^1.0.0" -is-core-module@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" - integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== - dependencies: - has "^1.0.3" +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= +is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.13.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.14.0.tgz#43b8ef9f46a6a08888db67b1ffd4ec9e3dfd59d1" + integrity sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A== dependencies: - kind-of "^3.0.2" + hasown "^2.0.2" -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== dependencies: - kind-of "^6.0.0" + is-typed-array "^1.1.13" is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-docker@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" - integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + has-tostringtag "^1.0.0" -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw== is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== is-fullwidth-code-point@^3.0.0: version "3.0.0" @@ -2777,499 +5980,576 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" -is-negative-zero@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" - integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== dependencies: - kind-of "^3.0.2" + has-tostringtag "^1.0.0" is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-plain-object@^2.0.3, is-plain-object@^2.0.4: +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== dependencies: isobject "^3.0.1" -is-potential-custom-element-name@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" - integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" -is-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== dependencies: - has-symbols "^1.0.1" + call-bind "^1.0.7" is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== is-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" - integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== dependencies: - has-symbols "^1.0.1" + has-tostringtag "^1.0.0" -is-typedarray@^1.0.0, is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-subdir@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-subdir/-/is-subdir-1.2.0.tgz#b791cd28fab5202e91a08280d51d9d7254fd20d4" + integrity sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw== + dependencies: + better-path-resolve "1.0.0" -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" -is-windows@^1.0.2: +is-windows@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw== -isarray@1.0.0, isarray@~1.0.0: +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" +iso-url@^1.1.5: + version "1.2.1" + resolved "https://registry.yarnpkg.com/iso-url/-/iso-url-1.2.1.tgz#db96a49d8d9a64a1c889fc07cc525d093afb1811" + integrity sha512-9JPDgCN4B7QPkLtYAAOrEuAWvP9rWvR5offAr0/SeF046wIkglqH3VXgYYP6NcsKslH80UIVgmPqNe3j7tG2ng== -isobject@^3.0.0, isobject@^3.0.1: +isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +isomorphic-webcrypto@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/isomorphic-webcrypto/-/isomorphic-webcrypto-2.3.8.tgz#4a7493b486ef072b9f11b6f8fd66adde856e3eec" + integrity sha512-XddQSI0WYlSCjxtm1AI8kWQOulf7hAN3k3DclF1sxDJZqOe0pcsOt675zvWW91cZH9hYs3nlA3Ev8QK5i80SxQ== + dependencies: + "@peculiar/webcrypto" "^1.0.22" + asmcrypto.js "^0.22.0" + b64-lite "^1.3.1" + b64u-lite "^1.0.1" + msrcrypto "^1.5.6" + str2buf "^1.3.0" + webcrypto-shim "^0.1.4" + optionalDependencies: + "@unimodules/core" "*" + "@unimodules/react-native-adapter" "*" + expo-random "*" + react-native-securerandom "^0.1.1" -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= +isomorphic-ws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" + integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== -istanbul-lib-coverage@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" - integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== -istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" - integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== +istanbul-lib-instrument@^5.0.4: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== dependencies: - "@babel/core" "^7.7.5" + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.0.0" + istanbul-lib-coverage "^3.2.0" semver "^6.3.0" +istanbul-lib-instrument@^6.0.0: + version "6.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" + integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== + dependencies: + "@babel/core" "^7.23.9" + "@babel/parser" "^7.23.9" + "@istanbuljs/schema" "^0.1.3" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + istanbul-lib-report@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" - integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== dependencies: istanbul-lib-coverage "^3.0.0" - make-dir "^3.0.0" + make-dir "^4.0.0" supports-color "^7.1.0" istanbul-lib-source-maps@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9" - integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg== + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== dependencies: debug "^4.1.1" istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" - integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== +istanbul-reports@^3.1.3: + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jest-changed-files@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" - integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== - dependencies: - "@jest/types" "^26.6.2" - execa "^4.0.0" - throat "^5.0.0" +jake@^10.8.5: + version "10.9.1" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.1.tgz#8dc96b7fcc41cb19aa502af506da4e1d56f5e62b" + integrity sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== + dependencies: + execa "^5.0.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^1.0.0" + is-generator-fn "^2.0.0" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + pretty-format "^29.7.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" -jest-cli@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" - integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== dependencies: - "@jest/core" "^26.6.3" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" chalk "^4.0.0" + create-jest "^29.7.0" exit "^0.1.2" - graceful-fs "^4.2.4" import-local "^3.0.2" - is-ci "^2.0.0" - jest-config "^26.6.3" - jest-util "^26.6.2" - jest-validate "^26.6.2" - prompts "^2.0.1" - yargs "^15.4.1" - -jest-config@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" - integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== - dependencies: - "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^26.6.3" - "@jest/types" "^26.6.2" - babel-jest "^26.6.3" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + yargs "^17.3.1" + +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" chalk "^4.0.0" + ci-info "^3.2.0" deepmerge "^4.2.2" - glob "^7.1.1" - graceful-fs "^4.2.4" - jest-environment-jsdom "^26.6.2" - jest-environment-node "^26.6.2" - jest-get-type "^26.3.0" - jest-jasmine2 "^26.6.3" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" - micromatch "^4.0.2" - pretty-format "^26.6.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" -jest-diff@^26.0.0, jest-diff@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" - integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== dependencies: chalk "^4.0.0" - diff-sequences "^26.6.2" - jest-get-type "^26.3.0" - pretty-format "^26.6.2" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" -jest-docblock@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" - integrity sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w== +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== dependencies: detect-newline "^3.0.0" -jest-each@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" - integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^29.6.3" chalk "^4.0.0" - jest-get-type "^26.3.0" - jest-util "^26.6.2" - pretty-format "^26.6.2" - -jest-environment-jsdom@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" - integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== - dependencies: - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/node" "*" - jest-mock "^26.6.2" - jest-util "^26.6.2" - jsdom "^16.4.0" - -jest-environment-node@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" - integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== - dependencies: - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" + +jest-environment-node@^29.2.1, jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" - jest-mock "^26.6.2" - jest-util "^26.6.2" + jest-mock "^29.7.0" + jest-util "^29.7.0" jest-get-type@^26.3.0: version "26.3.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== -jest-haste-map@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" - integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== dependencies: - "@jest/types" "^26.6.2" - "@types/graceful-fs" "^4.1.2" + "@jest/types" "^29.6.3" + "@types/graceful-fs" "^4.1.3" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" - graceful-fs "^4.2.4" - jest-regex-util "^26.0.0" - jest-serializer "^26.6.2" - jest-util "^26.6.2" - jest-worker "^26.6.2" - micromatch "^4.0.2" - sane "^4.0.3" - walker "^1.0.7" + graceful-fs "^4.2.9" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" + micromatch "^4.0.4" + walker "^1.0.8" optionalDependencies: - fsevents "^2.1.2" - -jest-jasmine2@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" - integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== - dependencies: - "@babel/traverse" "^7.1.0" - "@jest/environment" "^26.6.2" - "@jest/source-map" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - expect "^26.6.2" - is-generator-fn "^2.0.0" - jest-each "^26.6.2" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-runtime "^26.6.3" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - pretty-format "^26.6.2" - throat "^5.0.0" + fsevents "^2.3.2" -jest-leak-detector@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" - integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== dependencies: - jest-get-type "^26.3.0" - pretty-format "^26.6.2" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" -jest-matcher-utils@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" - integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== dependencies: chalk "^4.0.0" - jest-diff "^26.6.2" - jest-get-type "^26.3.0" - pretty-format "^26.6.2" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" -jest-message-util@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" - integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== dependencies: - "@babel/code-frame" "^7.0.0" - "@jest/types" "^26.6.2" + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" - graceful-fs "^4.2.4" - micromatch "^4.0.2" - pretty-format "^26.6.2" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" slash "^3.0.0" - stack-utils "^2.0.2" + stack-utils "^2.0.3" -jest-mock@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" - integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^29.6.3" "@types/node" "*" + jest-util "^29.7.0" jest-pnp-resolver@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" - integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== -jest-regex-util@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" - integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== +jest-regex-util@^27.0.6: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" + integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== -jest-resolve-dependencies@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" - integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== dependencies: - "@jest/types" "^26.6.2" - jest-regex-util "^26.0.0" - jest-snapshot "^26.6.2" + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" -jest-resolve@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" - integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== dependencies: - "@jest/types" "^26.6.2" chalk "^4.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" jest-pnp-resolver "^1.2.2" - jest-util "^26.6.2" - read-pkg-up "^7.0.1" - resolve "^1.18.1" + jest-util "^29.7.0" + jest-validate "^29.7.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" slash "^3.0.0" -jest-runner@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" - integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== dependencies: - "@jest/console" "^26.6.2" - "@jest/environment" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" - emittery "^0.7.1" - exit "^0.1.2" - graceful-fs "^4.2.4" - jest-config "^26.6.3" - jest-docblock "^26.0.0" - jest-haste-map "^26.6.2" - jest-leak-detector "^26.6.2" - jest-message-util "^26.6.2" - jest-resolve "^26.6.2" - jest-runtime "^26.6.3" - jest-util "^26.6.2" - jest-worker "^26.6.2" - source-map-support "^0.5.6" - throat "^5.0.0" - -jest-runtime@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" - integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== - dependencies: - "@jest/console" "^26.6.2" - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/globals" "^26.6.2" - "@jest/source-map" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/yargs" "^15.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" chalk "^4.0.0" - cjs-module-lexer "^0.6.0" + cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" - exit "^0.1.2" glob "^7.1.3" - graceful-fs "^4.2.4" - jest-config "^26.6.3" - jest-haste-map "^26.6.2" - jest-message-util "^26.6.2" - jest-mock "^26.6.2" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" slash "^3.0.0" strip-bom "^4.0.0" - yargs "^15.4.1" -jest-serializer@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" - integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== +jest-serializer@^27.0.6: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" + integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== dependencies: "@types/node" "*" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" -jest-snapshot@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" - integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== dependencies: - "@babel/types" "^7.0.0" - "@jest/types" "^26.6.2" - "@types/babel__traverse" "^7.0.4" - "@types/prettier" "^2.0.0" + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^26.6.2" - graceful-fs "^4.2.4" - jest-diff "^26.6.2" - jest-get-type "^26.3.0" - jest-haste-map "^26.6.2" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-resolve "^26.6.2" + expect "^29.7.0" + graceful-fs "^4.2.9" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" natural-compare "^1.4.0" - pretty-format "^26.6.2" - semver "^7.3.2" + pretty-format "^29.7.0" + semver "^7.5.3" -jest-util@^26.1.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.3.0.tgz#a8974b191df30e2bf523ebbfdbaeb8efca535b3e" - integrity sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw== +jest-util@^27.2.0: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" + integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" - graceful-fs "^4.2.4" - is-ci "^2.0.0" - micromatch "^4.0.2" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" -jest-util@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" - integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== +jest-util@^29.0.0, jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" - graceful-fs "^4.2.4" - is-ci "^2.0.0" - micromatch "^4.0.2" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" -jest-validate@^26.6.2: +jest-validate@^26.5.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" integrity sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ== @@ -3281,97 +6561,152 @@ jest-validate@^26.6.2: leven "^3.1.0" pretty-format "^26.6.2" -jest-watcher@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.2.tgz#a5b683b8f9d68dbcb1d7dae32172d2cca0592975" - integrity sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ== +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== dependencies: - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/types" "^29.6.3" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.6.3" + leven "^3.1.0" + pretty-format "^29.7.0" + +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + dependencies: + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^26.6.2" + emittery "^0.13.1" + jest-util "^29.7.0" string-length "^4.0.1" -jest-worker@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" - integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== +jest-worker@^27.2.0: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== dependencies: "@types/node" "*" merge-stream "^2.0.0" - supports-color "^7.0.0" + supports-color "^8.0.0" -jest@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" - integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== dependencies: - "@jest/core" "^26.6.3" - import-local "^3.0.2" - jest-cli "^26.6.3" - -js-sha256@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" - integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" -js-tokens@^4.0.0: +jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + dependencies: + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" + import-local "^3.0.2" + jest-cli "^29.7.0" + +joi@^17.2.1: + version "17.13.3" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.3.tgz#0f5cc1169c999b30d344366d384b12d92558bcec" + integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA== + dependencies: + "@hapi/hoek" "^9.3.0" + "@hapi/topo" "^5.1.0" + "@sideway/address" "^4.1.5" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + +js-base64@^3.7.6: + version "3.7.7" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" + integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== + +js-sha3@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1: - version "3.14.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" - integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== +js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.6.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== dependencies: argparse "^1.0.7" esprima "^4.0.0" -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -jsdom@^16.4.0: - version "16.5.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.5.0.tgz#9e453505600cc5a70b385750d35256f380730cc4" - integrity sha512-QxZH0nmDTnTTVI0YDm4RUlaUPl5dcyn62G5TMDNfMmTW+J1u1v9gCR8WR+WZ6UghAa7nKJjDOFaI00eMMWvJFQ== - dependencies: - abab "^2.0.5" - acorn "^8.0.5" - acorn-globals "^6.0.0" - cssom "^0.4.4" - cssstyle "^2.3.0" - data-urls "^2.0.0" - decimal.js "^10.2.1" - domexception "^2.0.1" - escodegen "^2.0.0" - html-encoding-sniffer "^2.0.1" - is-potential-custom-element-name "^1.0.0" - nwsapi "^2.2.0" - parse5 "6.0.1" - request "^2.88.2" - request-promise-native "^1.0.9" - saxes "^5.0.1" - symbol-tree "^3.2.4" - tough-cookie "^4.0.0" - w3c-hr-time "^1.0.2" - w3c-xmlserializer "^2.0.0" - webidl-conversions "^6.1.0" - whatwg-encoding "^1.0.5" - whatwg-mimetype "^2.3.0" - whatwg-url "^8.0.0" - ws "^7.4.4" - xml-name-validator "^3.0.0" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsc-android@^250231.0.0: + version "250231.0.0" + resolved "https://registry.yarnpkg.com/jsc-android/-/jsc-android-250231.0.0.tgz#91720f8df382a108872fa4b3f558f33ba5e95262" + integrity sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw== + +jsc-safe-url@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz#141c14fbb43791e88d5dc64e85a374575a83477a" + integrity sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q== + +jscodeshift@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.14.0.tgz#7542e6715d6d2e8bde0b4e883f0ccea358b46881" + integrity sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA== + dependencies: + "@babel/core" "^7.13.16" + "@babel/parser" "^7.13.16" + "@babel/plugin-proposal-class-properties" "^7.13.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.13.8" + "@babel/plugin-proposal-optional-chaining" "^7.13.12" + "@babel/plugin-transform-modules-commonjs" "^7.13.8" + "@babel/preset-flow" "^7.13.13" + "@babel/preset-typescript" "^7.13.0" + "@babel/register" "^7.13.16" + babel-core "^7.0.0-bridge.0" + chalk "^4.1.2" + flow-parser "0.*" + graceful-fs "^4.2.4" + micromatch "^4.0.4" + neo-async "^2.5.0" + node-dir "^0.1.17" + recast "^0.21.0" + temp "^0.8.4" + write-file-atomic "^2.3.0" jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -3392,58 +6727,92 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json-stringify-safe@~5.0.1: +json-stringify-safe@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@2.x, json5@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" - integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== +json-text-sequence@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/json-text-sequence/-/json-text-sequence-0.3.0.tgz#6603e0ee45da41f949669fd18744b97fb209e6ce" + integrity sha512-7khKIYPKwXQem4lWXfpIN/FEnhztCeRPSxH4qm3fVlqulwujrRDD54xAwDDn/qVKpFtV550+QAkcWJcufzqQuA== dependencies: - minimist "^1.2.5" + "@sovpro/delimited-stream" "^1.1.0" -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" + minimist "^1.2.0" -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -kind-of@^4.0.0: +jsonfile@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== dependencies: - is-buffer "^1.1.5" + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== +jsonld-signatures@^11.0.0: + version "11.2.1" + resolved "https://registry.yarnpkg.com/jsonld-signatures/-/jsonld-signatures-11.2.1.tgz#e2ff23ac7476fcdb92e5fecd9a1734ceaf904bb0" + integrity sha512-RNaHTEeRrX0jWeidPCwxMq/E/Ze94zFyEZz/v267ObbCHQlXhPO7GtkY6N5PSHQfQhZPXa8NlMBg5LiDF4dNbA== + dependencies: + "@digitalbazaar/security-context" "^1.0.0" + jsonld "^8.0.0" + serialize-error "^8.1.0" + +jsonld@^8.0.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-8.3.2.tgz#7033f8994aed346b536e9046025f7f1fe9669934" + integrity sha512-MwBbq95szLwt8eVQ1Bcfwmgju/Y5P2GdtlHE2ncyfuYjIdEhluUVyj1eudacf1mOkWIoS9GpDBTECqhmq7EOaA== + dependencies: + "@digitalbazaar/http-client" "^3.4.1" + canonicalize "^1.0.1" + lru-cache "^6.0.0" + rdf-canonize "^3.4.0" + +jsonpath@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/jsonpath/-/jsonpath-1.1.1.tgz#0ca1ed8fb65bb3309248cc9d5466d12d5b0b9901" + integrity sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w== + dependencies: + esprima "1.2.2" + static-eval "2.0.2" + underscore "1.12.1" + +jwt-decode@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" + integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== -kind-of@^6.0.0, kind-of@^6.0.2: +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== @@ -3453,6 +6822,44 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +ky-universal@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.11.0.tgz#f5edf857865aaaea416a1968222148ad7d9e4017" + integrity sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw== + dependencies: + abort-controller "^3.0.0" + node-fetch "^3.2.10" + +ky-universal@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.8.2.tgz#edc398d54cf495d7d6830aa1ab69559a3cc7f824" + integrity sha512-xe0JaOH9QeYxdyGLnzUOVGK4Z6FGvDVzcXFTdrYA1f33MZdEa45sUDaMBy98xQMcsd2XIBrTXRrRYnegcSdgVQ== + dependencies: + abort-controller "^3.0.0" + node-fetch "3.0.0-beta.9" + +ky@^0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/ky/-/ky-0.25.1.tgz#0df0bd872a9cc57e31acd5dbc1443547c881bfbc" + integrity sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA== + +ky@^0.33.3: + version "0.33.3" + resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543" + integrity sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw== + +language-subtag-registry@^0.3.20: + version "0.3.23" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" + integrity sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== + +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== + dependencies: + language-subtag-registry "^0.3.20" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -3469,41 +6876,50 @@ levn@^0.4.1: levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== dependencies: prelude-ls "~1.1.2" type-check "~0.3.2" -libphonenumber-js@^1.9.7: - version "1.9.13" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.13.tgz#41ecc8635103c5af44be422f0606a518022248fd" - integrity sha512-DOvAj9Now6KqP+L1Q3JrM3iNhH/mXiOPTj6kxb9OnJbYsVYRlVdvRY1kCpU3Tz9VegIEi6MgDrviBaAnvB3aSw== +libphonenumber-js@^1.10.53: + version "1.11.4" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.4.tgz#e63fe553f45661b30bb10bb8c82c9cf2b22ec32a" + integrity sha512-F/R50HQuWWYcmU/esP5jrH5LiWYaN7DpN0a/99U8+mnGGtnx8kmRE+649dQh3v+CowXXZc8vpkf5AmYkO0AQ7Q== + +libsodium-wrappers@^0.7.6: + version "0.7.14" + resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.14.tgz#b21d9e8d58de686c6318a772805ee1c5d02035a5" + integrity sha512-300TtsePizhJZ7HjLmWr6hLHAgJUxIGhapSw+EwfCtDuWaEmEdGXSQv6j6qFw0bs9l4vS2NH9BtOHfXAq6h5kQ== + dependencies: + libsodium "^0.7.14" + +libsodium@^0.7.14: + version "0.7.14" + resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.14.tgz#d9daace70dbc36051b947d37999bb6337c364c88" + integrity sha512-/pOd7eO6oZrfORquRTC4284OUJFcMi8F3Vnc9xtRBT0teLfOUxWIItaBFF3odYjZ7nlJNwnLdUVEUFHxVyX/Sw== lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= +load-yaml-file@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/load-yaml-file/-/load-yaml-file-0.2.0.tgz#af854edaf2bea89346c07549122753c07372f64d" + integrity sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw== dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" + graceful-fs "^4.1.5" + js-yaml "^3.13.0" + pify "^4.0.1" + strip-bom "^3.0.0" -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" + p-locate "^3.0.0" + path-exists "^3.0.0" locate-path@^5.0.0: version "5.0.0" @@ -3512,28 +6928,101 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.startcase@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz#9436e34ed26093ed7ffae1936144350915d9add8" + integrity sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg== + +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== -lodash@4.x, lodash@^4.17.20: +lodash@^4.17.19, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -lodash@^4.17.15, lodash@^4.17.19: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= +logkitty@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/logkitty/-/logkitty-0.7.1.tgz#8e8d62f4085a826e8d38987722570234e33c6aa7" + integrity sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ== dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" + ansi-fragments "^0.2.1" + dayjs "^1.8.15" + yargs "^15.1.0" + +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +long@^5.0.0, long@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" lru-cache@^6.0.0: version "6.0.0" @@ -3542,79 +7031,76 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -make-dir@^3.0.0: +lru_map@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.4.1.tgz#f7b4046283c79fb7370c36f8fca6aee4324b0a98" + integrity sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg== + +luxon@^3.3.0: + version "3.4.4" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af" + integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA== + +make-dir@^2.0.0, make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: semver "^6.0.0" -make-error@1.x, make-error@^1.1.1: +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + +make-error@1.x, make-error@^1.1.1, make-error@^1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -makeerror@1.0.x: - version "1.0.11" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" - integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= - dependencies: - tmpl "1.0.x" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= +make-promises-safe@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/make-promises-safe/-/make-promises-safe-5.1.0.tgz#dd9d311f555bcaa144f12e225b3d37785f0aa8f2" + integrity sha512-AfdZ49rtyhQR/6cqVKGoH7y4ql7XkS5HJI1lZm0/5N6CQosy1eYbBJ/qbhkKHzo17UH7M918Bysf6XB9f3kS1g== -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== dependencies: - object-visit "^1.0.0" + tmpl "1.0.5" media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== -memorystream@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" - integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI= - -meow@^3.3.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" +memoize-one@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0: +merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== @@ -3622,352 +7108,728 @@ merge2@^1.3.0: methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" +metro-babel-transformer@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.73.10.tgz#b27732fa3869f397246ee8ecf03b64622ab738c1" + integrity sha512-Yv2myTSnpzt/lTyurLvqYbBkytvUJcLHN8XD3t7W6rGiLTQPzmf1zypHQLphvcAXtCWBOXFtH7KLOSi2/qMg+A== + dependencies: + "@babel/core" "^7.20.0" + hermes-parser "0.8.0" + metro-source-map "0.73.10" + nullthrows "^1.1.1" -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== +metro-cache-key@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.73.10.tgz#8d63591187d295b62a80aed64a87864b1e9d67a2" + integrity sha512-JMVDl/EREDiUW//cIcUzRjKSwE2AFxVWk47cFBer+KA4ohXIG2CQPEquT56hOw1Y1s6gKNxxs1OlAOEsubrFjw== + +metro-cache@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.73.10.tgz#02e9cb7c1e42aab5268d2ecce35ad8f2c08891de" + integrity sha512-wPGlQZpdVlM404m7MxJqJ+hTReDr5epvfPbt2LerUAHY9RN99w61FeeAe25BMZBwgUgDtAsfGlJ51MBHg8MAqw== + dependencies: + metro-core "0.73.10" + rimraf "^3.0.2" + +metro-config@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.73.10.tgz#a9ec3d0a1290369e3f46c467a4c4f6dd43acc223" + integrity sha512-wIlybd1Z9I8K2KcStTiJxTB7OK529dxFgogNpKCTU/3DxkgAASqSkgXnZP6kVyqjh5EOWAKFe5U6IPic7kXDdQ== + dependencies: + cosmiconfig "^5.0.5" + jest-validate "^26.5.2" + metro "0.73.10" + metro-cache "0.73.10" + metro-core "0.73.10" + metro-runtime "0.73.10" + +metro-core@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.73.10.tgz#feb3c228aa8c0dde71d8e4cef614cc3a1dc3bbd7" + integrity sha512-5uYkajIxKyL6W45iz/ftNnYPe1l92CvF2QJeon1CHsMXkEiOJxEjo41l+iSnO/YodBGrmMCyupSO4wOQGUc0lw== + dependencies: + lodash.throttle "^4.1.1" + metro-resolver "0.73.10" + +metro-file-map@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.73.10.tgz#55bd906fb7c1bef8e1a31df4b29a3ef4b49f0b5a" + integrity sha512-XOMWAybeaXyD6zmVZPnoCCL2oO3rp4ta76oUlqWP0skBzhFxVtkE/UtDwApEMUY361JeBBago647gnKiARs+1g== + dependencies: + abort-controller "^3.0.0" + anymatch "^3.0.3" + debug "^2.2.0" + fb-watchman "^2.0.0" + graceful-fs "^4.2.4" + invariant "^2.2.4" + jest-regex-util "^27.0.6" + jest-serializer "^27.0.6" + jest-util "^27.2.0" + jest-worker "^27.2.0" + micromatch "^4.0.4" + nullthrows "^1.1.1" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.3.2" + +metro-hermes-compiler@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-hermes-compiler/-/metro-hermes-compiler-0.73.10.tgz#4525a7835c803a5d0b3b05c6619202e2273d630f" + integrity sha512-rTRWEzkVrwtQLiYkOXhSdsKkIObnL+Jqo+IXHI7VEK2aSLWRAbtGNqECBs44kbOUypDYTFFE+WLtoqvUWqYkWg== + +metro-inspector-proxy@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-inspector-proxy/-/metro-inspector-proxy-0.73.10.tgz#752fed2ab88199c9dcc3369c3d59da6c5b954a51" + integrity sha512-CEEvocYc5xCCZBtGSIggMCiRiXTrnBbh8pmjKQqm9TtJZALeOGyt5pXUaEkKGnhrXETrexsg6yIbsQHhEvVfvQ== + dependencies: + connect "^3.6.5" + debug "^2.2.0" + ws "^7.5.1" + yargs "^17.5.1" + +metro-minify-terser@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.73.10.tgz#557eab3a512b90b7779350ff5d25a215c4dbe61f" + integrity sha512-uG7TSKQ/i0p9kM1qXrwbmY3v+6BrMItsOcEXcSP8Z+68bb+t9HeVK0T/hIfUu1v1PEnonhkhfzVsaP8QyTd5lQ== + dependencies: + terser "^5.15.0" + +metro-minify-uglify@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-minify-uglify/-/metro-minify-uglify-0.73.10.tgz#4de79056d502479733854c90f2075374353ea154" + integrity sha512-eocnSeJKnLz/UoYntVFhCJffED7SLSgbCHgNvI6ju6hFb6EFHGJT9OLbkJWeXaWBWD3Zw5mYLS8GGqGn/CHZPA== + dependencies: + uglify-es "^3.1.9" + +metro-react-native-babel-preset@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.10.tgz#304b24bb391537d2c987732cc0a9774be227d3f6" + integrity sha512-1/dnH4EHwFb2RKEKx34vVDpUS3urt2WEeR8FYim+ogqALg4sTpG7yeQPxWpbgKATezt4rNfqAANpIyH19MS4BQ== + dependencies: + "@babel/core" "^7.20.0" + "@babel/plugin-proposal-async-generator-functions" "^7.0.0" + "@babel/plugin-proposal-class-properties" "^7.0.0" + "@babel/plugin-proposal-export-default-from" "^7.0.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.0.0" + "@babel/plugin-proposal-object-rest-spread" "^7.0.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.0.0" + "@babel/plugin-proposal-optional-chaining" "^7.0.0" + "@babel/plugin-syntax-dynamic-import" "^7.0.0" + "@babel/plugin-syntax-export-default-from" "^7.0.0" + "@babel/plugin-syntax-flow" "^7.18.0" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.0.0" + "@babel/plugin-syntax-optional-chaining" "^7.0.0" + "@babel/plugin-transform-arrow-functions" "^7.0.0" + "@babel/plugin-transform-async-to-generator" "^7.0.0" + "@babel/plugin-transform-block-scoping" "^7.0.0" + "@babel/plugin-transform-classes" "^7.0.0" + "@babel/plugin-transform-computed-properties" "^7.0.0" + "@babel/plugin-transform-destructuring" "^7.0.0" + "@babel/plugin-transform-flow-strip-types" "^7.0.0" + "@babel/plugin-transform-function-name" "^7.0.0" + "@babel/plugin-transform-literals" "^7.0.0" + "@babel/plugin-transform-modules-commonjs" "^7.0.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.0.0" + "@babel/plugin-transform-parameters" "^7.0.0" + "@babel/plugin-transform-react-display-name" "^7.0.0" + "@babel/plugin-transform-react-jsx" "^7.0.0" + "@babel/plugin-transform-react-jsx-self" "^7.0.0" + "@babel/plugin-transform-react-jsx-source" "^7.0.0" + "@babel/plugin-transform-runtime" "^7.0.0" + "@babel/plugin-transform-shorthand-properties" "^7.0.0" + "@babel/plugin-transform-spread" "^7.0.0" + "@babel/plugin-transform-sticky-regex" "^7.0.0" + "@babel/plugin-transform-template-literals" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.5.0" + "@babel/plugin-transform-unicode-regex" "^7.0.0" + "@babel/template" "^7.0.0" + react-refresh "^0.4.0" + +metro-react-native-babel-transformer@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.73.10.tgz#4e20a9ce131b873cda0b5a44d3eb4002134a64b8" + integrity sha512-4G/upwqKdmKEjmsNa92/NEgsOxUWOygBVs+FXWfXWKgybrmcjh3NoqdRYrROo9ZRA/sB9Y/ZXKVkWOGKHtGzgg== + dependencies: + "@babel/core" "^7.20.0" + babel-preset-fbjs "^3.4.0" + hermes-parser "0.8.0" + metro-babel-transformer "0.73.10" + metro-react-native-babel-preset "0.73.10" + metro-source-map "0.73.10" + nullthrows "^1.1.1" + +metro-resolver@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.73.10.tgz#c39a3bd8d33e5d78cb256110d29707d8d49ed0be" + integrity sha512-HeXbs+0wjakaaVQ5BI7eT7uqxlZTc9rnyw6cdBWWMgUWB++KpoI0Ge7Hi6eQAOoVAzXC3m26mPFYLejpzTWjng== + dependencies: + absolute-path "^0.0.0" + +metro-runtime@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.73.10.tgz#c3de19d17e75ffe1a145778d99422e7ffc208768" + integrity sha512-EpVKm4eN0Fgx2PEWpJ5NiMArV8zVoOin866jIIvzFLpmkZz1UEqgjf2JAfUJnjgv3fjSV3JqeGG2vZCaGQBTow== + dependencies: + "@babel/runtime" "^7.0.0" + react-refresh "^0.4.0" + +metro-source-map@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.73.10.tgz#28e09a28f1a2f7a4f8d0845b845cbed74e2f48f9" + integrity sha512-NAGv14701p/YaFZ76KzyPkacBw/QlEJF1f8elfs23N1tC33YyKLDKvPAzFJiYqjdcFvuuuDCA8JCXd2TgLxNPw== + dependencies: + "@babel/traverse" "^7.20.0" + "@babel/types" "^7.20.0" + invariant "^2.2.4" + metro-symbolicate "0.73.10" + nullthrows "^1.1.1" + ob1 "0.73.10" + source-map "^0.5.6" + vlq "^1.0.0" + +metro-symbolicate@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.73.10.tgz#7853a9a8fbfd615a5c9db698fffc685441ac880f" + integrity sha512-PmCe3TOe1c/NVwMlB+B17me951kfkB3Wve5RqJn+ErPAj93od1nxicp6OJe7JT4QBRnpUP8p9tw2sHKqceIzkA== + dependencies: + invariant "^2.2.4" + metro-source-map "0.73.10" + nullthrows "^1.1.1" + source-map "^0.5.6" + through2 "^2.0.1" + vlq "^1.0.0" + +metro-transform-plugins@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.73.10.tgz#1b762330cbbedb6c18438edc3d76b063c88882af" + integrity sha512-D4AgD3Vsrac+4YksaPmxs/0ocT67bvwTkFSIgWWeDvWwIG0U1iHzTS9f8Bvb4PITnXryDoFtjI6OWF7uOpGxpA== + dependencies: + "@babel/core" "^7.20.0" + "@babel/generator" "^7.20.0" + "@babel/template" "^7.0.0" + "@babel/traverse" "^7.20.0" + nullthrows "^1.1.1" + +metro-transform-worker@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.73.10.tgz#bb401dbd7b10a6fe443a5f7970cba38425efece0" + integrity sha512-IySvVubudFxahxOljWtP0QIMMpgUrCP0bW16cz2Enof0PdumwmR7uU3dTbNq6S+XTzuMHR+076aIe4VhPAWsIQ== + dependencies: + "@babel/core" "^7.20.0" + "@babel/generator" "^7.20.0" + "@babel/parser" "^7.20.0" + "@babel/types" "^7.20.0" + babel-preset-fbjs "^3.4.0" + metro "0.73.10" + metro-babel-transformer "0.73.10" + metro-cache "0.73.10" + metro-cache-key "0.73.10" + metro-hermes-compiler "0.73.10" + metro-source-map "0.73.10" + metro-transform-plugins "0.73.10" + nullthrows "^1.1.1" + +metro@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/metro/-/metro-0.73.10.tgz#d9a0efb1e403e3aee5cf5140e0a96a7220c23901" + integrity sha512-J2gBhNHFtc/Z48ysF0B/bfTwUwaRDLjNv7egfhQCc+934dpXcjJG2KZFeuybF+CvA9vo4QUi56G2U+RSAJ5tsA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/core" "^7.20.0" + "@babel/generator" "^7.20.0" + "@babel/parser" "^7.20.0" + "@babel/template" "^7.0.0" + "@babel/traverse" "^7.20.0" + "@babel/types" "^7.20.0" + absolute-path "^0.0.0" + accepts "^1.3.7" + async "^3.2.2" + chalk "^4.0.0" + ci-info "^2.0.0" + connect "^3.6.5" + debug "^2.2.0" + denodeify "^1.2.1" + error-stack-parser "^2.0.6" + graceful-fs "^4.2.4" + hermes-parser "0.8.0" + image-size "^0.6.0" + invariant "^2.2.4" + jest-worker "^27.2.0" + jsc-safe-url "^0.2.2" + lodash.throttle "^4.1.1" + metro-babel-transformer "0.73.10" + metro-cache "0.73.10" + metro-cache-key "0.73.10" + metro-config "0.73.10" + metro-core "0.73.10" + metro-file-map "0.73.10" + metro-hermes-compiler "0.73.10" + metro-inspector-proxy "0.73.10" + metro-minify-terser "0.73.10" + metro-minify-uglify "0.73.10" + metro-react-native-babel-preset "0.73.10" + metro-resolver "0.73.10" + metro-runtime "0.73.10" + metro-source-map "0.73.10" + metro-symbolicate "0.73.10" + metro-transform-plugins "0.73.10" + metro-transform-worker "0.73.10" + mime-types "^2.1.27" + node-fetch "^2.2.0" + nullthrows "^1.1.1" + rimraf "^3.0.2" + serialize-error "^2.1.0" + source-map "^0.5.6" + strip-ansi "^6.0.0" + temp "0.8.3" + throat "^5.0.0" + ws "^7.5.1" + yargs "^17.5.1" + +micromatch@^4.0.2, micromatch@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== dependencies: - braces "^3.0.1" - picomatch "^2.0.5" + braces "^3.0.3" + picomatch "^2.3.1" -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: - mime-db "1.44.0" + mime-db "1.52.0" mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@^2.4.1: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + +minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" -minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" - integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== +minimatch@^8.0.2: + version "8.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" + integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" + brace-expansion "^2.0.1" -minizlib@^1.2.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" - integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: - minipass "^2.9.0" + brace-expansion "^2.0.1" -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^4.2.4: + version "4.2.8" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" + integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^0.5.1, mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" + minimist "^1.2.6" -mkdirp@1.x, mkdirp@^1.0.4: +mkdirp@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@^0.5.0: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" +mri@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -nan@^2.11.1: - version "2.14.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" - integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +msrcrypto@^1.5.6: + version "1.5.8" + resolved "https://registry.yarnpkg.com/msrcrypto/-/msrcrypto-1.5.8.tgz#be419be4945bf134d8af52e9d43be7fa261f4a1c" + integrity sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q== + +multer@^1.4.5-lts.1: + version "1.4.5-lts.1" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.5-lts.1.tgz#803e24ad1984f58edffbc79f56e305aec5cfd1ac" + integrity sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + +multiformats@^12.1.3: + version "12.1.3" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-12.1.3.tgz#cbf7a9861e11e74f8228b21376088cb43ba8754e" + integrity sha512-eajQ/ZH7qXZQR2AgtfpmSMizQzmyYVmCql7pdhldPuYQi4atACekbJaQplk6dWyIi10jCaFnd6pqvcEFXjbaJw== + +multiformats@^9.4.2, multiformats@^9.6.5, multiformats@^9.9.0: + version "9.9.0" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" + integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.5.0, neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +neon-cli@0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/neon-cli/-/neon-cli-0.10.1.tgz#9705b7b860550faffe6344fc9ab6f6e389c95ed6" + integrity sha512-kOd9ELaYETe1J1nBEOYD7koAZVj6xR9TGwOPccAsWmwL5amkaXXXwXHCUHkBAWujlgSZY5f2pT+pFGkzoHExYQ== + dependencies: + chalk "^4.1.0" + command-line-args "^5.1.1" + command-line-commands "^3.0.1" + command-line-usage "^6.1.0" + git-config "0.0.7" + handlebars "^4.7.6" + inquirer "^7.3.3" + make-promises-safe "^5.1.0" + rimraf "^3.0.2" + semver "^7.3.2" + toml "^3.0.0" + ts-typed-json "^0.3.2" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^3.0.0" -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-fetch@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +nocache@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/nocache/-/nocache-3.0.4.tgz#5b37a56ec6e09fc7d401dceaed2eab40c8bfdf79" + integrity sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw== -node-gyp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-4.0.0.tgz#972654af4e5dd0cd2a19081b4b46fe0442ba6f45" - integrity sha512-2XiryJ8sICNo6ej8d0idXDEMKfVfFK7kekGCtJAuelGsYHQxhj13KTf95swTCN2dZ/4lTfZ84Fu31jqJEEgjWA== +nock@^13.3.0: + version "13.5.4" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.4.tgz#8918f0addc70a63736170fef7106a9721e0dc479" + integrity sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw== dependencies: - glob "^7.0.3" - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "^2.87.0" - rimraf "2" - semver "~5.3.0" - tar "^4.4.8" - which "1" + debug "^4.1.0" + json-stringify-safe "^5.0.1" + propagate "^2.0.0" -node-int64@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" - integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= +node-addon-api@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + +node-dir@^0.1.17: + version "0.1.17" + resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" + integrity sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg== + dependencies: + minimatch "^3.0.2" -node-modules-regexp@^1.0.0: +node-domexception@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" - integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== -node-notifier@^8.0.0: - version "8.0.1" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.1.tgz#f86e89bbc925f2b068784b31f382afdc6ca56be1" - integrity sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA== +node-fetch@3.0.0-beta.9: + version "3.0.0-beta.9" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.0.0-beta.9.tgz#0a7554cfb824380dd6812864389923c783c80d9b" + integrity sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg== dependencies: - growly "^1.3.0" - is-wsl "^2.2.0" - semver "^7.3.2" - shellwords "^0.1.1" - uuid "^8.3.0" - which "^2.0.2" + data-uri-to-buffer "^3.0.1" + fetch-blob "^2.1.1" -"nopt@2 || 3": - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= +node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.12, node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: - abbrev "1" + whatwg-url "^5.0.0" -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== +node-fetch@^3.2.10: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= +node-gyp-build@^4.2.1: + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + +node-stream-zip@^1.9.1: + version "1.15.0" + resolved "https://registry.yarnpkg.com/node-stream-zip/-/node-stream-zip-1.15.0.tgz#158adb88ed8004c6c49a396b50a6a5de3bca33ea" + integrity sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw== + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== dependencies: - remove-trailing-separator "^1.0.1" + abbrev "1" -normalize-path@^3.0.0, normalize-path@~3.0.0: +normalize-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-all@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba" - integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ== - dependencies: - ansi-styles "^3.2.1" - chalk "^2.4.1" - cross-spawn "^6.0.5" - memorystream "^0.3.1" - minimatch "^3.0.4" - pidtree "^0.3.0" - read-pkg "^3.0.0" - shell-quote "^1.6.1" - string.prototype.padend "^3.0.0" - npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== dependencies: path-key "^2.0.0" -npm-run-path@^4.0.0: +npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== dependencies: path-key "^3.0.0" -"npmlog@0 || 1 || 2 || 3 || 4": - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" -nwsapi@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" - integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== +nullthrows@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" + integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== +ob1@0.73.10: + version "0.73.10" + resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.73.10.tgz#bf0a2e8922bb8687ddca82327c5cf209414a1bd4" + integrity sha512-aO6EYC+QRRCkZxVJhCWhLKgVjhNuD6Gu1riGjxrIm89CqLsmKgxzYDDEsktmKsoDeRdWGQM5EdMzXDl5xcVfsw== -object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@^4, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== +object-inspect@^1.10.3, object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== -object-keys@^1.0.12, object-keys@^1.1.1: +object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= +object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== dependencies: - isobject "^3.0.0" + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" -object.assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" - integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== +object.fromentries@^2.0.7: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.0" - has-symbols "^1.0.1" - object-keys "^1.1.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= +object.groupby@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== dependencies: - isobject "^3.0.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + +object.values@^1.1.7: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== dependencies: ee-first "1.1.1" +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" -onetime@^5.1.0: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" +open@^6.2.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9" + integrity sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg== + dependencies: + is-wsl "^1.1.0" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -3980,53 +7842,76 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== 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.3" - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + word-wrap "^1.2.5" + +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" -os-tmpdir@^1.0.0: +os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== -osenv@0: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" +outdent@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/outdent/-/outdent-0.5.0.tgz#9e10982fdc41492bb473ad13840d22f9655be2ff" + integrity sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q== -p-each-series@^2.1.0: +p-filter@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48" - integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ== + resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-2.1.0.tgz#1b1472562ae7a0f742f0f3d3d3718ea66ff9c09c" + integrity sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw== + dependencies: + p-map "^2.0.0" p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== -p-limit@^2.2.0: +p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -4034,11 +7919,28 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pako@^2.0.4, pako@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -4046,52 +7948,33 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - parse-json@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw== dependencies: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -parse-json@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646" - integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ== +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== dependencies: "@babel/code-frame" "^7.0.0" error-ex "^1.3.1" json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse5@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== - parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== path-exists@^4.0.0: version "4.0.0" @@ -4101,92 +7984,72 @@ path-exists@^4.0.0: path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.6.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - -pidtree@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a" - integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA== +peek-readable@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72" + integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg== -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= +picocolors@^1.0.0, picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +pirates@^4.0.4, pirates@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== -pirates@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" - integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== dependencies: - node-modules-regexp "^1.0.0" + find-up "^3.0.0" pkg-dir@^4.2.0: version "4.2.0" @@ -4195,10 +8058,20 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + +preferred-pm@^3.0.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/preferred-pm/-/preferred-pm-3.1.4.tgz#b99cf3da129cdb63555649b226b7530e82769769" + integrity sha512-lEHd+yEm22jXdCphDrkvIJQU66EuLojPPtvZkpKIkiD+l0DMThF/niqZKJSoU8Vl7iuvtmzyMhir9LdVy5WMnA== + dependencies: + find-up "^5.0.0" + find-yarn-workspace-root2 "1.2.16" + path-exists "^4.0.0" + which-pm "^2.2.0" prelude-ls@^1.2.1: version "1.2.1" @@ -4208,7 +8081,7 @@ prelude-ls@^1.2.1: prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== prettier-linter-helpers@^1.0.0: version "1.0.0" @@ -4217,12 +8090,12 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" - integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== +prettier@^2.3.1, prettier@^2.7.1: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -pretty-format@^26.0.0, pretty-format@^26.6.2: +pretty-format@^26.5.2, pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== @@ -4232,36 +8105,98 @@ pretty-format@^26.0.0, pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" +pretty-format@^29.0.0, pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +promise@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a" + integrity sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg== + dependencies: + asap "~2.0.6" -prompts@^2.0.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.2.tgz#480572d89ecf39566d2bd3fe2c9fccb7c4c0b068" - integrity sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA== +prompts@^2.0.1, prompts@^2.4.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== dependencies: kleur "^3.0.3" - sisteransi "^1.0.4" + sisteransi "^1.0.5" -proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== +prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== dependencies: - forwarded "~0.1.2" + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +propagate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" + integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== + +protobufjs@^6.8.8, protobufjs@~6.11.2, protobufjs@~6.11.3: + version "6.11.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.4.tgz#29a412c38bf70d89e537b6d02d904a6f448173aa" + integrity sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + +protobufjs@^7.2.4: + version "7.3.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.3.2.tgz#60f3b7624968868f6f739430cfbc8c9370e26df4" + integrity sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" ipaddr.js "1.9.1" -psl@^1.1.28, psl@^1.1.33: - version "1.8.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" - integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== pump@^3.0.0: version "3.0.0" @@ -4271,95 +8206,207 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +pure-rand@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== + +pvtsutils@^1.3.2, pvtsutils@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.5.tgz#b8705b437b7b134cd7fd858f025a23456f1ce910" + integrity sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA== + dependencies: + tslib "^2.6.1" + +pvutils@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" + integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.11.2: + version "6.12.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.3.tgz#e43ce03c8521b9c7fd7f1f13e514e5ca37727754" + integrity sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ== + dependencies: + side-channel "^1.0.6" -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +query-string@^7.0.1: + version "7.1.3" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" + integrity sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg== + dependencies: + decode-uri-component "^0.2.2" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" queue-microtask@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3" - integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg== + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== dependencies: - bytes "3.1.0" - http-errors "1.7.2" + bytes "3.1.2" + http-errors "2.0.0" iconv-lite "0.4.24" unpipe "1.0.0" -react-is@^17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" - integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== +rdf-canonize@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/rdf-canonize/-/rdf-canonize-3.4.0.tgz#87f88342b173cc371d812a07de350f0c1aa9f058" + integrity sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA== + dependencies: + setimmediate "^1.0.5" -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= +react-devtools-core@^4.26.1: + version "4.28.5" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.28.5.tgz#c8442b91f068cdf0c899c543907f7f27d79c2508" + integrity sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA== dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" + shell-quote "^1.6.1" + ws "^7" -read-pkg-up@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" - integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== +"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-native-codegen@^0.71.6: + version "0.71.6" + resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.71.6.tgz#481a610c3af9135b09e1e031da032e7270e0cc1b" + integrity sha512-e5pR4VldIhEaFctfSAEgxbng0uG4gjBQxAHes3EKLdosH/Av90pQfSe9IDVdFIngvNPzt8Y14pNjrtqov/yNIg== dependencies: - find-up "^4.1.0" - read-pkg "^5.2.0" - type-fest "^0.8.1" + "@babel/parser" "^7.14.0" + flow-parser "^0.185.0" + jscodeshift "^0.14.0" + nullthrows "^1.1.1" -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= +react-native-fs@^2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6" + integrity sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ== dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" + base-64 "^0.1.0" + utf8 "^3.0.0" -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= +react-native-get-random-values@^1.8.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz#1ca70d1271f4b08af92958803b89dccbda78728d" + integrity sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ== dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" + fast-base64-decode "^1.0.0" -read-pkg@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" - integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== +react-native-gradle-plugin@^0.71.19: + version "0.71.19" + resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.19.tgz#3379e28341fcd189bc1f4691cefc84c1a4d7d232" + integrity sha512-1dVk9NwhoyKHCSxcrM6vY6cxmojeATsBobDicX0ZKr7DgUF2cBQRTKsimQFvzH8XhOVXyH8p4HyDSZNIFI8OlQ== + +react-native-securerandom@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/react-native-securerandom/-/react-native-securerandom-0.1.1.tgz#f130623a412c338b0afadedbc204c5cbb8bf2070" + integrity sha512-CozcCx0lpBLevxiXEb86kwLRalBCHNjiGPlw3P7Fi27U6ZLdfjOCNRHD1LtBKcvPvI3TvkBXB3GOtLvqaYJLGw== + dependencies: + base64-js "*" + +react-native@^0.71.4: + version "0.71.19" + resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.19.tgz#966c91b2154058a9e5987cf5eb9fc0e4f1ab4157" + integrity sha512-E6Rz4lpe4NC9ZR6zq9AWiB0MAoVeidr+aPu/pmwk5ezvVL/wbQ1Gdj70v7fKMC8Nz5wYFO1S0XUNPUKYaPhfeg== + dependencies: + "@jest/create-cache-key-function" "^29.2.1" + "@react-native-community/cli" "10.2.7" + "@react-native-community/cli-platform-android" "10.2.0" + "@react-native-community/cli-platform-ios" "10.2.5" + "@react-native/assets" "1.0.0" + "@react-native/normalize-color" "2.1.0" + "@react-native/polyfills" "2.0.0" + abort-controller "^3.0.0" + anser "^1.4.9" + ansi-regex "^5.0.0" + base64-js "^1.1.2" + deprecated-react-native-prop-types "^3.0.1" + event-target-shim "^5.0.1" + invariant "^2.2.4" + jest-environment-node "^29.2.1" + jsc-android "^250231.0.0" + memoize-one "^5.0.0" + metro-react-native-babel-transformer "0.73.10" + metro-runtime "0.73.10" + metro-source-map "0.73.10" + mkdirp "^0.5.1" + nullthrows "^1.1.1" + pretty-format "^26.5.2" + promise "^8.3.0" + react-devtools-core "^4.26.1" + react-native-codegen "^0.71.6" + react-native-gradle-plugin "^0.71.19" + react-refresh "^0.4.0" + react-shallow-renderer "^16.15.0" + regenerator-runtime "^0.13.2" + scheduler "^0.23.0" + stacktrace-parser "^0.1.3" + use-sync-external-store "^1.0.0" + whatwg-fetch "^3.0.0" + ws "^6.2.2" + +react-refresh@^0.4.0: + version "0.4.3" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.4.3.tgz#966f1750c191672e76e16c2efa569150cc73ab53" + integrity sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA== + +react-shallow-renderer@^16.15.0: + version "16.15.0" + resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457" + integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA== + dependencies: + object-assign "^4.1.1" + react-is "^16.12.0 || ^17.0.0 || ^18.0.0" + +read-yaml-file@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-yaml-file/-/read-yaml-file-1.1.0.tgz#9362bbcbdc77007cc8ea4519fe1c0b821a7ce0d8" + integrity sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA== dependencies: - "@types/normalize-package-data" "^2.4.0" - normalize-package-data "^2.5.0" - parse-json "^5.0.0" - type-fest "^0.6.0" + graceful-fs "^4.1.5" + js-yaml "^3.6.1" + pify "^4.0.1" + strip-bom "^3.0.0" -readable-stream@^2.0.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== +readable-stream@^2.2.2, readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -4369,107 +8416,122 @@ readable-stream@^2.0.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== - dependencies: - picomatch "^2.2.1" - -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= +readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - -reflect-metadata@^0.1.13: - version "0.1.13" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" - integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== +readable-web-to-node-stream@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz#5d52bb5df7b54861fd48d015e93a2cb87b3ee0bb" + integrity sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw== dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" + readable-stream "^3.6.0" -regexpp@^3.0.0, regexpp@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" - integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== +readline@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/readline/-/readline-1.3.0.tgz#c580d77ef2cfc8752b132498060dc9793a7ac01c" + integrity sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg== -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= +readonly-date@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/readonly-date/-/readonly-date-1.0.0.tgz#5af785464d8c7d7c40b9d738cbde8c646f97dcd9" + integrity sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ== -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== +recast@^0.21.0: + version "0.21.5" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.21.5.tgz#e8cd22bb51bcd6130e54f87955d33a2b2e57b495" + integrity sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg== + dependencies: + ast-types "0.15.2" + esprima "~4.0.0" + source-map "~0.6.1" + tslib "^2.0.1" -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= +reduce-flatten@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" + integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= +ref-array-di@1.2.2, ref-array-di@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ref-array-di/-/ref-array-di-1.2.2.tgz#ceee9d667d9c424b5a91bb813457cc916fb1f64d" + integrity sha512-jhCmhqWa7kvCVrWhR/d7RemkppqPUdxEil1CtTtm7FkZV8LcHHCK3Or9GinUiFP5WY3k0djUkMvhBhx49Jb2iA== dependencies: - is-finite "^1.0.0" + array-index "^1.0.0" + debug "^3.1.0" -request-promise-core@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" - integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== +ref-struct-di@1.1.1, ref-struct-di@^1.1.0, ref-struct-di@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ref-struct-di/-/ref-struct-di-1.1.1.tgz#5827b1d3b32372058f177547093db1fe1602dc10" + integrity sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g== dependencies: - lodash "^4.17.19" + debug "^3.1.0" -request-promise-native@^1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28" - integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g== - dependencies: - request-promise-core "1.1.4" - stealthy-require "^1.1.1" - tough-cookie "^2.3.3" - -request@^2.87.0, request@^2.88.2: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" +reflect-metadata@^0.1.13: + version "0.1.14" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" + integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== + +regenerate-unicode-properties@^10.1.0: + version "10.1.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" + integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.13.2: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + +regexpu-core@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" + integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== require-from-string@^2.0.2: version "2.0.2" @@ -4488,6 +8550,11 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -4498,54 +8565,73 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -resolve@^1.0.0, resolve@^1.10.0, resolve@^1.3.2: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== - dependencies: - path-parse "^1.0.6" +resolve.exports@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== -resolve@^1.18.1: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== +resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.4: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@2, rimraf@^2.6.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" +rfc4648@1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.5.2.tgz#cf5dac417dd83e7f4debf52e3797a723c1373383" + integrity sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg== -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" -rsvp@^4.8.4: - version "4.8.5" - resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" - integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== +rimraf@^4.0.7, rimraf@^4.4.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.4.1.tgz#bd33364f67021c5b79e93d7f4fa0568c7c21b755" + integrity sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og== + dependencies: + glob "^9.2.0" + +rimraf@~2.2.6: + version "2.2.8" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" + integrity sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg== + +rimraf@~2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== run-parallel@^1.1.9: version "1.2.0" @@ -4554,132 +8640,173 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^6.6.6: - version "6.6.6" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.6.tgz#14d8417aa5a07c5e633995b525e1e3c0dec03b70" - integrity sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg== +rxjs@^6.6.0: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== dependencies: tslib "^1.9.0" +rxjs@^7.2.0, rxjs@^7.5.5, rxjs@^7.8.0: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.0.1, safe-buffer@^5.1.2: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== dependencies: - ret "~0.1.10" + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sane@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" - integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== - dependencies: - "@cnakazawa/watch" "^1.0.3" - anymatch "^2.0.0" - capture-exit "^2.0.0" - exec-sh "^0.3.2" - execa "^1.0.0" - fb-watchman "^2.0.0" - micromatch "^3.1.4" - minimist "^1.1.1" - walker "~1.0.5" - -saxes@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" - integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== +scheduler@^0.23.0: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== dependencies: - xmlchars "^2.2.0" - -"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + loose-envify "^1.1.0" -semver@7.x, semver@^7.2.1, semver@^7.3.2: - version "7.3.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" - integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== - dependencies: - lru-cache "^6.0.0" +semver@^5.5.0, semver@^5.6.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.0.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= +semver@^7.3.2, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== dependencies: debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" + depd "2.0.0" + destroy "1.2.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "~1.7.2" + http-errors "2.0.0" mime "1.6.0" - ms "2.1.1" - on-finished "~2.3.0" + ms "2.1.3" + on-finished "2.4.1" range-parser "~1.2.1" - statuses "~1.5.0" + statuses "2.0.1" -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== +serialize-error@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" + integrity sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw== + +serialize-error@^8.0.1, serialize-error@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" + integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ== + dependencies: + type-fest "^0.20.2" + +serve-static@1.15.0, serve-static@^1.13.1: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.17.1" + send "0.18.0" -set-blocking@^2.0.0, set-blocking@~2.0.0: +set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== +set-function-name@^2.0.1, set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +sha.js@^2.4.11: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== dependencies: shebang-regex "^1.0.0" @@ -4693,29 +8820,34 @@ shebang-command@^2.0.0: shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== shebang-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.6.1: - version "1.7.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" - integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== +shell-quote@^1.6.1, shell-quote@^1.7.3: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -shellwords@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" - integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +side-channel@^1.0.4, side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -sisteransi@^1.0.4: +sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== @@ -4725,73 +8857,35 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== +slice-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" + buffer-from "^1.0.0" + source-map "^0.6.0" -source-map-support@^0.5.12, source-map-support@^0.5.17, source-map-support@^0.5.19, source-map-support@^0.5.6: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== +source-map-support@^0.5.16, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.5.0, source-map@^0.5.6: +source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" @@ -4799,22 +8893,30 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== source-map@^0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +spawndamnit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/spawndamnit/-/spawndamnit-2.0.0.tgz#9f762ac5c3476abb994b42ad592b5ad22bb4b0ad" + integrity sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA== + dependencies: + cross-spawn "^5.1.0" + signal-exit "^3.0.2" spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + version "3.2.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" + integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== dependencies: spdx-expression-parse "^3.0.0" spdx-license-ids "^3.0.0" spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + version "2.5.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" + integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== spdx-expression-parse@^3.0.0: version "3.0.1" @@ -4825,119 +8927,140 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.6" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" - integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== + version "3.0.18" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz#22aa922dcf2f2885a6494a261f2d8b75345d0326" + integrity sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ== -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -stack-utils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.2.tgz#5cf48b4557becb4638d0bc4f21d23f5d19586593" - integrity sha512-0H7QK2ECz3fyZMzQ8rH0j2ykpfbnd20BFtfg/SqVC2+sCTtcw0aDTGB7dk+de4U4uUeuz6nOtJcrkFFLG1B0Rg== + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== dependencies: escape-string-regexp "^2.0.0" -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== + +stacktrace-parser@^0.1.3: + version "0.1.10" + resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a" + integrity sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg== + dependencies: + type-fest "^0.7.1" + +static-eval@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.2.tgz#2d1759306b1befa688938454c546b7871f806a42" + integrity sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg== dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" + escodegen "^1.8.1" -"statuses@>= 1.5.0 < 2", statuses@~1.5.0: +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stealthy-require@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" - integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= +str2buf@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/str2buf/-/str2buf-1.3.0.tgz#a4172afff4310e67235178e738a2dbb573abead0" + integrity sha512-xIBmHIUHYZDP4HyoXGHYNVmxlXLXDrtFHYT0eV6IOdEj3VO9ccaF1Ejl9Oq8iFjITllpT8FhaXb4KsNmw+3EuA== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== string-length@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1" - integrity sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw== + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== dependencies: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string.prototype.padend@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.0.tgz#dc08f57a8010dc5c153550318f67e13adbb72ac3" - integrity sha512-3aIv8Ffdp8EZj8iLwREGpQaUZiPyrWrpzMBHvkiSW/bK/EGve9np07Vwy7IJ5waydpGXzQZu/F8Oze2/IWkBaA== + strip-ansi "^6.0.1" + +string.prototype.matchall@^4.0.10: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" -string.prototype.trimend@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" -string.prototype.trimstart@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" + safe-buffer "~5.2.0" string_decoder@~1.1.1: version "1.1.1" @@ -4946,38 +9069,24 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== +strip-ansi@^5.0.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== dependencies: - ansi-regex "^5.0.0" + ansi-regex "^4.1.0" -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - is-utf8 "^0.2.0" + ansi-regex "^5.0.1" strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== strip-bom@^4.0.0: version "4.0.0" @@ -4987,30 +9096,36 @@ strip-bom@^4.0.0: strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - -strip-json-comments@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== + +strtok3@^6.2.4: + version "6.3.0" + resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-6.3.0.tgz#358b80ffe6d5d5620e19a073aa78ce947a90f9a0" + integrity sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw== + dependencies: + "@tokenizer/token" "^0.3.0" + peek-readable "^4.1.0" + +sudo-prompt@^9.0.0: + version "9.2.1" + resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-9.2.1.tgz#77efb84309c9ca489527a4e749f287e6bdd52afd" + integrity sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw== + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -5018,56 +9133,86 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^7.0.0, supports-color@^7.1.0: +supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" -supports-hyperlinks@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47" - integrity sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA== +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" - supports-color "^7.0.0" -symbol-tree@^3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" - integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +symbol-observable@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-2.0.3.tgz#5b521d3d07a43c351055fa43b8355b62d33fd16a" + integrity sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA== -table@^6.0.4: - version "6.0.7" - resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34" - integrity sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g== +table-layout@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" + integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== dependencies: - ajv "^7.0.2" - lodash "^4.17.20" - slice-ansi "^4.0.0" - string-width "^4.2.0" + array-back "^4.0.1" + deep-extend "~0.6.0" + typical "^5.2.0" + wordwrapjs "^4.0.0" + +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +tar@^6.1.11: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" -tar@^4.4.8: - version "4.4.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" - integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== +temp@0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" + integrity sha512-jtnWJs6B1cZlHs9wPG7BrowKxZw/rf6+UpGAkr8AaYmiTyTO7zQlLoST8zx/8TcUPnZmeBoB+H8ARuHZaSijVw== dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.8.6" - minizlib "^1.2.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.3" + os-tmpdir "^1.0.0" + rimraf "~2.2.6" -terminal-link@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" - integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== +temp@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.4.tgz#8c97a33a4770072e0a05f919396c7665a7dd59f2" + integrity sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg== dependencies: - ansi-escapes "^4.2.1" - supports-hyperlinks "^2.0.0" + rimraf "~2.6.2" + +term-size@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" + integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== + +terser@^5.15.0: + version "5.31.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.2.tgz#b5ca188107b706084dca82f988089fa6102eba11" + integrity sha512-LGyRZVFm/QElZHy/CPr/O4eNZOZIzsrQ92y4v9UJe/pFJjypje2yI3C2FmPtvUEnhadlSbmG2nXtdcjHOjCfxw== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" test-exclude@^6.0.0: version "6.0.0" @@ -5081,37 +9226,42 @@ test-exclude@^6.0.0: text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== throat@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== -tmpl@1.0.x: - version "1.0.4" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" - integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= +through2@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== dependencies: - kind-of "^3.0.2" + os-tmpdir "~1.0.2" -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== to-regex-range@^5.0.1: version "5.0.1" @@ -5120,140 +9270,104 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -tough-cookie@^2.3.3, tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -tough-cookie@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" - integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== +token-types@^4.1.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/token-types/-/token-types-4.2.1.tgz#0f897f03665846982806e138977dbe72d44df753" + integrity sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ== dependencies: - psl "^1.1.33" - punycode "^2.1.1" - universalify "^0.1.2" + "@tokenizer/token" "^0.3.0" + ieee754 "^1.2.1" -tr46@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479" - integrity sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg== - dependencies: - punycode "^2.1.1" +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== -tree-kill@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" - integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= +ts-api-utils@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== -ts-jest@^26.5.3: - version "26.5.3" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.3.tgz#a6ee00ba547be3b09877550df40a1465d0295554" - integrity sha512-nBiiFGNvtujdLryU7MiMQh1iPmnZ/QvOskBbD2kURiI1MwqvxlxNnaAB/z9TbslMqCsSbu5BXvSSQPc5tvHGeA== +ts-jest@^29.1.2: + version "29.2.2" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.2.tgz#0d2387bb04d39174b20a05172a968f258aedff4d" + integrity sha512-sSW7OooaKT34AAngP6k1VS669a0HdLxkQZnlC7T76sckGCokXFnvJ3yRlQZGRTAoV5K19HfSgCiSwWOSIfcYlg== dependencies: bs-logger "0.x" - buffer-from "1.x" + ejs "^3.0.0" fast-json-stable-stringify "2.x" - jest-util "^26.1.0" - json5 "2.x" - lodash "4.x" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "4.x" make-error "1.x" - mkdirp "1.x" - semver "7.x" - yargs-parser "20.x" - -ts-node-dev@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.1.6.tgz#ee2113718cb5a92c1c8f4229123ad6afbeba01f8" - integrity sha512-RTUi7mHMNQospArGz07KiraQcdgUVNXKsgO2HAi7FoiyPMdTDqdniB6K1dqyaIxT7c9v/VpSbfBZPS6uVpaFLQ== - dependencies: - chokidar "^3.5.1" - dateformat "~1.0.4-1.2.3" - dynamic-dedupe "^0.3.0" - minimist "^1.2.5" - mkdirp "^1.0.4" - resolve "^1.0.0" - rimraf "^2.6.1" - source-map-support "^0.5.12" - tree-kill "^1.2.2" - ts-node "^9.0.0" - tsconfig "^7.0.0" - -ts-node@^9.0.0: - version "9.1.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" - integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== - dependencies: + semver "^7.5.3" + yargs-parser "^21.0.1" + +ts-node@^10.0.0, ts-node@^10.4.0: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" arg "^4.1.0" create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" - source-map-support "^0.5.17" + v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tsconfig@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" - integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw== +ts-typed-json@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ts-typed-json/-/ts-typed-json-0.3.2.tgz#f4f20f45950bae0a383857f7b0a94187eca1b56a" + integrity sha512-Tdu3BWzaer7R5RvBIJcg9r8HrTZgpJmsX+1meXMJzYypbkj8NK2oJN0yvm4Dp/Iv6tzFa/L5jKRmEVTga6K3nA== + +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== dependencies: - "@types/strip-bom" "^3.0.0" - "@types/strip-json-comments" "0.0.30" + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" strip-bom "^3.0.0" - strip-json-comments "^2.0.0" -tslib@^1.8.1, tslib@^1.9.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== +tslib@^1.9.0, tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslog@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/tslog/-/tslog-3.1.2.tgz#fff4a821214a4bef64d5622e80389c94b8bea000" - integrity sha512-sKhNPUMjf+POPxcWuGxinjilEjIzPCdehF/zIdp33ttv9ohIBWHMV9gpdGvJjWbZn09a5IqP1dmaYq56IPXZAg== - dependencies: - source-map-support "^0.5.19" +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.1, tslib@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== -tsutils@^3.17.1: - version "3.17.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" - integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== - dependencies: - tslib "^1.8.1" +tslog@^4.8.2: + version "4.9.3" + resolved "https://registry.yarnpkg.com/tslog/-/tslog-4.9.3.tgz#d4167d5f51748bdeab593945bc2d8f9827ea0dba" + integrity sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw== -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= +tsyringe@^4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/tsyringe/-/tsyringe-4.8.0.tgz#d599651b36793ba872870fee4f845bd484a5cac1" + integrity sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA== dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + tslib "^1.9.3" type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" @@ -5265,7 +9379,7 @@ type-check@^0.4.0, type-check@~0.4.0: type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== dependencies: prelude-ls "~1.1.2" @@ -5274,22 +9388,27 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" - integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" - integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" + integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== + +type-fest@^3.2.0: + version "3.13.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706" + integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g== -type-is@~1.6.17, type-is@~1.6.18: +type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -5297,98 +9416,215 @@ type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" +type@^2.7.2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486" + integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== -typescript@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" - integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" -union-value@^1.0.0: +typed-array-byte-length@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +typescript@~5.5.2: + version "5.5.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa" + integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== + +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +typical@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" + integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== + +uglify-es@^3.1.9: + version "3.3.9" + resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" + integrity sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ== + dependencies: + commander "~2.13.0" + source-map "~0.6.1" + +uglify-js@^3.1.4: + version "3.18.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.18.0.tgz#73b576a7e8fda63d2831e293aeead73e0a270deb" + integrity sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A== + +uint8arrays@3.1.1, uint8arrays@^3.0.0, uint8arrays@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.1.tgz#2d8762acce159ccd9936057572dade9459f65ae0" + integrity sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg== + dependencies: + multiformats "^9.4.2" + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +underscore@1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" + integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +undici@^5.21.2: + version "5.28.4" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" + integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" + "@fastify/busboy" "^2.0.0" -universalify@^0.1.2: +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== dependencies: - has-value "^0.3.1" - isobject "^3.0.0" + escalade "^3.1.2" + picocolors "^1.0.1" uri-js@^4.2.2: - version "4.4.0" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" - integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +use-sync-external-store@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +utf8@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" + integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ== -util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== -uuid@^8.3.0: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -v8-compile-cache@^2.0.3: - version "2.1.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" - integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -v8-to-istanbul@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.0.tgz#5b95cef45c0f83217ec79f8fc7ee1c8b486aee07" - integrity sha512-uXUVqNUCLa0AH1vuVxzi+MI4RfxEOKt9pBgKwHbgH7st8Kv2P1m+jvWNnektzBh5QShF3ODgKmUFCf38LnVz1g== +v8-to-istanbul@^9.0.1: + version "9.3.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" + integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== dependencies: + "@jridgewell/trace-mapping" "^0.3.12" "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^1.6.0" - source-map "^0.7.3" + convert-source-map "^2.0.0" -validate-npm-package-license@^3.0.1: +validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== @@ -5396,109 +9632,169 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -validator@^13.5.2: - version "13.5.2" - resolved "https://registry.yarnpkg.com/validator/-/validator-13.5.2.tgz#c97ae63ed4224999fb6f42c91eaca9567fe69a46" - integrity sha512-mD45p0rvHVBlY2Zuy3F3ESIe1h5X58GPfAtslBjY7EtTqGquZTj+VX/J4RnHWN8FKq0C9WRVt1oWAcytWRuYLQ== +validate-npm-package-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" + integrity sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw== + dependencies: + builtins "^1.0.3" + +validator@^13.9.0: + version "13.12.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f" + integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== + +varint@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" + integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vlq@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468" + integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w== -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= +walker@^1.0.7, walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" + makeerror "1.0.12" -w3c-hr-time@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" - integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== dependencies: - browser-process-hrtime "^1.0.0" + defaults "^1.0.3" -w3c-xmlserializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" - integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== +web-did-resolver@^2.0.21: + version "2.0.27" + resolved "https://registry.yarnpkg.com/web-did-resolver/-/web-did-resolver-2.0.27.tgz#21884a41d64c2042c307acb2d6e2061244e09806" + integrity sha512-YxQlNdeYBXLhVpMW62+TPlc6sSOiWyBYq7DNvY6FXmXOD9g0zLeShpq2uCKFFQV/WlSrBi/yebK/W5lMTDxMUQ== dependencies: - xml-name-validator "^3.0.0" + cross-fetch "^4.0.0" + did-resolver "^4.0.0" -walker@^1.0.7, walker@~1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" - integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= +web-streams-polyfill@^3.0.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== + +webcrypto-core@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.8.0.tgz#aaea17f3dd9c77c304e3c494eb27ca07cc72ca37" + integrity sha512-kR1UQNH8MD42CYuLzvibfakG5Ew5seG85dMMoAM/1LqvckxaF6pUiidLuraIu4V+YCIFabYecUZAW0TuxAoaqw== dependencies: - makeerror "1.0.x" + "@peculiar/asn1-schema" "^2.3.8" + "@peculiar/json-schema" "^1.1.12" + asn1js "^3.0.1" + pvtsutils "^1.3.5" + tslib "^2.6.2" -webidl-conversions@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" - integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== +webcrypto-shim@^0.1.4: + version "0.1.7" + resolved "https://registry.yarnpkg.com/webcrypto-shim/-/webcrypto-shim-0.1.7.tgz#da8be23061a0451cf23b424d4a9b61c10f091c12" + integrity sha512-JAvAQR5mRNRxZW2jKigWMjCMkjSdmP5cColRP1U/pTg69VgHXEi1orv5vVpJ55Zc5MIaPc1aaurzd9pjv2bveg== -webidl-conversions@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" - integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== -whatwg-encoding@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" - integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== - dependencies: - iconv-lite "0.4.24" +whatwg-fetch@^3.0.0: + version "3.6.20" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" + integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== -whatwg-mimetype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" - integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" -whatwg-url@^8.0.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.3.0.tgz#d1e11e565334486cdb280d3101b9c3fd1c867582" - integrity sha512-BQRf/ej5Rp3+n7k0grQXZj9a1cHtsp4lqj01p59xBWFKdezR8sO37XnpafwNqiFac/v2Il12EIMjX/Y4VZtT8Q== +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== dependencies: - lodash.sortby "^4.7.0" - tr46 "^2.0.2" - webidl-conversions "^6.1.0" + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + version "2.0.1" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + +which-pm@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/which-pm/-/which-pm-2.2.0.tgz#6b5d8efd7b5089b97cd51a36c60dd8e4ec7eca59" + integrity sha512-MOiaDbA5ZZgUjkeMWM5EkJp4loW5ZRoa5bc3/aeMox/PJelMhE6t7S/mLuiY43DBupyxH+S0U1bTui9kWUlmsw== + dependencies: + load-yaml-file "^0.2.0" + path-exists "^4.0.0" + +which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" -which@1, which@^1.2.9: +which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" -which@^2.0.1, which@^2.0.2: +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== +wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== dependencies: - string-width "^1.0.2 || 2" + string-width "^1.0.2 || 2 || 3 || 4" -word-wrap@^1.2.3, word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +word-wrap@^1.2.5, word-wrap@~1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + +wordwrapjs@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" + integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== + dependencies: + reduce-flatten "^2.0.0" + typical "^5.2.0" -wrap-ansi@^6.2.0: +wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== @@ -5507,47 +9803,83 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== +write-file-atomic@^2.3.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== dependencies: + graceful-fs "^4.1.11" imurmurhash "^0.1.4" - is-typedarray "^1.0.0" signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" -ws@^7.4.4: - version "7.4.4" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" - integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" -xml-name-validator@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" - integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +ws@^6.2.2: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.3.tgz#ccc96e4add5fd6fedbc491903075c85c5a11d9ee" + integrity sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA== + dependencies: + async-limiter "~1.0.0" -xmlchars@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" - integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +ws@^7, ws@^7.5.1: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + +ws@^8.13.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +xstream@^11.14.0: + version "11.14.0" + resolved "https://registry.yarnpkg.com/xstream/-/xstream-11.14.0.tgz#2c071d26b18310523b6877e86b4e54df068a9ae5" + integrity sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw== + dependencies: + globalthis "^1.0.1" + symbol-observable "^2.0.3" -xtend@^4.0.0: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yallist@^3.0.0, yallist@^3.0.3: +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== + +yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== @@ -5557,10 +9889,10 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@20.x: - version "20.2.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.1.tgz#28f3773c546cdd8a69ddae68116b48a5da328e77" - integrity sha512-yYsjuSkjbLMBp16eaOt7/siKTjNVjMm3SoJnIg3sEh/JsvqVVDyjRKmaJV4cl+lNIgq6QEco2i3gDebJl7/vLA== +yaml@^2.1.3: + version "2.4.5" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e" + integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg== yargs-parser@^18.1.2: version "18.1.3" @@ -5570,7 +9902,12 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@^15.4.1: +yargs-parser@^21.0.1, yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^15.1.0: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== @@ -5587,7 +9924,25 @@ yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^17.3.1, yargs@^17.5.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==